Uh oh!
There was an error while loading. Please reload this page.
fix(kanban): fail closed in the three mutators that exit 0 on failure (backend#1412) - #122
Merged
Merged
Conversation
Four call sites counted their failures, printed them, and exited 0. A
green run therefore meant "the workflow finished", not "the board is
correct" -- and because every one of these is a scheduled daily pass,
a persistent cause (revoked project write, deleted Status option,
token scope loss) failed identically every day behind an unbroken
green history. Ports the RUN_FAILED pattern already used in
advance-deploy-env.yml rather than inventing a second one.
What fails closed now that did not before:
- kanban-archive.yml "Archive them": a failed archiveProjectV2Item
left terminal work on the board -- the one thing the workflow
exists to prevent -- and reported success. Now asserts fail -eq 0
after the loop. The error text was also destroyed by
`> /dev/null 2>&1`; stderr is now captured and printed with the
warning. Step summary is still written before the exit.
- kanban-reconcile.yml "Apply moves": same shape, both the ARCHIVE
and the Status-update branch. Every [FAIL] was a card left in the
column the pass had just decided was wrong. Now asserts fail -eq 0
and captures stderr into the [FAIL] line.
- kanban-reconcile.yml membership add loop: [FAIL] ("could not add")
and [WARN] ("Status set failed") were printed and never counted, so
the step could fail on every item and still go green. Both now
increment add_fail and the step exits non-zero. [WARN] counts as a
failure because a Status-less card renders in no column and no pass
repairs it: classify reads null Status as "No status" -> no-action,
and the sweep skips it because it is now on the board.
- kanban-reconcile.yml scope discovery: the add-to-kanban.yml probe
used gh's exit code only, making 404 ("not tracked") identical to
403/429/5xx ("could not determine"), so a repo silently dropped out
of scope and its drift became invisible. Now reads the HTTP status
and fails on anything that is neither 2xx nor 404. Adds a MIN_SCOPE
floor because the per-repo sweep emits "Missing from board: 0"
identically whether nothing is missing or nothing was examined --
MAX_ADDS only caught the opposite failure.
Success paths are unchanged; this is failure handling only. Verified
with actionlint 1.7.12 + shellcheck 0.11.0 (0 findings) and by
simulating each failure mode against stubbed gh calls on bash 5.
Refs tracebloc/backend#1412
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
tracebloc/backend#1412. Ports the fail-closed pattern fromadvance-deploy-env.yml(RUN_FAILEDset at the failure sites, asserted after the loop, stderr captured rather than discarded) into the four call sites that counted failures, printed them, and exited 0.The shared failure mode: a green run meant "the workflow finished", not "the board is correct". All of these are scheduled daily passes, so a persistent cause — revoked project write, a deleted Status option, a token that lost a scope — failed identically every day behind an unbroken green history. Nothing here is a new capability; in three of the four cases the counters already existed and only the assertion was missing.
What changed, per file
.github/workflows/kanban-archive.yml— "Archive them" (:103-136)A failed
archiveProjectV2Itemleft terminal work on the board, which is the single thing this workflow exists to prevent, and reported success. At ~800 items/month reaching Prod the board would degrade slowly enough to look like normal growth.fail -eq 0after the loop.> /dev/null 2>&1destroyed the API error entirely. Now2>&1 >/dev/null, so stderr is kept and the payload (never read) is dropped. The warning carries the actual error..github/workflows/kanban-reconcile.yml— "Apply moves" (:346-400)Same shape, in both the
ARCHIVEand the Status-update branch. Every[FAIL]was a card left in the column this pass had just decided was wrong — i.e. the drift the run existed to remove survived it, and tomorrow's pass re-planned the identical move.fail -eq 0; stderr captured into the[FAIL]line..github/workflows/kanban-reconcile.yml— membership add loop (:592-651)[FAIL]("could not add") and[WARN]("Status set failed") were printed and never counted — no counter existed at all, so this step could fail on every single item and still go green.add_fail; the step exits non-zero.[WARN]is counted as a failure, not tolerated, and is the worse of the two: a Status-less card renders in no column and is invisible in every column view. Confirmed nothing repairs it — the classify pass reads a null Status as"No status"and routes that to no-action for both PRs (:262) and issues (:307), and this sweep skips the card because it now is on the board. A[FAIL]card at least stays missing and gets rediscovered tomorrow..github/workflows/kanban-reconcile.yml— scope discovery (:426-487)The
add-to-kanban.ymlprobe used gh's exit code only, so 404 ("this repo does not opt into the board" — the answer we want) was indistinguishable from 403 / 429 / 5xx ("we could not find out"). Both silently dropped the repo fromSCOPE, and a repo out of scope is never swept, so its drift became invisible while the run reported success.gh api -i --silent(headers only, no base64 body) and fails on anything that is neither 2xx nor 404. A transport failure yields no status line and lands in the fatal branch — correctly, since that is also "we could not find out".MIN_SCOPE: 1floor assertion. The per-repo sweep emits "Missing from board: 0" identically whether nothing is missing or nothing was examined; the existingMAX_ADDSceiling only catches the opposite failure (everything looks missing). The floor is deliberately loose — a "did we look at anything at all" tripwire, not a coverage target, so it never fires when a repo legitimately opts out.How to verify
Lint —
actionlint1.7.12 +shellcheck0.11.0, the exact versions pinned inactionlint.yml, run with the same invocation as CI: 0 findings, exit 0 over the whole tree.Behaviour — each failure mode was simulated against stubbed
ghcalls on bash 5 (matching the ubuntu runner; macOS bash 3.2 lacksmapfile). Reviewers can reproduce by stubbing agh()shell function:mapfileon empty input declares an empty array, so the floor is what fires rather than anunbound variableerror underset -u.{"data":null}) correctly not in the message.added.tsvunchanged; Status-set failure → rc=1; add failure → rc=1.Downstream steps still render. Checked that the new exits do not suppress reporting: "Per-label summary" is
always(); "Discover + add items missing from the board" isalways() && steps.ids.outcome == 'success', so the newApply movesexit does not stop membership reconcile (preserving that step's documented intent); "Membership summary" readssteps.membership.outputs.count, written at:550, well before the new assertion at:645.Success paths are unchanged — this is a failure-handling change only. No refactors, no reformatting.
Notes for the reviewer
fr-pass-comment.ymlandadvance-deploy-env.yml/fr-gate.ymlare deliberately untouched.advance-deploy-env.ymlwas read first and its pattern ported, not reinvented; the new assertions reference it by name in their comments.Refs
tracebloc/backend#1412🤖 Generated with Claude Code
Note
Low Risk
CI-only workflow shell changes; success paths unchanged and failures become visible rather than altering happy-path behavior.
Overview
Scheduled kanban workflows no longer report success when GraphQL mutations or scope discovery fail silently. The change ports the fail-closed pattern from
advance-deploy-env.yml: capture API stderr with2>&1 >/dev/null, surface it in warnings/[FAIL]lines, andexit 1when any counted failure remains.In
kanban-archive.yml, failedarchiveProjectV2Itemcalls now fail the step after the step summary is written, so terminal cards cannot pile up behind a green daily run.In
kanban-reconcile.yml, Apply moves exits non-zero if any archive or Status update fails. Membership scope discovery treats only HTTP 404 as “not on board”; other statuses abort instead of dropping repos from the sweep, andMIN_SCOPE: 1blocks a “clean” run over an empty repo list. The add loop tracksadd_failfor both[FAIL]adds and[WARN]Status-set failures (Status-less cards are treated as hard failures).Reviewed by Cursor Bugbot for commit d1d4439. Bugbot is set up for automated code reviews on this repo. Configure here.