fix(ci): stop the frozen-CHANGELOG check firing on a merged branch (#525)

* fix(ci): stop the frozen-CHANGELOG check firing on a merged branch

The check diffed line ranges since the merge base, so any branch that
merged `main` after a release saw the new `## [X.Y.Z]` section as lines it
had touched — even though the section arrived through the merge. It fired
on #508 within a day of being added.

It now compares the released half of the file against the base branch
directly, which is the question it was always asking: does this branch's
copy of an already-released section differ from the base's? A section the
base has and the branch does not is a stale branch, not a rewrite, and is
allowed.

Two pipelines also exited 141 under `pipefail`: `head -1` and `grep -q`
both close the pipe on completion, and the resulting SIGPIPE counted as
failure. The second made the check announce "HEAD predates <release>" on a
branch that contained it. Both replaced with parameter expansion.

Controlled both ways rather than assumed: run against 4844020, where
#517's entry genuinely sat inside the released [1.33.0], it still fails and
names the lines; run against a branch that merged main post-release, it
passes.

* fix(changelog): move this branch's entry out of the released 1.35.0 section

The branch predates v1.35.0. Merging main brought the release heading down
over an entry that was written under [Unreleased], so the bullet ended up
inside a tagged section. Relocated, not rewritten: [1.35.0] keeps exactly
the two entries that shipped in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Fabio Akita
2026-08-29 00:41:58 -03:00
committed by GitHub
co-authored by Claude Opus 5
parent 42bde19f6a
commit d329fba82a
2 changed files with 44 additions and 21 deletions
+9
View File
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- The CHANGELOG frozen-section check no longer fires on a branch that merged
`main` after a release. It compared line ranges since the merge base, so a
newly released section arriving through the merge read as lines the branch
had touched. It now compares the released half of the file directly against
the base branch, which is the question it was always asking. Two pipelines
also exited 141 under `pipefail` when `head -1` and `grep -q` closed a pipe
early, so the check reported "HEAD predates" on a branch that did not.
## [1.35.0] - 2026-08-29
### Added
+35 -21
View File
@@ -32,40 +32,54 @@ git rev-parse --verify "$BASE_REF" >/dev/null 2>&1 || {
exit 0
}
MERGE_BASE="$(git merge-base "$BASE_REF" HEAD)"
if [[ "$MERGE_BASE" == "$(git rev-parse HEAD)" ]]; then
echo "check-changelog-frozen: HEAD is an ancestor of $BASE_REF; nothing to check."
# Compare the released half of the file directly against the base branch,
# rather than diffing line ranges since the merge base.
#
# The line-range form fired falsely on any branch that merged the base after a
# release: from the old merge base, the whole new `## [X.Y.Z]` section reads as
# lines this branch touched, even though it arrived through the merge. The
# question that matters is simpler — does this branch's copy of an
# already-released section differ from the base branch's copy?
#
# A section the base has and this branch does not is fine (a stale branch, not
# a rewrite), so only sections present in both are compared.
released_half() {
git show "$1:CHANGELOG.md" 2>/dev/null | awk '/^## \[[0-9]/{f=1} f'
}
BASE_RELEASED="$(released_half "$BASE_REF")"
HEAD_RELEASED="$(released_half HEAD)"
if [[ -z "$BASE_RELEASED" ]]; then
echo "check-changelog-frozen: base has no released section yet."
exit 0
fi
# First line number of the first released section in the *new* file.
FROZEN_FROM="$(awk '/^## \[[0-9]/{print NR; exit}' CHANGELOG.md)"
if [[ -z "$FROZEN_FROM" ]]; then
echo "check-changelog-frozen: no released section yet."
# The newest released heading on the base. Anything at or below it in HEAD must
# match the base byte for byte.
# `head -1` under `pipefail` exits 141 on SIGPIPE, so read the first line
# without a pipe.
NEWEST="${BASE_RELEASED%%$'\n'*}"
# Substring tests, not pipes: `grep -q` closes the pipe on its first match and
# `pipefail` reports that SIGPIPE as failure, which made this branch look like
# it predated the release.
if [[ "$HEAD_RELEASED" != *"$NEWEST"* ]]; then
echo "check-changelog-frozen: HEAD predates $NEWEST; nothing to compare."
exit 0
fi
# Every new-file line number this diff touches, via the unified hunk headers.
TOUCHED="$(git diff --unified=0 "$MERGE_BASE" HEAD -- CHANGELOG.md \
| awk '/^@@/{ split($3, a, ","); start = a[1] + 0; if (start < 0) start = -start;
count = (a[2] == "" ? 1 : a[2] + 0);
for (i = 0; i < count; i++) print start + i }')"
HEAD_FROM_NEWEST="${HEAD_RELEASED#*"$NEWEST"}"
HEAD_FROM_NEWEST="${NEWEST}${HEAD_FROM_NEWEST}"
BAD=""
for ln in $TOUCHED; do
if (( ln >= FROZEN_FROM )); then
BAD+=" line $ln: $(sed -n "${ln}p" CHANGELOG.md)"$'\n'
fi
done
if [[ -z "$BAD" ]]; then
if [[ "$HEAD_FROM_NEWEST" == "$BASE_RELEASED" ]]; then
echo "CHANGELOG: no released section was modified."
exit 0
fi
echo "error: this change modifies an already-released CHANGELOG section." >&2
echo >&2
printf '%s' "$BAD" >&2
diff <(printf '%s\n' "$BASE_RELEASED") <(printf '%s\n' "$HEAD_FROM_NEWEST") | head -40 >&2
cat >&2 <<'EOF'
Released sections are frozen. An entry for unreleased work belongs under