Repository navigation
ci(publish): back off a secondary rate limit, upload one at a time, log the refusal - #25470
Conversation
…og the refusal Run 40298250989 lost a built nightly to 18 upload POSTs refused "API rate limit exceeded for installation" with no x-ratelimit headers while the token's core budget had 4899 of 5000 left: GitHub's secondary limit on write bursts. publish-release-assets.py now: - honors retry-after / x-ratelimit-reset, else backs off 60, 120, 240, then 300 s per wait, within its budget (CMUX_RELEASE_RATE_LIMIT_SECONDS, default 600; the nightly publish sets 1800 and its job 120 min); - uploads one asset at a time with a 1 s pause between writes; - logs every response header and a /rate_limit snapshot on a refusal; - a rate-limited refusal no longer uses up one of the three attempts. gh_budget_wait.sh backs off the same way when no budget reads exhausted (6 tries, within the job's hour). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
|
|
Owned by an agent session · session |
|
Bead |
…he new publisher budget - The publisher adds its rate-limit waits to gh_budget_wait.sh's ledger and stops at the job's hour, so notes, assets, archive and gh waits in publish-nightly never add up past its 120 minutes. - republish-nightly-deltas runs the workflow commit's publisher (sparse tooling checkout) instead of the build's. - The source archive and auto-resume publish steps get the 1800 s budget. - release.yml build-sign-notarize: 90 -> 100 min for the publisher's 600 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
… ledger fails closed - republish-nightly-deltas checks out the workflow commit's tooling on main too (publisher, ring, guard, gh_budget_wait.sh), installs the wrapper from it, and gives its publish the 1800 s budget. - The publisher reserves each wait in the job ledger under its lock before sleeping, and counts its own waits too, so an unwritable ledger cannot lift the hour cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
…25484) Tick 40325957281 lost its promotion to "API rate limit exceeded for installation" at 12:53Z while /rate_limit showed thousands of calls left. The tick (which runs from main) now installs this workflow commit's scripts/ci/gh_budget_wait.sh (the same script feat-cmux-next's nightly uses, #25434/#25470): a refused gh call waits until the reset when a budget reads exhausted, else backs off 60, 120, 240, then 300 s, at most an hour per job. Its first refusal logs GitHub's message and the token's /rate_limit headers at that moment, to name the limiter. Timeout 15 -> 75. Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d-only The publisher counts the ledger as it was when it first waited plus its own waits, so a ledger that stops being writable cannot reopen the hour. The auto-resume workflow runs main's publisher (it checks out main), so its budget settings move to the main-side port instead of being ignored here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
|
|
|
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
…dger fails closed - asset_digest's authenticated download treats a rate-limited 403/429 like any other request (backoff within the budget, headers logged); other HTTP errors still fail at once. - A job ledger that exists but cannot be read or parsed counts as the hour spent, in the publisher and in gh_budget_wait.sh; a missing one is zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
…s are atomic An empty file (an interrupted write), a negative, fractional or non-finite value now counts as the hour spent. Both the publisher and gh_budget_wait.sh write the ledger to a temporary file and rename it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
|
Shipped in:
|
…sume's publish jobs (#25534) - scripts/ci/publish-release-assets.py and its tests from feat-cmux-next (#25470): Retry-After, the reset, else 60/120/240/300 s backoff in CMUX_RELEASE_RATE_LIMIT_SECONDS, serial uploads, refusal headers logged, one wait ledger per job shared with gh_budget_wait.sh. - scripts/ci/gh_budget_wait.sh: feat-cmux-next's ledger fixes (fail closed, reserve before sleeping, never wait unrecorded) with main's first-refusal logging (#25484) kept. - auto-resume-nightly-notarization.yml publish and republish-deltas install the wrapper and give the publisher 1800 s. - The secondary-limit and 429 wrapper tests assert the retry; ledger tests added. - ci-owned-pool-rescue.yml's header says the rescue grace is 40 minutes. Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lback (tooling, inert) Squash of ff82c6d, 35684a8 and cfd99b2 before rebasing onto feat-cmux-next (which gained #25470's publisher backoff). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3
…lback (tooling, inert) (#25490) * ci(publish): gh_app_token.sh and the uploaders' release-App-first fallback (tooling, inert) Squash of ff82c6d, 35684a8 and cfd99b2 before rebasing onto feat-cmux-next (which gained #25470's publisher backoff). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 * ci(publish): the wrapper's ledger fails closed and reserves before sleeping; finalize's asset download falls back - gh_app_token.sh reads the job ledger as gh_budget_wait.sh does: an unreadable or malformed ledger counts as the whole budget, and a wait is written (atomically) before the sleep; a wait it cannot record is not taken. - finalize-release.py download: a refused App token is tried once more with github.token (the client's shared switch), like the publisher's digest download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-85ca59f74f02d81734c3 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The release publisher no longer loses a built nightly to a 10-minute burst of refused uploads. It backs off GitHub's secondary rate limit within a 30-minute budget, uploads one asset at a time, and logs exactly which limit refused it.
Why. Run 40298250989 attempt 3 (nightly-next 52d22f3) had already built, signed and verified the nightly when 18 upload POSTs were refused with "API rate limit exceeded for installation", between 12:04:51Z and 12:14Z. The refused responses had no
retry-afterand nox-ratelimit-*headers. At the same moment, the janitor's/rate_limitread withgithub.tokenshowed core at 4899/5000 (12:05:11Z). So this is GitHub's secondary limit on write bursts, not the hourly budget: every workflow's GITHUB_TOKEN counts as one actor. The publisher waited a flat 60 s per refusal, hit its 600 s cap, and failed.What changes:
publish-release-assets.py:retry-afterandx-ratelimit-resetwhen present. Otherwise it backs off 60, 120 and 240 s, then 300 s per wait, inside its budget.CMUX_RELEASE_RATE_LIMIT_SECONDS, default 600. The nightly asset publish sets 1800, and publish-nightly's timeout goes from 90 to 120 min./rate_limitsnapshot (that read is free), so the next red names the resource. Snapshot calls can't recurse.gh_budget_wait.sh(ci(nightly): publish-path gh calls wait through a rate limit instead of failing #25434): when no budget reads exhausted, it backs off the same way (60, 120, 240, 300 s) instead of a flat 60 s. It now makes up to 6 tries, still within the job's one-hour cap.Ordering, verification, immutability and feed-last guarantees are unchanged. Tests:
tests/test_github_release_publication.py: 40 pass. New ones cover the backoff sequence, header and snapshot logging, and the env budget. The parallel-peak test now expects 1.tests/test_gh_budget_wait.py: 16 pass, including the backoff sequence with a fakesleep.Risk: normal. Serial uploads make a publish a little slower (about a dozen assets, each a few seconds to a minute), in exchange for not tripping the secondary limit ourselves.
Agent session: agent session
🤖 Generated with Claude Code