Skip to content

fix(labels): the triage labels exist fleet-wide, and now a check says so (backend#2598) - #364

Open
LukasWodka wants to merge 4 commits into
developfrom
fix/2598-triage-label-existence
Open

fix(labels): the triage labels exist fleet-wide, and now a check says so (backend#2598)#364
LukasWodka wants to merge 4 commits into
developfrom
fix/2598-triage-label-existence

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What

caller-drift.py asserts a repo carriescustomer-priority-bump.yml. It says
nothing about whether the labels that workflow fires on exist there — and GitHub
silently drops a template label the target repo lacks: no error, no annotation, no
run. So a repo can pass the conformance audit as fully wired while the bug-to-ready
rule cannot fire in it at all. The issue is filed unlabelled and the card sits in
Backlog, the refinement queue nobody pulls from.

Measured before / after

Derived from repo-inventory.yml (20 repos declare customer-priority-bump.yml: required, none exempt) — not from a hand-written list.

repobeforeafter
design-system-v20 of 77 of 7
release-train0 of 77 of 7
rfcs0 of 77 of 7
e2e-test-agent2 of 77 of 7
the other 167 of 77 of 7

e2e-test-agent is a fourth repo the ticket could not see. backend#2598 derived
over the work-type:*prefix and reported three repos. That repo had
work-type:bug and priority and none of the other five, so a prefix sweep called it
covered while four labels its templates apply were being dropped on the floor.
Deriving a domain from a prefix rather than from the producers is CLAUDE.md rule 6's
vocabulary gap — committed by the ticket written to close it. Two of the labels it was
missing (from:customer, and the three template labels) mean the bump job was
equally dead there and in the other three; the ticket recorded only the bug half.

The labels were created out of band

26 labels created with gh label create, plus one colour/description alignment
(e2e-test-agent's work-type:bug was GitHub's ad-hoc grey ededed with no
description — the tell that it was created by hand rather than by the rollout). Names,
colours and descriptions were read from the fleet, not invented: 16 of 17
already-labelled repos agreed byte-for-byte on all seven.

That is deliberate sequencing, per arm while green: had the check landed first it
would have been red on four repos on its first run. The labels are a repo-settings
change and cannot travel in a PR, so this PR carries only the check and the prose.
No branch protection, ruleset or other repo setting was touched.

The check

scripts/triage-labels-check.py + .github/workflows/triage-labels.yml, in the
caller-drift / kanban-columns audit tier (selftest job gates the audit job; PR
paths on both producers, plus a daily cron — a label can be deleted in a repo's
Settings UI with no PR to hang a check on).

It holds no label list. The domain is derived from two producers:

  • customer-priority-bump.yml — every workflow_call input whose name ends in
    -label contributes its default, and every --add-label X its run: blocks
    execute contributes X.
  • .github/ISSUE_TEMPLATE/*.yml — every entry of each template's top-level labels:.

Add a template, rename an input, or add a third label rule and the domain moves with
it. Deriving over the suffix rather than over two known input names is what makes
that true, and there is a case for it.

Fails closed. Exit 2 — never 0, and deliberately never 1 — on: an unreadable or
unparseable inventory / workflow / template; zero enrolled repos; a zero-length
derived domain in either family; a stale matcher (--add-label present but unmatched);
and any repo whose label list cannot be read. A 403 is UNKNOWN, never "this repo
has no labels" — that conflation is the backend#1415 failure one layer along. A repo
answering 200 with an empty list is a successful read, and every label is then missing
by the ordinary path.

Evidence

make check — green (ruff, shellcheck, house-rules, action-pins, mint-scope,
actionlint, mutations-dry, selftests-cover, 20 selftests):

triage-labels-selftest: 29/29 passed
==> check: green (gitleaks runs in 'make check-all')

make credential-scan: 438 commits scanned. no leaks found.

The live check, green across the fleet:

Derived 7 triage label(s) that must exist in each of the 20 enrolled repo(s):
from:customer <- input `trigger-label` default
needs-refinement <- applied by `feature.yml`
priority <- written by `--add-label`
work-type:bug <- applied by `bug.yml`; input `bug-label` default
work-type:docs <- applied by `docs.yml`
work-type:feature <- applied by `feature.yml`
work-type:tech-debt <- applied by `tech-debt.yml`
...
All 7 derived label(s) exist in all 20 enrolled repo(s): every label rule can fire everywhere it is declared.

Mutation proof, two tiers.

  1. make mutation-triage-labels21/21 caught, every anchor resolved exactly
    once. Every mutation edits scripts/triage-labels-check.py or org-standards.md
    and the suite is re-run against them; there is no inline copy of the rule
    (CLAUDE.md rule 9). Covered: an exempt repo audited as enrolled, an empty fleet
    passing, the suffix derivation broken, --add-label dropped, each fail-closed
    refusal softened, audit() folding an unreadable repo into "complies", the
    comparison inverted, and the canon renaming the label out from under the workflow.

  2. The real thing, on a real repo. Deleted needs-refinement from
    tracebloc/rfcs, ran the live check, restored it:

--- MUTATE: delete needs-refinement from tracebloc/rfcs ---
deleted
EXIT=2
x rfcs missing needs-refinement
ERROR: these enrolled repos are missing a triage label the org's automation fires on:
- rfcs: needs-refinement
--- RESTORE ---
restored
All 7 derived label(s) exist in all 20 enrolled repo(s)
EXIT=0

Prose this change makes false

  • org-standards.md:37 — the exception sentence is deleted, not kept accurate by
    hand. It named two repos while the inventory said three, which is the argument the
    reviewer on client#848 made: a hand-written exception list drifts on every entry, so
    empty it rather than re-write it. This repo's CLAUDE.md is re-spliced from the
    canon using standards-sync.py's own build_desired, so the block is IN_SYNC; the
    other 19 copies need a standards-sync.ymlcreate-prs dispatch.
  • .github/workflows/customer-priority-bump.yml header — the block recording the live
    defect now records the fix, the daily assertion, and the two derivation lessons.

Not done, stated rather than left implicit

  • The App token scope is unverified until the first run.permission-issues: read,
    org-scoped, no repositories: narrowing. The evidence that the installation covers
    all twenty at that scope is add-to-kanban.yml — a per-repo copy in every enrolled
    repo minting exactly permission-issues: read (backend#2166). An under-scoped token
    fails at the call, not at the mint, so the first scheduled run is the test. This is
    an audit, not a required status context, so a red first run is a finding to fix that
    day rather than a merge blocker.
  • Colour and description are not asserted, only existence. They agree fleet-wide
    today, but a wrong colour drops no label and blocks no automation.
  • from:customer's description is stale across all 21 repos: "auto-bumps Priority
    to P1", while RFC-BACKEND-0008 D5 removed the Priority field and the workflow adds
    a binary priority label. Replicated verbatim into the four new repos deliberately —
    a fleet that agrees on a wrong string is fixable in one sweep, four repos disagreeing
    is not. Worth its own ticket.
  • repo-inventory.yml was not extended with a label family. That was the ticket's
    open question. Adding a property means a row for all 20 repos and passing
    conformance-gate.yml on an exact head sha every other merge invalidates — the same
    reason customer-priority-bump.yml's own header gives for not splitting the reusable.
    A standalone sibling audit gets the assertion running now and does not stand in the
    way of folding it in later.

Closes tracebloc/backend#2598


Note

Low Risk
New advisory CI audit and local test tooling; no changes to merge gates or runtime triage workflows beyond documentation comments.

Overview
Adds ongoing enforcement that every repo declaring customer-priority-bump.yml actually has the labels automation and issue templates depend on—closing the gap where caller-drift only proved the workflow file existed while GitHub could still drop missing labels with no error.

scripts/triage-labels-check.py derives the required label set from two producers (no hard-coded list): customer-priority-bump.yml*-label defaults plus live --add-label writes (parsed YAML/shell, not comments), and .github/ISSUE_TEMPLATE/*.ymllabels:. It audits enrolled repos from repo-inventory.yml via gh api and fails closed (exit 2) on unreadable inputs, stale matchers, API failures, or gaps.

.github/workflows/triage-labels.yml runs a selftest-gated fleet check on PRs touching producers/inventory/check code and on a daily cron (labels deleted in Settings have no PR trigger). It is an advisory audit tier like kanban-columns, not a required merge context.

Makefile wires selftest-triage-labels and mutation-triage-labels into the existing selftest/mutation tiers. org-standards.md / CLAUDE.md drop the drifting per-repo bug-label exception and document fleet-wide labels plus this guard. customer-priority-bump.yml header comments are updated to record wiring complete, label gap closed, and derivation lessons (work-type:* prefix sweeps miss partial coverage).

Offline selftest (29 cases) and mutation harness (21 anchors) cover derivations, staleness guards (including parsed-vs-raw-text fixes from .github#364), and fail-open API paths.

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

… so (backend#2598)
A caller without its label is wired in exactly the way that does nothing, and
nothing could tell. `caller-drift.py` asserts a repo CARRIES
`customer-priority-bump.yml`; it says nothing about whether the labels that
workflow fires on exist there -- and GitHub silently DROPS a template label the
target repo lacks: no error, no annotation, no run. The issue is filed
unlabelled, `bug-to-ready` never sees its label, and the card sits in `Backlog`.
Measured 2026-08-27 over the 20 repos `repo-inventory.yml` declares:
`design-system-v2`, `release-train` and `rfcs` had ZERO of the seven triage
labels; `e2e-test-agent` had TWO of seven. The 26 missing labels were created
out of band with the fleet's own names, colours and descriptions, so this lands
green rather than as a red gate.
`e2e-test-agent` was invisible to the ticket as filed, because that derived over
the `work-type:*` PREFIX. Deriving a domain from a prefix rather than from the
producers is CLAUDE.md rule 6's vocabulary gap -- committed by the ticket written
to close it. So the check holds no label list: it parses this reusable's
`*-label` input defaults and its `--add-label` writes, plus every
`.github/ISSUE_TEMPLATE/*.yml` `labels:` entry, and takes its repo scope from the
inventory. Add a template or a third label rule and the domain moves with it.
Fails closed throughout: an unreadable inventory, workflow, template or repo, a
zero-length derived domain, an empty fleet, and a stale matcher are each exit 2.
A 403 is UNKNOWN, never "this repo has no labels".
The exception sentence in `org-standards.md` is deleted rather than kept accurate
by hand -- it named two repos while the inventory said three -- and this repo's
CLAUDE.md is re-spliced from the canon by `standards-sync.py`'s own splice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodkaLukasWodka self-assigned this Aug 27, 2026

@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 babb48c. Configure here.

Comment threadscripts/triage-labels-check.py Outdated
…d#2598)
Bugbot on .github#364, and it is right in the way this repo keeps finding:
`stale_idiom` asked whether the strings `-label` and `--add-label` appeared
anywhere in the workflow TEXT -- which a comment satisfies, including the
comments the same PR added.
The cost is specific, not stylistic. Rename the `*-label` inputs away and the
backstop stays quiet; the caller family then silently loses `from:customer`,
which NO template applies, so the derived domain shrinks and the check goes
GREEN over a dead `bump` rule. That is the inert-verification shape
backend#1729 catalogued, and `kanban-columns-check.py`'s `names_in` carries the
same lesson from e2e#176.
Both halves are now answered from the PARSED document, by the same functions the
derivation itself uses, so no comment anywhere can feed either:
* `label_inputs(doc)` -- the `workflow_call` inputs whose names end in
`-label`. Also handles `on: push` parsing to a string rather than crashing.
* `run_scripts(doc)` -- every `run:` body with shell comment lines stripped.
`ADD_LABEL` matches over this, never over the file, so a commented-out
`--add-label ghost` cannot put a name that exists nowhere under a
fleet-wide assertion.
Three new selftest cases, each pinning a direction the raw-text version got
wrong: a `*-label` in a comment must not satisfy the guard (false green), an
`--add-label` in a comment must not raise a finding (false red), and a
commented-out `--add-label` must not enter the domain.
Suite 32/32; mutation tier 25/25 caught, including a mutation that reverts each
half to the exact raw-text shape Bugbot flagged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Bugbot finding addressed in e0cbc7d — and it was a real defect of the class this repo catalogues, not a nit.

stale_idiom asked whether the strings -label and --add-label appeared anywhere in the workflow text, which a comment satisfies — including the comments this PR itself added. The cost is specific: rename the *-label inputs away and the backstop stays quiet, the caller family silently loses from:customer (no template applies it), the derived domain shrinks, and the check goes green over a dead bump rule. kanban-columns-check.py's names_in carries the same lesson from e2e#176; I had read that file and still wrote the raw-text version.

Both halves are now answered from the parsed document, by the same functions the derivation itself uses, so no comment can feed either:

  • label_inputs(doc) — the workflow_call inputs whose names end in -label (and on: push parsing to a string is handled rather than crashed on).
  • run_scripts(doc) — every run: body with shell comment lines stripped. ADD_LABEL matches over this, never over the file, so a commented-out --add-label ghost cannot put a name that exists nowhere under a fleet-wide assertion.

Three new cases, one per direction the raw-text version got wrong:

PASS: a `*-label` mentioned only in a COMMENT does NOT satisfy the guard
PASS: an `--add-label` mentioned only in a COMMENT raises no false finding
PASS: a commented-out `--add-label` does not enter the domain

The second exists because the two halves fail in opposite directions — the *-label half fails open (false green), the --add-label half fails closed (bogus red). One case could not cover both, and my first attempt at the mutation went UNCAUGHT for exactly that reason before I added it.

Evidence: suite 32/32; mutation tier 25/25 caught, including a mutation that reverts each half to the precise raw-text shape flagged here. make check green. Live check still green across all 20 enrolled repos.

LukasWodkaand others added 2 commits August 27, 2026 20:30
…ackend#2598)
The residual @LukasWodka recorded on .github#364. `caller_labels` returned
`{"priority": ...}` from the `--add-label` idiom alone when the `*-label` inputs
were gone -- a domain silently missing `from:customer`, which no template
applies. Unreachable as a SILENT failure today only because `main()` calls
`stale_idiom` before the sweep.
"Unreachable via its sibling" is a weaker property than "refuses on its own",
and it stops being true the day someone reorders `main()`. CLAUDE.md rule 7: a
claim that something cannot happen belongs in a machine check where one fits,
and here one fits in four lines.
Suite 33/33; mutation tier 26/26, the new one deleting the refusal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both PRs added a runner to MUTATION_TARGETS and SELFTEST_TARGETS, so both lists
conflicted. Resolved as the UNION rather than by picking a side: `selftests-cover`
fails closed on any file under scripts/tests/ that no target names, so keeping
one side would have made the other branch runner invisible AND reddened the
coverage guard.
Verified on the merged tree:
selftests-cover 21 selftests, 11 mutation runners, all wired
triage-labels 33/33 selftest, 26/26 mutations
conflict-gate 115 assertions
Co-Authored-By: Claude Opus 4.8 <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.

1 participant

@LukasWodka