Skip to content

fix(release): replace the GH_TOKEN PAT with a GitHub App installation token - #101

Merged
LukasParke merged 13 commits into
mainfrom
LukasParke/release-app-token
Aug 6, 2026
Merged

fix(release): replace the GH_TOKEN PAT with a GitHub App installation token#101
LukasParke merged 13 commits into
mainfrom
LukasParke/release-app-token

Conversation

@LukasParke

@LukasParke LukasParke commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Problem — all releases silently stalled since Aug 4

The OpenRouter enterprise now forbids fine-grained PATs with lifetimes over 366 days, and the shared GH_TOKEN PAT exceeds it. GitHub rejects every API call it makes:

GraphQL: The 'OpenRouter' enterprise forbids access via a fine-grained personal
access tokens if the token's lifetime is greater than 366 days.
  • both scheduled release-train runs (Aug 4, Aug 6) died at the first gh pr list
  • all six publish.yaml runs since Aug 4 died in the changesets changelog fetch (@changesets/changelog-github uses the token for PR metadata)
  • Version PR chore: version packages #88 sits unmerged; nobody owns the PAT, so fixing its lifetime isn't an option

Nobody was paged because CI_RELEASE_ALERT_SLACK_BOT_TOKEN / CI_RELEASE_ALERT_SLACK_CHANNEL_ID are also unset — the train's ERR trap fired and logged (slack not configured; would have posted). Adding those secrets is strongly recommended alongside this PR.

Fix — GitHub App installation tokens, like the port repos

All three release workflows now mint short-lived App tokens via actions/create-github-app-token@v2 (the openrouter-port-bot pattern from python-agent/go-agent):

