Skip to content

feat(2348): a bug-labelled issue lands in Ready, not Backlog (backend#2348) - #313

Merged
LukasWodka merged 2 commits into
developfrom
feat/2348-bug-label-lands-in-ready-via-labeled
Aug 23, 2026
Merged

feat(2348): a bug-labelled issue lands in Ready, not Backlog (backend#2348)#313
LukasWodka merged 2 commits into
developfrom
feat/2348-bug-label-lands-in-ready-via-labeled

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Closes tracebloc/backend#2348.

The org rule — "bugs get work-type:bug and go straight into Ready (defects skip refinement)" — is written in org-standards.md and therefore in every repo's CLAUDE.md, and nothing implemented it. add-to-kanban.yml adds every new issue at Backlog; no workflow read the label afterwards. The string work-type:bug appeared in no workflow and no script in this repo.

Measured 2026-08-22 (the ticket's own evidence): seven bug-labelled tickets filed in one day — backend#2324, #2327, #2329, #2340, #2341, #2344 and frontend-app#871 — all seven landed in Backlog and each needed a hand-run updateProjectV2ItemFieldValue. A 100% miss rate is the tell that nothing does it at all; a rule people mostly follow produces a mixed record.

Ready is the queue engineers pull from and Backlog is the refinement queue nobody pulls from — that split is the entire point — so a defect filed correctly and labelled correctly was invisible by default to the people meant to pick it up. The release train's deferral story rides the same path: it files a ticket for every Medium/Low it ships as "a record rather than a dismissal", and those landed in Backlog too, accurate and unread.

Placement: a job on the reusable that already fires on issues: labeled

customer-priority-bump.yml is the only reusable in the org already triggered by issues: labeled, and 16 of 19 repos already call it with secrets: inherit and no inputs. A new job inside it reaches all sixteen with no per-repo edit, because a reusable is resolved from @main at run time.

No guarded file is touched. conformance-gate.yml's GUARDED list is repo-inventory.yml, scripts/caller-drift.py, scripts/tests/caller-drift-selftest.py, .github/workflows/caller-drift.yml, .github/workflows/conformance-gate.yml. This PR touches none of them, so the gate takes its "does not touch the repo contract" branch and is green in seconds with zero audit API calls. There is no merge ordering to respect and nothing to re-run when another PR lands.

That was the deciding constraint. A new reusable would need a repo-inventory.yml row for all 19 repos — and repo-inventory.yml is guarded, so that PR cannot merge until caller-drift has passed on its exact head sha, which every other merge into this serialising repo invalidates. A rule that has been unimplemented since it was written should not queue behind that. (The alternative placement, add-to-kanban.yml, is worse still: it is the org's one per-repo copy, byte-compared by caller-drift.py, so editing it here marks all eighteen other copies DRIFTED until the last repo's PR lands.)

The cost, stated plainly rather than buried: the file name and the 16 caller names still say "customer priority bump", so a bug-label run shows up in each repo's Actions tab under that name. The reusable's own name: is the half fixable without touching 16 repos, so it now reads Label-driven issue triage. Renaming the file is a caller rollout (BUGBOT.md property 1 — land the callee first) and belongs with the wiring work below.

Monotonic, and the direction is asked of the board

A card is promoted only from Backlog, or from no Status at all. Nothing else moves, ever.

  • promote — at Backlog, or on the board with no Status. Those are the only two states from which Ready is forward. An unplaced card is not "past Ready"; leaving it unplaced is what keeps the defect invisible.
  • hold — everywhere else, green and logged: North Stars, In progress, Code review, On dev, Staging (agent review), FR on staging, Ready for prod, Prod, Done, Cancelled, already Ready, and archived. The label routinely arrives after triage moved the card, and demoting a shipped card would un-ship it on the board (.github#87#92).
  • noboard — an anchor is missing, or Ready does not sit strictly after Backlog on the live board. That second half is the monotonicity assertion: position is load-bearing, so one drag of the Status options in the UI turns this "promotion" into a demotion. It is checked before the no-Status shortcut, because a board that cannot be trusted to be in pipeline order cannot be trusted for any card. (backend#1994 is the same hole one file over: existence was checked and ORDER was not.)
  • unknown — a column the board does not report. Defence in depth: the single atomic read makes it unreachable today, and it exists so the case has no fall-through to the write.

Positions come from the board's own option order, via a col_index that is byte-identical to kanban-closure-router.yml's (asserted by the selftest — same $PROJ shape, one definition). No rank table is held here: a second copy of the column order agrees with itself while disagreeing with reality (backend#1729 rule 1). advance-deploy-env.yml's rank() is where the pipeline order is declared, and this suite parses it rather than restating it.

Fail closed — and which sibling's direction this is

backend#2243 settled the three board consumers' failure paths as a set: kanban-closure-router.yml writes a non-terminal holding state, kanban-reconcile.yml skips, advance-deploy-env.yml has no fallback and aborts.

This job is the advance-deploy-env case: no fallback, abort. Unreadable board, GraphQL errors[] at exit 0, a truncated field page, an unresolvable Status/Ready option, an unreadable project-items response, a card still absent after 5×5 s, an unplaceable current column, a failed write — every one is a red run, and the only green no-op is a state that was positively read and understood (hold, and a different label).

Why not the router's quiet exit 0: there, declining to write is the conservative end state — it is protecting shipped state from being overwritten. Declining here is not conservative, it reproduces the exact defect this job exists to fix: a defect parked in Backlog that nobody reads. And reconcile may skip because it runs weekly and gets another go; a labeled event fires once, so a green no-op is the last anyone hears of it. Quoting advance-deploy-env.yml on its own version of this: "a graceful degrade here means the board stops advancing while every run reports success."

The other constraints

  • Pagination. The card is resolved through the issue's own projectItems, never a scan of project chore: add auto-add to engineer kanban workflow #2's ~700 items. totalCount is read on both connections: > 20 project items with the kanban card not among them, or a field page shorter than the field count, is a finding, not a "not on the board" (advance-deploy-env's .github#190 lesson). A missing totalCount is an unreadable response, not an empty one — so a non-resolving issue is named as that instead of being retried four more times and reported as "not on the board".
  • The option id is derived at runtime, never held: a stored Ready option id keeps writing after the board moves under it, and writing the wrong column is strictly worse than writing nothing.
  • The label and both column names are derived from the canon. org-standards.md is where the rule is written, so the selftest parses the label and both columns out of that sentence and asserts the workflow's bug-label default, SOURCE_COLUMN and TARGET_COLUMN against them. Reword the rule and this reddens — both directions are mutation-proved.
  • customer-priority-bump.yml joins kanban-columns-check.py's WRITERS (and the check's paths: filter, which its selftest asserts against WRITERS). That is what puts Backlog and Ready under the daily live-board assertion — a rename in the Projects UI would otherwise turn this job into a silent no-op, since the option is resolved by name.
  • The mint is scoped (backend#2157): permission-issues: read + permission-organization-projects: write, derived from the two calls the step makes, with permissions: {} on the job. repositories: is deliberately not narrowed: that interaction is measured for an org-project read (kanban-columns.yml, backend#2181) and not for a write, and an unmeasured narrowing on a workflow that fires for every bug in the fleet fails red on every defect filed. mint-scope's existing EXEMPT row covers the old bump job's full grant, not this one — 15 mint steps, 0 findings.

Evidence

make checkexit 0, including selftests-cover: all 15 selftests and 4 mutation runner(s) are wired to a target, and CI runs both tiers, actionlint: 0 findings (with shellcheck over the new run: block), house-rules: no findings, mint-scope: 15 steps, no findings.

The new suite is 46 assertions, and the decision under test is extracted from the YAML by its # selftest: markers and run verbatim — never re-implemented (CLAUDE.md rule 9; .github#114/#115 twice proved an inline copy goes on proving a regex nothing uses would have caught the bug). A dropped marker or a renamed function fails the run rather than falling back to a copy.

PASS: the Status vocabulary derived from rank() agrees with the sibling suite's board
        12 columns: Backlog, North Stars, Ready, In progress, Code review, On dev,
        Staging (agent review), FR on staging, Ready for prod, Prod, Done, Cancelled
PASS: and in the same ORDER, which is what the monotonic gate is derived from
PASS: of all 12 declared columns, exactly 'Backlog' promotes
PASS: a card at 'North Stars' -> hold          … all 11 non-Backlog columns
PASS: '<no Status>' is promoted, not left unplaced
PASS: an ARCHIVED card holds even when it sits in the source column
PASS: a board whose Backlog/Ready order is INVERTED refuses — including for the
      card AT Backlog and for the unplaced card the shortcut would let through
PASS: col_index() is byte-identical to kanban-closure-router.yml's
PASS: label gate: 'work-type:bugfix' is not 'work-type:bug'
PASS: label gate: a pull_request payload is refused loudly
…
bug-to-ready-selftest: 46 passed, 0 failed

The vocabulary is derived, not written down (CLAUDE.md rule 6 — mutation coverage cannot see a vocabulary gap): the 12 columns come from advance-deploy-env.yml's rank() arms and are cross-checked against kanban-deploy-state-selftest.py's independently written BOARD. Two derivations disagreeing is a finding, not a tie this file breaks. A monotonicity check that tried two columns would have passed with North Stars promoting too.

Every refusal names itself (refuse:other-label, refuse:pull-request-payload, refuse:unreadable-label, refuse:no-configured-label, refuse:not-an-issues-event) and every case pins the specific reason — CLAUDE.md rule 10: a refusal test that cannot say which refusal is a coin toss that reports success.

Mutation-proved, 21 mutations, every anchor asserted applied (an inert mutation and real coverage produce an identical green log):

21 mutation(s): 0 stale, 0 uncaught

caught  the monotonic guard: promote from ANY placeable column, not just the source
caught  the anchor ORDER half: check existence only, not that the target follows the source
caught  the anchor check runs SECOND, after the no-Status shortcut
caught  the unplaceable column falls open and is promoted
caught  an ARCHIVED card is promoted like a live one
caught  a card with NO Status is left unplaced
caught  col_index reports an ABSENT column as position 0 instead of -1
caught  noboard exits 0, so an unreadable board order becomes a green no-op
caught  unknown exits 0, the router's direction rather than this job's
caught  the case falls through to the write on an unrecognised verdict
caught  the label is matched as a SUBSTRING, so 'work-type:bugfix' promotes
caught  an unreadable label reads as 'some other label' instead of failing closed
caught  an empty configured label matches everything instead of refusing
caught  a pull_request payload is accepted
caught  any event may promote, not only `issues`
caught  the job `if:` becomes a contains(), so the cost gate is LOOSER than the decision
caught  the job `if:` stops pinning the event name
caught  the new mint drops its scopes and takes the App's full grant
caught  the workflow's label default drifts from the written rule
caught  the workflow writes a different column than the rule names
caught  the CANON renames the label and the workflow is not updated with it

make check runs --dry (anchors only, milliseconds); make mutations runs the full pass and is executed by selftests.yml, an already-required context — so this arms with no branch-protection edit.

What this does NOT cover

Named, because an unstated gap is how a suite comes to be trusted for more than it proves.

  • No network seam is exercised. The two GraphQL reads, the retry loop, the errors[]/truncation/card-absent fail-closed arms and the updateProjectV2ItemFieldValue write are asserted by inspection and by copying the shape of the sibling workflows. The suite drives the three decision regions with stubbed payloads and a synthetic board. This is also why those arms carry no mutation: an UNCAUGHT verdict there would be about the suite's scope, not its rigour.
  • Three repos are not wired. .github, release-train and rfcs are exempt for this reusable in repo-inventory.yml (customer_priority_bump_caller_missing, UNREMEDIATED since 2026-08-03), so a bug filed in one of those three still lands in Backlog. The other 16 — including backend and frontend-app, where all seven measured misses were filed — are covered. This wants its own ticket: the exemption's stated reason ("wiring more repos to it is sequenced behind backend#1408") is now weaker, because the reusable has grown a second thing worth having. The org-standards sentence names the three repos in the meantime, and that clause should be deleted when they are wired.
  • It goes live on main, not on merge. The 16 callers resolve @main, so this takes effect when the train promotes it. Nothing here proves the wiring end-to-end; the first bug labelled after that promotion is the real evidence, and it should be checked rather than assumed (add-to-kanban.yml's own comment block records what a green mint proves).
  • The opened+labeled double-fire is reasoned about, not measured. The Bug template applies the label at creation, so this can race add-to-kanban.yml; the 5×5 s retry is for that, and no test drives two concurrent runs. Both would compute the same idempotent write.

Overlap with #309 — only one of these should land

#309 implements this same ticket as a new reusable (bug-to-ready.yml) plus 19 repo-inventory.yml rows, and is BLOCKED with audit and gate red. Its own body argues the audit failures are the unrelated backend#1979 rollout, which is likely true — and it still needs a green audit on its head sha to clear the conformance gate, which every merge into this repo invalidates. That is the queue this PR was written to avoid.

The two differ in placement and in the caller story, not much in the decision: both derive the option id from the live board, both refuse an inverted board, both extract the decision out of YAML for the selftest. Pick one. If #309 is preferred, close this; the ordering argument above is the only reason it exists.

🤖 Generated with Claude Code


Note

Medium Risk
Writes GitHub Projects Status via an org App token on every bug label in 16 repos. Fail-closed and monotonic, but a logic bug could still leave defects in Backlog or (if guards fail) move the wrong card.

Overview
When work-type:bug is added, automation now moves the kanban card from Backlog to Ready so defects skip refinement. This lives as a new bug-to-ready job on the existing issues: labeled reusable (customer-priority-bump.yml), so 16 already-wired repos pick it up from @main with no caller or inventory change.

Promotion is monotonic: only Backlog or no Status; never backward, never archived. Option IDs and column order come from a live board read. Unreadable GraphQL, inverted columns, missing cards after retry, and unconfirmed writes fail red (a labeled event fires once). Token mint is scoped to issues read + org projects write.

The reusable display name is now Label-driven issue triage. org-standards/CLAUDE.md note that .github, release-train, and rfcs still need a hand move. Column names are registered with kanban-columns-check. A selftest extracts the decision from YAML plus a 21-mutation harness.

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

…#2348)

The org rule -- "bugs get `work-type:bug` and go straight into `Ready`
(defects skip refinement)" -- was written in org-standards.md and
implemented nowhere: add-to-kanban puts every issue at `Backlog` and no
workflow read the label. Measured 2026-08-22: seven bug-labelled tickets
in one day, all seven left in `Backlog`.

Implemented as a job on `customer-priority-bump.yml`, the only reusable
already triggered by `issues: labeled` and already called by 16 of 19
repos -- so it reaches the fleet with no per-repo edit and touches none of
conformance-gate's GUARDED contract files.

MONOTONIC: promotes only from `Backlog` or from no Status, and asks the
BOARD for the direction (col_index byte-identical to the closure router's)
rather than holding a rank table. An inverted Backlog/Ready order refuses
rather than performing a demotion.

FAILS CLOSED like advance-deploy-env, not like the closure router: an
unreadable board, a truncated page, an unplaceable column or a card that
never reaches the project are all RED. Declining quietly here would
reproduce the very defect being fixed -- and a `labeled` event fires once,
where a cron gets another go next week.

Tests: 46 assertions, extracted from the YAML by `# selftest:` markers and
run verbatim; the Status vocabulary is derived from advance-deploy-env's
rank() and cross-checked against the sibling suite, so all twelve columns
are driven. 21 mutations, 0 stale, 0 uncaught. `make check` green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka LukasWodka self-assigned this Aug 23, 2026
@LukasWodka
LukasWodka requested a review from saadqbal August 23, 2026 12:52
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread .github/workflows/customer-priority-bump.yml
…as the reads (backend#2348)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot 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.

✅ 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 4672df6. Configure here.

@saqlainsyed007 saqlainsyed007 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.

Verified the routing, the token scope, and the test rigour against the code.

The gap was real and total. The org rule "bugs get work-type:bug and go straight to Ready" was in org-standards.md and every CLAUDE.md, but nothing implemented it — the string appeared in no workflow. Measured: 7/7 bug tickets on 2026-08-22 landed in Backlog and each needed a hand-run field update. A 100% miss is the tell that nothing did it.

The implementation is least-privilege and clobber-safe:

  • The job mints an App token scoped to exactly issues: read + organization-projects: write, runs with permissions: {} (no caller GITHUB_TOKEN), and the non-narrowing of repositories: is justified (the org ProjectV2 write grant is unmeasured under repo-narrowing, and an unmeasured narrowing on a fleet-wide workflow would fail red on every defect filed).
  • It promotes only from Backlog ("but only from Backlog" — it won't move a card someone already advanced), the if: is explicitly a cost gate with the authoritative decision in a label_gate written where the selftest can run it, and it guards PR-not-issue payloads.
  • SOURCE_COLUMN="Backlog" / TARGET_COLUMN="Ready" are quoted so scripts/kanban-columns-check.py (extended here) asserts they exist on the live board — a Projects-UI rename reddens rather than silently no-ops.

Tested and mutation-tested honestly. bug-to-ready-selftest.py runs the extracted gate; bug-to-ready-mutations.py edits the code under test and treats a stale anchor (zero matches) as a failure exactly like an uncaught mutation, so inert coverage can't masquerade as good coverage (rules 5/9), and every refusal path names itself (rule 10). Armed intentionally on issues: labeled via the reusable 16/19 repos already call, rather than adding a second reusable.

CI green, the one review thread is resolved. LGTM.

@LukasWodka
LukasWodka merged commit 5564c6d into develop Aug 23, 2026
15 checks passed
@LukasWodka
LukasWodka deleted the feat/2348-bug-label-lands-in-ready-via-labeled branch August 23, 2026 13:51
@LukasWodka

Copy link
Copy Markdown
Contributor Author

/fr-pass

Sign up for free to 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