Problem
The Canary Rollout gate (scripts/canary-rollout.sh, _run_json, ~L378-403)
fetches per-workflow run history with gh run list --repo <r> --workflow <wf> in a
tight per-agent × per-tier loop. Under the fleet-wide burst these calls hit
transient failures ("_run_json: failed to fetch run list … after 3 attempts",
~7/run across both repos), which yield empty gates (gate= for ring '' [] (triage=)) → agents don't promote. This is the actual blocker on dev-lead's
next→ring0 promotion (its gate shows cum_fail=0, triage=- — no health data, NOT
a real regression). It also breaks sync-issues (regression auto-tracking:
issue sync failed (non-fatal)), so real regressions like feature-ideation's go
un-tracked.
Root cause is almost certainly secondary rate-limiting from the per-workflow
call volume — but we can't confirm because _run_json swallows the underlying
error (the retry wrapper prints only "transient failure", never the HTTP
status/Retry-After). That observability gap must be closed first.
Scope (all in scripts/canary-rollout.sh)
- Surface the real error (do first). In
_run_json, capture gh's stderr and
include the HTTP status / message in the ::warning::/::error:: lines
(distinguish 403 secondary-rate-limit vs 5xx vs auth). No more blind "transient
failure". Keep fail-closed: a persistent failure must remain a hard error, never
an empty-but-green gate.
- Backoff. Replace the fixed
CANARY_GH_RETRY_SLEEP linear retry with
exponential backoff + jitter; honor Retry-After / x-ratelimit-reset when
present; raise default attempts. (Keep the CANARY_GH_RETRIES /
CANARY_GH_RETRY_SLEEP env overrides.)
- Cut call volume (the real fix). Replace per-workflow
gh run list
enumeration with far fewer calls — e.g. one paginated gh run list --repo <repo>
(or a single GraphQL query) per repo, filtered to the registered workflows
locally. This removes the burst that triggers the secondary limit.
- Make
sync-issues resilient. A run-history fetch failure must not silently
drop the REGRESSION→needs-human issue upsert; on partial data, still upsert what
it can and annotate the gap (so regressions are never silently un-tracked).
Acceptance criteria
- A forced fetch failure prints the real HTTP status/reason, not just "transient".
- Backoff is exponential+jittered;
Retry-After honored.
- Per-evaluation
gh run list call count is materially reduced (state the before/after in the PR).
sync-issues still opens/updates the regression issue when some fetches fail.
- Fails closed: no empty-data gate is ever reported as promotable.
- Existing bats for canary-rollout stay green; add a case for
_run_json error surfacing.
Problem
The Canary Rollout gate (
scripts/canary-rollout.sh,_run_json, ~L378-403)fetches per-workflow run history with
gh run list --repo <r> --workflow <wf>in atight per-agent × per-tier loop. Under the fleet-wide burst these calls hit
transient failures ("
_run_json: failed to fetch run list … after 3 attempts",~7/run across both repos), which yield empty gates (
gate= for ring '' [] (triage=)) → agents don't promote. This is the actual blocker on dev-lead'snext→ring0promotion (its gate showscum_fail=0, triage=-— no health data, NOTa real regression). It also breaks
sync-issues(regression auto-tracking:issue sync failed (non-fatal)), so real regressions like feature-ideation's goun-tracked.
Root cause is almost certainly secondary rate-limiting from the per-workflow
call volume — but we can't confirm because
_run_jsonswallows the underlyingerror (the retry wrapper prints only "transient failure", never the HTTP
status/
Retry-After). That observability gap must be closed first.Scope (all in
scripts/canary-rollout.sh)_run_json, capturegh's stderr andinclude the HTTP status / message in the
::warning::/::error::lines(distinguish 403 secondary-rate-limit vs 5xx vs auth). No more blind "transient
failure". Keep fail-closed: a persistent failure must remain a hard error, never
an empty-but-green gate.
CANARY_GH_RETRY_SLEEPlinear retry withexponential backoff + jitter; honor
Retry-After/x-ratelimit-resetwhenpresent; raise default attempts. (Keep the
CANARY_GH_RETRIES/CANARY_GH_RETRY_SLEEPenv overrides.)gh run listenumeration with far fewer calls — e.g. one paginated
gh run list --repo <repo>(or a single GraphQL query) per repo, filtered to the registered workflows
locally. This removes the burst that triggers the secondary limit.
sync-issuesresilient. A run-history fetch failure must not silentlydrop the REGRESSION→needs-human issue upsert; on partial data, still upsert what
it can and annotate the gap (so regressions are never silently un-tracked).
Acceptance criteria
Retry-Afterhonored.gh run listcall count is materially reduced (state the before/after in the PR).sync-issuesstill opens/updates the regression issue when some fetches fail._run_jsonerror surfacing.