Workflow Scope Used for
publish.yaml typescript-agent + openrouter-web + python-agent + go-agent changesets step, git push creds, HOP B/C dispatches
release-train.yaml typescript-agent find / scope-vet / gate+merge
bump-openrouter-sdk.yaml typescript-agent (minted per job — tokens can't cross jobs and expire in 1h) checkout+push, PR open, gate

Side benefits over the PAT:

Setup required before merge (org admin)

  1. Create a GitHub App (e.g. "openrouter-release-bot") in the OpenRouterTeam org — or extend openrouter-port-bot — with repo permissions contents: write and pull-requests: write, no webhook
  2. Install it on: typescript-agent, openrouter-web, python-agent, go-agent
  3. On typescript-agent, add:
    • variable RELEASE_BOT_APP_ID (the App's numeric ID)
    • secret RELEASE_BOT_PRIVATE_KEY (an App private key, PEM)
  4. Optional but recommended: CI_RELEASE_ALERT_SLACK_BOT_TOKEN + CI_RELEASE_ALERT_SLACK_CHANNEL_ID so stalls page instead of logging into the void
  5. After merge: re-run the latest failed publish run (regenerates Version PR chore: version packages #88's checks under the App identity), then workflow_dispatch release-train with dry_run: true to verify end to end
  6. The old GH_TOKEN secret can be deleted from this repo once green

🤖 Generated with Claude Code


Open in Devin Review

… token

The OpenRouter enterprise now forbids fine-grained PATs with lifetimes
over 366 days, and the shared GH_TOKEN PAT exceeds it — since Aug 4
every release-train run failed at its first gh call and every
publish.yaml run failed in the changesets changelog fetch, silently
stalling releases (nobody was paged because the Slack alert secrets are
also unset; see PR notes).

Swap all three release workflows to short-lived App installation
tokens via actions/create-github-app-token@v2, the same pattern the
port repos use for openrouter-port-bot:

- publish.yaml: one mint covering typescript-agent + the HOP B/C
  dispatch targets (openrouter-web, python-agent, go-agent); used for
  the changesets step, git push credentials, and both dispatches.
- release-train.yaml: repo-scoped mint for find/scope/gate.
- bump-openrouter-sdk.yaml: repo-scoped mint per job (tokens cannot
  cross job boundaries and expire in 1h).

Side benefits: pushes/PRs still trigger CI + perry (App tokens are not
GITHUB_TOKEN), the token auto-expires within an hour instead of living
in .git/config as a long-lived credential, and there is no annual
rotation to forget.

Requires (documented in the PR): a "release bot" GitHub App installed
on all four repos with contents:write + pull-requests:write, plus the
RELEASE_BOT_APP_ID variable and RELEASE_BOT_PRIVATE_KEY secret on this
repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread .github/workflows/publish.yaml Outdated
Comment thread .github/workflows/release-train.yaml
Comment thread .github/workflows/publish.yaml
Review feedback on PR #101: minting one token across all four repos put
the HOP targets on the release's critical path — losing bot access on
e.g. go-agent would have blocked npm publishes outright, contradicting
the HOP steps' own "must not fail the release" contract.

Split into two mints: the publish-path token is scoped to this repo
only and still fails loudly when the App setup is missing; the
cross-repo dispatch token is minted best-effort (continue-on-error)
right before the HOP steps, which now skip with a ::warning:: when it
couldn't be minted — the same degradation as a failed dispatch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…endently

Listing all four repos in the dispatch mint made it all-or-nothing:
actions/create-github-app-token fails when any listed repo is missing
from the installation, so one uninstalled port repo would have skipped
the unrelated monorepo notification too. Mint with owner-only scope
instead — the token covers whatever the installation includes, and a
missing repo surfaces as an individual dispatch 404 hitting the
existing per-repo warning paths. HOP B gains continue-on-error to match
that contract (its 404 must warn, not redden a completed release).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…ocument workflows:write

Third feedback round on PR #101:

- release-train.yaml: the mint step is now continue-on-error with a
  guard step that posts a 🚨 Slack alert and exits 1 when
  minting failed — a broken App credential on the unwatched cron paged
  nobody before, recreating the silent stall this PR fixes.

- publish.yaml HOP B: wrap the dispatch in if/else emitting :⚠️:
  on failure (same as HOP C's per-repo loop). Under continue-on-error a
  bare failure was invisible: green job, no annotation.

- publish.yaml: document the App-token workflows:write constraint —
  pushes touching .github/workflows/ are rejected unless the App grants
  it; grant on the App if a Version PR ever needs it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

TIMEOUT restarts per vetted head (by design — a fresh head's CI needs a
full window), so repeated changesets/action refreshes could keep the
gate polling past the 1-hour lifetime of the App installation token,
after which every gh call 401s with a confusing failure. New MAX_WALL
(default 50 min) never resets: the gate stops with a clear "re-run to
continue" alert while its credential still works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…setup

Fourth feedback round on PR #101:

- publish.yaml: the owner-wide dispatch mint is replaced by three
  per-target mints (openrouter-web, python-agent, go-agent), each
  best-effort. This ends the recurring scope-vs-coupling tension: a
  missing target degrades only its own notification (skip + ::warning::
  naming the repo), and no step ever holds a token broader than the one
  repo it talks to. HOP C's tag lookup uses the typescript-agent-scoped
  publish token; each dispatch selects its own token inside the loop.

- publish.yaml: the publish-token mint and push-credentials steps are
  skipped on dry-run — that leg publishes/pushes/dispatches nothing, so
  broken App setup must not block the diagnostic escape hatch you'd use
  while fixing exactly that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…patches

Fifth feedback round on PR #101:

- The mint/push-credentials dry-run skip is narrowed to the
  mode=publish leg. mode=version ignores dry-run entirely (the
  changesets step runs for real, as the job's own comment documents),
  so skipping the mint there handed changesets an empty token and
  broke a previously-working manual path.

- Degraded dispatch paths now page the release-alert Slack channel,
  not just annotate: both the missing-token warn step and a failed
  HOP B dispatch post to CI_RELEASE_ALERT_* (no-op with the usual
  "would have posted" log until those secrets exist). A permanently
  broken openrouter-web installation is now loud on every release
  instead of an annotation nobody reads on a green run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…ually shipped

Sixth feedback round on PR #101: publish.yaml runs on every push to
main and most runs release nothing, but the dispatch-token mints and
the missing-token page ran unconditionally — an uninstalled HOP target
would have paged "release notification skipped" on every ordinary
push. The mint + warn block now sits after publish detection and is
gated on steps.published.outputs.version, so it only runs (and only
alerts) when there is a release to notify about.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…lifecycle scripts in the SDK bump

Seventh feedback round on PR #101:

- publish.yaml: the mint gets the same continue-on-error + paging
  guard as the train — a broken App credential on an unwatched
  push-to-main run posts 🚨 to the release channel and
  exits 1, instead of a bare red first step (bounded before only by
  the train's stranded-changesets alert days later).

- bump-openrouter-sdk.yaml + bump-sdk.sh: split into prepare/remote
  phases. Checkout uses persist-credentials: false; the prepare phase
  (edit, pnpm install --no-frozen-lockfile with its lifecycle scripts,
  changeset, local commit) runs tokenless; the App token is minted
  only afterwards and passed per-command to the push — never persisted
  in .git/config. Same credential posture publish.yaml already has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Review caught a set -e trap in the phase split: invoking prepare in a
conditional (prepare || true / if prepare) disables errexit for the
whole function body, so a failed pnpm relock would have continued to
commit and open a PR with a stale lockfile — and in "all" mode a
failure was indistinguishable from a no-op. The no-op case is now a
NOOP flag with return 0; prepare is invoked plainly, so any real
failure inside it aborts the script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

… SDK bump chain

Ninth feedback round on PR #101:

- pr-gate.sh: TIMEOUT and MAX_WALL checks moved to the top of the poll
  loop. At the bottom they were unreachable from any branch that
  `continue`s — a PR oscillating green/not-green through the settle
  re-check could spin past both deadlines until the runner's 6-hour
  limit, exactly the expired-credential scenario MAX_WALL exists to
  prevent.

- bump-openrouter-sdk.yaml: both mints (bump job + gate job) get the
  continue-on-error + Slack paging treatment from publish.yaml and the
  train — the flow is dispatch-driven and unwatched, and the gate-job
  variant names the dangling PR it leaves ungated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread .github/workflows/publish.yaml
Comment thread .github/scripts/bump-sdk.sh
Tenth feedback round on PR #101: a token embedded in the push URL (or
a -c flag) is visible in argv (/proc/<pid>/cmdline), set -x traces,
and git error output. The bump script now supplies the extraheader via
GIT_CONFIG_COUNT/KEY_0/VALUE_0 — process-tree-local env, the same
mechanism actions/checkout uses, with nothing token-bearing in argv or
.git/config. (publish.yaml's remote-URL credential is unchanged: it is
the documented floor for changesets/action, which needs persisted push
credentials.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…OP C fallback

The SHA fallback is benign either way (same tree), but the warning
said "tag not on origin" even when the real cause was an expired App
token or an API failure — misleading during triage. Probe gh auth
separately (rate_limit) and name the actual failure class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@LukasParke
LukasParke merged commit b902f51 into main Aug 6, 2026
10 of 21 checks passed
@LukasParke
LukasParke deleted the LukasParke/release-app-token branch August 6, 2026 21:19
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