Skip to content

Harden canary-rollout _run_json: surface real error + backoff + cut call volume (fixes empty gates blocking promotion) #803

Description

@don-petry

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)

  1. 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.
  2. 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.)
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions