forked from heavy-duty/rig
docs: CONTRIBUTING keeps only rig's specifics — the doctrine is the vendored .ceremony/ mirror (ceremony#13)
Root LABELS.md was rig's own copy of the family taxonomy doc; the vendored .ceremony/LABELS.md is the driftless replacement, so the copy goes. drills/README.md's gate pointer follows the guard upstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
36da497352
commit
119fb68359
3 changed files with 49 additions and 359 deletions
218
CONTRIBUTING.md
218
CONTRIBUTING.md
|
|
@ -1,61 +1,32 @@
|
|||
# Contributing
|
||||
|
||||
How change lands in this repo. The short version: PRs are born as drafts,
|
||||
three reviewer bots take the first rounds, a human takes the last word — and
|
||||
labels tell you where everything is without opening anything.
|
||||
This repo is governed by
|
||||
[heavy-duty/ceremony](https://github.com/heavy-duty/ceremony). **Agents:
|
||||
read [`.ceremony/AGENTS.md`](.ceremony/AGENTS.md) first** — it routes you to
|
||||
your role file (builder, reviewer, triage), vendored beside it,
|
||||
byte-identical to ceremony at the pin named in
|
||||
[`.github/workflows/release.yml`](.github/workflows/release.yml) and
|
||||
guarded by the `docs-sync` step in CI. The review-round doctrine — drafts,
|
||||
whole-round replies, verdicts, the handoff — lives there and in
|
||||
[`.ceremony/LABELS.md`](.ceremony/LABELS.md); this file keeps only what is
|
||||
genuinely rig's.
|
||||
|
||||
## The PR loop
|
||||
## The PR loop, rig specifics
|
||||
|
||||
1. **Fork and branch.** Contributors work from forks; upstream branches are
|
||||
for maintainers. Title the PR conventionally (`feat:`, `fix:`, `docs:`).
|
||||
2. **Open as a draft** while you build. Drafts are invisible to the reviewer
|
||||
bots on purpose.
|
||||
3. **When it's ready**: mark ready-for-review and request all three bots —
|
||||
`claude-bot-andresmgsl`, `codex-bot-andresmgsl`, `grok-bot-andresmgsl`.
|
||||
They poll roughly every 15 minutes.
|
||||
4. **Rounds are answered whole.** Wait until all three have reviewed, then
|
||||
answer the entire round in a **single reply**, push the fixes, and
|
||||
re-request the bots that didn't approve. Prefer verification over
|
||||
argument: a test settles what a comment thread can't.
|
||||
5. **Reviews end in a verdict.** A reviewer — bot or human — either
|
||||
**approves** or **requests changes**, never a bare comment. A
|
||||
comment-only review is a non-verdict: it doesn't say whether the round
|
||||
passed, and the state machine (and anyone scanning the board) has to
|
||||
guess. The verdict carries *blockingness only*, the body carries the
|
||||
feedback: non-blocking nits ride an **approval** and the author addresses
|
||||
them at their discretion; anything blocking — including a question that
|
||||
gates the verdict — is **request changes**, saying what unblocks it. The
|
||||
reconciler treats a comment-only review as not-approved, so commenting
|
||||
without a verdict only stalls the PR. The machine never reads review
|
||||
bodies: when a comment-only reviewer's line is really an agreement, that
|
||||
judgment belongs to the **author** — escalate by requesting the
|
||||
maintainer's review (step 6), and the reconciler flips the label on that
|
||||
request, because an explicit request is a fact it can trust.
|
||||
6. **When the round passes, the author hands the PR to the maintainer** in
|
||||
three acts, in this order: post the tagged round summary, request the
|
||||
maintainer's review, then set `state:needs-human` yourself — removing the
|
||||
state label it replaces. The review request is what *earns* the label,
|
||||
provided the PR carries **no `blocker:*` label**. A blocker means the work
|
||||
is still yours whatever the round said, so on a conflicted or red PR
|
||||
neither the request nor your own label write will stick — the sweep takes
|
||||
it straight back off. With three formal head-current approvals the labels
|
||||
workflow requests the maintainer automatically; when part of the panel is
|
||||
comment-only, reading their agreement is the author's judgment, so the
|
||||
author makes the request.
|
||||
|
||||
Writing the label by hand is an **optimistic write, not a transfer of
|
||||
ownership**. The machine stays the authority — but because the workflow
|
||||
wakes on `labeled`, the author's own write fires the sweep that validates
|
||||
it, and a handoff that had not earned the label is corrected seconds later.
|
||||
Forgetting the write is not a failure either; it only means the label waits
|
||||
for the cron, which is the lag this replaced.
|
||||
7. **Checks must be green**: `shellcheck`, `bash test/cli.sh` and
|
||||
2. **The review panel** (`.github/labels.conf`'s `panel=` line):
|
||||
`claude-bot-andresmgsl`, `codex-bot-andresmgsl`, `grok-bot-andresmgsl` —
|
||||
the required verdicts for a PR are the panel minus its author. The
|
||||
maintainer (`danmt`) takes the last word and merges.
|
||||
3. **Checks must be green**: `shellcheck`, `bash test/cli.sh` and
|
||||
`bash test/release.sh` locally mirror what CI runs; the db dump/restore
|
||||
round-trip (`test/db-integration.sh`) executes in CI where Docker is
|
||||
present.
|
||||
8. **Feature PRs land their changelog entry as part of the PR** (box's
|
||||
convention): add it under `CHANGELOG.md`'s `## Unreleased` heading —
|
||||
that section becomes the release notes verbatim when a release is cut.
|
||||
present. The release guards (`changelog-armed`, `changelog-monotonic`,
|
||||
`drill-recorded`, `docs-sync`) run as ceremony's pinned actions.
|
||||
4. **Feature PRs land their changelog entry as part of the PR**: add it
|
||||
under `CHANGELOG.md`'s `## Unreleased` heading — that section becomes
|
||||
the release notes verbatim when a release is cut.
|
||||
|
||||
## Changelog entries
|
||||
|
||||
|
|
@ -91,134 +62,39 @@ Not an entry — that is a PR body:
|
|||
|
||||
## Releasing
|
||||
|
||||
A release is a PR, and merging it is the release (#47; box#96's design, on
|
||||
top of #32/box#83's tag flow). It takes the ordinary PR loop above, with one
|
||||
extra gate before the handoff:
|
||||
A release is a PR, and merging it is the release. The ceremony — the two
|
||||
doors, the decide table, the stamps, the post-release re-arm — is
|
||||
heavy-duty/ceremony's machinery, consumed by reference:
|
||||
[its README](https://github.com/heavy-duty/ceremony/blob/main/README.md)
|
||||
is the doctrine, `.github/workflows/release.yml` here is the ≤20-line
|
||||
caller pinning it, and the guards run in `ci.yml` from the same pin.
|
||||
Bare `X.Y.Z` tags, no `v`; the tag's source tarball is the package
|
||||
`install.sh` downloads — rig ships no other artifact.
|
||||
|
||||
**draft → ready → bot round → drill → `state:needs-human` → maintainer merge
|
||||
(which IS the release).**
|
||||
|
||||
The **drill** is a real-hardware run — tenant guests minted and converged via
|
||||
box, `test/db-integration.sh`, the GitHub runner lifecycle against a fork, a
|
||||
coolify install — recorded in **one file per version**:
|
||||
|
||||
```
|
||||
drills/<version>.md
|
||||
```
|
||||
|
||||
named for the version exactly as `VERSION` carries it. See
|
||||
[`drills/README.md`](drills/README.md) for what a record should contain.
|
||||
|
||||
`.github/scripts/drill-recorded.sh` enforces it on every release: a bare
|
||||
`VERSION` with no non-empty `drills/<version>.md` turns CI red, naming the
|
||||
version. It is **not a thing a reviewer has to remember** — that is how every
|
||||
release in this family shipped undrilled until a bot finally blocked on one. On
|
||||
a `-dev` tree it asserts nothing, so it is invisible to ordinary PRs. rig reads
|
||||
rig's own record and never box's repo: a cross-repo lookup fails on a token,
|
||||
a fork checkout or a network blip, and all of those degrade to "pass" —
|
||||
the UNREADABLE-vs-NONE shape #90 fixed.
|
||||
|
||||
One file per version is what keeps the guard small. Records used to share a
|
||||
single log, which forced a heading grammar, an optional-date tail, a
|
||||
whole-version comparison and a non-blank-body rule just to read them back — and
|
||||
both sibling repos shipped a defect out of that complexity in review. Now
|
||||
`0.3.0.md` and `0.3.0-rc1.md` are simply different files.
|
||||
|
||||
**The three repos' drills are INDEPENDENT.** Run them in any order, on any
|
||||
schedule, in separate sittings. What makes that safe is that every drill **pins
|
||||
the same fixed set of candidate refs**: rig's drill runs `--host yes` with
|
||||
`BOX_REF=release/<box-version>`, so it exercises the box that will actually
|
||||
ship; box's drill mints with `RIG_REF=release/<rig-version>`, so it exercises
|
||||
the rig that will actually ship. Both measure the same pair.
|
||||
|
||||
That — not sequencing — is what dissolves the box↔rig recursion. box and rig
|
||||
are mutually recursive (`rig bootstrap … --host yes` installs box and runs
|
||||
box's `setup-host`; box's `box new` seeds converge back through rig's installer
|
||||
at `@RIG_REPO@/@RIG_REF@`), but the refs are static identifiers that exist as
|
||||
soon as the release branches do, long before any drill runs, so a cycle at
|
||||
runtime becomes independent tests against one fixed pair. Within a single drill
|
||||
you naturally bring the substrate up before probing it — a host before a guest
|
||||
— but that is how you run a drill, not an ordering rule between repos.
|
||||
|
||||
Each repo drills in a **different way** and asserts a different thing: rig
|
||||
asserts **convergence** (a machine reaches its role, idempotently), box asserts
|
||||
the **isolation contract** (the VM trust boundary), cast asserts **promotion**
|
||||
(A→B reproduces, the diff is idempotent). Three different exercises sharing a
|
||||
substrate, not three phases of one script — which is exactly why the records
|
||||
are per-repo.
|
||||
|
||||
It drills **candidate refs, not released artifacts.** `RIG_REPO`/`RIG_REF` are
|
||||
mint-time environment variables (default `heavy-duty/rig@main`), so a run pins
|
||||
the exact commits under test. That is what dissolves the chicken-and-egg: no
|
||||
repo has to be released before another can be drilled.
|
||||
|
||||
**Drilling the candidate IS drilling the release.** A release PR's diff is
|
||||
`VERSION` + `CHANGELOG.md` and nothing else — no executable difference exists
|
||||
between the tree that was drilled and the tree that ships.
|
||||
|
||||
Drills that share a substrate share **one run ID**. Each repo records *its own*
|
||||
legs in its own `drills/<version>.md`, citing that run ID and the other two
|
||||
repos' commit SHAs, so the records can be joined after the fact by anyone
|
||||
reading them. The guard still reads only this repo's file — there is no
|
||||
cross-repo lookup anywhere in the gate. Releases do **not** have to be
|
||||
published in a fixed order. If a defect shows up only in the combination:
|
||||
patch, re-drill, re-record. The three releases converge on a set that holds
|
||||
together; they are not required to be right in one pass.
|
||||
|
||||
A **maintainer waiver** is possible — a doc-only release, a hardware outage —
|
||||
but it must be **recorded in `drills/<version>.md` for that version**, saying
|
||||
who waived it and why. The guard asks for a *record*, not a passing result,
|
||||
precisely so that skipping is a deliberate, reviewable commit instead of a
|
||||
silence. Deleting the check is not the move.
|
||||
|
||||
The mechanics:
|
||||
|
||||
1. A small PR — `release: X.Y.Z`, carrying the `release` label — bumps
|
||||
`VERSION` from `X.Y.Z-dev` and stamps `CHANGELOG.md`'s Unreleased
|
||||
section as `## X.Y.Z — YYYY-MM-DD`. **Then re-arm the file in the same
|
||||
PR**: add a fresh, empty `## Unreleased` immediately above the section
|
||||
you just stamped (#66). Stamping alone *disarms* main — a PR authored
|
||||
before the release and merged after it wrote its entry under
|
||||
`## Unreleased`, and with that heading gone git files the entry under
|
||||
whatever now occupies the position, which is the release that already
|
||||
shipped. It lands cleanly, with no conflict and nothing for the author
|
||||
to notice, so the empty section is the only thing standing between a
|
||||
late merge and a changelog that misattributes a shipped release. No
|
||||
workflow does this for you: `release.yml` re-arms `VERSION`, never the
|
||||
changelog. `test/release.sh` enforces the pairing — whenever `VERSION`
|
||||
ends in `-dev` the top section must be `## Unreleased`. CI green on it,
|
||||
same loop as any PR.
|
||||
2. Merge it — that IS the ship decision. `release.yml`'s
|
||||
`release-on-merge` job asserts, in order, fail-loud, creating nothing:
|
||||
the merged tree's `VERSION` is non-`-dev`; this PR is the one that
|
||||
changed it (a mislabeled ordinary PR fails here); the changelog section
|
||||
for that version extracts non-empty; no tag or release exists yet.
|
||||
Then, same job, it tags the merge commit bare `X.Y.Z` (no `v` prefix —
|
||||
box's tag scheme) and publishes the GitHub release with that section as
|
||||
the body. No assets — the source tarball for the tag is the package
|
||||
`install.sh` downloads.
|
||||
3. The release re-arms main itself: the same workflow run bumps `VERSION`
|
||||
to `X.Y.(Z+1)-dev` and pushes the commit straight to main — no
|
||||
follow-up PR (it opens one only if branch protection refuses the
|
||||
direct push, loudly). A dev install therefore never impersonates the
|
||||
release in the `versions/<v>` layout. On the *manual* tag path the
|
||||
bump stays yours: open the one-line PR after publishing.
|
||||
|
||||
Manual fallback (and backfill): if the merge-path run fails, fix what it
|
||||
named, then tag the merge commit `X.Y.Z` by hand and push the tag — the
|
||||
original tag-push job still turns any correct tag into the release, and
|
||||
the merge path's nothing-exists-yet assert keeps the two from
|
||||
double-publishing.
|
||||
What stays rig's is the **drill** — the real-hardware gate before the
|
||||
handoff of a release PR: tenant guests minted and converged via box,
|
||||
`test/db-integration.sh`, the runner lifecycle against a fork, a coolify
|
||||
install. Rig's drill asserts **convergence** (a machine reaches its role,
|
||||
idempotently), it runs `--host yes` with `BOX_REF=release/<box-version>` so
|
||||
it exercises the box that will actually ship, and drills that share a
|
||||
substrate share **one run ID** so the per-repo records can be joined after
|
||||
the fact. The full meaning — the fixed candidate-ref pinning that dissolves
|
||||
the box↔rig recursion, the per-version record files, the waiver rule — is
|
||||
[`drills/README.md`](drills/README.md); the `drill-recorded` guard enforces
|
||||
the record on every release tree.
|
||||
|
||||
## Labels — who sets what
|
||||
|
||||
The full taxonomy lives in [LABELS.md](LABELS.md). What matters day to day is
|
||||
who sets each kind — most of it is machinery, and hand-moving a
|
||||
machine-owned label just gets corrected on the next pass:
|
||||
The taxonomy and state machine are
|
||||
[`.ceremony/LABELS.md`](.ceremony/LABELS.md); rig's `scope:*` rows live in
|
||||
`.github/labels.conf` (reconciled by the labels caller) and their path map
|
||||
in `.github/labeler.yml`. What matters day to day is who sets each kind —
|
||||
most of it is machinery, and hand-moving a machine-owned label just gets
|
||||
corrected on the next pass:
|
||||
|
||||
| Labels | Set by |
|
||||
|---|---|
|
||||
| `state:*` | the labels workflow ([.github/workflows/labels.yml](.github/workflows/labels.yml)) — recomputed from GitHub's own facts on PR events (label changes included) and every 15 minutes. Machine-owned, with one exception: the author sets `state:needs-human` at handoff (step 6) and the workflow reconciles it. Otherwise never by hand. Exactly one per PR: *whose ball is it.* |
|
||||
| `state:*` | the labels workflow ([.github/workflows/labels.yml](.github/workflows/labels.yml)) — recomputed from GitHub's own facts on PR events (label changes included) and every 15 minutes. Machine-owned, with one exception: the author sets `state:needs-human` at handoff and the workflow reconciles it. Otherwise never by hand. Exactly one per PR: *whose ball is it.* |
|
||||
| `blocker:*` | the same workflow, from the same facts — *what is in the way.* Any number per PR, or none. Never by hand: applying one does not stop a merge, and removing one does not unblock anything. Fix the thing and the next sweep drops the label. |
|
||||
| `stale` | the same workflow — 48h without commits, comments, or reviews. `blocked` PRs are exempt: they are quiet legitimately. |
|
||||
| `scope:*` on PRs | actions/labeler, from the changed paths ([.github/labeler.yml](.github/labeler.yml)). Additive — you may add more, the machine won't remove them. |
|
||||
|
|
|
|||
187
LABELS.md
187
LABELS.md
|
|
@ -1,187 +0,0 @@
|
|||
# Labels
|
||||
|
||||
How this repo uses GitHub labels. The taxonomy is shared across the
|
||||
heavy-duty repos (box, rig, cast) — only the `scope:` set differs per repo,
|
||||
because it names this repo's actual surfaces.
|
||||
|
||||
## State — who is the ball with? (PRs, exactly one)
|
||||
|
||||
Every open PR carries exactly one `state:` label, and it answers the only
|
||||
question a board scan actually asks: *who is this PR waiting on?* The states
|
||||
mirror the review loop this repo runs — PRs open as drafts, three reviewer
|
||||
bots pick up ready PRs with reviews requested, each round is answered in a
|
||||
single reply, and a human takes the final review.
|
||||
|
||||
| Label | Color | Waiting on | Enters when | Leaves when |
|
||||
|---|---|---|---|---|
|
||||
| `state:building` | `#FBCA04` | the coding agent, still building | PR opened as draft | marked ready + bot reviews requested |
|
||||
| `state:bots-reviewing` | `#1D76DB` | the reviewer bots to finish the round | ready with reviews requested, or fixes pushed and reviews re-requested | all three bots have reviewed the round |
|
||||
| `state:addressing` | `#D93F0B` | the coding agent to reply, fix, or ask | all bots reviewed and not all approved; or nobody was asked; or a blocker is up | the round-reply is posted and fixes pushed — and any blocker named alongside is cleared |
|
||||
| `state:needs-human` | `#8250DF` | the human reviewer | the PR **could be merged right now**: no blockers, three formal head-current approvals — and the human review is requested | merged — or changes requested, which cycles back to `state:addressing` |
|
||||
|
||||
`bots-reviewing` and `addressing` are deliberately distinct: staleness in the
|
||||
first means *poke the bots*, staleness in the second means *the agent dropped
|
||||
the ball*. Collapsing them loses exactly the information a sweep needs.
|
||||
`bots-reviewing` therefore means strictly *a request is live and an answer is
|
||||
coming* — a PR nobody was asked to review is the agent's ball, not the bots'.
|
||||
|
||||
## The second axis: `blocker:*`
|
||||
|
||||
State answers *whose ball is it*. Blockers answer *what is in the way*, and
|
||||
unlike states they are *facts about the branch* — mutually independent, so a
|
||||
PR carries as many as apply.
|
||||
|
||||
| Label | Color | Means | Clears when |
|
||||
|---|---|---|---|
|
||||
| `blocker:conflict` | `#B60205` | GitHub says `CONFLICTING` — the agent owes a **rebase** | it merges cleanly |
|
||||
| `blocker:ci-red` | `#B60205` | a check failed — the agent owes a **fix**, which a rebase will not provide | checks are green |
|
||||
| `blocker:unrequested` | `#E99695` | this head has no verdict from somebody — never reviewed, or staled by a push — and **nobody was asked** for one | reviews are requested |
|
||||
| `blocker:drill-pending` | `#E99695` | a `release` PR whose version has **no drill record** at [`drills/<version>.md`](drills/README.md) — the ceremony is correct but *unevidenced* | the drill is run and recorded, or a maintainer waiver is recorded for that version |
|
||||
|
||||
`blocker:drill-pending` is the one blocker that is not about the code: the
|
||||
branch merges, the checks that read the tree are green, and the release is
|
||||
still not shippable because nothing says it was ever run on real hardware.
|
||||
`.github/scripts/drill-recorded.sh` is the authority — the label just makes
|
||||
the reason legible on the board, so a release PR sitting still reads as
|
||||
"waiting on an afternoon of hardware", not as "forgotten". It only ever
|
||||
appears on a `release` PR: every `-dev` tree satisfies the guard vacuously.
|
||||
|
||||
It is the one `blocker:*` the reconciler does **not** compute — its `BLOCKERS`
|
||||
set is the three above — so it is applied by hand and, being outside that set,
|
||||
is not stripped on the next sweep. (The red check itself still shows up as
|
||||
`blocker:ci-red`; this label says *which* red.)
|
||||
|
||||
**A maintainer account must create this label.** The bot account 403s on label
|
||||
creation, so until someone with push access runs the `gh label create` line
|
||||
below, use plain `blocked` on such a PR — it carries the right meaning
|
||||
(waiting on something else to happen first) and the staleness sweep already
|
||||
exempts it.
|
||||
|
||||
One rule joins the axes: **`state:needs-human` requires zero blockers.** Any
|
||||
blocker means the work is the agent's, whatever the review round says.
|
||||
|
||||
This split exists because the single-label version kept lying. Independent
|
||||
facts were projected onto one totally-ordered label, so one always had to win
|
||||
and the losers vanished off the board: a PR that was *both* conflicted and red
|
||||
could only say one of them, and `needs-rebase` told an agent to rebase when
|
||||
what it actually owed was a bug fix. Precedence between two blockers is not a
|
||||
question a set has to answer, which is why every ordering bug this machine has
|
||||
had — `needs-human` surviving a conflict, `MISSING` swallowing `STALE` — lived
|
||||
on the axis that had to be totally ordered.
|
||||
|
||||
`state:needs-rebase` was the first attempt at this and is **retired**; the
|
||||
reconciler strips it on sight so no PR is left carrying a label nothing
|
||||
recomputes.
|
||||
|
||||
**`state:needs-human` means one thing: a human could merge this right now.**
|
||||
The label is the only signal a maintainer scanning the board (or a phone)
|
||||
actually reads, and one that says "your turn" on an unmergeable PR is worse
|
||||
than no label at all. So beyond the blockers, one review fact also outranks an
|
||||
explicit human request:
|
||||
|
||||
- **nobody reviewed *this* head** — every approval staled by a push → `state:addressing`,
|
||||
because the agent owes a re-request
|
||||
|
||||
That case is more dangerous than any blocker: a blocked PR at least shows an X
|
||||
or a disabled merge button, while a staled-approval PR reads green, mergeable
|
||||
and "waiting on the human" over code no reviewer has seen.
|
||||
|
||||
`UNKNOWN` mergeability is deliberately **not** treated as a conflict. GitHub
|
||||
reports it for about a minute after every merge while it recomputes, and
|
||||
flapping every open PR through `blocker:conflict` on each merge would be worse
|
||||
than the bug this fixes. A failed read of either branch fact degrades to the
|
||||
same "do not know" value, for the same reason.
|
||||
|
||||
An *unfinished* round still yields to an explicit human request — a maintainer
|
||||
pulling a PR to themselves early is a deliberate act. `MISSING` (nobody has
|
||||
reviewed yet) and `STALE` (everyone reviewed something else) are different
|
||||
facts and are treated differently.
|
||||
|
||||
## Cross-cutting (PRs and issues)
|
||||
|
||||
| Label | Color | Meaning |
|
||||
|---|---|---|
|
||||
| `stale` | `#B60205` | No activity for 48h. Sweep-managed, never hand-applied. `state:building` + `stale` is precisely a forgotten draft. |
|
||||
| `blocked` | `#6A737D` | Waiting on another PR or issue to land first. Quiet *legitimately* — the staleness sweep skips it. |
|
||||
| `release` | `#0E8A16` | Release flow, versioning, and packaging work. |
|
||||
| `merge-next` | `#0E8A16` | Head of the merge queue — **merge this one next**. Queue order is *intent* (which PR lands first, given how they conflict), so the reconciler never sets it: you or the agent maintaining the queue do. The reconciler only **clears** it, the moment the PR stops being something a human could merge — so it cannot go stale the way `state:needs-human` did. |
|
||||
|
||||
## Scope — which surface? (PRs and issues, any number)
|
||||
|
||||
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert.
|
||||
|
||||
| Label | Covers |
|
||||
|---|---|
|
||||
| `scope:bootstrap` | `commands/bootstrap.sh` — hardening a pristine server into a node |
|
||||
| `scope:users` | `commands/users-*` — the root-door model, apply/status, close-root |
|
||||
| `scope:runner` | `commands/runner-*` — GitHub runner install/remove/repoint/status |
|
||||
| `scope:coolify` | `commands/coolify-*` — Coolify and its backup install |
|
||||
| `scope:db` | `commands/db.sh` — dump/restore and the round-trip proof |
|
||||
| `scope:installer` | `install.sh` — how rig itself lands on a machine |
|
||||
|
||||
## Issue types
|
||||
|
||||
`bug`, `enhancement`, `documentation` — issues only. PRs carry their type in
|
||||
the conventional title (`feat:`, `fix:`, `docs:`), so typing a PR with a label
|
||||
would just say the same thing twice, drifting apart eventually.
|
||||
|
||||
## Maintenance
|
||||
|
||||
State labels are machine-owned, with exactly one exception. Every state above
|
||||
is derivable from GitHub's own facts — the draft flag, requested reviewers,
|
||||
review states, push timestamps — so the labels workflow
|
||||
([.github/workflows/labels.yml](.github/workflows/labels.yml)) recomputes the
|
||||
state and reconciles labels statelessly, on PR events (label changes included)
|
||||
plus a 15-minute cron. A hand-moved label is a lie waiting to happen; the
|
||||
workflow asserts the effective state instead.
|
||||
|
||||
The exception is `state:needs-human`, which the author sets at handoff
|
||||
([CONTRIBUTING.md](CONTRIBUTING.md), step 6). That is an optimistic write, not
|
||||
a transfer of ownership: because `pull_request_target: labeled` wakes the
|
||||
workflow, the author's own label write fires the sweep that validates it, and
|
||||
a handoff that had not earned the label is corrected within seconds.
|
||||
|
||||
It exists because the wake signal was missing. There is no
|
||||
`pull_request_review_target` — on fork PRs, which is all of them here,
|
||||
`pull_request_review` runs read-only and cannot label anything — so the moment
|
||||
the label becomes true, the third approval landing, fired nothing at all. What
|
||||
was left was the `*/15` cron, and GitHub deprioritises short intervals hard
|
||||
enough that the delivered rate is closer to hourly. The label could therefore
|
||||
lag the round it described by hours, worst on the quietest repo: every sweep
|
||||
reconciles the whole board, so a busy repo stays fresh by piggybacking on
|
||||
unrelated PR events, while a quiet one depends on the cron most and receives
|
||||
it least. `scope:` labels on PRs are applied from the changed
|
||||
paths by actions/labeler ([.github/labeler.yml](.github/labeler.yml));
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md) says who sets what.
|
||||
|
||||
The same workflow bootstraps the taxonomy: a manual dispatch creates any
|
||||
missing label idempotently. To create them by hand (needs push access):
|
||||
|
||||
```sh
|
||||
gh label create "state:building" --color FBCA04 --description "PR is a draft — the coding agent is still building" --force
|
||||
gh label create "state:bots-reviewing" --color 1D76DB --description "Waiting on the bot reviewers to finish the round" --force
|
||||
gh label create "state:addressing" --color D93F0B --description "All bots reviewed — coding agent owes the single reply + fixes" --force
|
||||
gh label create "blocker:conflict" --color B60205 --description "Does not merge — the branch conflicts and the agent owes a rebase" --force
|
||||
gh label create "blocker:ci-red" --color B60205 --description "A check is failing — the agent owes a fix (not a rebase)" --force
|
||||
gh label create "blocker:unrequested" --color E99695 --description "Somebody still owes a verdict and nobody was asked for one" --force
|
||||
# Needs a MAINTAINER account — the bot 403s on label creation. Until it exists, `blocked` stands in.
|
||||
gh label create "blocker:drill-pending" --color E99695 --description "Release PR with no drill record at drills/<version>.md — correct but unevidenced" --force
|
||||
# retired — the reconciler strips it; delete it once no PR carries it
|
||||
# gh label delete "state:needs-rebase"
|
||||
gh label create "state:needs-human" --color 8250DF --description "No blockers, all bots approve — waiting on the human reviewer" --force
|
||||
gh label create "merge-next" --color 0E8A16 --description "Head of the merge queue — merge this one next (set by hand/agent, cleared here)" --force
|
||||
gh label create "stale" --color B60205 --description "No activity for 48h — needs a poke (sweep-managed)" --force
|
||||
gh label create "blocked" --color 6A737D --description "Waiting on another PR or issue to land first" --force
|
||||
gh label create "release" --color 0E8A16 --description "Release flow and version/packaging work" --force
|
||||
gh label create "scope:bootstrap" --color C5DEF5 --description "bootstrap — hardening a pristine server into a node" --force
|
||||
gh label create "scope:users" --color C5DEF5 --description "users-* — root-door model, apply/status, close-root" --force
|
||||
gh label create "scope:runner" --color C5DEF5 --description "runner-* — GitHub runner lifecycle" --force
|
||||
gh label create "scope:coolify" --color C5DEF5 --description "coolify-* — Coolify and backup install" --force
|
||||
gh label create "scope:db" --color C5DEF5 --description "db.sh — dump/restore" --force
|
||||
gh label create "scope:installer" --color C5DEF5 --description "install.sh — how rig lands on a machine" --force
|
||||
# delete is not an upsert: a label that is already gone exits non-zero. Swallow
|
||||
# that, so this block converges on re-run instead of erroring after first success.
|
||||
for L in duplicate invalid question wontfix "help wanted" "good first issue"; do
|
||||
gh label delete "$L" --yes 2>/dev/null || true
|
||||
done
|
||||
```
|
||||
|
|
@ -24,7 +24,8 @@ The gate reads a file in this repo, and nothing else.
|
|||
|
||||
## What the gate requires
|
||||
|
||||
`.github/scripts/drill-recorded.sh` runs on every PR. On a `-dev` tree it
|
||||
The `drill-recorded` guard (heavy-duty/ceremony's action, pinned in
|
||||
`ci.yml`) runs on every PR. On a `-dev` tree it
|
||||
asserts nothing — a development tree has no release to evidence. On a bare
|
||||
`VERSION` — a release ceremony tree — it requires `drills/<version>.md` to
|
||||
exist and to hold at least one non-whitespace character. An empty file, or one
|
||||
|
|
|
|||
Loading…
Reference in a new issue