Uh oh!
There was an error while loading. Please reload this page.
chore(promote): develop → main — wire the Done column, and give the closer lookup a token (backend#1411) - #127
Merged
Conversation
feat(kanban): wire the Done column into the automation (backend#1411)
Uh oh!
There was an error while loading. Please reload this page.
… from the column
Bugbot on .github#127, and it is the third variant of the same hole -- this time in the
guard I added for the second one.
My reconcile check used the CARD'S COLUMN as evidence for "did anything deploy". That is
circular: the column is exactly what is wrong when kanban-closure-router misses a close.
The router's closer lookup can fail, and it then deliberately leaves Status alone -- so
the card stays in Backlog or Code review while the work has actually shipped to dev.
Reconcile then read that column, saw no deploy state, and asserted Done. kanban-archive
hides it the next day. Precisely the fail-WRONG outcome the router's guard was written
to prevent, reached through the backstop instead.
Done needs TWO things true: no deploy state on the card, AND nothing closed the issue.
Reconcile now asks the second question directly instead of inferring it:
no closer -> Done
closed by PR or Commit -> SKIP; it shipped, the router owns the stage
lookup failed -> SKIP; unreadable is not evidence of absence
already in a deploy col -> SKIP, as before
Verified by extracting the decision and running the table.
Also: REPO here is the bare repository name, not owner/name, so ${REPO#*/} was correct
only by accident (no slash to strip). Uses $REPO directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>fix(kanban): reconcile must check for a closer, not infer it from the column (backend#1411)
Uh oh!
There was an error while loading. Please reload this page.
saadqbal
approved these changes
Aug 3, 2026
…the card Bugbot on .github#127, and it is the flip side of my own previous fix. I stopped reconcile asserting Done for a closed-completed issue that has a PR or Commit closer but sits in a non-deploy column. That removed a false claim and created a worse outcome: the card is closed, in an active column, and archive only takes terminal ones -- so it stayed there permanently. That is the invariant this job exists to enforce, broken by the job itself. Skipping was never the right answer, because that case IS the router-miss this job backstops. So do what the router failed to do: read the closing PR's base ref and derive the stage from it. no closer -> Done closed by PR, base main/master -> Prod closed by PR, base staging -> FR on staging closed by PR, base develop -> On dev Commit closer, or unknown base -> On dev (the router's own default) lookup unreadable -> SKIP, unreadable is not evidence already in a deploy column -> SKIP, the stage is already recorded The mapping is character-for-character the router's, deliberately. When those two disagree the faster one wins and the slower one silently undoes it, which was Bugbot's third finding on .github#126 -- the same two files, the same rule, out of step. ON_DEV_OPT and FR_STAGING_OPT were only resolved in a later step, so the classify step could not have set those columns even if it wanted to. Declared there now. Verified by extracting the decision and running all nine combinations rather than reading the case arms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the org detected a missing caller. merge-settings-drift.yml reads three booleans. kanban-reconcile.yml:436 probes a single filename to decide board scope, so a 403 there is indistinguishable from "repo not tracked". A caller that is simply absent produces no run, no annotation and no red check, and the absence of a signal is not a signal — which is how eight repos drifted unnoticed and how e2e-test-agent#1 closed without routing. What the guard now detects that nothing did: - a required caller missing from a repo, on the develop-first branch - a caller pinned to anything other than @main - a caller present where the inventory says exempt (a stale exemption) - a caller for a reusable the inventory has never heard of - add-to-kanban.yml / stale-backlog.yml drifted in content — they are copies, not callers, so no other mechanism would ever notice - a repo that joined the org, or left it, without the inventory following - a repo whose visibility or release-train membership contradicts the inventory, cross-checked against release-train/repos.yml repo-inventory.yml is the source of truth: every repo carries an entry for every reusable and every copy, and each is `required` or carries a written reason. A missing key is a guard failure, not a default; an exemption with no reason is rejected before any network call. Absence can never be implicit. Matching is on the resolved `uses:` value of a parsed workflow, never on filename. Two filename conventions are mixed within nearly every repo, kanban-closure-router's callers match neither, and code-quality.yml:60 is a commented-out example that a grep would count as a caller. Fail-closed throughout: a 403, a rate limit, a truncated git tree, an unparseable workflow, an unreadable inventory or an empty org listing each produce a failure, never an all-clear. scripts/tests/caller-drift-selftest.py asserts all of that offline with a stubbed gh, and the audit job needs it. The sync job that opens PRs for missing callers is deliberately deferred. Refs tracebloc/backend#1415, tracebloc/backend#1408 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The inventory is read from the checkout, but every repo's caller state - including tracebloc/.github's own - is read from its audit branch over the API. A PR that adds a caller to .github and flips that entry to `required` in the same commit therefore fails, because the caller is not on develop yet. That is the fail-closed direction, and reading .github's workflows from the checkout instead would break the develop-first policy on a run triggered from main. Documented rather than papered over, with the remedy: land the caller first, flip the entry after. Refs tracebloc/backend#1415 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
saadqbal
previously approved these changes
Aug 3, 2026
fix(kanban): reconcile derives the missed stage instead of stranding the card (backend#1411)
feat(ci): caller inventory and drift guard (backend#1415)
LukasWodka
commented
Aug 3, 2026
ContributorAuthor
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f7a50e8. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
The caller-drift guard went red on its first real run — on .github#127, the
develop -> main promotion, which is the worst place to discover it:
tracebloc-website: release-train/repos.yml says on-train=True, the
inventory says False.
The guard is right. release-train#14 ("enroll tracebloc-website in the release
train") is merged, so repos.yml on main lists the repo; the inventory was
written from the pre-merge state and is one merge behind. release-train has no
develop branch, so the guard reads main there and sees the enrollment
immediately.
Nothing is actually ungated, despite the finding's wording: fr-gate.yml is
already `required` for this repo and fr-gate-caller.yml is present on develop.
Only the recorded fact was stale.
Verified by running the guard locally against the live org, before and after:
before: 1 drift finding(s) (reproduces CI exactly)
after: No drift. Every repo read, every entry matched.
both: Audited 20 of 20 on the develop-first branch
selftest: exit 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Bugbot on .github#127, High. The closer lookup's --jq collapsed FIVE distinct answers into the single token NONE, and NONE now means Done, which kanban-archive hides the next day: ClosedEvent present, closer null -> NONE genuinely hand-closed no ClosedEvent at all -> NONE the timeline said nothing timelineItems.nodes null -> NONE ditto issue null -> NONE nothing was read repository null -> NONE nothing was read Only the first is evidence of a hand-close. The rest are absence of evidence, and just one of them needs to happen once -- a close-event race, a null read -- for shipped work to be terminalized and archived. Command failure was already separated out as ERROR (Bugbot's earlier finding on .github#126); this is the same mistake one level in, in the success path. The filter now answers three ways instead of two, so "the timeline had no ClosedEvent" is its own token: jq: if $e == null then "NOEVENT|" else <typename>|<baseRef> end Both shells already fail closed on an unrecognised token, so NOEVENT lands in exactly the right place without touching either decision: router: not in the PullRequest|Commit|NONE allowlist -> UNREADABLE -> no change reconcile: no NONE / PullRequest / Commit arm matches -> SKIP, no Done asserted That is the allowlist doing its job, so the only behaviour change is that a no-evidence read no longer masquerades as a hand-close. Also fixes two log lines that would now lie: reconcile called NOEVENT a failed lookup when the read succeeded and simply found nothing, and the router's warning said "could not read" for a case where it read fine. NOEVENT gets its own message; the router's says "could not confirm". Verified by extracting both decision blocks and running every token through them under bash -e: jq filter 9 response shapes, incl. last-event-wins -> correct token each router PR/develop|staging|main -> On dev|FR on staging|Prod Commit -> On dev, NONE -> Done NOEVENT, ERROR, empty -> Status unchanged + warning reconcile NONE -> Done, PR/Commit -> stage derived NOEVENT, ERROR -> SKIP, no Done actionlint clean on both files Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(inventory): tracebloc-website is on the release train (backend#1415)
fix(kanban): "no ClosedEvent" is not a hand-close (backend#1411)
saadqbal
approved these changes
Aug 3, 2026
Uh oh!
There was an error while loading. Please reload this page.
LukasWodka added a commit
that referenced
this pull request
Aug 27, 2026
…nd#2722) (#361) * fix(kanban): a completed issue is terminal, not a deploy state (backend#2722) Closed issues were landing in deploy columns because two mechanisms deliberately put them there, not because either was failing. 1. kanban-closure-router.yml mirrored the closing PR's Status onto the issue, so a PR-closed issue inherited On dev / FR on staging / Prod. Its own declared contract said so. 2. advance-deploy-env.yml then marched those cards onward (backend#1600), because issues parked at On dev never advanced when the code shipped and drifted permanently -- all 20 drifted cards on 2026-08-06 were closed issues, 0 PRs. #1600 was right that a closed issue must not be stranded and wrong about where to put it. `Done` is terminal, so nothing can drift, and kanban-archive sweeps it off the board daily -- which answers #1600 more completely than advancing did. So: the router routes a completed issue to Done regardless of how it was closed, and the closing-issue block in advance-deploy-env is removed. Both halves, because fixing only one looks fixed while the other drags the cards straight back in. The closer lookup goes too. It existed only to tell a hand-close from a PR-close so they could route differently; with one destination there is nothing to tell apart. It was also the source of two fail-wrong defects (.github#126, #127), and a call never made cannot fail. CLOSER_TYPE / CLOSING_PR_BASE were read nowhere else. `permission-issues` drops from advance-deploy-env with the only reads that needed it. Checked rather than assumed: fr-gate ranks Done 11 against On dev 6, so terminal cards satisfy both gates instead of blocking promotions. Cost of the status quo, measured: 117 closed issues cleared out of deploy columns by hand in one session (18 + 55 from FR on staging, 44 from On dev). Two of those passes were functional-review batches, so the next prod payload would have read 73 items larger than the work it contained. Guarded now, because it was not: flipping the router's destination left all 29 selftest suites green. kanban-deploy-state-selftest asserts both halves, reading the completed arm out of the workflow rather than restating it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(kanban): the weekly backstop must terminalise too (backend#2722) Bugbot, High. The router now sends every completed issue to Done, but kanban-reconcile.yml still derived a deploy stage from the closer`s base branch and wrote On dev / FR on staging / Prod for exactly those cards. Fixing one writer and not the other is worse than fixing neither: the weekly sweep would have put every card back, the slower job silently undoing the faster one -- which is the failure .github#295`s own comment was written to prevent, arriving from the other direction. A deploy column is a property of a PR. A finished ISSUE belongs in Done whether or not its fix shipped, and issue cards parked in deploy columns are what stops kanban-archive clearing them. The closer lookup stays, because having one still distinguishes the two cases in the LOG -- but both arms now reach the same column, which is the rule. Second finding, same shape one layer out: the selftest read only the router and advance-deploy-env, so a green run confirmed the two EDITED files rather than the invariant. It now extracts the reconcile`s closer arm too and asserts it writes only DONE_OPT, names no deploy column, and reaches no branch->stage mapper. Mutation-proved: restoring the On dev write reddens it. Also removes two now-dead option ids shellcheck flagged, and the comment that justified them, which my own change had made false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(kanban): realign the mapper selftest with a terminal completed issue (backend#2722) The selftest already encoded #2722 for the router -- one call site, not two -- but still required kanban-reconcile.yml to keep one. Removing reconcile`s router-miss derivation made that false, so three assertions had to follow: * reconcile`s mapper call sites: 1 -> 0. Its only one was the closed-completed issue arm. The merged-PR lookback still places PR cards in deploy columns but always mapped its branches INLINE, so the count did not move because of it. * the DEST-arm invariant is inverted rather than deleted. It asserted every Status with an option id needed an arm, or the backstop silently skipped the repos an override exists for (.github#304) -- true while reconcile DERIVED a column, meaningless now there is no $DEST. It now asserts there are no arms, plus that the extractor can still find one in a fixture, so "no arms" cannot be confused with "the reader broke". * the stderr-preservation loop drops reconcile, which has no stderr to preserve. Its absence is asserted above rather than left implicit. All 15 selftests pass and actionlint reports 0 across every workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- 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.

Promotes #126 — wiring the
Donecolumn into the automation (backend#1411). Until this lands none of it does anything, because all 122 callers pin@main.Safety check done before opening this
#125("retire the WIP-limit nudge") merged todevelopyesterday and deletes a reusable that 12 repos still reference. If that deletion were ondevelop, this promotion would carry it tomainand every one of those 12 repos would go red — and a reviewer approving "promote the Done wiring" would be merging a reusable deletion they never reviewed.Verified against the API, not the local clone:
developmainwip-limit-check.yml(the reusable)wip-limit-caller.yml(.github's own caller)So
#125removed only this repo's own caller, and correctly held the reusable back until the remaining callers are gone.git log origin/main..origin/developis exactly one commit — #126. This promotion carries nothing else.What goes live
Done/Cancelledare terminal in bothrank()functions (10, aboveProd's 9)Done. Before this,advance-deploy-envreturned 0 for it, so the next back-merge push demoted them;fr-gatehad no case, so a singleDonecard blocked every prod promotion carrying it.kanban-archivearchivesDoneDonecards never leave the board.DoneDoneis not a deploy state, which is why the router's old "don't invent a deploy state here" objection no longer applies.GH_TOKEN, sogh api graphqlfailed on every invocation, silently, because a failed lookup collapsed into "no closer" and that meant "leave Status unchanged". The entire closed-by-PR routing in that step has been dead; reconcile's cron has been doing all of it.Expect this
The
Doneguards refuse rather than overwrite, so if a hand-closed issue sits in a deploy column you will now see a::notice::saying it was not set toDone. That is deliberate — see the D8 conflict written up onbackend#1411, which needs a human decision and is knowingly left violating one of D8's two competing rules rather than asserting something false.Review history worth knowing
Bugbot found four issues on #126 and all four were real: an error handler that meant "change nothing" and started meaning "assert
Done"; reconcile mislabelling deployed work; the router missing the guard I had just added to reconcile; and the pre-existing missing token. Three of the four were paths that previously ended in "leave it alone" — introducing a new terminal value means every such path needs re-examining, including in files the diff never touched.🤖 Generated with Claude Code
Note
High Risk
Changes promotion gates, closure routing, and archival behavior for every repo that pins these workflows at
@main; mis-routing or a broken closer guard could hide or mis-label board state org-wide.Overview
This promotion wires
Doneas a first-class terminal kanban status (RFC-BACKEND-1405 D8 /backend#1411) and adds org-wide caller-inventory drift detection (backend#1415).Done/Cancelledas terminal (rank 10) inadvance-deploy-envandfr-gateso back-merge pushes cannot demoteDonecards andDoneno longer blocks prod promotions.kanban-archivenow archivesDonealongside Prod/Cancelled.Closure and reconcile routing change materially:
kanban-closure-routergetsPROJECTS_KANBAN_TOKENfor the issue closer GraphQL lookup (previously always failed), routes genuine hand-closes toDone, distinguishesERROR/NOEVENT/NONEso API failures do not assertDone, and refusesDonewhen the card is already in a deploy column.kanban-reconcilestops blanketProdfor completed closed issues—usesDoneonly when there is no closer, derives deploy stage from the closer when the router missed, and skips when evidence is missing.New drift guard:
repo-inventory.yml(explicit required/exempt/divergent per repo),scripts/caller-drift.py, offline selftest, and.github/workflows/caller-drift.yml(scheduled + path-filtered PR checks, fail-closed audit, issue comments on failure only).Reviewed by Cursor Bugbot for commit 5eaad72. Bugbot is set up for automated code reviews on this repo. Configure here.