ci(nightly-next): publish-path gh calls go out as the release App first, github.token as fallback
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 feat-cmux-next after #25490 landed. It now also wires #25656's publish-nightly-next-github jobs. Size: GitHub refused nightly.yml at 518,322 bytes before any job started (push runs 40422918338 and 40424952395, 0 jobs). Comment-padded copies of base found the cutoff: 512,100 bytes failed at startup and 511,900 did not. #25849 (YAML anchors) and #25879 (run blocks into scripts) shrank base's file, and this PR's four App-token steps (mint read, mint write, install from the checkout, install from .publish-tooling) are anchors reused by alias. At this head the file is 480,853 bytes, and tests/test_nightly_workflow_size.py keeps it under the limit. Codex gpt-6-astra xhigh: SHIP at the final head.
Part 2 of 2: the workflow wiring. The cmux NEXT nightly publish path (nightly.yml on nightly-next, which runs feat-cmux-next's copy) now sends its gh calls as the release GitHub App first, and uses github.token only as the fallback. The tooling (scripts/ci/gh_app_token.sh and the uploaders' fallback) is part 1, #25490. #25490 has landed; this PR is on feat-cmux-next. The classic NIGHTLY, DEV and promote publishers run main's copies; those are #25491 and #25479.
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 403 responses carried no x-ratelimit-* headers. So the hourly core budget was not exhausted. 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, so a burst anywhere refuses the publish. The release App is a separate actor with its own limits.
What changed
- Each job below mints a contents-only App token with
actions/create-github-app-token(continue-on-error, 2 min), then runsgh_app_token.sh --installand then #25434'sgh_budget_wait.sh --install. The budget wrapper therefore wraps this one and keeps its wait. generate-nightly-deltasandpublish-release-notestake the wrapper from a scripts-only sparse checkout atgithub.sha(.publish-tooling,persist-credentials: false, the shapetest_nightly_cmux_tui_resolve.pyexempts from the build-commit pin), because their build checkout may predate it.republish-nightly-deltasaddsgh_app_token.shto its existing.publish-toolingcheckout, which already runs the uploader. The other jobs already check out the workflow commit.- These steps get
GH_APP_TOKEN:- the uploader steps (
publish-release-assets.py) - prune (
prune_nightly_release_assets.py) - finalize (
finalize-release.py), only off nightly-next. On nightly-next, finalize'sGH_TOKENis already the release App'srelease-tag-tokenwith Workflows write, and it stays as is.
- the uploader steps (
Fallback. A failed mint installs nothing, and the job runs exactly as before on github.token with #25434's wait. If both tokens are refused, the call backs off and retries both: Retry-After when the output shows one, otherwise 60 s, 120 s, 240 s. This happens in #25434's per-job ledger, so the two wrappers together still wait at most GH_BUDGET_WAIT_MAX_SECONDS (3600). A 403 with no headers backs off; it does not fail fast. Only "Resource not accessible" (a permission refusal) does not wait. #25434's wait for a core reset still runs around this one, and --reserve is unchanged. The uploader's no-header backoff is left to #25470.
Permissions / environment / secrets diff (file by file)
.github/workflows/nightly.ymlsecrets.CMUX_RELEASE_APP_ID/secrets.CMUX_RELEASE_APP_KEY(both already exist inreleaseandrelease-next; no new secret names) are now read by a new mint step in these jobs:recover-nightly-next-notarization: tokencontents: readpublish-nightly:contents: writepublish-nightly-next-github:contents: writepublish-nightly-next-github-names:contents: writekeep-published-dsyms:contents: readlegacy-identity-target:contents: readsign-nightly-identity-legacy:contents: readpublish-nightly-identity-legacy:contents: writegenerate-nightly-deltas:contents: readrepublish-nightly-deltas:contents: writepublish-release-notes:contents: read
- Each token is scoped by
permission-contentsalone, matching that job's existingcontents:level, and never exceeds the job's own GITHUB_TOKENcontentslevel. environment:gainsrelease-nextonlegacy-identity-target, which had none before. It needs the App credentials, and the job already runs only onrefs/heads/nightly-next, release-next's one deployment branch. The environment has no reviewers, so nothing waits.tests/test_nightly_sign_runner.py(#25417) allows exactly this one.- No job's
permissions:block changes.
.github/workflows/ci-guards.yml: a new step runstests/test_publish_app_token_workflow.py. No permission changes.- Other files:
.github/nightly-step-coverage.json(the 18 new steps; the wrapper checkouts are derived plumbing),tests/test-execution.toml.tests/test_gh_budget_wait.pynow allows the App install before its own.
Tests (on a cmux-ci Linux box): tests/test_publish_app_token_workflow.py (new, in ci-guards; asserts every publish job mints the App token from the existing secrets, installs it from the workflow commit before the budget wait, and keeps github.token as the fallback), test_gh_app_token.py, test_gh_budget_wait.py, test_github_release_publication.py, test_nightly_step_coverage.py, test_nightly_sign_runner.py and test_nightly_next_ring.py all pass. Six other workflow tests (test_ci_change_areas, test_ci_fork_runner_routing, test_ci_macos_xcode_selection, test_ci_workflow_path_filter_parity, test_check_ghostty_zig_workflows, test_ci_signing_keychain…) fail on files this PR doesn't touch (cmux-next.yml, cmux-next-perf-budgets.yml, a missing bashlex).
Not covered here: build-nightly-app's daemon attestation. actions/attest-build-provenance needs attestations: write and id-token, which the release App does not have. It keeps #25434's reserve-and-retry.
Risk: high (secrets reach the App token in 11 jobs, plus one new environment binding). Any job whose mint fails behaves exactly as it does today.
Agent session: agent session
🤖 Generated with Claude Code