#!/usr/bin/env bash set -euo pipefail # changelog-armed.sh [] [] — 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@` 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 <&2 <&2 <