ci(publish): gh_app_token.sh and the uploaders' release-App-first fallback (tooling, inert; main)
Approved by Leo on 2026-10-26 (the secrets/permissions change this PR held for): "for all those waiting on you things go ahead but just follow lawrence for now i havent seen much yet".
Rebased onto main at 5a3fa7545fb (squashed), resolving a conflict with #25534 in publish-release-assets.py the same way as #25490 (backoff kept, App-token switch first). The re-review found two gaps, fixed in a follow-up commit: finalize-release.py download now falls back to github.token when the App token is refused, and gh_app_token.sh reads the job ledger fail-closed and reserves each wait before sleeping, as gh_budget_wait.sh does. Codex gpt-6-astra xhigh: SHIP at the final head.
Part 1 of 2 for main's classic nightly and DEV publishers. This PR adds only the tooling, and it does nothing until a job passes GH_APP_TOKEN or installs the wrapper. Part 2 does that wiring and holds every secrets, environment and permissions change. Both parts were held for the same approval.
Why. Run 40298250989 had already built and signed the nightly when its upload POSTs were refused with HTTP 403 "API rate limit exceeded for installation", from 12:04:51Z to about 12:14Z. During those refusals, core still read 4899/5000, and the 403s carried no x-ratelimit-* headers. So the hourly core budget was not spent: this was GitHub's secondary limit on bursts per actor (or possibly a per-repository cap). Every workflow's GITHUB_TOKEN counts as that one actor. The release App is a separate actor with its own limits.
What changed (no workflow permissions:, environment: or secrets change in this PR)
- New
scripts/ci/gh_app_token.sh --installputs aghon PATH. A call whoseGH_TOKENisgithub.tokengoes out with the App token first; a call that passes any other token is left alone.- When the App token is refused (401, 403, 429, a rate limit, or GraphQL's "Resource not accessible"), the same call is sent again with
github.token. A refused request did nothing on GitHub's side, so this is safe for writes. - "Resource not accessible" marks that API area (
actions,issues,graphql, …) as one the App can't reach; the rest of the job sends it straight togithub.token. - A 401 drops the App token for the job.
/rate_limitreads keepgithub.token, since that is the budgetgh_budget_wait.shwaits on.
- When the App token is refused (401, 403, 429, a rate limit, or GraphQL's "Resource not accessible"), the same call is sent again with
- The fallback backs off and retries both tokens when both are refused for a limit, or with a 403 that is not a permission refusal:
Retry-Afterwhen shown, otherwise 60 s, 120 s, 240 s. The waits go intogh_budget_wait.sh's per-job ledger and are capped byGH_BUDGET_WAIT_MAX_SECONDS. A 403 with no headers backs off; it does not fail fast. publish-release-assets.py,finalize-release.pyandprune_nightly_release_assets.pyreadGH_APP_TOKENfirst and switch toGH_TOKENfor the rest of the run on a 401/403/429.- The switch is thread-safe: the upload workers share one client, and a concurrent refusal switches once.
- Hashing an asset with a null digest goes through the same fallback, and no longer sends the token across the asset API's redirect to the storage host.
tests/test_gh_app_token.py(new, registered in ci-guardsrelease-notaryandtests/test-execution.toml) drives the wrapper against a fake gh, including the gh_budget_wait.sh interplay, and tests the uploaders' switching, concurrency, redirect and prune fallback.test_github_release_publication.pynow patches the opener.
Tests (on a cmux-ci Linux box): test_gh_app_token.py, test_github_release_publication.py, test_prune_nightly_release_assets.py and test_gh_budget_wait.py pass.
Risk: normal on its own (inert). #25534 (main's port of #25470's uploader backoff) is on the base now; the App-token switch runs first, and a rate-limited refusal after it backs off as before.
Agent session: agent session
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by CodeRabbit
- Improvements
- Release publishing now tries an application token first and can fall back to a regular token when the application token is refused.
- Publishing and asset downloads can retry after rate limits or permission refusals, helping release operations recover from temporary API issues.
- Authorization credentials are no longer sent when an asset download redirects to a different host.
- Nightly release asset cleanup now tries an application token first and falls back to the regular token when it is refused.