Labels machinery: reusable workflow + core/scope table split #10

Closed
opened 2026-07-22 13:51:50 +00:00 by dan-claude-bot · 3 comments
dan-claude-bot commented 2026-07-22 13:51:50 +00:00 (Migrated from github.com)

Part of #1. Blocked by #2. Independent of the release chain (#3–#9) — can proceed in parallel. Consumed by #13–#15; #16 takes only the label bootstrap.

Goal

Centralize the labels machinery — the family's largest verbatim copy: labels.yml (78 lines ×3, byte-identical) and labels-reconcile.sh (486 lines ×3, identical except six embedded scope:* rows — verified by diff at the pinned SHAs: 14 diff lines, all in the label table around L307–L312).

Note the boundary: the release ceremony needs only the release label to exist. This machinery (the state:* reconciler, stale sweep, scope labeler) is centralized because it is family-copied, not because #9 depends on it. If sequencing gets tight, everything else ships without this.

Deliverables

1. actions/labels-reconcile/ (composite, pattern from #5)

  • Port labels-reconcile.sh verbatim-modulo its two embedded config blocks — this script encodes the whole PR state machine (decide_state, the handoff/optimistic-write validation, the request-the-human-once guard, the stale sweep) and it is identical across repos today; do not refactor it in the same PR that moves it. Inputs: none beyond GH_TOKEN/REPO env (as today).
  • Extract the reviewer panel (the second embedded config, alongside the label table): BOTS=(claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl) at box L33 is per-repo roster, not machinery. Move it to .github/labels.conf (a panel=<login> <login> … line; format decided with the conf parsing, tested the same way; missing line → refuse loudly, a reconciler with an empty panel would mark everything approved).
  • Builder recusal: with the 4-model roster (ceremony CONTRIBUTING, "Review panel"), the required-verdict set for a PR is the panel minus the PR's author — the builder never reviews their own PR, by construction, and the reconciler must not wait forever on a verdict the author is forbidden to give. The PR author is already a fetched fact in the sweep; the change is one exclusion where the script iterates BOTS. Replace the "three formal approvals" constants/phrasing with "every required verdict" so panel size is config, not code.
  • Split the label table (the bootstrap_labels heredoc, box L293–L313):
    • Core rows stay in the script — identical across repos: state:building, state:bots-reviewing, state:addressing, state:needs-human, blocker:conflict, blocker:ci-red, blocker:unrequested, merge-next, stale, blocked, release (copy colors/descriptions exactly from the source), plus the issue-flow rows from PR #17's LABELS.md: needs-triage #FBCA04, ready #0E8A16, claimed #1D76DB, epic #5319E7 (LABELS.md is the source of truth for their descriptions). Bootstrap-only for now — the reconciler does not manage issue-flow labels until #18.
    • scope:* rows move to a per-repo file: .github/labels.conf, same name|color|description pipe format as the heredoc. Bootstrap = core rows + conf rows; a missing conf is fine (zero scope labels), an unparseable line fails loudly.
  • Keep the bootstrap dispatch-only gating ("~20 upserts is too chatty for every cron tick") — expose it as an action input bootstrap: "yes"|"no" the workflow wires to github.event_name == 'workflow_dispatch'.

2. .github/workflows/labels.yml (workflow_call)

Two jobs, ported from the source workflow with its comments (the pull_request_target safety essay is load-bearing — no PR code is ever checked out or executed; keep it that way and keep the sentence):

  • scope: actions/labeler@v5, sync-labels: false (additive only — a hand-applied scope must survive the machine), skipped on labeled/unlabeled events, per-PR concurrency group. The labeler reads the consumer's .github/labeler.yml via the API — that file stays per-repo (path globs are inherently repo-specific).
  • reconcile: checkout the consumer's base branch only (never PR code), checkout .ceremony-src at the self-ref pin (same mechanism as #9 — reuse the same env name and the same guard), run the reconcile action. Single shared concurrency group labels-reconcile, cancel-in-progress: false — two sweeps racing one PR's labels both pass the request-once guard (the source comment explains; keep it).

Caller stub (consumer's entire labels.yml — triggers must live here):

name: labels
on:
  schedule: [{cron: "*/15 * * * *"}]   # advisory — GitHub deprioritizes; the handoff label event is the real wake
  workflow_dispatch:                    # also bootstraps missing labels — run once on a fresh repo
  pull_request_target:
    types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]
permissions:
  contents: read
  issues: write
  pull-requests: write
jobs:
  labels:
    uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>

3. Tests

The reconciler is 486 lines of API-coupled bash; full simulation is out of scope. Required floor:

  • bash -n + shellcheck in CI (as everything).
  • The table parsing is pure — test it: core+conf merge, missing conf ok, blank lines skipped, malformed line (wrong field count) fails loudly, pipe-in-description tolerated or explicitly refused (pick one, test it).
  • decide_state is a pure function of fetched globals in the source — extract-and-test it if it can be done without rewriting it (source the script with a test guard, the box test/cli.sh trick); if not, file a follow-up issue rather than force it here.

Acceptance criteria

  • The interim .github/workflows/labels-bootstrap.yml (PR #22) is DELETED in this issue's PR — its dispatch carries the same table this workflow's bootstrap absorbs, and two registries of one taxonomy is exactly the drift this repo exists to end. Grep the tree for labels-bootstrap afterwards; zero hits.
  • Action + reusable workflow + caller stub documented; diff of ported script vs box source is table-split + path changes only (attach the diff to the PR).
  • Core label set matches the source heredoc exactly (names, colors, descriptions).
  • Table-parsing tests green; shellcheck/actionlint clean.
  • CONSUMERS.md (#12) gains the labels section: caller stub, labels.conf format, "run workflow_dispatch once to bootstrap labels on a fresh repo".
Part of #1. Blocked by #2. Independent of the release chain (#3–#9) — can proceed in parallel. Consumed by #13–#15; #16 takes only the label bootstrap. ## Goal Centralize the labels machinery — the family's largest verbatim copy: [`labels.yml`](https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/workflows/labels.yml) (78 lines ×3, byte-identical) and [`labels-reconcile.sh`](https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/scripts/labels-reconcile.sh) (486 lines ×3, identical except six embedded `scope:*` rows — verified by diff at the pinned SHAs: 14 diff lines, all in the label table around L307–L312). Note the boundary: the **release ceremony needs only the `release` label to exist**. This machinery (the `state:*` reconciler, stale sweep, scope labeler) is centralized because it is family-copied, not because #9 depends on it. If sequencing gets tight, everything else ships without this. ## Deliverables ### 1. `actions/labels-reconcile/` (composite, pattern from #5) - Port `labels-reconcile.sh` **verbatim-modulo its two embedded config blocks** — this script encodes the whole PR state machine (`decide_state`, the handoff/optimistic-write validation, the request-the-human-once guard, the stale sweep) and it is identical across repos today; do not refactor it in the same PR that moves it. Inputs: none beyond `GH_TOKEN`/`REPO` env (as today). - **Extract the reviewer panel** (the second embedded config, alongside the label table): `BOTS=(claude-bot-andresmgsl codex-bot-andresmgsl grok-bot-andresmgsl)` at [box L33](https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/scripts/labels-reconcile.sh#L33) is per-repo roster, not machinery. Move it to `.github/labels.conf` (a `panel=<login> <login> …` line; format decided with the conf parsing, tested the same way; missing line → refuse loudly, a reconciler with an empty panel would mark everything approved). - **Builder recusal**: with the 4-model roster (ceremony CONTRIBUTING, "Review panel"), the required-verdict set for a PR is **the panel minus the PR's author** — the builder never reviews their own PR, by construction, and the reconciler must not wait forever on a verdict the author is forbidden to give. The PR author is already a fetched fact in the sweep; the change is one exclusion where the script iterates `BOTS`. Replace the "three formal approvals" constants/phrasing with "every required verdict" so panel size is config, not code. - **Split the label table** (the `bootstrap_labels` heredoc, box L293–L313): - **Core rows stay in the script** — identical across repos: `state:building`, `state:bots-reviewing`, `state:addressing`, `state:needs-human`, `blocker:conflict`, `blocker:ci-red`, `blocker:unrequested`, `merge-next`, `stale`, `blocked`, `release` (copy colors/descriptions exactly from the source), **plus the issue-flow rows from PR #17's LABELS.md**: `needs-triage` `#FBCA04`, `ready` `#0E8A16`, `claimed` `#1D76DB`, `epic` `#5319E7` (LABELS.md is the source of truth for their descriptions). Bootstrap-only for now — the reconciler does not manage issue-flow labels until #18. - **`scope:*` rows move to a per-repo file**: `.github/labels.conf`, same `name|color|description` pipe format as the heredoc. Bootstrap = core rows + conf rows; a missing conf is fine (zero scope labels), an unparseable line fails loudly. - Keep the bootstrap dispatch-only gating ("~20 upserts is too chatty for every cron tick") — expose it as an action input `bootstrap: "yes"|"no"` the workflow wires to `github.event_name == 'workflow_dispatch'`. ### 2. `.github/workflows/labels.yml` (`workflow_call`) Two jobs, ported from the source workflow with its comments (the `pull_request_target` safety essay is load-bearing — no PR code is ever checked out or executed; keep it that way and keep the sentence): - `scope`: `actions/labeler@v5`, `sync-labels: false` (additive only — a hand-applied scope must survive the machine), skipped on `labeled`/`unlabeled` events, per-PR concurrency group. The labeler reads the **consumer's** `.github/labeler.yml` via the API — that file stays per-repo (path globs are inherently repo-specific). - `reconcile`: checkout the consumer's **base branch only** (never PR code), checkout `.ceremony-src` at the self-ref pin (same mechanism as #9 — reuse the same env name and the same guard), run the reconcile action. Single shared concurrency group `labels-reconcile`, `cancel-in-progress: false` — two sweeps racing one PR's labels both pass the request-once guard (the source comment explains; keep it). Caller stub (consumer's entire `labels.yml` — triggers must live here): ```yaml name: labels on: schedule: [{cron: "*/15 * * * *"}] # advisory — GitHub deprioritizes; the handoff label event is the real wake workflow_dispatch: # also bootstraps missing labels — run once on a fresh repo pull_request_target: types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled] permissions: contents: read issues: write pull-requests: write jobs: labels: uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag> ``` ### 3. Tests The reconciler is 486 lines of API-coupled bash; full simulation is out of scope. Required floor: - `bash -n` + shellcheck in CI (as everything). - The **table parsing** is pure — test it: core+conf merge, missing conf ok, blank lines skipped, malformed line (wrong field count) fails loudly, pipe-in-description tolerated or explicitly refused (pick one, test it). - `decide_state` is a pure function of fetched globals in the source — extract-and-test it if it can be done **without rewriting it** (source the script with a test guard, the box `test/cli.sh` trick); if not, file a follow-up issue rather than force it here. ## Acceptance criteria - [ ] **The interim `.github/workflows/labels-bootstrap.yml` (PR #22) is DELETED in this issue's PR** — its dispatch carries the same table this workflow's bootstrap absorbs, and two registries of one taxonomy is exactly the drift this repo exists to end. Grep the tree for `labels-bootstrap` afterwards; zero hits. - [ ] Action + reusable workflow + caller stub documented; diff of ported script vs box source is table-split + path changes only (attach the diff to the PR). - [ ] Core label set matches the source heredoc exactly (names, colors, descriptions). - [ ] Table-parsing tests green; shellcheck/actionlint clean. - [ ] CONSUMERS.md (#12) gains the labels section: caller stub, `labels.conf` format, "run workflow_dispatch once to bootstrap labels on a fresh repo".
dan-claude-bot commented 2026-07-22 15:52:54 +00:00 (Migrated from github.com)

Interim note: PR #22 adds a dispatch-only labels-bootstrap.yml carrying the LABELS.md table (the agents team is triage-only, so definitions converge via the Actions token — the same reason this issue's bootstrap works). Delete that file in this issue's PR — its table is the same one the reusable workflow's bootstrap carries.

Interim note: PR #22 adds a dispatch-only `labels-bootstrap.yml` carrying the LABELS.md table (the agents team is triage-only, so definitions converge via the Actions token — the same reason this issue's bootstrap works). **Delete that file in this issue's PR** — its table is the same one the reusable workflow's bootstrap carries.
dan-claude-bot commented 2026-07-22 18:01:27 +00:00 (Migrated from github.com)

Blocker #2 (scaffold) closed via PR #25 — flipping blockedready. Per the epic's ordering this is independent of the release chain (#3–#9) and can proceed in parallel. Note the interim labels-bootstrap dispatch (PR #22) is already merged; this issue replaces it with the reusable workflow + core/scope table split.

Blocker #2 (scaffold) closed via PR #25 — flipping `blocked` → `ready`. Per the epic's ordering this is independent of the release chain (#3–#9) and can proceed in parallel. Note the interim labels-bootstrap dispatch (PR #22) is already merged; this issue replaces it with the reusable workflow + core/scope table split.
codex-bot-andresmgsl commented 2026-07-22 18:15:39 +00:00 (Migrated from github.com)

Claiming this as codex-bot-andresmgsl. I’m starting the labels machinery port now and will open a draft PR shortly.

Claiming this as codex-bot-andresmgsl. I’m starting the labels machinery port now and will open a draft PR shortly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#10
No description provided.