Skip to content

ci(publish): back off a secondary rate limit, upload one at a time, log the refusal - #25470

Merged
teamleaderleo merged 9 commits into
feat-cmux-nextfrom
gt-quota/publish-rate-limit-wait
Oct 26, 2026
Merged

teamleaderleo merged 9 commits into
feat-cmux-nextfrom
gt-quota/publish-rate-limit-wait

Conversation

@teamleaderleo

Copy link
Copy Markdown
Collaborator

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-after and no x-ratelimit-* headers. At the same moment, the janitor's /rate_limit read with github.token showed 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:
    • It honors retry-after and x-ratelimit-reset when present. Otherwise it backs off 60, 120 and 240 s, then 300 s per wait, inside its budget.
    • The budget comes from CMUX_RELEASE_RATE_LIMIT_SECONDS, default 600. The nightly asset publish sets 1800, and publish-nightly's timeout goes from 90 to 120 min.
    • Uploads run one at a time instead of 2 in parallel, with a 1 s pause between writes.
    • On every refusal it logs all response headers plus a /rate_limit snapshot (that read is free), so the next red names the resource. Snapshot calls can't recurse.
    • A rate-limited refusal no longer uses up one of the 3 transport attempts; the budget bounds it instead.
  • 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 fake sleep.
  • Also passing: the nightly resolve, coverage, concurrency, recovery and ring tests, and the universal-build script.

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

…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
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 71bd413: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr25470 -f ref=gt-quota/publish-rate-limit-wait -f platform=macos -f backend_mode=local

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-3vqxy created for this PR by the cmux coordinator.

…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
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next fast gates / post cmux-next/fast-gates is red on 12 of 16 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

high-signal review link / link is red on 10 of 16 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 5 commits October 26, 2026 07:12
…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
@teamleaderleo
teamleaderleo merged commit 61ea34b into feat-cmux-next Oct 26, 2026
133 of 136 checks passed
@teamleaderleo
teamleaderleo deleted the gt-quota/publish-rate-limit-wait branch October 26, 2026 15:08
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…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>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…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
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant