diff --git a/BUILDER.md b/BUILDER.md index 0ab03f1..9066716 100644 --- a/BUILDER.md +++ b/BUILDER.md @@ -11,6 +11,8 @@ triage bug, and the move is to say so on the issue, not to guess. - Respect dependency order: inside an epic, take the earliest unblocked unclaimed child. Between epics and strays, prefer the issue that unblocks the most other work. +- In a repository that adopts version epics, read [RELEASES.md](RELEASES.md) + before choosing among release-window members. - **Your own red head outranks a new claim.** A failing check at the head of a PR you authored is picked up **before claiming another issue** — repairing your own red PR comes ahead of new work, which is why the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b1666c..9c8a7b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ Two consumption modes, split by what has a runtime: "runtime" is an agent reading the working tree of the repo it stands in — a doc that requires a cross-repo fetch before it governs is a doc that sometimes goes unread. So the agent-facing set — **AGENTS.md, TRIAGE.md, - BUILDER.md, REVIEWER.md, LABELS.md** — is vendored into each governed + BUILDER.md, REVIEWER.md, LABELS.md, RELEASES.md** — is vendored into each governed repo at **`.ceremony/`**, byte-identical to this repo at the pinned ref, by the sync tool (issue #19). A CI guard diffs the mirror against the pin on every PR: hand-editing a vendored file, or bumping the pin without diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 0000000..d3d6816 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,113 @@ +# Release management + +This file describes the release-management pattern available to governed +repositories. Adoption is per repository and operator-ruled: a repository +without version epics is not out of compliance. A repo-local roadmap is the +map; each epic remains the source of truth for its own release. Where an older +repo-local description differs from this file, this file governs. + +## The ladder + +Represent each planned release with one version epic. The epic is the working +surface for that release: it states the goal, names the members, and records +the ordered waves as checklists. Keep the machine-readable progress checklist +under a heading matching `## Task list`, case-insensitively; the issue-flow +sweep reads task rows there until the next heading when it decides whether to +nudge triage about a completed epic. Other member or wave headings are not +completion inputs. + +Keep a short repo-local roadmap beside the epics. The roadmap shows the whole +ladder and points to each working surface; it does not duplicate the live +member lists or ordering. crew's roadmap discussion [heavy-duty/crew#338](https://github.com/heavy-duty/crew/discussions/338) +maps the ladder whose `0.1.2` working surface moved from the crufty ledger +[heavy-duty/crew#162](https://github.com/heavy-duty/crew/issues/162) to +[heavy-duty/crew#346](https://github.com/heavy-duty/crew/issues/346). + +## Gates + +Each version epic declares `Blocked by `. Special ordering — a +double gate or an out-of-chain gate — is written explicitly on that epic; +there is no hidden global schedule. The epic carries `epic` and the +repository's release label, with no queue label. Its `Blocked by` line is a +declaration a human reads: shipping closes the predecessor, then triage opens +the next window by hand as the first step of release-init. The issue-flow +sweep does not promote version epics; automating that gate would require a +separately specified change to its queue-category model. + +The gate orders windows, not their contents. Members enter a release only by +decision during release-init. The double gate on +[heavy-duty/crew#163](https://github.com/heavy-duty/crew/issues/163) and the +out-of-chain track on [heavy-duty/crew#348](https://github.com/heavy-duty/crew/issues/348) +are worked examples of exceptions declared where they apply. + +## Release-init + +The predecessor closing and clearing the next epic's declared gate is the +trigger, and today triage must notice it and open that window by hand. +[heavy-duty/ceremony#253](https://github.com/heavy-duty/ceremony/issues/253) +tracks the not-yet-shipped sweep announcement of that duty; do not treat the +announcement as present until the consumer's pin carries it. Triage runs five +steps: + +1. Mint the epic's “to mint when this arc opens” list together with findings, + deferred work, and discussion outcomes accumulated since the epic was + written. Each member initially declares `Blocked by `. +2. Graph hard `Blocked by` edges and same-file clusters on the epic. +3. Write the waves into the epic body as checklists in claim order, with a + separate verification lane and the progress view under `## Task list`. +4. Ask the operator to bless the order, then have triage open the first wave + by applying the flip mechanics below. The operator's blessing is the one + step this chain never automates. +5. Ship through the repository's cut process, close the epic, and treat that + close as the trigger for the next window. + +heavy-duty/crew#346 is the worked wave plan; its graph made both hard edges +and shared-file contention visible before builders entered the queue. If init +finds no work worth minting, the operator either folds the empty window into a +later release or skips the version, recording that ruling on the epic before +closing it unshipped. + +## One primary window, declared parallel tracks + +Run one primary release window by default. A cut takes whatever has landed, so +interleaving unrelated windows blurs both the release story and the evidence +behind it. Gates open windows; they do not silently admit members, so builders +still see one deliberately ordered queue. + +The operator may declare a parallel track at init when its footprint is +disjoint from the primary window: another repository, another artifact, or +provably non-overlapping clusters. The declaration names the boundary and any +bridge work that must rejoin the primary. [heavy-duty/crew#348](https://github.com/heavy-duty/crew/issues/348) +is the worked example: its app and artifact form a parallel track while its +small crew-side bridge remains in the primary window. + +## Flip mechanics + +To admit a member, delete or rewrite its literal, parseable +`Blocked by ` declaration and swap `blocked` to `ready` in the same +edit. Markdown or HTML strikethrough is insufficient: the blocker parser reads +the raw marker text and still returns the reference. Never preserve history by +negating the marker phrase — the parser unions declarations even when prose +says they no longer apply. Preserve the history only after rewriting the +marker into non-parseable prose, then verify that the parser returns an empty +set for the release gate. + +Release membership is a decision, never a sweep default. Triage performs each +flip only after the operator blesses the wave; the issue-flow sweep may resolve +ordinary issue dependencies, but it does not choose a release's contents. +heavy-duty/crew#346 records the member-by-member flip that opened its first +wave. + +## The ledger pattern + +When a release epic has become too crufty to remain a clear working surface, +create a replacement and treat the old epic as a ledger. Do not close the old +epic until every live member declaration points at the replacement and the +blocker parser verifies the new set. Closing early can release every member +that still names the old issue. + +The [heavy-duty/crew#162](https://github.com/heavy-duty/crew/issues/162) to +[heavy-duty/crew#346](https://github.com/heavy-duty/crew/issues/346) +transition is the worked example: all member declarations were re-pointed and +parse-verified before #162 closed; #162 remains the historical record while +#346 is the release's working surface. diff --git a/TRIAGE.md b/TRIAGE.md index 840c09a..75e1b6d 100644 --- a/TRIAGE.md +++ b/TRIAGE.md @@ -122,6 +122,7 @@ label): the approach, the decisions, the constraint list, and a dependency-ordered task list of child issues. Children reference the epic; the epic's checklist is the progress view. Builders never pick the epic itself. Keep the checklist current — a stale epic misleads every scan. +Repositories that adopt version epics follow [RELEASES.md](RELEASES.md). ## Backlog hygiene diff --git a/changelog.d/248.md b/changelog.d/248.md new file mode 100644 index 0000000..d6c3bbd --- /dev/null +++ b/changelog.d/248.md @@ -0,0 +1,3 @@ +### Added + +- Document the optional, operator-ruled release-epic flow for governed repositories. (#248) diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 99e1ce2..437d64b 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -550,14 +550,18 @@ when the pinned taxonomy declares a core label the repository lacks. Machinery is consumed by reference — GitHub fetches the workflows and actions above from the pin at run time — but documents have no runtime: an agent reads the working tree it stands in. So the agent-facing doc set -(ceremony's `docs/VENDORED.txt`: AGENTS.md, TRIAGE.md, BUILDER.md, -REVIEWER.md, LABELS.md) is vendored into each consumer at **`.ceremony/`**, +declared by ceremony's `docs/VENDORED.txt` is vendored into each consumer at **`.ceremony/`**, byte-identical to ceremony at the pin, plus a generated `.ceremony/README.md` marking the directory machine-managed. `actions/docs-sync` owns the copy: `--fix` writes it (and deletes what the manifest dropped — mirror means mirror), `--check` re-diffs it in CI on every PR, so a hand edit or a stale pin goes red instead of quietly governing. +`RELEASES.md` joins that mirror with the first tag carrying ceremony#248. +It is **unreleased** until that tag exists: consumers add +`.ceremony/RELEASES.md` only with the ordinary pin bump and re-sync, never by +copying it ahead of their pinned doctrine set. + The consumer's ci.yml gains the guard alongside the others: ```yaml diff --git a/docs/VENDORED.txt b/docs/VENDORED.txt index 10c20a3..ff41f35 100644 --- a/docs/VENDORED.txt +++ b/docs/VENDORED.txt @@ -3,3 +3,4 @@ TRIAGE.md BUILDER.md REVIEWER.md LABELS.md +RELEASES.md diff --git a/test/docs-sync.test.sh b/test/docs-sync.test.sh index 6987304..f5ecae9 100644 --- a/test/docs-sync.test.sh +++ b/test/docs-sync.test.sh @@ -18,6 +18,12 @@ SCRIPT="$ROOT/actions/docs-sync/docs-sync.sh" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT +# RELEASES.md's consumer-availability promise is true only when the real +# manifest carries it (#248's review round). The fixture cases below prove +# manifest-driven behavior; this row binds that behavior to the promised file. +check "real manifest includes the release doctrine" 0 "RELEASES.md" \ + grep -Fx RELEASES.md "$ROOT/docs/VENDORED.txt" + # --- fixture builders -------------------------------------------------------- # The main fake ceremony tree: three manifest entries, one in a subdirectory