Skip to content

Commit 060cf9c

Browse files
committed
fix(dx): pre-hook changelog gate before gh pr create
Blocks gh pr create when shipped-code paths are changed but ## Unreleased in CHANGELOG.md is empty — catches the same gate that changelog-entry-required CI enforces, before the push. Escape hatches (mirroring CI): release/* branch, chore(release) title, or [skip changelog] in the PR body. Also unblocks .claude/settings.json and .claude/hooks/ from .gitignore (changed .claude to .claude/* so negation patterns work) so project-scoped hook config is tracked by the team.
1 parent c4ca024 commit 060cf9c

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

.claude/hooks/check-changelog.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
# Pre-hook: block `gh pr create` when shipped-code paths are changed but
3+
# CHANGELOG.md has nothing under ## Unreleased.
4+
# Mirrors the logic in .github/workflows/changelog-entry-required.yml so the
5+
# gate fires locally before CI does.
6+
7+
set -uo pipefail
8+
9+
input=$(cat)
10+
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
11+
12+
# Only intercept gh pr create invocations.
13+
if ! printf '%s' "$cmd" | grep -q 'gh pr create'; then
14+
exit 0
15+
fi
16+
17+
# Skip release-prep branches and titles (they consume ## Unreleased).
18+
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
19+
case "$branch" in
20+
release/*) exit 0 ;;
21+
esac
22+
if printf '%s' "$cmd" | grep -qF 'chore(release)'; then
23+
exit 0
24+
fi
25+
# [skip changelog] anywhere in the command body is also an escape hatch.
26+
if printf '%s' "$cmd" | grep -qiF '[skip changelog]'; then
27+
exit 0
28+
fi
29+
30+
# Determine which files changed vs the merge-base with origin/main.
31+
base=$(git merge-base HEAD origin/main 2>/dev/null || echo "")
32+
if [ -z "$base" ]; then
33+
# Can't determine base — don't block.
34+
exit 0
35+
fi
36+
changed=$(git diff --name-only "$base" HEAD 2>/dev/null || echo "")
37+
38+
# Check for shipped-code paths (matches the CI workflow exactly).
39+
touched=0
40+
while IFS= read -r f; do
41+
[ -n "$f" ] || continue
42+
case "$f" in
43+
src/*|packages/*) touched=1; break ;;
44+
scripts/install*.sh|scripts/install*.ps1) touched=1; break ;;
45+
pythinker.spec) touched=1; break ;;
46+
.github/workflows/linux-installer.yml|\
47+
.github/workflows/windows-installer.yml|\
48+
.github/workflows/homebrew-tap.yml|\
49+
.github/workflows/release-*.yml|\
50+
.github/workflows/promote-release.yml) touched=1; break ;;
51+
esac
52+
done <<< "$changed"
53+
54+
[ "$touched" -eq 0 ] && exit 0
55+
56+
# Pass if ## Unreleased has at least one non-blank line.
57+
if awk '
58+
/^## Unreleased[[:space:]]*$/ { inblk=1; next }
59+
inblk && /^## / { inblk=0 }
60+
inblk { print }
61+
' CHANGELOG.md 2>/dev/null | grep -q '[^[:space:]]'; then
62+
exit 0
63+
fi
64+
65+
# Block and tell the author exactly what to do.
66+
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"CHANGELOG gate: this branch touches shipped code but ## Unreleased in CHANGELOG.md is empty.\n\nAdd a bullet under ## Unreleased before opening the PR, for example:\n - **Your change.** Brief description.\n\nEscape hatches:\n - Add [skip changelog] in the PR body\n - Use branch name release/* or title chore(release)*"}}'

.claude/settings.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Bash",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "bash .claude/hooks/check-changelog.sh",
10+
"timeout": 15,
11+
"statusMessage": "Checking CHANGELOG gate..."
12+
}
13+
]
14+
}
15+
]
16+
}
17+
}

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ node_modules/
5858
static/
5959
.memo/
6060
.entire
61-
.claude
61+
.claude/*
62+
!.claude/settings.json
63+
!.claude/hooks/
64+
!.claude/hooks/**
6265
.pythinker/
6366
.worktrees/
6467
blackbox/

0 commit comments

Comments
 (0)