feat(kanban): assert the board has every Status column the writers emit - #247
Merged
Conversation
The board's column names and the workflows that write them are two systems that must agree, and nothing checked that they did. #1592 carried the `FR on staging` rename by hand across three PRs; in between, the writers emitted a column the board did not have and only a fallback made it work. That gap is exactly what .github#243 reported and what #245 closed by hand. This makes it un-driftable instead of remembered. The failure it prevents is a silent one. A written name that does not resolve means the card is not moved -- and until #246 that was a `::warning::` on a green run, so a frozen board and a working board looked identical. SCOPE IS DELIBERATELY NARROW: * WRITERS are asserted. A name that is written must exist or the write cannot land. `advance-deploy-env.yml` and `kanban-closure-router.yml`, listed rather than globbed -- `STATUS=` is a common shell variable and other workflows use it for "ok"/"absent"/"unreadable", which are not column names. * RESOLVERS are NOT asserted. They ask for names on both sides of a rename on purpose: `opt_either("Staging (human review)", "FR on staging")` is the #1592 shim and its first argument is SUPPOSED to be absent today. Enforcing there would make the tolerance impossible to express. * `STATUS_NAME="$OVERRIDE"` is skipped: a per-repo .kanban.yml value cannot be known here. Runs on PRs touching the writers, and DAILY -- the board can be renamed in the UI at any time with no PR to hang a check on, which is the failure this is really for. Fails closed throughout: an unreadable board, an unparseable response, zero options returned, or a literal pattern that matches nothing are all errors. The last one matters most -- a stale regex would make the check pass vacuously, which is the failure mode of every guard it is modelled on. Verified: passes on today's board (5 names); reproducing the pre-#245 state (writer emitting "Staging (human review)") fails and names the exact file and value; selftest 6/6 with the board stubbed, covering absent names, a stale name among good ones, and a case near-miss.
LukasWodka
requested review from
saadqbal
and removed request for
shujaatTracebloc
August 13, 2026 18:46
Bugbot (Medium): the check job ran in parallel with selftest and had no needs: selftest. If the checker failure paths regress while the board still matches, "Selftest the checker" goes red but "Written Status names exist on the board" can still report success -- a conformance-named green from a broken guard. caller-drift + standards-sync gate their audit on selftest for the same reason. Add needs: selftest so the conformance verdict is only trusted once the checker is proven able to fail. .github#247. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8b381f9. Configure here.
LukasWodka
added a commit
that referenced
this pull request
Aug 13, 2026
#248) * fix(kanban): the column check missed three writers, and could not tell Two Bugbot findings on the staging promotion PR #243, both on code I added today. MEDIUM — the conformance check from #247 was itself incomplete, which is the exact defect it exists to prevent. `WRITERS` named two files and `LITERAL` matched one idiom, so it never saw: set-pr-status.yml `echo "status_name=In progress"` (UNQUOTED value) fr-pass-comment.yml `NEXT="Ready for prod"` Three column names -- In progress, Code review, Ready for prod -- were invisible to a guard built to make a rename impossible to miss. It reported a clean sweep of a subset, which reads exactly like a clean sweep of everything. Widening the hand-list alone would rot the same way, so the names are now derived a SECOND, independent way and the two are compared: any board column name ASSIGNED in a writer file that no idiom matched means the idiom list is stale, and that is an error rather than a smaller answer. Assignment sites only, whole values -- so comments, the rank `case` arms and prose are ignored, and `Ready` does not match inside `Ready for prod`. It earned that immediately: the cross-check caught my own first attempt at the widened regex, which required a trailing quote and silently stopped matching `STATUS_NAME="FR on staging" ;;`. Without it I would have shipped a check that found fewer names than before while printing "all names exist". Coverage went 5 names / 2 files -> 8 names / 4 files. LOW — the abort message added in #246 said "The Deploy environment write may already have happened". It cannot have: that check runs at :190 and the first `update_field` is at :314, so on that path nothing was written at all. Corrected to say so, since the point of the message is to tell an operator where they stand. Selftest 8/8 (adds: an assigned-but-unmatched name is reported; a comment mentioning a column is not). caller-drift selftest 162/0. ruff + actionlint clean. * fix(kanban): the PR trigger must cover every writer, and now cannot drift Bugbot Medium on this PR. I broadened `WRITERS` to four workflows and left `kanban-columns.yml`'s `paths:` filter listing the original two — so a PR touching only `set-pr-status.yml` or `fr-pass-comment.yml` never ran this check and a bad column write could merge until the next cron. Same shape as the defect this PR is already fixing: the guard is present, reports green, and is not watching the thing it names. Two files apart this time instead of two variables. The filter now lists all four. But listing them is what went wrong once already, so the selftest DERIVES the requirement: every entry in WRITERS must appear in the workflow's `paths:` block, read out of the YAML rather than restated. Adding a fifth writer without wiring its trigger is now a red test, not a silent gap. Verified by removing `set-pr-status.yml` from the filter: the assertion fails. Selftest 10/10 with it restored. ruff + actionlint clean. This is the third instance today of "fixed the code, left the trigger config" -- e2e#81 was the same bug in a tsc dry-run paths filter. Recorded in the message because the pattern is worth more than the fix. * style(selftest): split a combined import (ruff E401) It was in the block I added two commits ago and I claimed ruff clean without reading the exit code -- the `&&` swallowed it. Checked properly this time.
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 free
to 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.
Follow-up to #243/#245/#246, and the third item of the plan agreed with Lukas: leave the board alone, keep the abort, make the drift impossible.
Why
The board’s column names and the workflows that write them are two systems that must agree, and nothing checked that they did. backend#1592 carried the
FR on stagingrename by hand across three PRs; in between, the writers emitted a column the board did not have and only a fallback made it work.The failure it prevents is silent: a written name that does not resolve means the card is not moved — and until #246 that was a
::warning::on a green run. A frozen board and a working board looked identical.Scope, deliberately narrow
advance-deploy-env.yml+kanban-closure-router.yml, listed rather than globbed —STATUS=is a common shell variable and other workflows use it forok/absent/unreadable, which are not column names.opt_either("Staging (human review)", "FR on staging")is the #1592 shim and its first argument is supposed to be absent today. Enforcing there would make that tolerance impossible to express.STATUS_NAME="$OVERRIDE"is skipped — a per-repo.kanban.ymlvalue cannot be known here.Runs daily, not just on PRs
The board can be renamed in the UI at any time, with no PR to hang a check on. That is the failure this is really for.
Fails closed
Unreadable board, unparseable response, zero options returned, or a literal pattern matching nothing — all errors. The last matters most: a stale regex would make the check pass vacuously, which is the failure mode of every guard this is modelled on.
Verified
Staging (human review)) fails, naming the exact file and valueNote
Low Risk
Read-only conformance checks and new CI scripts; no runtime or auth logic changes beyond using an existing org token in Actions.
Overview
Adds kanban column conformance so GitHub Project Status names that deploy/kanban workflows write must exist on the live board—catching UI renames or stale literals that would leave cards unmoved while CI stayed green.
A new
kanban-columns-check.pyscans only the listed writer workflows (advance-deploy-env.yml,kanban-closure-router.yml) for literalSTATUS/STATUS_NAMEvalues (not resolver shims or$OVERRIDE), fetches the org project’s Status options via GraphQL, and fails closed on API errors, empty options, or no matches (vacuous pass).kanban-columns.ymlruns a selftest first, then the live check withPROJECTS_KANBAN_TOKEN, on PR path filters, daily at 06:15 UTC, andworkflow_dispatch.Reviewed by Cursor Bugbot for commit 8b381f9. Bugbot is set up for automated code reviews on this repo. Configure here.