diff --git a/actions/changelog-assembled/changelog-assembled.sh b/actions/changelog-assembled/changelog-assembled.sh index 93f03b1..bccafe4 100644 --- a/actions/changelog-assembled/changelog-assembled.sh +++ b/actions/changelog-assembled/changelog-assembled.sh @@ -33,6 +33,12 @@ set -euo pipefail # no '## ' heading, and changelog_section extracts the body below HEAD's # heading — so the date HEAD stamped into its heading never enters the # comparison, and a date difference can never masquerade as a prose one. +# +# This guard narrows, but cannot close, the target-movement window: it sees a +# fragment present when CI reads the target ref, but one can still land after +# the final run and before merge. Requiring release PRs to be up to date with +# their target branch before merge is the repository setting that closes that +# residual window (#253). base_ref="${1:-${CHANGELOG_ASSEMBLED_BASE:-origin/main}}" changelog="${2:-${CHANGELOG:-CHANGELOG.md}}" @@ -169,6 +175,37 @@ frag_count="$(printf '%s' "$base_frags" | grep -c . || true)" failures=0 +# Refusal: the target branch gained a fragment after this release PR's merge +# base, so the ceremony could not have consumed it. Merging this tree would +# strand that fragment for the next release and misattribute when it shipped. +stranded="" +while IFS= read -r -d '' entry; do + meta="${entry%%$'\t'*}" + path="${entry#*$'\t'}" + otype="$(printf '%s\n' "$meta" | awk '{ print $2 }')" + name="${path##*/}" + case "$otype:$name" in + blob:README.md) ;; + blob:*.md) + if ! printf '%s' "$base_frags" | grep -Fxq "$path"; then + stranded="${stranded} ${path}"$'\n' + fi + ;; + esac +done < <(git ls-tree -z "$base_ref" -- "$dir/") +if [ -n "$stranded" ]; then + { + echo "changelog-assembled: fragment(s) on target '$base_ref' were not consumed by this release PR:" + echo + printf '%s' "$stranded" + echo + echo " Merging now would strand these entries for the next release and" + echo " misattribute when they shipped." + echo " Fix: rebase onto the target head and re-run bin/changelog-assemble '$ver'." + } >&2 + failures=$((failures + 1)) +fi + # Refusal: a fragment the ceremony consumed is still present on HEAD. The # ceremony deletes exactly what it assembles (#112) — a fragment that # survives its own release sits in the directory and is assembled AGAIN