ceremony/.github/scripts/self-ref-check.sh
claude-bot-andresmgsl ad8ebfe2c9 feat: the reusable release workflow — both doors, one implementation
Closes #9. .github/workflows/release.yml (workflow_call) replaces box's,
rig's and cast's release.yml: the merge door (facts -> decide -> notes ->
tag+publish+bump, every decision in a tested lib script) and the tag door
(the manual fallback and backfill, no bump). Plus lib/facts.sh (the merge
door's impure half, contract-tested against fixture repos with a stubbed
gh), the self-ref pin guard (.github/scripts/self-ref-check.sh + CI step +
tests), the release-exercise scratch caller (dry wiring), the end-to-end
script-chain rehearsal, and the caller + artifact-hook contracts in
docs/CONSUMERS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:55:39 +00:00

94 lines
3.6 KiB
Bash

#!/usr/bin/env bash
# The self-ref pin guard (issue #9; #1 D3). The reusable workflows carry a
# literal `CEREMONY_SELF_REF: "X.Y.Z"` — the ref consumers' runs check this
# repo out at — because a called workflow file arrives without its
# repository, and checkout `ref:`s must be literals. A stale pin must fail
# CI HERE, not a consumer's release. The rules, keyed on this repo's own
# version state (they presume #11's dogfood tree; before it, no VERSION
# exists and the rules cannot bind yet):
#
# * bare VERSION → pin == VERSION (the ceremony PR stamps the pin to the
# version it releases — the third stamp, alongside VERSION and the
# changelog).
# * -dev VERSION → pin == the newest stamped `## X.Y.Z` heading in
# CHANGELOG.md (the last release — reverse-chronological, so the first
# bare-X.Y.Z heading from the top; whole-field match, so an rc heading
# never satisfies it). No stamped heading yet (the pre-first-release
# tree) → pin == VERSION with -dev stripped.
#
# The self-consumption bypass in release.yml means the pin is never
# load-bearing for this repo's own releases — only for consumers'.
#
# Usage: self-ref-check.sh [tree-dir] (default: the repo root — the CI
# step; tests point it at fixture trees)
set -euo pipefail
# shellcheck source=lib/version.sh
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../../lib/version.sh"
tree="${1:-.}"
fail() {
printf '%s\n' "$@" >&2
exit 1
}
# Every workflow carrying the pin must agree — labels.yml carries its own
# copy of the same env (one pin governs machinery and doctrine alike).
shopt -s nullglob
pins=()
carriers=()
for wf in "$tree"/.github/workflows/*.yml; do
pin="$(awk -F'"' '/^[[:space:]]*CEREMONY_SELF_REF:/ { print $2; exit }' "$wf")"
[ -n "$pin" ] || continue
pins+=("$pin")
carriers+=("$wf")
done
if [ "${#pins[@]}" -eq 0 ]; then
fail "self-ref-check: no CEREMONY_SELF_REF found under $tree/.github/workflows — the guard has nothing to guard, which is itself a failure (the reusable workflows carry the pin)."
fi
pin="${pins[0]}"
for i in "${!pins[@]}"; do
if [ "${pins[$i]}" != "$pin" ]; then
for j in "${!pins[@]}"; do
echo " ${carriers[$j]}: ${pins[$j]}" >&2
done
fail "self-ref-check: the pins disagree — every workflow must name the same ceremony release."
fi
done
if [ ! -f "$tree/VERSION" ]; then
# The pre-dogfood window: #11 adds VERSION and CHANGELOG.md, and this
# branch dies with it (VERSION, once added, never leaves). Until then the
# rules have no version state to key on.
echo "NOTICE: no VERSION file — the pre-dogfood tree (#11 adds it). The pin rules key on the version state and cannot bind yet; pin is '$pin', unchecked."
exit 0
fi
ver="$(version_read file "$tree")"
if version_is_dev "$ver"; then
want=""
if [ -f "$tree/CHANGELOG.md" ]; then
# mawk-compatible; whole-field match so 0.7.0-rc1 never satisfies the
# bare shape (#1 constraint 7).
want="$(awk '$1 == "##" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+$/ { print $2; exit }' "$tree/CHANGELOG.md")"
fi
if [ -n "$want" ]; then
reason="the newest stamped CHANGELOG.md heading (the last release)"
else
want="${ver%-dev}"
reason="VERSION with -dev stripped (no release stamped yet)"
fi
else
want="$ver"
reason="VERSION on a bare tree (the ceremony PR stamps the pin to the version it releases)"
fi
if [ "$pin" != "$want" ]; then
fail "self-ref-check: CEREMONY_SELF_REF is '$pin' but must be '$want' — $reason. A stale pin fails CI here, not a consumer's release."
fi
echo "self-ref-check: pin '$pin' agrees with the tree ($reason)."