Skip to content

Commit 566bf65

Browse files
titouanmathisclaude
andcommitted
Add git hooks for commit validation and tag prefix rejection
Co-authored-by: Claude <claude@anthropic.com>
1 parent 0b47372 commit 566bf65

4 files changed

Lines changed: 141 additions & 0 deletions

File tree

.husky/commit-msg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
sh scripts/check-commit-msg.sh "$1"

.husky/pre-push

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
#!/bin/sh
22

3+
# --- Reject tags with v prefix ---
4+
# Read pushed refs from stdin (format: <local ref> <local sha> <remote ref> <remote sha>)
5+
while read -r local_ref local_sha remote_ref remote_sha; do
6+
if echo "$local_ref" | grep -qE "^refs/tags/v"; then
7+
tag_name=$(echo "$local_ref" | sed 's|refs/tags/||')
8+
echo "❌ Tag rejected: \"$tag_name\" uses a v prefix."
9+
echo ""
10+
echo " This project uses bare semver tags (e.g., 0.10.0, not v0.10.0)."
11+
echo " Delete the tag and recreate it without the v prefix:"
12+
echo ""
13+
echo " git tag -d $tag_name"
14+
echo " git tag ${tag_name#v}"
15+
echo ""
16+
exit 1
17+
fi
18+
done
19+
20+
# --- Build and test ---
321
echo "🔨 Building (includes type checking)..."
422
npm run build || exit 1
523

scripts/check-commit-msg.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/bin/sh
2+
#
3+
# Commit message validation for productive-tools.
4+
#
5+
# Enforces:
6+
# 1. First line starts with a capitalized English verb (from allowlist)
7+
# 2. Rejects conventional commit prefixes (feat:, fix:, chore:, etc.)
8+
# 3. Requires Co-authored-by: Claude <claude@anthropic.com> trailer
9+
#
10+
# Skips: merge commits, Renovate bot commits, initial commits.
11+
12+
COMMIT_MSG_FILE="$1"
13+
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
14+
FIRST_LINE=$(head -1 "$COMMIT_MSG_FILE")
15+
FIRST_WORD=$(echo "$FIRST_LINE" | awk '{print $1}')
16+
17+
# --- Skip rules ---
18+
19+
# Merge commits
20+
if echo "$FIRST_LINE" | grep -qE "^Merge (branch|pull request|tag|remote)"; then
21+
exit 0
22+
fi
23+
24+
# Renovate bot / dependabot
25+
if echo "$COMMIT_MSG" | grep -qE "^(chore\(deps\)|fix\(deps\)|Update dependency|Lock file maintenance)"; then
26+
exit 0
27+
fi
28+
29+
# Revert commits (auto-generated by git)
30+
if echo "$FIRST_LINE" | grep -qE '^Revert "'; then
31+
exit 0
32+
fi
33+
34+
# Initial commit
35+
if echo "$FIRST_LINE" | grep -qiE "^Initial commit$"; then
36+
exit 0
37+
fi
38+
39+
# --- Rule 1: Reject conventional commit prefixes ---
40+
41+
if echo "$FIRST_LINE" | grep -qE "^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?(!)?:"; then
42+
echo "❌ Commit message rejected: conventional commit prefixes are not used in this project."
43+
echo ""
44+
echo " Got: $FIRST_LINE"
45+
echo " Expected: Start with a capitalized verb (e.g., \"Add ...\", \"Fix ...\", \"Update ...\")"
46+
echo ""
47+
exit 1
48+
fi
49+
50+
# --- Rule 2: First word must be a capitalized English verb ---
51+
52+
ALLOWED_VERBS="Add|Fix|Update|Remove|Refactor|Improve|Bump|Prepare|Replace|Exclude|Support|Move|Rename|Clean|Create|Extract|Implement|Wire|Enable|Disable|Delete|Migrate|Rewrite|Simplify|Split|Merge|Revert|Document|Release|Publish|Configure|Normalize|Inline|Deduplicate|Reorganize|Consolidate|Integrate|Validate|Handle|Extend|Expose|Deprecate|Drop|Skip|Set|Use|Make|Run|Allow|Prevent|Ensure|Restore|Reset|Convert|Wrap|Unwrap|Format|Lint|Test|Mock|Stub|Pin|Unpin|Upgrade|Downgrade|Sync|Export|Import|Load|Unload|Register|Unregister|Init|Bootstrap|Scaffold|Generate|Emit|Suppress|Guard|Narrow|Widen|Relax|Restrict|Limit|Expand|Collapse|Flatten|Nest|Group|Ungroup|Sort|Filter|Map|Reduce|Batch|Queue|Retry|Cache|Flush|Purge|Prune|Trim|Pad|Truncate|Swap|Toggle|Flip|Invert|Negate|Clarify|Annotate|Comment|Log|Debug|Profile|Benchmark|Optimize|Parallelize|Serialize|Deserialize|Encode|Decode|Encrypt|Decrypt|Hash|Sign|Verify|Authenticate|Authorize|Revoke|Throttle|Debounce|Defer|Delay|Schedule|Cancel|Abort|Interrupt|Resume|Pause|Start|Stop|Restart|Shutdown|Connect|Disconnect|Bind|Unbind|Attach|Detach|Mount|Unmount|Inject|Eject|Patch|Hotfix|Backport|Cherry|Squash|Amend|Stash|Pop|Apply|Resolve|Reject|Throw|Catch|Recover|Fallback|Default|Override|Overload|Overwrite|Preserve|Retain|Persist|Save|Read|Write|Open|Close|Lock|Unlock|Acquire|Release|Yield|Await|Fetch|Pull|Push|Send|Receive|Stream|Pipe|Route|Redirect|Forward|Proxy|Relay|Broadcast|Notify|Alert|Warn|Inform|Report|Summarize|Aggregate|Compute|Calculate|Count|Measure|Track|Monitor|Watch|Observe|Listen|Subscribe|Unsubscribe|Publish|Unpublish|Activate|Deactivate|Show|Hide|Reveal|Conceal|Display|Render|Paint|Draw|Animate|Transition|Transform|Translate|Scale|Rotate|Skew|Clip|Crop|Resize|Zoom|Pan|Scroll|Focus|Blur|Select|Deselect|Highlight|Dim|Elevate|Lower|Raise|Lift|Embed|Unembed|Introduce|Finalize|Complete|Finish|Conclude|Terminate|End|Begin|Commence|Launch|Deploy|Provision|Tear|Destroy|Demolish|Rebuild|Reconstruct|Refurbish|Renovate|Overhaul|Revamp|Rework|Redo|Undo|Rebase|Reland|Re-export|Re-add"
53+
54+
if ! echo "$FIRST_WORD" | grep -qE "^($ALLOWED_VERBS)$"; then
55+
echo "❌ Commit message rejected: first word must be a capitalized English verb."
56+
echo ""
57+
echo " Got: \"$FIRST_WORD\" in \"$FIRST_LINE\""
58+
echo " Expected: Start with a verb like Add, Fix, Update, Remove, Refactor, Improve, Bump..."
59+
echo ""
60+
echo " Common verbs: Add, Fix, Update, Remove, Refactor, Improve, Bump, Prepare,"
61+
echo " Replace, Create, Implement, Simplify, Migrate, Rewrite"
62+
echo ""
63+
exit 1
64+
fi
65+
66+
# --- Rule 3: Require Co-authored-by trailer ---
67+
68+
if ! echo "$COMMIT_MSG" | grep -qF "Co-authored-by: Claude <claude@anthropic.com>"; then
69+
echo "❌ Commit message rejected: missing Co-authored-by trailer."
70+
echo ""
71+
echo " Add this line at the end of your commit message (after a blank line):"
72+
echo ""
73+
echo " Co-authored-by: Claude <claude@anthropic.com>"
74+
echo ""
75+
exit 1
76+
fi
77+
78+
exit 0

scripts/check-test-colocation.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/sh
2+
#
3+
# Check that source files have colocated test files.
4+
#
5+
# Scans packages/*/src/**/*.ts for files missing a corresponding .test.ts.
6+
# Excludes: index.ts, types.ts, *.d.ts, and dist/ directories.
7+
#
8+
# Exit code 0 always (informational). Prints a report of missing tests.
9+
10+
MISSING=0
11+
TOTAL=0
12+
13+
for f in $(find packages/*/src -name '*.ts' \
14+
! -name '*.test.ts' \
15+
! -name '*.d.ts' \
16+
! -name 'index.ts' \
17+
! -name 'types.ts' \
18+
! -path '*/dist/*' \
19+
| sort); do
20+
21+
TOTAL=$((TOTAL + 1))
22+
test_file="${f%.ts}.test.ts"
23+
24+
if [ ! -f "$test_file" ]; then
25+
if [ "$MISSING" -eq 0 ]; then
26+
echo "⚠️ Source files missing colocated tests:"
27+
echo ""
28+
fi
29+
echo " $f"
30+
MISSING=$((MISSING + 1))
31+
fi
32+
done
33+
34+
COVERED=$((TOTAL - MISSING))
35+
36+
echo ""
37+
echo "📊 Test colocation: $COVERED/$TOTAL source files have tests ($MISSING missing)"
38+
39+
if [ "$MISSING" -gt 0 ]; then
40+
echo ""
41+
echo " Create missing test files: foo.ts → foo.test.ts (same directory)"
42+
fi

0 commit comments

Comments
 (0)