ci(nightly-next): publish-path gh calls go out as the release App first, github.token as fallback

Merged
TE
10d ago
+279-2

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 runs gh_app_token.sh --install and then #25434's gh_budget_wait.sh --install. The budget wrapper therefore wraps this one and keeps its wait.
  • generate-nightly-deltas and publish-release-notes take the wrapper from a scripts-only sparse checkout at github.sha (.publish-tooling, persist-credentials: false, the shape test_nightly_cmux_tui_resolve.py exempts from the build-commit pin), because their build checkout may predate it. republish-nightly-deltas adds gh_app_token.sh to its existing .publish-tooling checkout, 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's GH_TOKEN is already the release App's release-tag-token with Workflows write, and it stays as is.

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.yml
    • secrets.CMUX_RELEASE_APP_ID / secrets.CMUX_RELEASE_APP_KEY (both already exist in release and release-next; no new secret names) are now read by a new mint step in these jobs:
      • recover-nightly-next-notarization: token contents: read
      • publish-nightly: contents: write
      • publish-nightly-next-github: contents: write
      • publish-nightly-next-github-names: contents: write
      • keep-published-dsyms: contents: read
      • legacy-identity-target: contents: read
      • sign-nightly-identity-legacy: contents: read
      • publish-nightly-identity-legacy: contents: write
      • generate-nightly-deltas: contents: read
      • republish-nightly-deltas: contents: write
      • publish-release-notes: contents: read
    • Each token is scoped by permission-contents alone, matching that job's existing contents: level, and never exceeds the job's own GITHUB_TOKEN contents level.
    • environment: gains release-next on legacy-identity-target, which had none before. It needs the App credentials, and the job already runs only on refs/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 runs tests/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.py now 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