Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion scripts/check-changelog-parity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,42 @@ changelog_versions() {
grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?([+-][0-9A-Za-z][0-9A-Za-z.-]*)?'
}

# Version headings present at the fork point but absent at head. Uses
# changelog_versions on both sides so plugin, convention, and two-component
# heading forms share one reader. Reads the base blob with command substitution
# (never process substitution) so a git failure fails closed. A changelog that
# is new at the fork point is a legitimate pass — probe with git cat-file -e
# first so "absent" is not conflated with "git failed".
missing_preserved_headings() {
local merge_base="$1" changelog="$2"
local base_body missing v
declare -A head_seen

if ! git cat-file -e "$merge_base:$changelog" 2>/dev/null; then
Comment thread
kyle-sexton marked this conversation as resolved.
return 0
Comment thread
kyle-sexton marked this conversation as resolved.
fi
if ! base_body="$(git show "$merge_base:$changelog")"; then
echo "check-changelog-parity: 'git show $merge_base:$changelog' failed; refusing to pass without checking." >&2
return 2
fi

mapfile -t base_versions < <(printf '%s\n' "$base_body" | changelog_versions -)
Comment thread
kyle-sexton marked this conversation as resolved.
((${#base_versions[@]} > 0)) || return 0

mapfile -t head_versions < <(changelog_versions "$changelog")
for v in "${head_versions[@]}"; do head_seen["$v"]=1; done

missing=""
for v in "${base_versions[@]}"; do
[[ -n "${head_seen[$v]:-}" ]] && continue
missing+="${missing:+$'\n'}$v"
done
if [[ -n "$missing" ]]; then
printf '%s\n' "$missing"
fi
return 0
}

if [[ "$mode" == "--check-order" ]]; then
changelogs=(plugins/*/CHANGELOG.md docs/conventions/*/CHANGELOG.md)
misordered=0
Expand Down Expand Up @@ -518,6 +554,25 @@ undocumented=0
malformed=0
preexisting=0
nonmonotonic=0
absorbed=0
declare -A absorbed_reported
Comment thread
kyle-sexton marked this conversation as resolved.

for changelog in "${touched_changelogs[@]}"; do
[[ -n "${absorbed_reported[$changelog]:-}" ]] && continue
absorbed_reported["$changelog"]=1
[[ -f "$changelog" ]] || continue
missing_headings="$(missing_preserved_headings "$merge_base" "$changelog")" ||
exit 2
if [[ -n "$missing_headings" ]]; then
echo "ABSORBED CHANGELOG HEADING: $changelog lost release section heading(s) vs the fork point (a merge-forward may have fused two releases into one section):" >&2
while IFS= read -r v; do
[[ -z "$v" ]] && continue
echo " ## [$v]" >&2
done <<<"$missing_headings"
absorbed=$((absorbed + 1))
fi
done

for manifest in "${manifests[@]}"; do
plugin_dir="${manifest%/.claude-plugin/plugin.json}"
name="${plugin_dir##*/}"
Expand Down Expand Up @@ -620,11 +675,12 @@ for manifest in "${manifests[@]}"; do
fi
done

if ((undocumented > 0 || malformed > 0 || preexisting > 0 || nonmonotonic > 0)); then
if ((undocumented > 0 || malformed > 0 || preexisting > 0 || nonmonotonic > 0 || absorbed > 0)); then
((undocumented > 0)) && echo "Add a '## [<version>]' entry for every plugin whose version changed." >&2
((malformed > 0)) && echo "Convert unbracketed changelog headings to the '## [<version>]' Keep-a-Changelog form." >&2
((preexisting > 0)) && echo "Add the bumped version's '## [<version>]' entry in this change set; it must be absent from the base changelog, not merely present at head." >&2
((nonmonotonic > 0)) && echo "Renumber every bumped version strictly above the base ref's CURRENT version, not the version the branch was cut from." >&2
((absorbed > 0)) && echo "Restore every '## [<version>]' heading that existed at the fork point; release notes must not be relabelled or absorbed into a newer section." >&2
exit 1
fi
echo "Every plugin whose version changed vs $base has a '## [<version>]' CHANGELOG.md entry."
36 changes: 34 additions & 2 deletions scripts/check-changelog-parity.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,35 @@ git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm bump
if (cd "$repo" && bash scripts/check-changelog-parity.sh --check-bump "$base" >/dev/null 2>&1); then ok "bump + '## [x.y.z]' entry passes --check-bump"; else fail "bump+entry wrongly failed"; fi
rm -rf "$repo"

# ABSORBED HEADING (#2324): merge-forward deletes a predecessor heading.
repo="$(mk_repo)"
git_init "$repo"
mk_plugin "$repo" alpha 0.51.8 yes
printf '# Changelog\n\n## [0.51.8]\n\n### Fixed\n\n- predecessor note\n\n## [0.51.7]\n' >"$repo/plugins/alpha/CHANGELOG.md"
git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm base
base="$(git -C "$repo" rev-parse HEAD)"
printf '{ "name": "alpha", "version": "0.51.9" }\n' >"$repo/plugins/alpha/.claude-plugin/plugin.json"
printf '# Changelog\n\n## [0.51.9]\n\n### Fixed\n\n- new note\n- predecessor note\n\n## [0.51.7]\n' >"$repo/plugins/alpha/CHANGELOG.md"
git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm bump
out="$(cd "$repo" && bash scripts/check-changelog-parity.sh --check-bump "$base" 2>&1)"
rc=$?
if [[ $rc -ne 0 && "$out" == *"ABSORBED CHANGELOG HEADING"*"alpha"* && "$out" == *"0.51.8"* ]]; then ok "absorbed predecessor release heading fails --check-bump"; else fail "absorbed heading not caught: rc=$rc out='$out'"; fi
rm -rf "$repo"

# ABSORBED without manifest bump (#2324 gap 2): only the changelog changed.
repo="$(mk_repo)"
git_init "$repo"
mk_plugin "$repo" alpha 0.51.9 yes
printf '# Changelog\n\n## [0.51.9]\n\n- nine\n\n## [0.51.8]\n\n- eight\n' >"$repo/plugins/alpha/CHANGELOG.md"
git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm base
base="$(git -C "$repo" rev-parse HEAD)"
printf '# Changelog\n\n## [0.51.9]\n\n- nine\n- eight\n\n## [0.51.7]\n\n- seven\n' >"$repo/plugins/alpha/CHANGELOG.md"
git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm 'absorb without bump'
out="$(cd "$repo" && bash scripts/check-changelog-parity.sh --check-bump "$base" 2>&1)"
rc=$?
if [[ $rc -ne 0 && "$out" == *"ABSORBED CHANGELOG HEADING"* && "$out" == *"0.51.8"* ]]; then ok "absorbed heading with no manifest bump fails --check-bump"; else fail "absorbed-no-bump not caught: rc=$rc out='$out'"; fi
rm -rf "$repo"

# LARGE CHANGELOG (SIGPIPE regression, #2130): the new entry sits near the top
# of a changelog far larger than the pipe buffer — the shape every mature
# changelog has. A has_heading reader that exits on first match kills
Expand Down Expand Up @@ -843,10 +872,13 @@ git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm 'absorb 0.51.8 int
out="$(cd "$repo" && bash scripts/check-changelog-parity.sh --check-preserved "$base" 2>&1)"
rc=$?
if [[ $rc -eq 1 && "$out" == *"DELETED CHANGELOG ENTRY"*"plugins/alpha/CHANGELOG.md"* && "$out" == *"0.51.8"* ]]; then ok "an absorbed predecessor section fails --check-preserved, naming the vanished version"; else fail "absorbed section not caught: rc=$rc out='$out'"; fi
for other in "--check-bump $base" "--check-order" "--check"; do
for other in "--check-order" "--check"; do
# shellcheck disable=SC2086 # deliberate split: the mode and its optional base ref
if (cd "$repo" && bash scripts/check-changelog-parity.sh $other >/dev/null 2>&1); then ok "the absorbed-section tree still passes '$other' (the gap --check-preserved exists to close)"; else fail "'$other' unexpectedly fired on the absorbed-section tree — the gap assertion is stale"; fi
if (cd "$repo" && bash scripts/check-changelog-parity.sh $other >/dev/null 2>&1); then ok "the absorbed-section tree still passes '$other'"; else fail "'$other' unexpectedly fired on the absorbed-section tree"; fi
done
out="$(cd "$repo" && bash scripts/check-changelog-parity.sh --check-bump "$base" 2>&1)"
rc=$?
if [[ $rc -ne 0 && "$out" == *"ABSORBED CHANGELOG HEADING"* && "$out" == *"0.51.8"* ]]; then ok "--check-bump now catches absorbed headings too (#2324)"; else fail "--check-bump should catch absorbed headings: rc=$rc out='$out'"; fi
rm -rf "$repo"

# RELABELLED PREDECESSOR: the same bad resolution renames 0.51.8's heading to
Expand Down
Loading