forked from heavy-duty/ceremony
README.md rewritten whole from the current tree
This commit is contained in:
parent
8080618a70
commit
57a7b156ac
1 changed files with 280 additions and 218 deletions
498
README.md
498
README.md
|
|
@ -1,15 +1,64 @@
|
||||||
# ceremony
|
# ceremony
|
||||||
|
|
||||||
One release ceremony for the whole heavy-duty family — implemented once,
|
The heavy-duty family's **governance repo**: the machinery every repo in the
|
||||||
tested once, documented here, consumed everywhere else by reference. The
|
family runs, and the doctrine every agent in the family reads. Implemented
|
||||||
approach and its constraints live in
|
once here, tested once here, consumed everywhere else — never copied.
|
||||||
[#1](https://github.com/heavy-duty/ceremony/issues/1); this README is the
|
|
||||||
operator-facing doctrine that used to live, three times over, in the
|
|
||||||
consumers' CONTRIBUTINGs.
|
|
||||||
|
|
||||||
- **Adopting or converting a repo** → [docs/CONSUMERS.md](docs/CONSUMERS.md).
|
Two kinds of thing live in this tree, and they are consumed in two different
|
||||||
- **Working in this repo as an agent** → [AGENTS.md](AGENTS.md) routes you;
|
ways because they have two different runtimes.
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md) has the repo specifics.
|
|
||||||
|
**Machinery is consumed by reference, at a pin.** The reusable workflows in
|
||||||
|
[`.github/workflows/`](.github/workflows/) and the composite actions in
|
||||||
|
[`actions/`](actions/) are fetched by GitHub at run time from the ref the
|
||||||
|
caller pins; no copy exists in the consumer. That machinery is two systems.
|
||||||
|
The **release ceremony** — [`release.yml`](.github/workflows/release.yml),
|
||||||
|
the decision and fact libraries under [`lib/`](lib/), and the guard actions
|
||||||
|
that keep a release honest — is the operator-facing half, and the runbook
|
||||||
|
below is its documentation. The **label and issue-flow machine** —
|
||||||
|
[`labels.yml`](.github/workflows/labels.yml) and its detached sweep half
|
||||||
|
[`labels-sweep.yml`](.github/workflows/labels-sweep.yml) (split in #209),
|
||||||
|
driving [`labels-scope`](actions/labels-scope/),
|
||||||
|
[`labels-reconcile`](actions/labels-reconcile/) and
|
||||||
|
[`issueflow-reconcile`](actions/issueflow-reconcile/) — converges PR state
|
||||||
|
and the issue work queue. What its labels *mean* is
|
||||||
|
[LABELS.md](LABELS.md)'s contract, not this page's.
|
||||||
|
|
||||||
|
**Doctrine is consumed as a machine-verified mirror.** A document's only
|
||||||
|
runtime is an agent reading the working tree it stands in, and a doc that
|
||||||
|
needs a cross-repo fetch before it governs is a doc that sometimes goes
|
||||||
|
unread. So the agent-facing set — the files named in
|
||||||
|
[`docs/VENDORED.txt`](docs/VENDORED.txt) — is vendored into each governed
|
||||||
|
repo at `.ceremony/`, byte-identical to this repo at the pinned ref, by
|
||||||
|
[`actions/docs-sync`](actions/docs-sync/). A CI guard diffs the mirror
|
||||||
|
against the pin on every PR: hand-editing a vendored file, or bumping the
|
||||||
|
pin without re-syncing, goes red. It is a copy that cannot drift, which is
|
||||||
|
the only kind of copy this org allows. This README is deliberately *not* in
|
||||||
|
that set — a consumer's router is its `AGENTS.md`, not this repo's front
|
||||||
|
page — and [`.github/scripts/vendored-check.sh`](.github/scripts/vendored-check.sh)
|
||||||
|
records that reason beside the three other ceremony-only root docs.
|
||||||
|
|
||||||
|
**One pin governs both halves.** The ref a repo's workflow callers name is
|
||||||
|
the ref its `.ceremony/` mirror is verified against, so a process change
|
||||||
|
rolls out as one reviewed PR per repo: the pin line plus the re-synced
|
||||||
|
mirror, checked by the same guard.
|
||||||
|
|
||||||
|
## Where to go
|
||||||
|
|
||||||
|
- **Adopting ceremony, or converting a repo that carries its own copy** →
|
||||||
|
[docs/CONSUMERS.md](docs/CONSUMERS.md) — the bootstrap and conversion
|
||||||
|
checklists, the caller stubs, the pin-bump procedure.
|
||||||
|
- **Working in this repo as an agent** → [AGENTS.md](AGENTS.md) routes you
|
||||||
|
to your role file ([TRIAGE.md](TRIAGE.md), [BUILDER.md](BUILDER.md),
|
||||||
|
[REVIEWER.md](REVIEWER.md)); [CONTRIBUTING.md](CONTRIBUTING.md) carries
|
||||||
|
this repo's own specifics — the review panel roster, the `scope:*` set,
|
||||||
|
the code and doctrine conventions.
|
||||||
|
- **The board: what a label means, and who may set it** →
|
||||||
|
[LABELS.md](LABELS.md). It is the shared state machine; misusing one label
|
||||||
|
lies to every other agent on the board.
|
||||||
|
- **Family release windows — what ships together, and when** →
|
||||||
|
[RELEASES.md](RELEASES.md).
|
||||||
|
- **How the operator fleet is actually wired** → [FLEET.md](FLEET.md), a
|
||||||
|
descriptive snapshot rather than doctrine.
|
||||||
- **Operating a release, or staring at a red run on main** → read on.
|
- **Operating a release, or staring at a red run on main** → read on.
|
||||||
|
|
||||||
## What a release is
|
## What a release is
|
||||||
|
|
@ -23,13 +72,14 @@ stamps:
|
||||||
([lib/version.sh](lib/version.sh)).
|
([lib/version.sh](lib/version.sh)).
|
||||||
2. **The changelog section is assembled — one edit, produced by the tool**
|
2. **The changelog section is assembled — one edit, produced by the tool**
|
||||||
(#112). Entries never accumulate in `CHANGELOG.md`: each PR wrote one
|
(#112). Entries never accumulate in `CHANGELOG.md`: each PR wrote one
|
||||||
fragment file, `changelog.d/<issue>.md`, and the ceremony PR runs
|
fragment file, `changelog.d/<issue>.md`
|
||||||
[bin/changelog-assemble](bin/changelog-assemble) — by hand, on purpose,
|
([the directory's marker](changelog.d/README.md) names the doctrine), and
|
||||||
so the section lands in the PR's diff where the panel reads it (#112
|
the ceremony PR runs [bin/changelog-assemble](bin/changelog-assemble) —
|
||||||
D12; a consumer's exact invocation is in
|
by hand, on purpose, so the section lands in the PR's diff where the
|
||||||
[docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)).
|
panel reads it (#112 D12; a consumer's exact invocation is in
|
||||||
The tool folds every fragment into a new `## X.Y.Z — DATE` section on
|
[docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)). The
|
||||||
top and deletes the fragments it consumed; the
|
tool folds every fragment into a new `## X.Y.Z — DATE` section on top and
|
||||||
|
deletes the fragments it consumed; the
|
||||||
[assembled guard](#changelog-assembled--the-stamp-is-exactly-the-fragments)
|
[assembled guard](#changelog-assembled--the-stamp-is-exactly-the-fragments)
|
||||||
replays that run and refuses a stamp that is not byte-for-byte the
|
replays that run and refuses a stamp that is not byte-for-byte the
|
||||||
fragments' assembly.
|
fragments' assembly.
|
||||||
|
|
@ -38,62 +88,61 @@ stamps:
|
||||||
`## Unreleased` back on top — because every PR inserted at that one
|
`## Unreleased` back on top — because every PR inserted at that one
|
||||||
shared anchor, and between the stamp and the re-arm a PR authored
|
shared anchor, and between the stamp and the re-arm a PR authored
|
||||||
*before* the release landed its entry under whatever now occupied the
|
*before* the release landed its entry under whatever now occupied the
|
||||||
position — **the section that just shipped** — cleanly, no conflict,
|
position — **the section that just shipped** — cleanly, no conflict, no
|
||||||
no signal (box#108; confirmed cross-repo as rig#66). Fragments make
|
signal (box#108; confirmed cross-repo as rig#66). Fragments make that
|
||||||
that failure structurally impossible rather than guarded-against: a
|
failure structurally impossible rather than guarded-against: a fragment
|
||||||
fragment merged after the release simply sits in the directory and is
|
merged after the release simply sits in the directory and is assembled
|
||||||
assembled into the *next* section. There is no anchor left to misplace,
|
into the *next* section. There is no anchor left to misplace, and nothing
|
||||||
and nothing to re-arm — the directory is always armed.
|
to re-arm — the directory is always armed.
|
||||||
|
|
||||||
3. **The drill record is present**: `drills/X.Y.Z.md`, non-blank — the
|
3. **The drill record is present**: `drills/X.Y.Z.md`, non-blank — the
|
||||||
evidence the release rests on
|
evidence the release rests on ([the drill doctrine](#the-drill-doctrine)).
|
||||||
([the drill doctrine](#the-drill-doctrine)).
|
|
||||||
|
|
||||||
(This repo's own ceremony adds a fourth stamp: `CEREMONY_SELF_REF` — the
|
(This repo's own ceremony adds a fourth stamp: `CEREMONY_SELF_REF` — the ref
|
||||||
ref consumers' runs fetch this repo at — moves to the version being
|
consumers' runs fetch this repo at — moves to the version being released, in
|
||||||
released, in [release.yml](.github/workflows/release.yml#L123-L132) and
|
[release.yml](.github/workflows/release.yml#L123-L132) and every other
|
||||||
every other workflow that carries it.
|
workflow that carries it.
|
||||||
[self-ref-check.sh](.github/scripts/self-ref-check.sh) fails CI here, not
|
[self-ref-check.sh](.github/scripts/self-ref-check.sh) fails CI here, not a
|
||||||
a consumer's release, when it is stale.)
|
consumer's release, when it is stale.)
|
||||||
|
|
||||||
**The merge is the ship decision; the tag is transcription.** After the
|
**The merge is the ship decision; the tag is transcription.** After the
|
||||||
merge, [release.yml](.github/workflows/release.yml#L136-L300) asserts its
|
merge, [release.yml](.github/workflows/release.yml#L136-L301) asserts its
|
||||||
way to certainty, tags the merge commit, publishes the GitHub release with
|
way to certainty, tags the merge commit, publishes the GitHub release with
|
||||||
the version's own changelog section as the body — the curated prose, never
|
the version's own changelog section as the body — the curated prose, never
|
||||||
the generated PR list
|
the generated PR list ([lib/changelog.sh](lib/changelog.sh) is the one
|
||||||
([lib/changelog.sh](lib/changelog.sh) is the one canonical extractor) —
|
canonical extractor, and [bin/changelog-section](bin/changelog-section) is
|
||||||
and re-arms main by bumping to `X.Y.(Z+1)-dev`
|
its command-line face) — and re-arms main by bumping to `X.Y.(Z+1)-dev`; the
|
||||||
([release.yml](.github/workflows/release.yml#L266-L300)) — the version is
|
version is the only re-arm left, the changelog needs none (#112). The
|
||||||
the only re-arm left; the changelog needs none (#112). The machine does
|
machine does the transcription because humans err silently and machines fail
|
||||||
the transcription because humans err silently and machines fail loudly:
|
loudly: **everything asserts its way to certainty and fails loudly, creating
|
||||||
**everything asserts its way to certainty and fails loudly, creating
|
|
||||||
nothing** — a wrong release is worse than a missing one, so every failed
|
nothing** — a wrong release is worse than a missing one, so every failed
|
||||||
assert leaves zero artifacts: no tag, no release, no bump.
|
assert leaves zero artifacts: no tag, no release, no bump.
|
||||||
|
|
||||||
## The two doors
|
## The two doors
|
||||||
|
|
||||||
- **The merge door — the paved road.** A push to main
|
- **The merge door — the paved road.** A push to main runs the
|
||||||
([release.yml](.github/workflows/release.yml#L140)) runs the
|
|
||||||
[decide table](#what-happens-when-my-pr-lands-on-main); a merged,
|
[decide table](#what-happens-when-my-pr-lands-on-main); a merged,
|
||||||
`release`-labeled PR whose version transitioned to bare is the ceremony,
|
`release`-labeled PR whose version transitioned to bare is the ceremony,
|
||||||
everything legitimate that isn't one is a green no-op, and every
|
everything legitimate that isn't one is a green no-op, and every
|
||||||
half-ceremony dies loudly. Use it for every normal release.
|
half-ceremony dies loudly
|
||||||
|
([release.yml](.github/workflows/release.yml#L136-L301)). Use it for every
|
||||||
|
normal release.
|
||||||
|
|
||||||
- **The tag door — the fallback and the backfill.** A bare `X.Y.Z` tag
|
- **The tag door — the fallback and the backfill.** A bare `X.Y.Z` tag push
|
||||||
push — **no `v` prefix**, box's 0.6.0 set the scheme
|
— **no `v` prefix**, box's 0.6.0 set the scheme
|
||||||
([release.yml](.github/workflows/release.yml#L302-L369)) — publishes the
|
([release.yml](.github/workflows/release.yml#L303-L371)) — publishes the
|
||||||
same way. The tag is the operator's explicit act, so there is no decide
|
same way. The tag is the operator's explicit act, so there is no decide
|
||||||
and no label check; the one assert is that **the tag names the tree's
|
and no label check; the one assert is that **the tag names the tree's own
|
||||||
own version**, and a mismatch refuses, creating nothing. No `-dev` bump
|
version**, and a mismatch refuses, creating nothing. No `-dev` bump either
|
||||||
either — the fallback does not rewrite main (cast's precedent). Use it
|
— the fallback does not rewrite main (cast's precedent). Use it when the
|
||||||
when the merge path is red, for backfills, and for the
|
merge path is red, for backfills, and for the
|
||||||
[first-release edge](#what-happens-when-my-pr-lands-on-main) (row 4).
|
[first-release edge](#what-happens-when-my-pr-lands-on-main) (row 4).
|
||||||
|
|
||||||
Tag + publish (+ the consumer's artifact hook) happen **in the same job,
|
Tag + publish (+ the consumer's artifact hook) happen **in the same job, on
|
||||||
on purpose**: a `GITHUB_TOKEN`-created tag fires no workflows (GitHub's
|
purpose**: a `GITHUB_TOKEN`-created tag fires no workflows (GitHub's
|
||||||
anti-recursion), so the merge door's tag can never re-enter the tag door
|
anti-recursion), so the merge door's tag can never re-enter the tag door and
|
||||||
and double-publish — and that job is the release's only chance to publish
|
double-publish — and that job is the release's only chance to publish (#1
|
||||||
([release.yml](.github/workflows/release.yml#L223-L234), #1 constraint 2).
|
constraint 2).
|
||||||
|
|
||||||
## What happens when my PR lands on main
|
## What happens when my PR lands on main
|
||||||
|
|
||||||
|
|
@ -101,8 +150,8 @@ The merge door runs on **every** push to main, and the `release` label
|
||||||
legitimately means two things (release ceremonies, and ordinary work *on*
|
legitimately means two things (release ceremonies, and ordinary work *on*
|
||||||
the release machinery), so the door's first act is a decision: the six-row
|
the release machinery), so the door's first act is a decision: the six-row
|
||||||
table in [lib/decide.sh](lib/decide.sh#L29-L61) (issue #8 — the comment
|
table in [lib/decide.sh](lib/decide.sh#L29-L61) (issue #8 — the comment
|
||||||
block *is* the spec, and the table is contract-tested offline). Rendered
|
block *is* the spec, and the table is contract-tested offline by
|
||||||
for operators:
|
[test/decide.test.sh](test/decide.test.sh)). Rendered for operators:
|
||||||
|
|
||||||
| # | the tree your merge produced | the run | what it means — and your move |
|
| # | the tree your merge produced | the run | what it means — and your move |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|
@ -113,9 +162,9 @@ for operators:
|
||||||
| 5 | version transitioned to bare, **no merged `release`-labeled PR** behind the commit | **red, nothing created** | A transition nobody declared — a release is a labeled ceremony PR, not a bare push. Label a proper ceremony PR and re-do it, or publish by the tag door if the tree is genuinely right. |
|
| 5 | version transitioned to bare, **no merged `release`-labeled PR** behind the commit | **red, nothing created** | A transition nobody declared — a release is a labeled ceremony PR, not a bare push. Label a proper ceremony PR and re-do it, or publish by the tag door if the tree is genuinely right. |
|
||||||
| 6 | version transitioned to bare, merged `release`-labeled PR behind the commit | **the ceremony** | Tag → notes → publish → `-dev` re-arm. Your move afterwards: verify the release exists and main reads `X.Y.(Z+1)-dev`. |
|
| 6 | version transitioned to bare, merged `release`-labeled PR behind the commit | **the ceremony** | Tag → notes → publish → `-dev` re-arm. Your move afterwards: verify the release exists and main reads `X.Y.(Z+1)-dev`. |
|
||||||
|
|
||||||
The green rows are the point as much as the red ones: the machinery must
|
The green rows are the point as much as the red ones: the machinery must be
|
||||||
be safe to work on, so every legitimate non-ceremony is a green `NOTICE`
|
safe to work on, so every legitimate non-ceremony is a green `NOTICE` no-op,
|
||||||
no-op — never a red run on main per infra PR
|
never a red run on main per infra PR
|
||||||
([lib/decide.sh](lib/decide.sh#L6-L12)). The label is hand-set intent and
|
([lib/decide.sh](lib/decide.sh#L6-L12)). The label is hand-set intent and
|
||||||
automation never guesses; the version transition is the interlock, and
|
automation never guesses; the version transition is the interlock, and
|
||||||
label-without-transition (row 4) and transition-without-label (row 5) both
|
label-without-transition (row 4) and transition-without-label (row 5) both
|
||||||
|
|
@ -123,36 +172,54 @@ refuse (#1 constraint 8).
|
||||||
|
|
||||||
## The guards
|
## The guards
|
||||||
|
|
||||||
Four composite actions run in every consumer's CI (and in this repo's
|
[`actions/`](actions/) holds ten composite actions. Three belong to the
|
||||||
own). Shared shape: version-keyed where the tree's state matters, loud
|
label machine named above and are not the operator's business here. Of the
|
||||||
where it fails, and **a file of its own so a test can drive it**. The full
|
remaining seven, a consumer's own `ci.yml` carries **five** guard steps —
|
||||||
war stories are in the scripts' header comments — authoritative and longer
|
`changelog-armed`, `changelog-monotonic`, `changelog-assembled`,
|
||||||
than this; what follows is the operator's cut.
|
`drill-recorded` and [`runner-isolated`](actions/runner-isolated/), the last
|
||||||
|
asserting that no `pull_request`-triggered workflow names a self-hosted
|
||||||
|
runner (#58) — plus [`refs-not-closing`](actions/refs-not-closing/) in its
|
||||||
|
own [`refs-guard.yml`](.github/workflows/refs-guard.yml) caller, because
|
||||||
|
body edits are load-bearing there (#200, #218), and
|
||||||
|
[`docs-sync`](actions/docs-sync/) once the repo adopts the agent team flow.
|
||||||
|
The exact steps and their pin-availability rules are in
|
||||||
|
[docs/CONSUMERS.md](docs/CONSUMERS.md).
|
||||||
|
|
||||||
|
The four below are the release's own, and this is the operator's cut of
|
||||||
|
them. Shared shape: version-keyed where the tree's state matters, loud where
|
||||||
|
it fails, and **a file of its own so a test can drive it**. The full war
|
||||||
|
stories are in the scripts' header comments — authoritative and longer than
|
||||||
|
this.
|
||||||
|
|
||||||
|
This repo eats what it serves: [`ci.yml`](.github/workflows/ci.yml) runs the
|
||||||
|
guard actions against its own real tree, and
|
||||||
|
[`release-exercise.yml`](.github/workflows/release-exercise.yml) replays the
|
||||||
|
merge door's step sequence on every PR.
|
||||||
|
|
||||||
### changelog-armed — main never sits disarmed
|
### changelog-armed — main never sits disarmed
|
||||||
|
|
||||||
**The rule** ([actions/changelog-armed/changelog-armed.sh](actions/changelog-armed/changelog-armed.sh)),
|
**The rule** ([actions/changelog-armed/changelog-armed.sh](actions/changelog-armed/changelog-armed.sh)),
|
||||||
keyed on the tree's shape, then its version. In **fragment mode** —
|
keyed on the tree's shape, then its version. In **fragment mode** —
|
||||||
`changelog.d/` exists, the arming property moved onto the directory
|
`changelog.d/` exists, the arming property moved onto the directory (#112
|
||||||
(#112 D7):
|
D7):
|
||||||
|
|
||||||
- always → the marker `changelog.d/README.md` must exist (what keeps the
|
- always → the marker `changelog.d/README.md` must exist (what keeps the
|
||||||
directory tracked when it holds no fragments), no `## Unreleased`
|
directory tracked when it holds no fragments), no `## Unreleased` section
|
||||||
section may survive in `CHANGELOG.md` (a second anchor with no owner),
|
may survive in `CHANGELOG.md` (a second anchor with no owner), and every
|
||||||
and every fragment must be publishable on its own — named `<issue>.md`
|
fragment must be publishable on its own — named `<issue>.md` or
|
||||||
or `<repo>-<issue>.md`, no `## ` heading, at least one bullet, no
|
`<repo>-<issue>.md`, no `## ` heading, at least one bullet, no `### `
|
||||||
`### ` heading without an entry. A malformed fragment fails the PR that
|
heading without an entry. A malformed fragment fails the PR that wrote it,
|
||||||
wrote it, not the release that consumes it (#112 D9).
|
not the release that consumes it (#112 D9).
|
||||||
- `-dev` tree → nothing more. The directory **is** the arming: the next
|
- `-dev` tree → nothing more. The directory **is** the arming: the next PR's
|
||||||
PR's entry is a new file, and a new file always has somewhere to land.
|
entry is a new file, and a new file always has somewhere to land.
|
||||||
- bare tree (the ceremony PR and its merge) → every fragment must be
|
- bare tree (the ceremony PR and its merge) → every fragment must be
|
||||||
consumed, and the top section must be the stamped, publishable section
|
consumed, and the top section must be the stamped, publishable section for
|
||||||
for exactly that version. Fragment mode has no re-armed shape — there
|
exactly that version. Fragment mode has no re-armed shape — there is
|
||||||
is nothing left to re-arm.
|
nothing left to re-arm.
|
||||||
|
|
||||||
In **legacy mode** — no `changelog.d/` — the version-keyed rules stand
|
In **legacy mode** — no `changelog.d/` — the version-keyed rules stand
|
||||||
verbatim; both shapes stay supported so a consumer adopts fragments on a
|
verbatim; both shapes stay supported so a consumer adopts fragments on a pin
|
||||||
pin bump, on its own schedule (#112 D8):
|
bump, on its own schedule (#112 D8):
|
||||||
|
|
||||||
- `-dev` tree → the top section **must** be `## Unreleased`.
|
- `-dev` tree → the top section **must** be `## Unreleased`.
|
||||||
- bare tree (the ceremony PR and its merge) → the top section may be
|
- bare tree (the ceremony PR and its merge) → the top section may be
|
||||||
|
|
@ -166,32 +233,30 @@ pin bump, on its own schedule (#112 D8):
|
||||||
uses, so the two cannot disagree about what a section is).
|
uses, so the two cannot disagree about what a section is).
|
||||||
|
|
||||||
**The incident**: box#108 / rig#66 — the silent mislanding described
|
**The incident**: box#108 / rig#66 — the silent mislanding described
|
||||||
[above](#what-a-release-is). Fragment mode retires the incident's
|
[above](#what-a-release-is). Fragment mode retires the incident's mechanism
|
||||||
mechanism outright; legacy mode guards it. **Red means** a PR entry has
|
outright; legacy mode guards it. **Red means** a PR entry has nowhere safe
|
||||||
nowhere safe to land — a missing marker, a surviving `## Unreleased`, a
|
to land — a missing marker, a surviving `## Unreleased`, a malformed
|
||||||
malformed fragment — or a stamped version would publish no entries, a
|
fragment — or a stamped version would publish no entries, a dangling grouped
|
||||||
dangling grouped heading, or a bare tree still carrying fragments the
|
heading, or a bare tree still carrying fragments the stamp did not consume
|
||||||
stamp did not consume (`not consumed` — re-run the assembler); the
|
(`not consumed` — re-run the assembler); the message names the fix in every
|
||||||
message names the fix in every case. What this guard cannot see is a
|
case. What this guard cannot see is a fragment that *was* consumed but whose
|
||||||
fragment that *was* consumed but whose entry the stamp omits — the
|
entry the stamp omits — the fragment is gone from HEAD, so only
|
||||||
fragment is gone from HEAD, so only
|
|
||||||
[changelog-assembled](#changelog-assembled--the-stamp-is-exactly-the-fragments)'s
|
[changelog-assembled](#changelog-assembled--the-stamp-is-exactly-the-fragments)'s
|
||||||
merge-base replay catches that loss.
|
merge-base replay catches that loss.
|
||||||
|
|
||||||
**Do not "simplify" this to "always require `## Unreleased`".** The
|
**Do not "simplify" this to "always require `## Unreleased`".** The
|
||||||
unconditional form is false by construction on the ceremony PR's own tree
|
unconditional form is false by construction on the ceremony PR's own tree —
|
||||||
— it makes every release unshippable — and rig#44 and cast#108 both had
|
it makes every release unshippable — and rig#44 and cast#108 both had to
|
||||||
to revert exactly that
|
revert exactly that. The version-keyed form is what rig and cast get back by
|
||||||
([the script's header](actions/changelog-armed/changelog-armed.sh#L8-L16)).
|
adopting this repo.
|
||||||
The version-keyed form is what rig and cast get back by adopting this repo.
|
|
||||||
|
|
||||||
One consequence worth knowing before it happens, legacy mode only: a
|
One consequence worth knowing before it happens, legacy mode only: a
|
||||||
ceremony PR that stamps and forgets to re-arm still passes this guard — a
|
ceremony PR that stamps and forgets to re-arm still passes this guard — a
|
||||||
bare tree is allowed to be stamped. It goes red **the moment the automatic
|
bare tree is allowed to be stamped. It goes red **the moment the automatic
|
||||||
`-dev` bump lands on main**. The guard does not block the release; it
|
`-dev` bump lands on main**. The guard does not block the release; it
|
||||||
refuses to let main *sit* disarmed, which is the window a late PR falls
|
refuses to let main *sit* disarmed, which is the window a late PR falls
|
||||||
into. Fragment mode has no such window: with no re-arm step there is
|
into. Fragment mode has no such window: with no re-arm step there is nothing
|
||||||
nothing to forget.
|
to forget.
|
||||||
|
|
||||||
### changelog-assembled — the stamp is exactly the fragments
|
### changelog-assembled — the stamp is exactly the fragments
|
||||||
|
|
||||||
|
|
@ -199,126 +264,122 @@ nothing to forget.
|
||||||
([actions/changelog-assembled/changelog-assembled.sh](actions/changelog-assembled/changelog-assembled.sh)):
|
([actions/changelog-assembled/changelog-assembled.sh](actions/changelog-assembled/changelog-assembled.sh)):
|
||||||
on a release PR in fragment mode, the stamped `## X.Y.Z` section must be
|
on a release PR in fragment mode, the stamped `## X.Y.Z` section must be
|
||||||
**byte-for-byte** what the fragments it consumed assemble to. The guard
|
**byte-for-byte** what the fragments it consumed assemble to. The guard
|
||||||
reads the fragments as of the merge base (they are gone from HEAD — that
|
reads the fragments as of the merge base (they are gone from HEAD — that is
|
||||||
is the point of the ceremony), replays `changelog-assemble --check` over
|
the point of the ceremony), replays `changelog-assemble --check` over that
|
||||||
that set, and diffs the result against HEAD's section body. Every tree it
|
set, and diffs the result against HEAD's section body. Every tree it does
|
||||||
does not apply to — a `-dev` tree, legacy mode, no consumed fragments —
|
not apply to — a `-dev` tree, legacy mode, no consumed fragments — passes
|
||||||
passes with a green `NOTICE`, so a non-ceremony PR is never red here.
|
with a green `NOTICE`, so a non-ceremony PR is never red here.
|
||||||
|
|
||||||
**The failure it catches** (#116): assembly is a hand-run step by design —
|
**The failure it catches** (#116): assembly is a hand-run step by design —
|
||||||
the section must land in the PR's diff where the panel reads it (#112 D12)
|
the section must land in the PR's diff where the panel reads it (#112 D12) —
|
||||||
— and a mis-run hand step can leave no trace. The two failure shapes
|
and a mis-run hand step can leave no trace. The two failure shapes differ,
|
||||||
differ, and the guards split them exactly as
|
and the guards split them exactly as
|
||||||
[test/changelog-assembled.test.sh](test/changelog-assembled.test.sh)'s
|
[test/changelog-assembled.test.sh](test/changelog-assembled.test.sh)'s trio
|
||||||
trio rows record: leave a fragment **out of the deletion** and it survives
|
rows record: leave a fragment **out of the deletion** and it survives on
|
||||||
on HEAD, where
|
HEAD, where [changelog-armed](#changelog-armed--main-never-sits-disarmed)
|
||||||
[changelog-armed](#changelog-armed--main-never-sits-disarmed) already
|
already refuses the bare tree (`not consumed`) — this guard goes red too,
|
||||||
refuses the bare tree (`not consumed`) — this guard goes red too, naming
|
naming the entry the section lost. But **delete** a fragment while omitting
|
||||||
the entry the section lost. But **delete** a fragment while omitting its
|
its entry from the stamp, or hand-edit one word of the assembled prose, and
|
||||||
entry from the stamp, or hand-edit one word of the assembled prose, and
|
nothing on HEAD is out of place: armed is green, monotonic is green, and the
|
||||||
nothing on HEAD is out of place: armed is green, monotonic is green, and
|
publisher would happily publish history that is not what the authors wrote.
|
||||||
the publisher would happily publish history that is not what the authors
|
Only the merge-base replay catches those. The replay is what makes a
|
||||||
wrote. Only the merge-base replay catches those. The replay is what
|
hand-run step safe. **This guard needs history** — same stance as the
|
||||||
makes a hand-run step safe. **This guard needs history** — same stance as
|
monotonic guard: `fetch-depth: 0`, and in CI an unresolvable base is a hard
|
||||||
the monotonic guard: `fetch-depth: 0`, and in CI an unresolvable base is a
|
failure, not a skip.
|
||||||
hard failure, not a skip.
|
|
||||||
|
|
||||||
### changelog-monotonic — shipped headings are append-only
|
### changelog-monotonic — shipped headings are append-only
|
||||||
|
|
||||||
**The rule**
|
**The rule**
|
||||||
([actions/changelog-monotonic/changelog-monotonic.sh](actions/changelog-monotonic/changelog-monotonic.sh#L4-L7)):
|
([actions/changelog-monotonic/changelog-monotonic.sh](actions/changelog-monotonic/changelog-monotonic.sh)):
|
||||||
the set of `## X.Y.Z` headings on your branch must be a **superset** of
|
the set of `## X.Y.Z` headings on your branch must be a **superset** of the
|
||||||
the set at the merge base, and no heading may appear twice on HEAD. The
|
set at the merge base, and no heading may appear twice on HEAD. The rule
|
||||||
rule needs no tuning because release headings are append-only by doctrine:
|
needs no tuning because release headings are append-only by doctrine: the
|
||||||
the ceremony adds one and nothing ever legitimately removes one — so
|
ceremony adds one and nothing ever legitimately removes one — so superset
|
||||||
superset has no exception to carve. The ceremony's own stamp passes by
|
has no exception to carve. The ceremony's own stamp passes by construction:
|
||||||
construction: the assembler writes a new `## X.Y.Z — DATE` heading and
|
the assembler writes a new `## X.Y.Z — DATE` heading and removes none.
|
||||||
removes none. Fragment mode changes nothing here (#112 D10): fragments add
|
Fragment mode changes nothing here (#112 D10): fragments add no `## `
|
||||||
no `## ` heading, and `Unreleased` was never in the guard's set — it is
|
heading, and `Unreleased` was never in the guard's set — it is not a version
|
||||||
not a version heading; it is
|
heading; it is
|
||||||
[changelog-armed](#changelog-armed--main-never-sits-disarmed)'s business —
|
[changelog-armed](#changelog-armed--main-never-sits-disarmed)'s business —
|
||||||
which is why a repo's adoption PR can delete it and stay green.
|
which is why a repo's adoption PR can delete it and stay green.
|
||||||
|
|
||||||
**The incidents**: box#122 (caught in review of box#118) — an author
|
**The incidents**: box#122 (caught in review of box#118) — an author adding
|
||||||
adding an entry under `## Unreleased` **replaced** the heading below it
|
an entry under `## Unreleased` **replaced** the heading below it instead of
|
||||||
instead of inserting above it; git merges that cleanly, and the shipped
|
inserting above it; git merges that cleanly, and the shipped section's body
|
||||||
section's body is silently absorbed into `## Unreleased`. And box#118
|
is silently absorbed into `## Unreleased`. And box#118 itself — a bad rebase
|
||||||
itself — a bad rebase *duplicated* a shipped heading, which containment is
|
*duplicated* a shipped heading, which containment is blind to, which is why
|
||||||
blind to, which is why uniqueness-on-HEAD is a separate assert
|
uniqueness-on-HEAD is a separate assert.
|
||||||
([the script](actions/changelog-monotonic/changelog-monotonic.sh#L96-L116)).
|
|
||||||
|
|
||||||
**Red means** a shipped section was deleted (put the heading back and
|
**Red means** a shipped section was deleted (put the heading back and insert
|
||||||
insert **above** it) or duplicated (collapse to one heading; the failure
|
**above** it) or duplicated (collapse to one heading; the failure message
|
||||||
message walks through both fixes with the diff to run). **This guard needs
|
walks through both fixes with the diff to run). **This guard needs
|
||||||
history**: the consumer's checkout must use `fetch-depth: 0`, and in CI an
|
history**: the consumer's checkout must use `fetch-depth: 0`, and in CI an
|
||||||
unresolvable base is a hard failure, not a skip — a guard that can quietly
|
unresolvable base is a hard failure, not a skip — a guard that can quietly
|
||||||
stop guarding is the failure shape this family of checks exists to refuse
|
stop guarding is the failure shape this family of checks exists to refuse.
|
||||||
([strict mode](actions/changelog-monotonic/changelog-monotonic.sh#L60-L79)).
|
|
||||||
|
|
||||||
### drill-recorded — a release carries its evidence
|
### drill-recorded — a release carries its evidence
|
||||||
|
|
||||||
**The rule**
|
**The rule**
|
||||||
([actions/drill-recorded/drill-recorded.sh](actions/drill-recorded/drill-recorded.sh#L23-L48)),
|
([actions/drill-recorded/drill-recorded.sh](actions/drill-recorded/drill-recorded.sh)),
|
||||||
keyed on the tree's version: a `-dev` tree passes with nothing to assert
|
keyed on the tree's version: a `-dev` tree passes with nothing to assert (a
|
||||||
(a development tree ships nothing); a bare tree — the ceremony PR and its
|
development tree ships nothing); a bare tree — the ceremony PR and its merge
|
||||||
merge — must carry `drills/<version>.md` with at least one
|
— must carry `drills/<version>.md` with at least one non-whitespace
|
||||||
non-whitespace character. One file per version, so `0.9.0.md` and
|
character. One file per version, so `0.9.0.md` and `0.9.0-rc1.md` are simply
|
||||||
`0.9.0-rc1.md` are simply different files and prefix confusion is
|
different files and prefix confusion is unrepresentable (#1 constraint 7).
|
||||||
unrepresentable (#1 constraint 7).
|
|
||||||
|
|
||||||
**The incident**: box's CONTRIBUTING said since box#96 that the release
|
**The incident**: box's CONTRIBUTING said since box#96 that the release
|
||||||
ritual must be run and recorded. No release ever did it — box#95, box#114
|
ritual must be run and recorded. No release ever did it — box#95, box#114
|
||||||
and box#148 all shipped as a version bump plus a changelog stamp, because
|
and box#148 all shipped as a version bump plus a changelog stamp, because
|
||||||
the gate was a sentence in a document and the only thing standing on it
|
the gate was a sentence in a document and the only thing standing on it was
|
||||||
was a reviewer remembering to ask. The rule moved into CI, where it fires
|
a reviewer remembering to ask. The rule moved into CI, where it fires
|
||||||
whether or not anyone is paying attention.
|
whether or not anyone is paying attention.
|
||||||
|
|
||||||
**Red means** the release is asserting a ritual it left no evidence of.
|
**Red means** the release is asserting a ritual it left no evidence of.
|
||||||
**The fix is to run the drill** and record it — or to waive it *in
|
**The fix is to run the drill** and record it — or to waive it *in writing*
|
||||||
writing* at the same path: the guard demands a **record, not a passing
|
at the same path: the guard demands a **record, not a passing result**
|
||||||
result** ([below](#the-drill-doctrine)).
|
([below](#the-drill-doctrine)).
|
||||||
|
|
||||||
## The drill doctrine
|
## The drill doctrine
|
||||||
|
|
||||||
**Evidence, not success.** The guard asserts a record exists — a failed
|
**Evidence, not success.** The guard asserts a record exists — a failed
|
||||||
drill honestly written down satisfies it, and so does a maintainer waiver
|
drill honestly written down satisfies it, and so does a maintainer waiver
|
||||||
that says plainly the drill was waived and why. What it refuses is
|
that says plainly the drill was waived and why. What it refuses is silence:
|
||||||
silence: a skip must cost a deliberate, reviewable file in the diff,
|
a skip must cost a deliberate, reviewable file in the diff, which is
|
||||||
which is precisely what box's three silent skips never produced. CI
|
precisely what box's three silent skips never produced. CI cannot run a
|
||||||
cannot run a consumer's drill (box's wants real hardware and the better
|
consumer's drill (box's wants real hardware and the better part of an hour);
|
||||||
part of an hour); it can only refuse a release that never ran one.
|
it can only refuse a release that never ran one.
|
||||||
|
|
||||||
**Each repo defines what its drill *means*** — the gate only reads the
|
**Each repo defines what its drill *means*** — the gate only reads the
|
||||||
record. box asserts the **isolation contract**; rig asserts
|
record. box asserts the **isolation contract**; rig asserts **convergence**
|
||||||
**convergence** (a machine reaches its role, idempotently); cast asserts
|
(a machine reaches its role, idempotently); cast asserts **promotion** (A→B
|
||||||
**promotion** (A→B reproduces, the diff is idempotent); ceremony's own
|
reproduces, the diff is idempotent); ceremony's own drill is a **door
|
||||||
drill is a **door rehearsal** — both doors exercised end-to-end on a
|
rehearsal** — both doors exercised end-to-end on a disposable repo, written
|
||||||
disposable repo (#11 names the six probes); incubator's is TBD in
|
out step by step in [drills/README.md](drills/README.md), with the records
|
||||||
|
themselves in [drills/](drills/); incubator's is TBD in
|
||||||
heavy-duty/incubator. Each repo states its meaning in its own
|
heavy-duty/incubator. Each repo states its meaning in its own
|
||||||
`drills/README.md`. Three different exercises sharing a substrate is why
|
`drills/README.md`. Three different exercises sharing a substrate is why the
|
||||||
the records are per-repo — they are not phases of one script.
|
records are per-repo — they are not phases of one script.
|
||||||
|
|
||||||
**Drills exercise candidate refs, not released artifacts.** A ref is a
|
**Drills exercise candidate refs, not released artifacts.** A ref is a
|
||||||
static identifier that exists as soon as the release branch does, so no
|
static identifier that exists as soon as the release branch does, so no repo
|
||||||
repo has to be released — or drilled — before another can be drilled:
|
has to be released — or drilled — before another can be drilled: what looks
|
||||||
what looks like a box↔rig recursion at runtime dissolves into two
|
like a box↔rig recursion at runtime dissolves into two independent tests
|
||||||
independent tests against one fixed pair of refs. And drilling the
|
against one fixed pair of refs. And drilling the candidate *is* drilling the
|
||||||
candidate *is* drilling the release: a ceremony PR's diff is the stamps
|
release: a ceremony PR's diff is the stamps and nothing else, so no
|
||||||
and nothing else, so no executable byte differs between the tree that was
|
executable byte differs between the tree that was drilled and the tree that
|
||||||
drilled and the tree that ships.
|
ships.
|
||||||
|
|
||||||
**A cross-repo release set shares one run ID.** Each repo records its own
|
**A cross-repo release set shares one run ID.** Each repo records its own
|
||||||
legs in its own `drills/X.Y.Z.md`, citing that run ID and the sibling
|
legs in its own `drills/X.Y.Z.md`, citing that run ID and the sibling SHAs,
|
||||||
SHAs, so the records reconcile afterwards — but the guard only ever reads
|
so the records reconcile afterwards — but the guard only ever reads the repo
|
||||||
the repo it runs in. If a defect shows up only in the combination: patch,
|
it runs in. If a defect shows up only in the combination: patch, re-drill,
|
||||||
re-drill, re-record. The set converges; it is not required to be right in
|
re-record. The set converges; it is not required to be right in one pass.
|
||||||
one pass.
|
|
||||||
|
|
||||||
## Troubleshooting red main
|
## Troubleshooting red main
|
||||||
|
|
||||||
Every refusal the release flow can emit, verbatim, with cause and remedy.
|
Every refusal the release flow can emit, verbatim, with cause and remedy.
|
||||||
The catalog is generated from the sources, not paraphrased — regenerate
|
The catalog is generated from the sources, not paraphrased — regenerate it
|
||||||
it with:
|
with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
grep -n -A2 'refuse \|>&2' lib/decide.sh lib/facts.sh .github/workflows/release.yml
|
grep -n -A2 'refuse \|>&2' lib/decide.sh lib/facts.sh .github/workflows/release.yml
|
||||||
|
|
@ -347,12 +408,11 @@ or — if the tree is genuinely the release — publish by the tag door.
|
||||||
> the version '$VER' is bare and unchanged, but RELEASED is empty — this state is decided by whether '$VER' is already released, and the caller did not establish that fact. Refusing to guess — creating nothing.
|
> the version '$VER' is bare and unchanged, but RELEASED is empty — this state is decided by whether '$VER' is already released, and the caller did not establish that fact. Refusing to guess — creating nothing.
|
||||||
> the version transitioned ('$BASE_VER' -> '$VER') but LABELED is empty — a transition ships only behind a merged, release-labeled PR, and the caller did not establish that fact. Refusing to guess — creating nothing.
|
> the version transitioned ('$BASE_VER' -> '$VER') but LABELED is empty — a transition ships only behind a merged, release-labeled PR, and the caller did not establish that fact. Refusing to guess — creating nothing.
|
||||||
|
|
||||||
The fact-gathering guards
|
The fact-gathering guards ([L92–L105](lib/decide.sh#L92-L105),
|
||||||
([L92–L105](lib/decide.sh#L92-L105), [L135](lib/decide.sh#L135),
|
[L135](lib/decide.sh#L135), [L151](lib/decide.sh#L151)): a missing fact must
|
||||||
[L151](lib/decide.sh#L151)): a missing fact must never fall through to
|
never fall through to "no". These indicate a bug upstream in
|
||||||
"no". These indicate a bug upstream in [lib/facts.sh](lib/facts.sh) or the
|
[lib/facts.sh](lib/facts.sh) or the workflow plumbing, not an operator
|
||||||
workflow plumbing, not an operator mistake — read the run's `facts:`
|
mistake — read the run's `facts:` stderr line and file what you find.
|
||||||
stderr line and file what you find.
|
|
||||||
|
|
||||||
### The facts could not be established ([lib/facts.sh](lib/facts.sh), [lib/version.sh](lib/version.sh))
|
### The facts could not be established ([lib/facts.sh](lib/facts.sh), [lib/version.sh](lib/version.sh))
|
||||||
|
|
||||||
|
|
@ -367,44 +427,44 @@ stderr line and file what you find.
|
||||||
> version_read: node is required for version-source: package-json
|
> version_read: node is required for version-source: package-json
|
||||||
|
|
||||||
[lib/version.sh](lib/version.sh#L16-L66): the tree's version source is
|
[lib/version.sh](lib/version.sh#L16-L66): the tree's version source is
|
||||||
missing, empty, or unreadable. A wrong release is worse than a missing
|
missing, empty, or unreadable. A wrong release is worse than a missing one,
|
||||||
one, so an unreadable state is never an empty print — restore the
|
so an unreadable state is never an empty print — restore the `VERSION` file
|
||||||
`VERSION` file (or `package.json` version field) on main.
|
(or `package.json` version field) on main.
|
||||||
|
|
||||||
### The merge door refused ([release.yml](.github/workflows/release.yml#L136-L300))
|
### The merge door refused ([release.yml](.github/workflows/release.yml#L136-L301))
|
||||||
|
|
||||||
> CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release
|
> CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release
|
||||||
|
|
||||||
[L202–L205](.github/workflows/release.yml#L202-L205): the ceremony merged
|
[L202–L205](.github/workflows/release.yml#L202-L205): the ceremony merged
|
||||||
without its stamp (a state the
|
without its stamp (a state the
|
||||||
[armed guard](#changelog-armed--main-never-sits-disarmed) already refuses
|
[armed guard](#changelog-armed--main-never-sits-disarmed) already refuses on
|
||||||
on the PR — red main here means it was overridden). Stamp the section on
|
the PR — red main here means it was overridden). Stamp the section on main,
|
||||||
main, then publish by the tag door.
|
then publish by the tag door.
|
||||||
|
|
||||||
> tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing.
|
> tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing.
|
||||||
> release '$VER' already exists — refusing to re-release, creating nothing.
|
> release '$VER' already exists — refusing to re-release, creating nothing.
|
||||||
|
|
||||||
[L207–L222](.github/workflows/release.yml#L207-L222), the nothing-exists
|
[L208–L223](.github/workflows/release.yml#L208-L223), the nothing-exists
|
||||||
assert — what makes a re-run of a completed ceremony refuse instead of
|
assert — what makes a re-run of a completed ceremony refuse instead of
|
||||||
clobber, and what catches a manual tag racing the merge. If the release
|
clobber, and what catches a manual tag racing the merge. If the release
|
||||||
truly exists, there is nothing to do: this red is the system declining to
|
truly exists, there is nothing to do: this red is the system declining to do
|
||||||
do the thing twice. If the tag exists but the release does not (a manual
|
the thing twice. If the tag exists but the release does not (a manual tag
|
||||||
tag won the race, or
|
won the race, or
|
||||||
[a failed artifact hook](docs/CONSUMERS.md#the-artifact-hook)), recover by
|
[a failed artifact hook](docs/CONSUMERS.md#the-artifact-hook)), recover by
|
||||||
the tag door: delete and re-push the tag, or `gh release create` by hand
|
the tag door: delete and re-push the tag, or `gh release create` by hand
|
||||||
from a fixed tree.
|
from a fixed tree.
|
||||||
|
|
||||||
> direct push refused (branch protection?) — opening the bump PR instead
|
> direct push refused (branch protection?) — opening the bump PR instead
|
||||||
|
|
||||||
[L292–L300](.github/workflows/release.yml#L292-L300) — loud, but not a
|
[L293–L301](.github/workflows/release.yml#L293-L301) — loud, but not a
|
||||||
refusal: the post-release `-dev` bump could not push directly, so the run
|
refusal: the post-release `-dev` bump could not push directly, so the run
|
||||||
opened a `release`-labeled bump PR itself. Your move: merge it promptly —
|
opened a `release`-labeled bump PR itself. Your move: merge it promptly —
|
||||||
until it lands, main is sitting bare, where a dev install
|
until it lands, main is sitting bare, where a dev install impersonates the
|
||||||
[impersonates the release](.github/workflows/release.yml#L291) and the
|
release and the
|
||||||
[armed guard's window](#changelog-armed--main-never-sits-disarmed) stays
|
[armed guard's window](#changelog-armed--main-never-sits-disarmed) stays
|
||||||
open.
|
open.
|
||||||
|
|
||||||
### The tag door refused ([release.yml](.github/workflows/release.yml#L302-L369))
|
### The tag door refused ([release.yml](.github/workflows/release.yml#L303-L371))
|
||||||
|
|
||||||
> tag '$GITHUB_REF_NAME' does not match the tree's version '$ver' — creating nothing.
|
> tag '$GITHUB_REF_NAME' does not match the tree's version '$ver' — creating nothing.
|
||||||
> A release is a PR, then a tag: the release PR bumps the version and stamps the changelog; the tag goes on its MERGE commit. Delete this tag and re-tag the right commit.
|
> A release is a PR, then a tag: the release PR bumps the version and stamps the changelog; the tag goes on its MERGE commit. Delete this tag and re-tag the right commit.
|
||||||
|
|
@ -416,40 +476,42 @@ remedy.
|
||||||
|
|
||||||
[L346–L349](.github/workflows/release.yml#L346-L349). The tagged tree was
|
[L346–L349](.github/workflows/release.yml#L346-L349). The tagged tree was
|
||||||
never stamped. Assemble the section
|
never stamped. Assemble the section
|
||||||
([docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)),
|
([docs/CONSUMERS.md](docs/CONSUMERS.md#assembling-a-release-section)), then
|
||||||
then delete and re-push the tag.
|
delete and re-push the tag.
|
||||||
|
|
||||||
### Red main that is not the release workflow
|
### Red main that is not the release workflow
|
||||||
|
|
||||||
Consumer CI runs its guard steps on pushes to main too (this repo's
|
Consumer CI runs its guard steps on pushes to main too (this repo's
|
||||||
[ci.yml](.github/workflows/ci.yml) does the same). The one guard red an
|
[ci.yml](.github/workflows/ci.yml) does the same). The one guard red an
|
||||||
operator will actually meet on main is **changelog-armed after a re-arm
|
operator will actually meet on main is **changelog-armed after a re-arm was
|
||||||
was forgotten — legacy mode only**: the ceremony stamped without putting
|
forgotten — legacy mode only**: the ceremony stamped without putting
|
||||||
`## Unreleased` back, the release's own `-dev` bump landed, and the guard
|
`## Unreleased` back, the release's own `-dev` bump landed, and the guard now
|
||||||
now says (first line):
|
says (first line):
|
||||||
|
|
||||||
> changelog-armed: the version is '$ver' (a development tree) but the top
|
> changelog-armed: the version is '$ver' (a development tree) but the top
|
||||||
> section of $changelog is: …
|
> section of $changelog is: …
|
||||||
|
|
||||||
The fix is a one-line PR: add an empty `## Unreleased` above the stamped
|
The fix is a one-line PR: add an empty `## Unreleased` above the stamped
|
||||||
section. The full message carries the same instruction. Fragment mode has
|
section. The full message carries the same instruction. Fragment mode has no
|
||||||
no re-arm to forget, so it has no equivalent red on main — its refusals
|
re-arm to forget, so it has no equivalent red on main — its refusals (a
|
||||||
(a missing marker, a surviving `## Unreleased`, a malformed or unconsumed
|
missing marker, a surviving `## Unreleased`, a malformed or unconsumed
|
||||||
fragment) all fire on the PR that caused them, where the author is still
|
fragment) all fire on the PR that caused them, where the author is still
|
||||||
holding it.
|
holding it.
|
||||||
|
|
||||||
## Design lineage
|
## Design lineage
|
||||||
|
|
||||||
The ceremony converged across box#83 → box#96, rig#32 → rig#47, and
|
The ceremony converged across box#83 → box#96, rig#32 → rig#47 and cast#96 →
|
||||||
cast#96 → cast#111; this repo is those three implementations folded into
|
cast#111; this repo is those three implementations folded into one, and the
|
||||||
one (the drift that motivated it is measured in
|
drift that motivated it is measured in
|
||||||
[#1](https://github.com/heavy-duty/ceremony/issues/1)). The load-bearing
|
[#1](https://github.com/heavy-duty/ceremony/issues/1), which also lists the
|
||||||
constraints — each bought with an incident, none of them safe to
|
load-bearing constraints — each bought with an incident, none of them safe
|
||||||
"simplify" away — are listed in
|
to "simplify" away. The label machine's own record is #10, #11 and #130; the
|
||||||
[#1](https://github.com/heavy-duty/ceremony/issues/1) and carried, with
|
issue-flow queue's is #15, #16 and #73; the fragment changelog's is #112 and
|
||||||
their war stories, in the headers of the scripts they bind:
|
#116; the sweep/trigger split is #209.
|
||||||
[release.yml](.github/workflows/release.yml#L1-L109),
|
|
||||||
[lib/decide.sh](lib/decide.sh#L1-L74),
|
The narrative lives in those issues, by design: the war stories are carried
|
||||||
[lib/facts.sh](lib/facts.sh#L1-L24), and the four
|
in the headers of the scripts they bind —
|
||||||
[guard scripts](actions/). The comments are the documentation of record;
|
[release.yml](.github/workflows/release.yml),
|
||||||
this README is their operator-facing cut.
|
[lib/decide.sh](lib/decide.sh), [lib/facts.sh](lib/facts.sh) and the
|
||||||
|
[guard scripts](actions/) — and those comments are the documentation of
|
||||||
|
record. This README is their operator-facing cut.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue