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
37 changes: 25 additions & 12 deletions .claude/scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
# checks.sh's own JSON-editing style).
# 2. Generate a dated Keep-a-Changelog section in
# .claude/.claude-plugin/CHANGELOG.md from commits since the last git
# tag — or, if there is no prior tag yet (this repo's current state:
# zero tags), from the full history (first-release path). Prefers merge
# commit subjects ("Merge pull request #N from owner/branch" -> one
# bullet per merged PR) when any exist in range; falls back to every
# non-merge commit subject in range otherwise (the no-merges case a
# fresh/test repo hits).
# tag — or, if there is no prior tag yet, from the full history
# (first-release path). Walks the base branch's FIRST-PARENT chain, so
# exactly one bullet is emitted per landed change no matter how it
# landed: a merge-committed PR contributes its "Merge pull request #N
# from owner/branch" subject (rewritten to "#N: branch"), a
# squash-merged PR contributes its squash subject, and a merged PR's
# internal commits are excluded (they are off the first-parent chain).
# Repos that mix merge and squash merges — this one does — are handled
# correctly; see the comment at the generator itself.
# 3. Tag vX.Y.Z, commit the bumps + changelog, push both.
# 4. "Publish" per the existing "Updating the plugin" runbook
# (docs/USAGE.md): this script CANNOT run the interactive
Expand Down Expand Up @@ -157,12 +160,22 @@ else
echo "release.sh: no prior git tag found — first-release path, using full history"
fi

merge_subjects="$(git -C "$root" log $range --merges --pretty=%s 2>/dev/null || true)"
if [ -n "$merge_subjects" ]; then
changelog_items="$(printf '%s\n' "$merge_subjects" | sed -E 's/^Merge pull request (#[0-9]+) from [^\/]+\/(.+)$/- \1: \2/')"
else
changelog_items="$(git -C "$root" log $range --no-merges --pretty=%s 2>/dev/null | sed 's/^/- /')"
fi
# One bullet per LANDED change, whatever shape it landed in. Walking the base
# branch's FIRST-PARENT chain is what makes both merge styles work at once:
# - a merge-committed PR appears as its "Merge pull request #N from …" commit,
# - a squash-merged PR appears as its single squash commit,
# - a merged PR's INTERNAL commits never appear at all — they hang off the
# second parent, off the first-parent chain.
# That last property is the double-listing the old --merges-only branch was
# really guarding against; --first-parent gets it without having to discard
# anything. Preferring --merges *wholesale* (the previous behaviour) silently
# dropped every squash-merged PR as soon as ONE merge commit existed in range —
# and this repo's history mixes both styles, so the v0.3.0..v0.3.1 range
# generated 2 bullets instead of 7, omitting five shipped fixes and naming the
# v0.3.0 release PR itself. A direct-to-base commit is on the first-parent
# chain too, and is a real shipped change, so it is now listed as well.
changelog_items="$(git -C "$root" log $range --first-parent --pretty=%s 2>/dev/null \
| sed -E -e 's/^Merge pull request (#[0-9]+) from [^\/]+\/(.+)$/\1: \2/' -e 's/^/- /')"
[ -n "$changelog_items" ] || changelog_items="- (no changes recorded)"

release_date="$(date -u +%Y-%m-%d)"
Expand Down
75 changes: 69 additions & 6 deletions .claude/scripts/release.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
# 9. the no-`--issue` real-path variant: milestone-close warns and skips
# (never crashes, never guesses), while the rollout companion issue is
# still filed with a graceful "titles unavailable" test-focus fallback.
# 10. MIXED merge-commit + squash-merge history in a single range (this
# repo's real v0.3.0..v0.3.1 shape): every landed change is listed
# exactly once regardless of how it landed, and a merge-committed PR's
# internal commits are never double-listed. Guards the regression where
# one merge commit in range caused every squash-merged PR to be dropped.
#
# Exit 0 on success, non-zero if any assertion fails. Runnable bare:
# bash .claude/scripts/release.test.sh
Expand Down Expand Up @@ -209,13 +214,15 @@ seed_fixture "$repo_a"
# is "since the last tag" and must be the ONLY thing the changelog picks up.
git -C "$repo_a" -c tag.gpgSign=false tag v1.0.0

# A commit landing AFTER the tag but BEFORE the merge commits below — it is
# in-range but not a merge commit, so with merge commits present it must be
# excluded from the changelog (merge subjects win over plain commits when any
# exist in range — see release.sh's changelog-generation comment).
# A commit landing AFTER the tag but BEFORE the merge commits below — a direct
# commit to the base branch. It is in-range and ON the first-parent chain, so
# it is a real shipped change and MUST appear in the changelog even though
# merge commits also exist in range. (This inverts the pre-fix behaviour, which
# discarded every non-merge commit as soon as one merge commit was present —
# the bug that dropped five squash-merged PRs from the v0.3.1 range.)
echo "pre-merge" > "$repo_a/premerge.txt"
git -C "$repo_a" add premerge.txt
git -C "$repo_a" commit -q -m "premerge: should not show up once merge commits exist in range"
git -C "$repo_a" commit -q -m "premerge: a direct-to-main commit alongside merge commits"

# Two fake merged-PR commits, real GitHub merge-commit shape.
git -C "$repo_a" checkout -q -b feat/thing-one
Expand Down Expand Up @@ -250,7 +257,15 @@ check "scenario A: new 1.1.0 section is ABOVE the prior 1.0.0 section" bash -c '
' _ "$changelog_a" "$work"
check "scenario A: changelog references merged PR #42" bash -c 'printf "%s" "$1" | grep -q "#42: feat/thing-one"' _ "$changelog_a"
check "scenario A: changelog references merged PR #43" bash -c 'printf "%s" "$1" | grep -q "#43: feat/thing-two"' _ "$changelog_a"
check "scenario A: in-range non-merge commit is excluded once merge commits exist" bash -c '! printf "%s" "$1" | grep -q "premerge:"' _ "$changelog_a"
check "scenario A: in-range direct-to-main commit is INCLUDED alongside merge commits" bash -c 'printf "%s" "$1" | grep -q -- "- premerge: a direct-to-main commit alongside merge commits"' _ "$changelog_a"
# The real anti-double-listing guarantee: a merged PR contributes its merge
# subject ONCE, never also its internal commits (which are off the first-parent
# chain). Without this, #42 would be listed as both "#42: feat/thing-one" and
# "feat: thing one".
check "scenario A: merged PRs' internal commits are NOT double-listed" bash -c '
! printf "%s" "$1" | grep -q -- "- feat: thing one" &&
! printf "%s" "$1" | grep -q -- "- feat: thing two"
' _ "$changelog_a"
check "scenario A: no real tag v1.1.0 was created" bash -c '! git -C "$1" tag --list | grep -qx v1.1.0' _ "$repo_a"
check "scenario A: original tag v1.0.0 untouched" bash -c 'git -C "$1" tag --list | grep -qx v1.0.0' _ "$repo_a"
check "scenario A: HEAD has no new commit (dry-run never commits)" bash -c '
Expand Down Expand Up @@ -460,6 +475,54 @@ check "scenario G: version was still bumped and pushed for real (only milestone-
git -C "$1" tag --list | grep -qx v1.3.0
' _ "$origin_bare_g"

# =============================================================================
# Scenario H: MIXED merge-commit and squash-merge history in one range — the
# exact shape of this repo's real v0.3.0..v0.3.1 range, and the regression that
# motivated the --first-parent generator. Before the fix, the presence of ANY
# merge commit made release.sh discard every squash-merged PR: the real range
# generated 2 bullets instead of 7. Both styles must now be represented, and
# the merge-committed PR must still contribute exactly one bullet.
# =============================================================================
repo_h="$work/repo-h"
seed_fixture "$repo_h"
git -C "$repo_h" -c tag.gpgSign=false tag v1.0.0

# (a) a merge-committed PR, with an internal commit that must NOT be listed
git -C "$repo_h" checkout -q -b feat/merged-style
echo "m" > "$repo_h/m.txt"
git -C "$repo_h" add m.txt
git -C "$repo_h" commit -q -m "internal: implementation detail of the merged PR"
git -C "$repo_h" checkout -q main
git -C "$repo_h" merge -q --no-ff -m "Merge pull request #90 from robercano/feat/merged-style" feat/merged-style

# (b) two squash-merged PRs — single commits on main, GitHub's "(#N)" subject
echo "s1" > "$repo_h/s1.txt"
git -C "$repo_h" add s1.txt
git -C "$repo_h" commit -q -m "fix(notify): use https:// in the ntfy notify command (#91)"
echo "s2" > "$repo_h/s2.txt"
git -C "$repo_h" add s2.txt
git -C "$repo_h" commit -q -m "feat(server): support multiple agent users on one box (#92)"

out_h="$(run_release "$repo_h" v1.4.0 --dry-run)"
rc_h=$?

check "scenario H: exits 0" [ "$rc_h" -eq 0 ]
changelog_h="$(cat "$repo_h/.claude/.claude-plugin/CHANGELOG.md")"
check "scenario H: merge-committed PR #90 is listed" bash -c 'printf "%s" "$1" | grep -q -- "- #90: feat/merged-style"' _ "$changelog_h"
check "scenario H: squash-merged PR #91 is listed despite a merge commit in range" bash -c '
printf "%s" "$1" | grep -q -- "- fix(notify): use https:// in the ntfy notify command (#91)"
' _ "$changelog_h"
check "scenario H: squash-merged PR #92 is listed despite a merge commit in range" bash -c '
printf "%s" "$1" | grep -q -- "- feat(server): support multiple agent users on one box (#92)"
' _ "$changelog_h"
check "scenario H: the merged PR's internal commit is not double-listed" bash -c '
! printf "%s" "$1" | grep -q -- "- internal: implementation detail"
' _ "$changelog_h"
check "scenario H: the 1.4.0 section holds exactly 3 bullets (one per landed change)" bash -c '
n=$(printf "%s\n" "$1" | awk "/^## \[1\.4\.0\]/{s=1;next} s&&/^## \[/{s=0} s&&/^- /{c++} END{print c+0}")
[ "$n" -eq 3 ]
' _ "$changelog_h"

echo ""
if [ "$fail" -eq 0 ]; then
echo "release.test.sh: PASS ($ok checks)"
Expand Down
Loading