You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[#811 re-split 2/2] canary-rollout: fail-closed sync-issues (never drop regression tracking on partial data) #820
Re-split of #811 (2/2 of #803). This is the sync-issues resilience half only — small, independent of the fetch refactor (#818).
Problem
sync-issues fetches run history through the same path; on a fetch failure the whole step fails "non-fatal" and NO regression issue is opened (e.g. feature-ideation's REGRESSION went untracked). The safety net is defeated by exactly the failures it should record.
Scope — scripts/canary-rollout.shsync-issues
On partial/failed run-history data, still upsert the tracked issue for each REGRESSION/BLOCKED agent it CAN determine, and annotate the data gap in the issue/summary — never silently skip a regression. Fail closed (a total inability to determine state stays a hard error, not a green no-op).
Acceptance criteria
A forced partial-fetch still opens/updates the REGRESSION→needs-human issue for the affected agent, with the gap annotated.
No regression is silently un-tracked.
Existing canary-rollout bats stay green; add a partial-data sync-issues case.
Issue:#820 — [#811 re-split 2/2] canary-rollout: fail-closed sync-issues (never drop regression tracking on partial data)
Scope
Make scripts/canary-rollout.shsync-issues resilient to a run-history fetch failure: instead of aborting to empty state (which silently drops — or worse, auto-closes — a regression's tracked issue), fail closed to a BLOCKED tracked issue with the data gap annotated, and treat a total inability to determine state as a hard error rather than a green no-op.
Root cause
cmd_sync_issues computes each agent's state via _frontier_state, which reads run history through _run_json. On a sustained fetch failure _run_json returns non-zero (fail-closed, #738); under set -euo pipefail that aborts _frontier_state to empty output. The unguarded read then either aborts the whole step or leaves state="", so the agent falls into the "not BLOCKED" branch — closing/skipping its blocker issue. The safety net is defeated by exactly the failures it should record.
Implementation Checklist
Add _frontier_state_resilient <agent> wrapper (sync-issues-only) in scripts/canary-rollout.sh: on a fetch-driven data gap, reconstruct candidate/frontier/transition/reusable-differs→triage from tag+blob resolution (no run history), fail closed to BLOCKED, and emit a trailing datagap flag. Return non-zero ONLY when even candidate/frontier is unresolvable (total inability → hard error).
Rewire cmd_sync_issues to read from the wrapper (guarded), track a hard_fail flag, annotate the data gap in the blocker title, and return 1 on total inability (still rendering the fleet dashboard first).
Thread a data_gap arg into _blocker_body: banner note + "run-history unavailable" cumulative-failures cell + gap-aware evidence.
Tests to Write
sync-issues under a forced partial fetch (gh run list fails every retry, tag/blob resolution intact) with a changed reusable still OPENS a canary-blocker + needs-human issue, triage REGRESSION, body annotates the gap — no silent skip, no auto-close.
sync-issues total inability (all gh unresolvable) fails closed: non-zero exit + ::error::, no green no-op.
Added _frontier_state_resilient wrapper — fails closed to a BLOCKED+datagap line on a run-history outage; returns non-zero only on a total inability to resolve candidate/frontier.
Rewired cmd_sync_issues to read from the wrapper (guarded), track hard_fail, annotate the gap in the blocker title, and return 1 (after rendering the dashboard) on total inability.
Threaded a data_gap arg into _blocker_body (PARTIAL DATA banner + "run history unavailable" cumulative-failures cell + gap-aware evidence).
Wired the fetch-failure signal in _run_json (file flag) — no-op for every non-sync-issues read path.
Root cause (corrected during TDD)
A sustained gh run list failure does not cleanly abort _frontier_state — command substitutions don't inherit errexit, so it limped on and emitted a misleadingly clean state line. sync-issues then read that agent as "not BLOCKED" and skipped/auto-closed its regression issue. The fix detects the fetch failure via a file flag _run_json writes, then discards the bogus state and fails closed.
Behaviour now
Partial fetch (run history down, tags/blobs OK) + changed reusable → opens/updates a canary-blocker issue, triage REGRESSION, labels dev-lead + needs-human, body carries a PARTIAL DATA banner. No silent skip, no auto-close.
Byte-identical reusable under a gap → still tracked as BLOCKED (PRE_EXISTING, report-only) — promotion held until data is readable.
Total inability (candidate/frontier unresolvable) → ::error:: + non-zero exit, never a green no-op. Dashboard still renders first.
Test Results
$ npx bats tests/canary_rollout.bats
244 passing, 0 failures (239 pre-existing + 5 new #820 cases)
$ bash .dev-lead/scripts/dev-lead-lint.sh
[lint] shellcheck on 27 file(s)... OK
[lint] all checks passed
New tests (tests/canary_rollout.bats):
partial-fetch → REGRESSION needs-human blocker opened, gap annotated
partial-fetch UPDATE keeps an existing blocker open (no auto-close)
wrapper passes a normal state line through with datagap=0
wrapper fails closed (non-zero) on total inability
sync-issues ends non-zero when an agent is totally undeterminable
Files Changed
scripts/canary-rollout.sh: _run_json fetch-failure flag; new _frontier_state_resilient; cmd_sync_issues resilient read + fail-closed exit + gap annotation; _blocker_bodydata_gap arg.
Activity
don-petry commentedon Jul 20, 2026
Dev-Lead Implementation Plan
Issue: #820 — [#811 re-split 2/2] canary-rollout: fail-closed sync-issues (never drop regression tracking on partial data)
Scope
Make
scripts/canary-rollout.shsync-issuesresilient to a run-history fetch failure: instead of aborting to empty state (which silently drops — or worse, auto-closes — a regression's tracked issue), fail closed to a BLOCKED tracked issue with the data gap annotated, and treat a total inability to determine state as a hard error rather than a green no-op.Root cause
cmd_sync_issuescomputes each agent's state via_frontier_state, which reads run history through_run_json. On a sustained fetch failure_run_jsonreturns non-zero (fail-closed, #738); underset -euo pipefailthat aborts_frontier_stateto empty output. The unguardedreadthen either aborts the whole step or leavesstate="", so the agent falls into the "not BLOCKED" branch — closing/skipping its blocker issue. The safety net is defeated by exactly the failures it should record.Implementation Checklist
_frontier_state_resilient <agent>wrapper (sync-issues-only) inscripts/canary-rollout.sh: on a fetch-driven data gap, reconstruct candidate/frontier/transition/reusable-differs→triage from tag+blob resolution (no run history), fail closed toBLOCKED, and emit a trailingdatagapflag. Return non-zero ONLY when even candidate/frontier is unresolvable (total inability → hard error).cmd_sync_issuesto read from the wrapper (guarded), track ahard_failflag, annotate the data gap in the blocker title, andreturn 1on total inability (still rendering the fleet dashboard first).data_gaparg into_blocker_body: banner note + "run-history unavailable" cumulative-failures cell + gap-aware evidence.Tests to Write
sync-issuesunder a forced partial fetch (gh run listfails every retry, tag/blob resolution intact) with a changed reusable still OPENS acanary-blocker+needs-humanissue, triage REGRESSION, body annotates the gap — no silent skip, no auto-close.sync-issuestotal inability (allghunresolvable) fails closed: non-zero exit +::error::, no green no-op.canary_rollout.batssuite stays green.Test Command
npx bats tests/canary_rollout.batsdon-petry commentedon Jul 20, 2026
Dev-Lead: Implementation Complete
Plan Execution
_frontier_state_resilientwrapper — fails closed to a BLOCKED+datagapline on a run-history outage; returns non-zero only on a total inability to resolve candidate/frontier.cmd_sync_issuesto read from the wrapper (guarded), trackhard_fail, annotate the gap in the blocker title, andreturn 1(after rendering the dashboard) on total inability.data_gaparg into_blocker_body(PARTIAL DATA banner + "run history unavailable" cumulative-failures cell + gap-aware evidence)._run_json(file flag) — no-op for every non-sync-issues read path.Root cause (corrected during TDD)
A sustained
gh run listfailure does not cleanly abort_frontier_state— command substitutions don't inheriterrexit, so it limped on and emitted a misleadingly clean state line.sync-issuesthen read that agent as "not BLOCKED" and skipped/auto-closed its regression issue. The fix detects the fetch failure via a file flag_run_jsonwrites, then discards the bogus state and fails closed.Behaviour now
canary-blockerissue, triage REGRESSION, labelsdev-lead+needs-human, body carries a PARTIAL DATA banner. No silent skip, no auto-close.::error::+ non-zero exit, never a green no-op. Dashboard still renders first.Test Results
New tests (
tests/canary_rollout.bats):needs-humanblocker opened, gap annotateddatagap=0sync-issuesends non-zero when an agent is totally undeterminableFiles Changed
scripts/canary-rollout.sh:_run_jsonfetch-failure flag; new_frontier_state_resilient;cmd_sync_issuesresilient read + fail-closed exit + gap annotation;_blocker_bodydata_gaparg.tests/canary_rollout.bats:_sync_gap_stub+ 5 resilience cases.Notes
sync-issuesper the issue (independent of the feat: implement issue #817 — Clean up upsert output before fleet apply: conditional negation tail + migrated-comment dedup #818 fetch refactor). The REGRESSION note's "a run failed" wording is superseded by the PARTIAL-DATA banner, which states counts are unreliable and triage rests on the reusable diff alone.feat: implement issue #820 — {"message": "No server is currently avai…
fix(canary): cache run-list per (repo,workflow) + not-found→empty (#819…
feat: implement issue #820 — {"message": "No server is currently avai…
feat: implement issue #820 — {"message": "No server is currently avai…