159 lines
7.5 KiB
Bash
159 lines
7.5 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
# changelog-armed.sh [<changelog>] [<version-source>] — assert that the
|
||
|
|
# changelog is ARMED: that there is a heading for the next PR's entry to
|
||
|
|
# land under, and that it is the right one for the state this tree is in.
|
||
|
|
#
|
||
|
|
# Ported from box .github/scripts/changelog-armed.sh (box#108, confirmed
|
||
|
|
# cross-repo as rig#66) — box is the only repo that carries this guard
|
||
|
|
# today. Rig and cast LOST it: the naive form — "always require
|
||
|
|
# '## Unreleased' on top" — is false by construction on the ceremony PR's
|
||
|
|
# own tree, which legitimately stamps that heading away, so rig#44 and
|
||
|
|
# cast#108 both had to revert exactly that. This version-keyed form is the
|
||
|
|
# guard rig and cast regain when they adopt ceremony (#13, #15). Anyone
|
||
|
|
# tempted to simplify this back to the unconditional form should read
|
||
|
|
# those two reverts first.
|
||
|
|
#
|
||
|
|
# The failure it exists to catch (box#108, rig#66) leaves no trace: the
|
||
|
|
# ceremony PR stamps '## Unreleased' into '## X.Y.Z — DATE' by hand, and
|
||
|
|
# nothing puts the heading back. A PR authored BEFORE the release wrote its
|
||
|
|
# entry under '## Unreleased'; that heading is gone by the time it merges, so
|
||
|
|
# git lands the entry under whatever heading now occupies that position — the
|
||
|
|
# just-shipped section — CLEANLY, with no conflict. The one signal an author
|
||
|
|
# would trust ("git told me to look") is absent exactly when the result is
|
||
|
|
# wrong, and the drift is only ever discovered by reading the file.
|
||
|
|
#
|
||
|
|
# The rule, keyed on the tree's version (a VERSION file or package.json,
|
||
|
|
# per version-source), because the two states are genuinely different:
|
||
|
|
#
|
||
|
|
# version ends in -dev -> the top section MUST be '## Unreleased'
|
||
|
|
# version is bare -> the top section may be '## Unreleased' (armed,
|
||
|
|
# the ceremony's own re-arm) or the stamped
|
||
|
|
# section for exactly that version — AND the
|
||
|
|
# section for that version must exist and carry
|
||
|
|
# prose, because it is the one about to ship
|
||
|
|
#
|
||
|
|
# The consequence worth stating plainly: a ceremony PR that stamps and forgets
|
||
|
|
# to re-arm still passes here — its version is bare, and a bare tree is
|
||
|
|
# allowed to be stamped. It goes red the moment the '-dev' bump lands on main,
|
||
|
|
# which the release workflow does automatically in the same job as the
|
||
|
|
# publish. So the guard does not block the release; it refuses to let main
|
||
|
|
# SIT disarmed, which is the window a late PR can fall into.
|
||
|
|
#
|
||
|
|
# A file of its own (not inlined in action.yml) so
|
||
|
|
# test/changelog-armed.test.sh can drive it against constructed trees for
|
||
|
|
# both states — the same discipline as the libs it sources.
|
||
|
|
|
||
|
|
changelog="${1:-${CHANGELOG:-CHANGELOG.md}}"
|
||
|
|
version_source="${2:-${VERSION_SOURCE:-file}}"
|
||
|
|
|
||
|
|
# The shared libs travel with this action: a consumer's
|
||
|
|
# `uses: heavy-duty/ceremony/actions/changelog-armed@<tag>` downloads this
|
||
|
|
# whole repository at that ref, so ../../lib is always present and always at
|
||
|
|
# the same ref — no checkout step, no version skew possible.
|
||
|
|
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||
|
|
# shellcheck source=lib/version.sh
|
||
|
|
. "$here/../../lib/version.sh"
|
||
|
|
# shellcheck source=lib/changelog.sh
|
||
|
|
. "$here/../../lib/changelog.sh"
|
||
|
|
|
||
|
|
[ -f "$changelog" ] || { echo "changelog-armed: no such file: $changelog" >&2; exit 1; }
|
||
|
|
|
||
|
|
# version_read refuses loudly on a missing or empty source; the wrapper line
|
||
|
|
# names the guard so a workflow log shows which check refused.
|
||
|
|
ver="$(version_read "$version_source")" || {
|
||
|
|
echo "changelog-armed: cannot read the version (version-source: $version_source)" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# The TOP section: the first '## ' heading in the file. Everything above it is
|
||
|
|
# the changelog's own preamble and belongs to no section.
|
||
|
|
top="$(grep -m1 '^## ' "$changelog" || true)"
|
||
|
|
[ -n "$top" ] || {
|
||
|
|
echo "changelog-armed: $changelog has no '## ' section at all — nothing for a PR entry to land under" >&2
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# '## 0.7.0 — 2026-07-19' -> '0.7.0'. Split on whitespace, the same shape
|
||
|
|
# changelog_section matches on, so the two cannot disagree about what a
|
||
|
|
# section header is.
|
||
|
|
top_ver="$(printf '%s\n' "$top" | awk '{ print $2 }')"
|
||
|
|
|
||
|
|
# version_is_dev is the single definition of the -dev special case (#3): an
|
||
|
|
# rc is a pre-release, not a dev tree, and keys on the bare rules below.
|
||
|
|
if version_is_dev "$ver"; then
|
||
|
|
if [ "$top_ver" != "Unreleased" ]; then
|
||
|
|
cat >&2 <<EOF
|
||
|
|
changelog-armed: the version is '$ver' (a development tree) but the top
|
||
|
|
section of $changelog is:
|
||
|
|
|
||
|
|
$top
|
||
|
|
|
||
|
|
A -dev tree MUST carry '## Unreleased' at the top. Without it, a PR that
|
||
|
|
wrote its entry under '## Unreleased' before the release merges CLEANLY into
|
||
|
|
the section above — the one that already shipped — and the changelog quietly
|
||
|
|
misattributes it (box#108, rig#66).
|
||
|
|
|
||
|
|
The fix is to re-arm: add an empty '## Unreleased' immediately above
|
||
|
|
'$top'. The release ceremony is supposed to do this in the same edit that
|
||
|
|
stamps the version.
|
||
|
|
EOF
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
# A bare version is the ceremony tree and the merge commit that publishes
|
||
|
|
# it. Both arrangements are legal there: re-armed ('## Unreleased' back on
|
||
|
|
# top, above the section just stamped) or not yet re-armed (the stamped
|
||
|
|
# section still on top). What is NOT legal is a stamped top section naming
|
||
|
|
# some OTHER version — that is a ceremony that stamped the wrong number,
|
||
|
|
# and the release workflow would publish a body that is not this release's.
|
||
|
|
if [ "$top_ver" != "Unreleased" ] && [ "$top_ver" != "$ver" ]; then
|
||
|
|
cat >&2 <<EOF
|
||
|
|
changelog-armed: the version is '$ver' but the top section of $changelog is:
|
||
|
|
|
||
|
|
$top
|
||
|
|
|
||
|
|
A bare version means this tree is a release. Its top section must be either
|
||
|
|
'## Unreleased' (re-armed after stamping) or the stamped section for '$ver'
|
||
|
|
itself. A stamped section naming a different version means the ceremony
|
||
|
|
stamped the wrong number, and the published release body would come from
|
||
|
|
the wrong section.
|
||
|
|
EOF
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
# The top heading is deliberately left UNCONSTRAINED above — both ceremony
|
||
|
|
# shapes must stay legal, which is the rig#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 the publisher 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 through
|
||
|
|
# the very extractor the publisher uses — changelog_section (#4) — so the
|
||
|
|
# guard and the publisher cannot disagree about what a section is or when
|
||
|
|
# one counts as empty (rig#67).
|
||
|
|
if [ -z "$(changelog_section "$changelog" "$ver")" ]; then
|
||
|
|
cat >&2 <<EOF
|
||
|
|
changelog-armed: the 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
|
||
|
|
the publisher refuse to extract the notes — on main, after the fact, with
|
||
|
|
the release already half-shipped.
|
||
|
|
|
||
|
|
The fix is the ceremony's first edit: stamp '## Unreleased' into
|
||
|
|
'## $ver — DATE', then put an empty '## Unreleased' back above it.
|
||
|
|
EOF
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "changelog-armed: version '$ver' agrees with the top section ($top_ver)"
|