fix(release): replace the GH_TOKEN PAT with a GitHub App installation token - #101
Merged
Conversation
… 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>
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>
…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>
…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>
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>
…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>
…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>
…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>
…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>
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>
… 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>
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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_TOKENPAT exceeds it. GitHub rejects every API call it makes:gh pr list@changesets/changelog-githubuses the token for PR metadata)Nobody was paged because
CI_RELEASE_ALERT_SLACK_BOT_TOKEN/CI_RELEASE_ALERT_SLACK_CHANNEL_IDare 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(theopenrouter-port-botpattern from python-agent/go-agent):publish.yamlrelease-train.yamlbump-openrouter-sdk.yamlSide benefits over the PAT:
GITHUB_TOKEN)Setup required before merge (org admin)
openrouter-port-bot— with repo permissions contents: write and pull-requests: write, no webhookRELEASE_BOT_APP_ID(the App's numeric ID)RELEASE_BOT_PRIVATE_KEY(an App private key, PEM)CI_RELEASE_ALERT_SLACK_BOT_TOKEN+CI_RELEASE_ALERT_SLACK_CHANNEL_IDso stalls page instead of logging into the voidworkflow_dispatchrelease-train withdry_run: trueto verify end to endGH_TOKENsecret can be deleted from this repo once green🤖 Generated with Claude Code