Skip to content

ci: dispatch agent-pane as the release App over workflow_dispatch - #252

Merged
jonathanKingston merged 1 commit into
mainfrom
chore/app-token-dispatch
Aug 20, 2026
Merged

jonathanKingston merged 1 commit into
mainfrom
chore/app-token-dispatch

Conversation

@jonathanKingston

Copy link
Copy Markdown
Collaborator

Retires the AGENT_PANE_DISPATCH_TOKEN PAT. Both notify paths — the release: published workflow and the explicit step in release.yml — now authenticate as copse-release-bot, the App this repo already uses to push version bumps past the main ruleset.

Consumer side: copse-dev/agent-pane#1816. The two triggers are swapped in lockstep, so merge these together (or agent-pane's first).

Why workflow_dispatch instead of repository_dispatch

repository_dispatch sits under the Contents permission. That meant the cross-repo credential needed Contents: write on agent-pane — the ability to push code there — purely to start a workflow. workflow_dispatch needs only Actions: write, which cannot push anything.

agent-pane's sync-streaming-markdown.yml already had a workflow_dispatch trigger with a version input as its manual backfill path, and already fell back to format('v{0}', version) when no tag was supplied — and this repo's tags are exactly v<version> — so the PR-body link it generates is unchanged.

Why two token mints

Left empty, create-github-app-token scopes the token to the current repository. That is why the existing release-token at the top of the job cannot be reused for a call against agent-pane — it genuinely cannot reach it.

release.yml mints a second token rather than adding agent-pane to the first, because actions/checkout persists the first one as the job's git push credential: widening it would leave a cross-repo credential on disk for the whole release.

One thing worth a look

The existing release mint now pins permission-contents: write instead of inheriting the installation's permissions. The App is installed org-wide, and agent-pane needs Pull requests: write and Actions: write from it — so without this pin, this token would have silently widened the moment those were granted. It is the one that gets written to disk.

Verification

Both workflows parse. Prerequisites confirmed live: the App holds actions: write, and RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY resolve here as before.

AGENT_PANE_DISPATCH_TOKEN can be deleted once both PRs land.

🤖 Generated with Claude Code

Retires the AGENT_PANE_DISPATCH_TOKEN PAT. Both notify paths — the
release: published workflow and the explicit step in release.yml — now
authenticate as copse-release-bot, the App this repo already uses to push
version bumps past the main ruleset.

The dispatch also moves from repository_dispatch to workflow_dispatch.
repository_dispatch sits under the Contents permission, so the credential
needed Contents: write on agent-pane — the ability to push code there —
purely to start a workflow. workflow_dispatch needs only Actions: write,
which cannot. The consumer side is copse-dev/agent-pane#1816.

Each call mints its own token scoped to agent-pane alone: left empty,
create-github-app-token scopes to the current repository, which is why the
existing release token could not be reused. release.yml mints a second one
rather than widening the first, because actions/checkout persists that one
as the job's git push credential for the whole release.

That existing mint now pins permission-contents: write instead of
inheriting. The App is installed org-wide and agent-pane's workflows need
Pull requests: write and Actions: write from it, so an inherited token would
have silently widened the moment those were added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jonathanKingston added a commit to copse-dev/agent-pane that referenced this pull request Aug 20, 2026
…1816)

Replaces `SYNC_PR_TOKEN` and `SCREENSHOTS_PAT` with short-lived
installation tokens minted per run from the **copse-release-bot** GitHub
App — the same App `streaming-markdown`'s `release.yml` already uses.

Why the App beats a PAT here:

- Tokens are minted per run and expire in ~1h. No annual rotation
deadline, no "the PAT expired and every sync workflow went quietly red".
- Not tied to a human, and consumes no seat (the org is 2/2).
- Permissions live on the App and are pinned per call site, so each job
gets only what it needs.
- Only the App id and private key are stored, once, as org secrets.
- It is a distinct actor from `GITHUB_TOKEN`, so both properties the
PATs existed for still hold: org policy lets it open PRs, and its pushes
and PRs trigger downstream CI instead of being recursion-guarded.

## What changed

| Workflow | Was | Now |
| --- | --- | --- |
| `sync-model-catalog`, `sync-model-cards`, `sync-intellect`,
`sync-streaming-markdown` | `SYNC_PR_TOKEN` | App, `contents` +
`pull-requests: write` |
| `promote-develop` | `SYNC_PR_TOKEN` | App, `contents` +
`pull-requests: write` |
| `ci.yml` `autoformat` / `commit-screenshots`, `reconcile-screenshots`
| `SCREENSHOTS_PAT \|\| github.token` | App (`contents: write`) `\|\|
github.token` |

`sync-streaming-markdown` also drops its `repository_dispatch` trigger
for `workflow_dispatch`. That endpoint sits under **Contents**, so the
cross-repo credential `streaming-markdown` holds needed `Contents:
write` here — the ability to push code — purely to start a workflow.
`workflow_dispatch` needs only `Actions: write`, which cannot. The
producer side is copse-dev/streaming-markdown#252; **merge that one
first or alongside this**, since the two triggers are swapped in
lockstep.

## Deliberate non-changes

**The App is not granted `workflows: write`.** `SCREENSHOTS_PAT`
deliberately lacked the equivalent `workflow` scope, and both screenshot
jobs have an escape hatch for merges that touch `.github/workflows/**`.
An App token hits the same wall for the same reason, so the guardrail
stays — an actor that can rewrite CI definitions org-wide is a bigger
grant than auto-resolving those merges is worth. Only the comments and
PR-comment text change, to name the App permission rather than the PAT
scope.

