forked from heavy-duty/box
fix: the armed guard also refuses a half-done ceremony (#108, cast#114 review)
The bare-VERSION branch left the top heading unconstrained on purpose — both ceremony shapes have to stay legal, which is the rig#44 / cast#108 lesson. A review round on the sibling fix found the gap that asymmetry leaves: a tree with VERSION bumped, '## Unreleased' still populated on top, and no stamped section for that version makes the wrong-number test false on its first clause, short-circuits, and passes. release.yml then refuses at publish time — after the merge, on main, with the release already half-shipped. So the bare branch now also requires the section it is about to publish to exist and be non-empty, asserted by running release-notes.sh itself so the guard and the publisher cannot drift over what a section is. The message is distinct from the wrong-number case: a missing stamp is not a misnumbered one. Matches heavy-duty/rig#67. test/release.sh constructs the half-ceremony tree and the stamped-but-empty tree and drives the real script at both — all three new assertions fail against the previous guard — while the re-armed and un-re-armed ceremony trees stay green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1a9d96168d
commit
67331ebecd
3 changed files with 78 additions and 2 deletions
39
.github/scripts/changelog-armed.sh
vendored
39
.github/scripts/changelog-armed.sh
vendored
|
|
@ -19,7 +19,9 @@ set -euo pipefail
|
||||||
# VERSION ends in -dev -> the top section MUST be '## Unreleased'
|
# VERSION ends in -dev -> the top section MUST be '## Unreleased'
|
||||||
# VERSION is bare -> the top section may be '## Unreleased' (armed,
|
# VERSION is bare -> the top section may be '## Unreleased' (armed,
|
||||||
# the ceremony's own re-arm) or the stamped
|
# the ceremony's own re-arm) or the stamped
|
||||||
# section for exactly that VERSION
|
# section for exactly that VERSION — AND the
|
||||||
|
# section for that VERSION must exist and carry
|
||||||
|
# prose, because it is the one about to ship
|
||||||
#
|
#
|
||||||
# Keying on VERSION is the whole design, and the reason this is not simply
|
# Keying on VERSION is the whole design, and the reason this is not simply
|
||||||
# "require '## Unreleased'". That unconditional form is what rig#44 and
|
# "require '## Unreleased'". That unconditional form is what rig#44 and
|
||||||
|
|
@ -41,6 +43,11 @@ set -euo pipefail
|
||||||
changelog="${1:-CHANGELOG.md}"
|
changelog="${1:-CHANGELOG.md}"
|
||||||
version_file="${2:-VERSION}"
|
version_file="${2:-VERSION}"
|
||||||
|
|
||||||
|
# release-notes.sh lives beside this script; the bare-VERSION branch runs it
|
||||||
|
# rather than re-implementing the extraction, so the guard and the publisher
|
||||||
|
# cannot disagree about what a section is or when one counts as empty.
|
||||||
|
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
[ -f "$changelog" ] || { echo "changelog-armed: no such file: $changelog" >&2; exit 1; }
|
[ -f "$changelog" ] || { echo "changelog-armed: no such file: $changelog" >&2; exit 1; }
|
||||||
[ -f "$version_file" ] || { echo "changelog-armed: no such file: $version_file" >&2; exit 1; }
|
[ -f "$version_file" ] || { echo "changelog-armed: no such file: $version_file" >&2; exit 1; }
|
||||||
|
|
||||||
|
|
@ -99,6 +106,36 @@ changelog-armed: VERSION is '$ver' but the top section of $changelog is:
|
||||||
itself. A stamped section naming a different version means the ceremony
|
itself. A stamped section naming a different version means the ceremony
|
||||||
stamped the wrong number, and the published release body would come from
|
stamped the wrong number, and the published release body would come from
|
||||||
the wrong section.
|
the wrong section.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The top heading is deliberately left UNCONSTRAINED above — both ceremony
|
||||||
|
# shapes must stay legal, which is the #44 / cast#108 lesson and is not
|
||||||
|
# negotiable. That asymmetry leaves a gap of its own, the HALF-ceremony
|
||||||
|
# tree: VERSION bumped to the release, a populated '## Unreleased' still on
|
||||||
|
# top, and no stamped section for the version anywhere. The test above is
|
||||||
|
# false on its first clause, short-circuits, and passes. Nothing else
|
||||||
|
# refuses until release.yml extracts the notes — which happens AFTER the
|
||||||
|
# merge, on main, and publishes a release with an empty body, the worst
|
||||||
|
# place for this to land. So make the same assert one step earlier by
|
||||||
|
# running the very script release.yml runs (heavy-duty/rig#67).
|
||||||
|
if ! bash "$here/release-notes.sh" "$ver" "$changelog" >/dev/null 2>&1; then
|
||||||
|
cat >&2 <<EOF
|
||||||
|
changelog-armed: VERSION is '$ver' but $changelog has no non-empty section for
|
||||||
|
'$ver'. The top section is:
|
||||||
|
|
||||||
|
$top
|
||||||
|
|
||||||
|
This is a HALF-DONE ceremony: the version was bumped but its section was
|
||||||
|
never stamped — the stamp is MISSING, not misnumbered. A bare VERSION means
|
||||||
|
this tree is a release, and the section it is about to publish has to exist
|
||||||
|
and have prose in it. Left alone, this passes CI, merges, and only then does
|
||||||
|
release.yml refuse to extract the notes — on main, after the fact, with the
|
||||||
|
release already half-shipped.
|
||||||
|
|
||||||
|
The fix is the ceremony's first edit (CONTRIBUTING.md, "Releases"): stamp
|
||||||
|
'## Unreleased' into '## $ver — DATE', then put an empty '## Unreleased'
|
||||||
|
back above it.
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -63,7 +63,22 @@ which records not just what changed but what each drill run proved.
|
||||||
false by construction on the ceremony PR's own tree, which is why rig#44
|
false by construction on the ceremony PR's own tree, which is why rig#44
|
||||||
and heavy-duty/cast#108 both had to revert it. So a forgotten re-arm
|
and heavy-duty/cast#108 both had to revert it. So a forgotten re-arm
|
||||||
does not block the release; it turns `main` red on the very next push,
|
does not block the release; it turns `main` red on the very next push,
|
||||||
the automatic `-dev` bump the release itself makes.
|
the automatic `-dev` bump the release itself makes. Leaving the bare
|
||||||
|
branch's top heading unconstrained is what keeps both ceremony shapes
|
||||||
|
legal, and a review round on the sibling fix (heavy-duty/cast#114) found
|
||||||
|
the gap that asymmetry leaves: a **half-ceremony** tree — `VERSION`
|
||||||
|
bumped, `## Unreleased` still populated on top, and the section for that
|
||||||
|
version never stamped — makes the wrong-number test false on its first
|
||||||
|
clause, short-circuits, and passes. Nothing then refuses until
|
||||||
|
`release.yml` extracts the notes, which is *after* the merge, on `main`,
|
||||||
|
with the release already half-shipped. So the bare branch now also
|
||||||
|
requires that the section it is about to publish exists and is non-empty,
|
||||||
|
and it asserts that by running `release-notes.sh` — the very script
|
||||||
|
`release.yml` runs — so the guard and the publisher cannot drift apart
|
||||||
|
over what a section is. The message is its own: a missing stamp is not a
|
||||||
|
misnumbered one, and an operator sent to correct a version number that is
|
||||||
|
already right will not find the real problem. Matches
|
||||||
|
heavy-duty/rig#67, so the three repos agree.
|
||||||
- **`box restore` asks before it destroys — and the confirmation prompt is
|
- **`box restore` asks before it destroys — and the confirmation prompt is
|
||||||
now the row's, not rm's** (#105) — `restore` and `rm` both irreversibly
|
now the row's, not rm's** (#105) — `restore` and `rm` both irreversibly
|
||||||
discard user state, and only one of them asked. The table gave `restore`
|
discard user state, and only one of them asked. The table gave `restore`
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,30 @@ check "armed: ...and so does the RE-ARMED ceremony tree (the shape #108 asks for
|
||||||
T="$(tree rel-wrong 0.7.1 '## 0.7.0 — 2026-07-19' '' '- **Some other release**')"
|
T="$(tree rel-wrong 0.7.1 '## 0.7.0 — 2026-07-19' '' '- **Some other release**')"
|
||||||
check "armed: a bare VERSION under someone ELSE's stamped section fails" 1 "wrong number" armed "$T"
|
check "armed: a bare VERSION under someone ELSE's stamped section fails" 1 "wrong number" armed "$T"
|
||||||
|
|
||||||
|
# --- the HALF-ceremony: the gap the two bare-VERSION clauses leave ---------
|
||||||
|
# VERSION bumped to the release, '## Unreleased' still populated on top, and
|
||||||
|
# the section for that version never stamped at all. The wrong-number test
|
||||||
|
# above is false on its FIRST clause here and short-circuits, so before
|
||||||
|
# heavy-duty/rig#67's rule this tree passed the guard and was refused instead
|
||||||
|
# by release.yml — at publish time, after the merge, on main, with the release
|
||||||
|
# already half-shipped. Caught here one step earlier, by running the same
|
||||||
|
# extraction release.yml runs.
|
||||||
|
T="$(tree rel-half 0.8.0 '## Unreleased' '' '- **A pending entry**' '' '## 0.7.0 — 2026-07-19' '' '- **Shipped**')"
|
||||||
|
check "armed: a bare VERSION whose section was never stamped fails (half-ceremony)" \
|
||||||
|
1 "no non-empty section" armed "$T"
|
||||||
|
check "armed: ...and names the stamp as MISSING, not misnumbered" \
|
||||||
|
1 "MISSING, not misnumbered" armed "$T"
|
||||||
|
# The wording is the whole point of the separate branch: an operator sent to
|
||||||
|
# fix a version number that is already correct will not find the real problem.
|
||||||
|
not_wrong_number() { ! armed "$1" 2>&1 | grep -qF 'wrong number'; }
|
||||||
|
check "armed: ...and not as the wrong-number case, which has a different fix" \
|
||||||
|
0 "" not_wrong_number "$T"
|
||||||
|
# A section that exists but carries no prose is the same failure: release.yml
|
||||||
|
# would publish an empty body, which is what release-notes.sh already refuses.
|
||||||
|
T="$(tree rel-empty 0.7.1 '## 0.7.1 — 2026-07-19' '' '## 0.7.0 — 2026-07-19' '' '- **Shipped**')"
|
||||||
|
check "armed: a bare VERSION whose section is stamped but EMPTY fails" \
|
||||||
|
1 "no non-empty section" armed "$T"
|
||||||
|
|
||||||
# --- degenerate trees refuse rather than pass by accident ------------------
|
# --- degenerate trees refuse rather than pass by accident ------------------
|
||||||
T="$(tree no-sections 0.7.1-dev 'Prose and no headings at all.')"
|
T="$(tree no-sections 0.7.1-dev 'Prose and no headings at all.')"
|
||||||
check "armed: a changelog with no '## ' section at all fails" 1 "no '## ' section at all" armed "$T"
|
check "armed: a changelog with no '## ' section at all fails" 1 "no '## ' section at all" armed "$T"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue