ceremony-ci-probe/LABELS.md
2026-07-23 14:57:51 +00:00

10 KiB
Raw Blame History

Labels

The taxonomy shared across the heavy-duty repos. Only the scope: set differs per repo (each repo's .github/labels.conf names its actual surfaces); everything else below is core and identical everywhere, created by the labels workflow's bootstrap dispatch (issue #10).

Two state machines share the taxonomy: the PR machine (proven in box/rig/cast, reconciled by machinery) and the issue flow (the triage → build queue, reconciled by the work-queue sweep). One rule joins everything: states are machine-owned, intent labels are hand-set — a hand-moved state label is a lie waiting to happen, and the reconciler recomputes it from GitHub's own facts.

PR state — who is the ball with? (exactly one per open PR)

Label Color Waiting on
state:building #FBCA04 the builder — PR is a draft
state:bots-reviewing #1D76DB the reviewer panel to finish the round (a request is live)
state:addressing #D93F0B the builder — round complete without full approval, or nobody was asked, or a blocker is up, or a ruling is pending
state:needs-human #8250DF the human — this PR could be merged right now: zero blockers, whole panel approved the current head

bots-reviewing vs addressing is deliberate: staleness in the first means poke the reviewers, in the second the builder dropped the ball. And state:needs-human means exactly one thing — a human could merge this now — so it requires zero blockers and head-current approvals; anything less and the reconciler takes it back. The author sets it at handoff (the one hand-set state); the labeled event fires the sweep that validates the write within seconds.

PR blockers — what is in the way? (facts, as many as apply)

Label Color Means
blocker:conflict #B60205 does not merge — the builder owes a rebase
blocker:ci-red #B60205 a check failed — the builder owes a fix, which a rebase will not provide
blocker:unrequested #E99695 this head has no verdict from somebody, and nobody was asked
blocker:drill-pending #B60205 a release PR whose version has no drills/X.Y.Z.md record — correct but unevidenced (maintainer-created label; the bot bootstrap 403s on it)

States answer whose ball; blockers answer what's in the way. They are separate axes because the single-label version kept lying — independent facts projected onto one totally-ordered label meant one always won and the losers vanished off the board (box's state:needs-rebase, retired: the reconciler strips it on sight).

Issue flow — the work queue (exactly one per open, triaged, non-epic issue)

Label Color Means Set by
needs-triage #FBCA04 an issue that did not come through triage — it owes normalization or conversion back to a discussion anyone who spots one; cleared by triage
ready #0E8A16 triaged, spec complete, unblocked — a builder can start now and succeed triage
claimed #1D76DB a builder owns it: assignee set, a draft PR expected shortly the claiming builder
blocked #6A737D waiting on another issue or PR (Blocked by #N in the body names it) triage; anyone may correct it
epic #5319E7 organizes other issues via a dependency-ordered task list; builders never pick an epic triage

The work-queue sweep enforces the invariant a board scan relies on: every open issue is either needs-triage, epic, or carries exactly one of ready / claimed / blocked. It flags conflicts rather than guessing intent. A claimed issue with no open PR and no activity for 48 hours is reclaimed by the sweep: it comments, unassigns the stale owner, and restores ready.

Cross-cutting (PRs and issues)

Label Color Meaning
stale #B60205 no activity for 48h — sweep-managed, never hand-applied
blocked #6A737D (see above — same label serves PRs waiting on another PR/issue; legitimately quiet, the staleness sweep skips it)
offsite #CFD3D7 issue deliverable is a PR in another repository; set by the builder with the draft link and cleared by the builder at handoff
needs-ruling #D4C5F9 a human-owned decision is required; use BUILDER.md's ruling template and ladder. Set by triage or the builder; a state, not a signal — it clears on agreement, not on a reply
release #0E8A16 release flow, versioning, packaging work — and the ceremony PR itself
merge-next #0E8A16 head of the merge queue — merge this one next. Queue order is intent: never set by the reconciler, only cleared by it

needs-ruling marks where the human's turn is when the pending thing is a decision, not a merge (#50 D1D14). It applies to any human-owned decision — org policy, published artifacts, secrets, prod, or any choice whose cost lands outside the work. A panel deadlock is one instance, not the definition (D11). It is not state:needs-human: that label means exactly "this PR could be merged right now", and the retired state:needs-rebase is the family's proof that a label meaning two things lies about both. It is not a blocker:* either: every blocker names work the builder owes, a ruling is owed by the human — and the flag must live on issues too, where blockers do not exist. On issues it coexists with the queue labels (the one-of-three invariant above ignores it); its color is the light shade of state:needs-human's, so the human axis reads as one family. It is a state, not a signal: set only with the canonical escalation contract (D12). A bare flag is noise. The comment carries exhaustive, mutually exclusive options (at most three), a mandatory recommendation, what stops and what continues, and either a default affirmatively known to be reversible inside the PR or none — hard block. Unsure is a block; published artifacts, secrets, prod, and org policy are hard blocks by construction (D13).

The ruling ladder runs from the current episode's needs-ruling labeled event (D13D14):

  • 012h: a clear, reversible decision may proceed when its stated default expires, saying out loud that it did; anything with reasonable doubt waits as a hard block.
  • at 12h: the setter re-reads the default against what has landed and asks whether it still holds and whether doubt remains. A stale default does not fire; new doubt makes it a hard block.
  • at 24h: the builder proceeds regardless, as a PR, stating the option chosen and the doubt that remains. Nothing merges by this; the human still gates the merge.
  • past 24h: triage picks the option, records it as a decision, and remains accountable. The operator may overturn it at merge.

A re-flag starts a new ladder. The rungs apply whatever Default: says, including a hard block. Active discussion still climbs the ladder; by contrast, the separate 7-day nudge resets on real activity. The machine observes the rungs but never sets, clears, or decides needs-ruling.

The flag stays up until agreement is reached — a human reply alone does not clear it — and its setter closes it out: records the ruling as a decision in one comment, removes the label, and returns the item to its flow in that same comment, never as a side effect. If the human disagrees that agreement was reached, the label goes back on. The reconciler refuses state:needs-human while it stands (the PR falls to state:addressing — the ball on the PR is the builder's, who carries the ruling in), and the staleness sweep skips it, because waiting on a human is legitimately quiet. Quiet, but not unwatched (#52, both surfaces): a flag set with no escalation comment from its setter is called out by the sweep — comment-only, scoped to the labeled event, the label never removed — and a ruling with no real activity for 7 days draws a comment-only nudge addressed to the decider, linking the escalation. The nudge carries no marker on purpose: the comment is itself activity, so it resets its own window and never repeats within a quiet week. Label churn is not activity — the clock reads comments, reviews and commits, or the sweep would reset itself.

offsite is issue-only and records that a claimed issue's deliverable lives in another repository, where a closing reference cannot make a local open PR visible to the sweep (#68). The builder sets it in the same step that posts the cross-repo draft link, then clears it at handoff in the same comment that reports whether that PR merged or closed. The machine reads the flag and never writes it. It stops only the claim-reclaim clock: missing assignees are still flagged, queue-label conflicts and missing queue state are still repaired, and epic-completion and PR-side stale behavior are unchanged. The sweep tells the assignee once when every visible cross-referenced PR has closed; it only tells, and never clears the flag or changes the claim.

Scope — which surface? (PRs and issues, any number)

All scopes share one calm color, #C5DEF5 — scopes locate, states alert. The set is per-repo (.github/labels.conf); PRs get theirs from changed paths via actions/labeler, issues get theirs from triage. This repo's set:

Label Covers
scope:release-flow the reusable release workflow, decide, the doors
scope:guards changelog-armed / changelog-monotonic / drill-recorded
scope:labels the labels workflow, reconciler, this taxonomy
scope:docs README doctrine, CONSUMERS.md, the role files

Issue types

bug, enhancement, documentation — issues only, set by triage. PRs carry their type in the conventional title (feat:, fix:, docs:); a type label on a PR would say the same thing twice and drift.

Maintenance

The labels workflow (issue #10) recomputes PR state statelessly on PR events plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on manual dispatch. The same workflow reconciles issue-flow labels on issue events and during the scheduled sweep. Default GitHub labels (duplicate, invalid, question, wontfix, help wanted, good first issue) are deleted at bootstrap — a question is a discussion, not an issue.