**Permissions are pinned at every mint rather than inherited.** The App
is installed org-wide, so an inherited token would silently widen the
moment the installation gains a permission. (`streaming-markdown`'s
existing release mint is pinned for the same reason in the companion PR
— it was inheriting, and it is persisted to disk by `actions/checkout`.)

**The `github.token` fallback survives.** `autoformat`,
`commit-screenshots` and `reconcile-screenshots` mint with
`continue-on-error`, so a missing or broken credential degrades exactly
as before — the commit lands, just without the CI re-trigger — instead
of reddening a PR. The sync workflows and `promote-develop` fail loudly
instead, matching their existing "require the token" preflight.

## Verification

- All workflows parse; every embedded `github-script` block passes `node
--check`.
- `node scripts/run-tests.mts ci-workflow-invariants sync-intellect` —
58/58 pass. Two invariants asserted the old secret names and were
updated.
- All nine changed files are Prettier-clean.

## Prerequisites (already done)

- `copse-release-bot` now holds `actions: write`, `contents: write`,
`pull_requests: write`, `metadata: read` — and no `workflows`.
- `RELEASE_APP_ID` / `RELEASE_APP_PRIVATE_KEY` are org secrets,
confirmed visible to this repo.

Once both PRs are green, `SYNC_PR_TOKEN`, `SCREENSHOTS_PAT` and
`AGENT_PANE_DISPATCH_TOKEN` can be deleted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Jonathan Kingston <KingstonMailBox@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@jonathanKingston
jonathanKingston merged commit 153581f into main Aug 20, 2026
6 checks passed
@jonathanKingston
jonathanKingston deleted the chore/app-token-dispatch branch August 20, 2026 12:44
jonathanKingston added a commit to copse-dev/agent-pane that referenced this pull request Aug 31, 2026
…1816)

Replaces `SYNC_PR_TOKEN` and `SCREENSHOTS_PAT` with short-lived
installation tokens minted per run from the **copse-release-bot** GitHub
App — the same App `streaming-markdown`'s `release.yml` already uses.

Why the App beats a PAT here:

- Tokens are minted per run and expire in ~1h. No annual rotation
deadline, no "the PAT expired and every sync workflow went quietly red".
- Not tied to a human, and consumes no seat (the org is 2/2).
- Permissions live on the App and are pinned per call site, so each job
gets only what it needs.
- Only the App id and private key are stored, once, as org secrets.
- It is a distinct actor from `GITHUB_TOKEN`, so both properties the
PATs existed for still hold: org policy lets it open PRs, and its pushes
and PRs trigger downstream CI instead of being recursion-guarded.

## What changed

| Workflow | Was | Now |
| --- | --- | --- |
| `sync-model-catalog`, `sync-model-cards`, `sync-intellect`,
`sync-streaming-markdown` | `SYNC_PR_TOKEN` | App, `contents` +
`pull-requests: write` |
| `promote-develop` | `SYNC_PR_TOKEN` | App, `contents` +
`pull-requests: write` |
| `ci.yml` `autoformat` / `commit-screenshots`, `reconcile-screenshots`
| `SCREENSHOTS_PAT \|\| github.token` | App (`contents: write`) `\|\|
github.token` |

`sync-streaming-markdown` also drops its `repository_dispatch` trigger
for `workflow_dispatch`. That endpoint sits under **Contents**, so the
cross-repo credential `streaming-markdown` holds needed `Contents:
write` here — the ability to push code — purely to start a workflow.
`workflow_dispatch` needs only `Actions: write`, which cannot. The
producer side is copse-dev/streaming-markdown#252; **merge that one
first or alongside this**, since the two triggers are swapped in
lockstep.

## Deliberate non-changes

**The App is not granted `workflows: write`.** `SCREENSHOTS_PAT`
deliberately lacked the equivalent `workflow` scope, and both screenshot
jobs have an escape hatch for merges that touch `.github/workflows/**`.
An App token hits the same wall for the same reason, so the guardrail
stays — an actor that can rewrite CI definitions org-wide is a bigger
grant than auto-resolving those merges is worth. Only the comments and
PR-comment text change, to name the App permission rather than the PAT
scope.

**Permissions are pinned at every mint rather than inherited.** The App
is installed org-wide, so an inherited token would silently widen the
moment the installation gains a permission. (`streaming-markdown`'s
existing release mint is pinned for the same reason in the companion PR
— it was inheriting, and it is persisted to disk by `actions/checkout`.)

**The `github.token` fallback survives.** `autoformat`,
`commit-screenshots` and `reconcile-screenshots` mint with
`continue-on-error`, so a missing or broken credential degrades exactly
as before — the commit lands, just without the CI re-trigger — instead
of reddening a PR. The sync workflows and `promote-develop` fail loudly
instead, matching their existing "require the token" preflight.

## Verification

- All workflows parse; every embedded `github-script` block passes `node
--check`.
- `node scripts/run-tests.mts ci-workflow-invariants sync-intellect` —
58/58 pass. Two invariants asserted the old secret names and were
updated.
- All nine changed files are Prettier-clean.

## Prerequisites (already done)

- `copse-release-bot` now holds `actions: write`, `contents: write`,
`pull_requests: write`, `metadata: read` — and no `workflows`.
- `RELEASE_APP_ID` / `RELEASE_APP_PRIVATE_KEY` are org secrets,
confirmed visible to this repo.

Once both PRs are green, `SYNC_PR_TOKEN`, `SCREENSHOTS_PAT` and
`AGENT_PANE_DISPATCH_TOKEN` can be deleted.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Jonathan Kingston <jonathan@copse.dev>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant