Skip to content

feat(kanban): wire the Done column into the automation (backend#1411) - #126

Merged
saadqbal merged 4 commits into
developfrom
fix/1411-wire-done
Aug 3, 2026
Merged

feat(kanban): wire the Done column into the automation (backend#1411)#126
saadqbal merged 4 commits into
developfrom
fix/1411-wire-done

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Closes tracebloc/backend#1411. Unblocked by #124 landing, which is what put #1412's fail-closed assertions on main — the ordering the work order requires.

Two of these were live exposure, not latent

Done was added to the board on 2026-08-01 and no code knew it existed. Two cards already sit there (backend#1262, #1371), so:

filewhat it did
advance-deploy-env.ymlrank()Done/Cancelled both returned 0, inverting the monotonic guard — so the next push carrying an old commit demoted a Done card. staging → develop back-merges re-carry old commits routinely, so this was not hypothetical.
fr-gate.ymlrank()both fell through to "" and then to strict equality, so a single Done card blocked every prod promotion carrying it.
kanban-archive.ymlarchived only Prod/Cancelled, so Done cards would never leave the board.
kanban-reconcile.ymlfiled hand-closed completed issues as Prod — asserting a deployment that never happened.
kanban-closure-router.ymlset nothing at all for a hand-closed completed issue.

Verified rather than read

Both rank() functions extracted and run over the full column set, instead of eyeballing the case arms:

Backlog 1 · On dev 6 · FR on staging 7 · Ready for prod 8 · Prod 9 · Done 10 · Cancelled 10 · unknown 0/""

10 > 9 is what makes them terminal: nothing can demote them, and fr-gate's "Ready for prod (8) or later" is now satisfied rather than blocked.

The router change, and why its old objection no longer holds

The router previously left a hand-closed completed issue alone, with the comment "Do not invent a deploy state here." That was right — and it is precisely why Done is the correct answer now: Done is not a deploy state. It means completed with nothing deployed, which is exactly what a hand-closed issue is.

Doing it on the close event rather than delegating to kanban-reconcile's cron makes the card terminal immediately. Reconcile stays as the backstop for anything the event misses, and its own rule changes from Prod to Done for the same reason.

Semantics throughout are RFC-BACKEND-1405 D8: Prod = something deployed · Done = completed but nothing deployed · Cancelled = not planned.

Ordering

Deliberately after#1412, which #124 promoted to main. The work order sequences it that way because arming those fail-closed assertions after wiring Done would enlarge the mutation population on the first run and turn a warning into a red cron. With #1412 already live, this lands against armed guards.

Note for the reviewer

This repo has no test harness, so actionlint and check are the only automated verification — hence the manual rank extraction above. Worth knowing that the epic's own #1415 would be the place to change that; four of the five files touched here have had a defect found by review rather than by CI.

🤖 Generated with Claude Code


Note

Medium Risk
Changes board Status mutations and prod FR-gate behavior across five workflows; mis-routing could hide cards (archive) or leave closed issues in wrong columns, though deploy-column guards and unreadable-closer handling reduce that risk.

Overview
Wires the Done column into engineer-kanban automation per RFC-BACKEND-1405 D8 (completed, nothing deployed vs Prod).

rank() in advance-deploy-env.yml and fr-gate.yml now maps Done and Cancelled to 10 (above Prod). Previously they ranked as 0 or unknown, which could demote Done cards on back-merge pushes and block prod promotions that carried a Done card.

kanban-archive.yml archives Done alongside Prod/Cancelled so terminal hand-completed work leaves the board.

kanban-closure-router.yml sets hand-closed completed issues to Done immediately (not left for reconcile). Adds PROJECTS_KANBAN_TOKEN to the closer GraphQL step so PR-based routing actually runs. Distinguishes lookup errors from no closer so transient failures do not incorrectly set Done (and get archived). Skips setting Done when the card is already in a deploy column (On dev through Prod).

kanban-reconcile.yml terminalizes closed-completed issues to Done instead of Prod, with the same deploy-column skip as the router.

Reviewed by Cursor Bugbot for commit 1832db6. Bugbot is set up for automated code reviews on this repo. Configure here.

…oc/backend#1411)
Done was added to the board on 2026-08-01 and no code knew it existed. Two of the
five gaps were live exposure, not latent: two cards (#1262, #1371) already sit there.
advance-deploy-env rank() Done and Cancelled both returned 0, which inverted the
monotonic guard -- so the next push carrying an old commit
DEMOTED a Done card, and staging->develop back-merges
re-carry old commits routinely.
fr-gate rank() both fell through to "" and then to strict equality, so a
single Done card BLOCKED every prod promotion carrying it.
kanban-archive archived only Prod/Cancelled, so Done cards would never
leave the board.
kanban-reconcile filed hand-closed completed issues as Prod, asserting a
deployment that never happened.
kanban-closure-router set nothing at all for a hand-closed completed issue.
Both rank functions now return 10 for Done and Cancelled -- above Prod's 9, so nothing
can demote them and fr-gate's "Ready for prod or later" is satisfied rather than
blocked. Verified by extracting both functions and running the full column set through
them rather than reading the case arms:
Ready for prod 8 Prod 9 Done 10 Cancelled 10 unknown 0 / ""
The router now routes a hand-closed completed issue to Done immediately instead of
delegating to reconcile's cron. Its old objection -- "do not invent a deploy state
here" -- is exactly why Done is the right answer: it is not a deploy state. Reconcile
stays as the backstop for anything the close event misses, and its own rule changes
from Prod to Done for the same reason.
Semantics are RFC-BACKEND-1405 D8: Prod = something deployed, Done = completed but
nothing deployed, Cancelled = not planned.
Sequenced deliberately after backend#1412 (merged and promoted to main in #124):
arming those fail-closed assertions AFTER wiring Done would have enlarged the mutation
population on the first run and turned a warning into a red cron.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread.github/workflows/kanban-closure-router.yml
Comment thread.github/workflows/kanban-reconcile.yml Outdated
… card is not Done
Two Bugbot findings on #126, both mine and both consequences of routing Done where
"leave it alone" used to be.
HIGH -- the closer lookup mapped any GraphQL or transport failure to NONE, which was
harmless while NONE meant "leave Status unchanged". Now NONE means Done, so a transient
API error on a PR-closed issue would move it to Done -- and kanban-archive, which this
same PR taught to archive Done, would hide it the next day. I converted a fail-open
default into a fail-WRONG one, which is worse than either.
ERROR is now distinct from NONE, and the closer type is checked against an allowlist so
an empty or unexpected read also leaves the Status alone with a warning. Verified by
extracting the decision and running all six inputs:
PullRequest main -> Prod · PullRequest develop -> On dev · Commit -> On dev
NONE -> Done · ERROR -> unchanged · empty -> unchanged
MEDIUM -- reconcile's terminalization covers cards in deploy columns too, so blanket
Done labelled deployed work as "completed, nothing deployed" and overwrote what the
closure router had correctly set. Only a card with no deploy state qualifies for Done.
That leaves a closed card sitting in a non-terminal column, which D8's third invariant
forbids while D8's own routing table says "follow the PR's stage". The two rules
genuinely conflict for work that shipped to dev or staging and then stopped. Asserting
something false is the worse option, so this reports and moves on; the conflict is
raised on backend#1411 rather than silently resolved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Both fixed, and both were mine — same root cause

Each finding is a consequence of putting Done where "leave it alone" used to be. That is the interesting part: neither site was wrong before this PR, and neither diff looks wrong in isolation.

Closer lookup failure sets Done (High) — the sharpest of the two. The lookup mapped any GraphQL or transport failure to NONE, which was harmless while NONE meant "leave Status unchanged". Now NONE means Done, so a transient API error on a PR-closed issue moves it to Done — and kanban-archive, which this same PR taught to archive Done, hides it the next day. I converted a fail-open default into a fail-wrong one, which is worse than either.

ERROR is now distinct from NONE, and the closer type is checked against an allowlist so an empty or unexpected read also leaves Status alone with a warning. Verified by extracting the decision and running every input rather than reading the branches:

closerbase
PullRequestmainProd
PullRequestdevelopOn dev
CommitOn dev
NONEDone
ERRORunchanged
(empty)unchanged

Reconcile Done ignores closing PRs (Medium) — also right. Blanket Done labelled deployed work as "completed, nothing deployed" and overwrote what the router had correctly set. Only a card with no deploy state qualifies now.

A D8 conflict this surfaces, which I have not resolved

Skipping deploy-column cards leaves a closed card in a non-terminal column — which D8's third invariant explicitly forbids ("no closed item may sit in a non-terminal column"), while D8's own routing table says "closed completed, closed by a PR merged to develop/staging → follow the PR's stage".

Those two rules genuinely conflict for work that shipped to dev or staging and then stopped. There is no reading of D8 that satisfies both. I have chosen the option that does not assert something false, logged the skip, and raised it on backend#1411 rather than picking a winner in a workflow file. It wants a decision, and it is the same open question the RFC lists as unsettled.

Comment thread.github/workflows/kanban-closure-router.yml
Third Bugbot finding on #126, and it is the asymmetry the previous commit created: I
added the deploy-column guard to kanban-reconcile.yml and not to the router, so the two
files disagreed about the same rule.
The router's hand-close path set Done with no reference to the card's current column.
Real path: reopen, then hand-close an issue sitting in On dev / FR on staging / Ready
for prod -> the deploy state is overwritten -> kanban-archive.yml, which this PR taught
to archive Done, hides the card the next day.
The item lookup now also reads the card's current Status, and Done is refused when that
Status records a deployment. Only Done is guarded: a PR-derived Status is a deploy fact
and may advance a card normally.
Done + On dev / FR on staging / Ready for prod / Prod -> skip, notice, exit 0
Done + anything else or unknown -> apply
Prod / On dev / FR on staging (from a PR) -> apply
Verified by extracting the guard and running the table rather than reading the case arm.
Same root cause as the two before it: Done was introduced where "leave it alone" used
to be, and every path that previously ended in "change nothing" needed re-examining --
including the ones in files I was not editing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Fixed — and this is the asymmetry my previous fix created

Correct, and the finding is sharper than it looks: your last review made me add the deploy-column guard to kanban-reconcile.yml, and I did not apply it to the router. Two files, same rule, disagreeing — and the router is the one that fires on the close event, so it wins the race.

The path is real: reopen, then hand-close an issue sitting in On dev / FR on staging / Ready for prod → the deploy state is overwritten → kanban-archive.yml, which this PR taught to archive Done, hides the card the next day.

The item lookup now also reads the card's current Status, and Done is refused when that Status records a deployment. Only Done is guarded — a PR-derived Status is a deploy fact and may advance a card normally:

settingcurrent columnoutcome
DoneOn dev / FR on staging / Ready for prod / Prodskip, notice, exit 0
Doneanything else, or unknownapply
Prod / On dev / FR on staging (from a PR)anyapply

Verified by extracting the guard and running the table, not by reading the case arm.

The pattern across all three findings on this PR

Every one is the same shape, and none of them is in a line this PR set out to change:

  1. an error handler that meant "change nothing" and now means "assert Done"
  2. a reconcile path that meant "terminalise everything" and now mislabels deployed work
  3. a router path that meant "change nothing" and now overwrites a deploy state

Introducing a new terminal value into a state machine means every path that previously ended in "leave it alone" has to be re-examined — including the ones in files the diff doesn't touch. I checked the five sites the ticket named and not the paths that reach them. That is the actual lesson from #1411, and it is worth more than the change itself.

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a19d5cb. Configure here.

Comment thread.github/workflows/kanban-closure-router.yml
Fourth Bugbot finding on #126, and the only pre-existing one: the "Determine target
Status" step runs `gh api graphql` to find an issue's closing PR, and the step has no
GH_TOKEN. So the lookup has failed on EVERY invocation since it was written.
It was invisible because a failed lookup collapsed into "no closer", and "no closer"
meant "leave Status unchanged". The closed-by-PR routing in this step -- the whole
PullRequest/Commit branch -- has therefore never executed. kanban-reconcile.yml's cron
has been doing all of it, which is precisely what its own comment describes:
"the closure-router only moves issues closed by a linked PR (and can miss even those
to a close-time race), and never moves hand-closed ones"
The first clause was never true. It moved nothing.
backend#1411 is what made this matter: the no-closer branch now sets Done, so a lookup
that always fails means the Done path never runs either. Audited every gh call in the
file -- this was the only step missing a token; the update step already uses the same
secret, and the lookup is cross-repo so it needs the PAT rather than GITHUB_TOKEN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@LukasWodka@saadqbal