diff --git a/scripts/check-changelog-parity.sh b/scripts/check-changelog-parity.sh index 132b7217d1..b5fc29726d 100755 --- a/scripts/check-changelog-parity.sh +++ b/scripts/check-changelog-parity.sh @@ -445,7 +445,28 @@ for manifest in "${manifests[@]}"; do # Named apart from $head_version / $base_version above: those are MANIFEST # versions the rest of this loop body compares and interpolates, and a # one-character alias between the two would be a silent bug. - mapfile -t fork_heading_versions < <(git show "$merge_base:$changelog" 2>/dev/null | changelog_versions -) + # + # Read the fork-point changelog via COMMAND substitution with status + # checked — never process substitution, which swallows git's exit status and + # turns a failed read into an empty heading list (fail-open, #2324). Probe + # with git ls-tree first: a path absent from the tree is exit 0 with empty + # output; an unusable rev is non-zero. (git cat-file -e cannot distinguish + # the two — it exits 128 for both — so a change set ADDING a changelog would + # fail the gate.) + fork_heading_versions=() + if base_listing="$(git ls-tree -r --name-only "$merge_base" -- "$changelog")"; then + if [[ -n "$base_listing" ]]; then + base_body="" + 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 + exit 2 + fi + mapfile -t fork_heading_versions < <(printf '%s\n' "$base_body" | changelog_versions -) + fi + else + echo "check-changelog-parity: 'git ls-tree -r --name-only $merge_base -- $changelog' failed; refusing to pass without checking." >&2 + exit 2 + fi mapfile -t head_heading_versions < <(changelog_versions "$changelog") head_documents=() if ((${#head_heading_versions[@]} > 0)); then diff --git a/scripts/check-changelog-parity.test.sh b/scripts/check-changelog-parity.test.sh index db3cd8910d..1e9c10b7cd 100755 --- a/scripts/check-changelog-parity.test.sh +++ b/scripts/check-changelog-parity.test.sh @@ -237,6 +237,32 @@ 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 HEADING + GIT SHOW FAILURE: a fork-point read that genuinely fails +# must fail loud, never read as "nothing to preserve" and pass (#2324). +repo="$(mk_repo)" +git_init "$repo" +mk_plugin "$repo" alpha 0.51.8 yes +printf '# Changelog\n\n## [0.51.8]\n\n- eight\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- eight\n\n## [0.51.7]\n' >"$repo/plugins/alpha/CHANGELOG.md" +git -C "$repo" add -A >/dev/null && git -C "$repo" commit -qm bump +shim_dir="$(mktemp -d)" +cat >"$shim_dir/git" <<'SHIM' +#!/usr/bin/env bash +if [[ "${1:-}" == show && "${2:-}" == *CHANGELOG.md ]]; then + echo "simulated object-store failure" >&2 + exit 128 +fi +exec /usr/bin/git "$@" +SHIM +chmod +x "$shim_dir/git" +out="$(cd "$repo" && PATH="$shim_dir:$PATH" bash scripts/check-changelog-parity.sh --check-bump "$base" 2>&1)" +rc=$? +rm -rf "$shim_dir" "$repo" +if [[ $rc -eq 2 && "$out" == *"git show"* && "$out" == *"failed"* ]]; then ok "absorbed-heading check fails loud when git show CHANGELOG fails"; else fail "absorbed-heading git failure not loud: rc=$rc out='$out'"; fi + # RELABELLED, NOT DELETED: the same bad resolution renames the predecessor's # heading to the new version instead of adding one. That IS a deletion of the # predecessor version, and must read as one however the line is written.