From 9fe8f554564a6cc7938f00a0e504f4385601e416 Mon Sep 17 00:00:00 2001 From: Review Bot Date: Tue, 21 Apr 2026 02:33:08 -0700 Subject: [PATCH] chore: add commit message normalizer Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- .github/workflows/release.yml | 2 +- CLAUDE.md | 5 +- Makefile | 8 +- README.md | 6 +- docs/guides/releasing-new-version.md | 6 +- scripts/commit-msg.sh | 113 +++++++++++++++++++++++++++ scripts/loop-prompt.md | 3 +- scripts/pre-commit.sh | 2 +- 8 files changed, 134 insertions(+), 11 deletions(-) create mode 100755 scripts/commit-msg.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 456b816e..df906820 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,5 +74,5 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add Formula/td.rb git diff --cached --quiet && echo "No changes" && exit 0 - git commit -m "td: bump to ${{ steps.version.outputs.version }}" + git commit -m "chore: bump homebrew formula to ${{ steps.version.outputs.version }}" git push diff --git a/CLAUDE.md b/CLAUDE.md index cd0ab661..c622ecac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ go test ./... # Test all ```bash # Commit changes with proper message git add . -git commit -m "feat: description of changes +git commit -m "feat: describe the change (td-) Details here @@ -42,6 +42,9 @@ Details here Co-Authored-By: Claude Haiku 4.5 " +# Release or automation-only commits without a td task may omit the suffix: +git commit -m "chore: update release metadata" + # Create version tag (bump from current version, e.g., v0.2.0 → v0.3.0) git tag -a v0.3.0 -m "Release v0.3.0: description" diff --git a/Makefile b/Makefile index 18da511f..e1cdde86 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ help: @printf "%s\n" \ "Targets:" \ " make fmt # gofmt -w ." \ - " make install-hooks # install git pre-commit hook" \ + " make install-hooks # install git pre-commit + commit-msg hooks" \ " make test # go test ./..." \ " make install # build and install with version from git" \ " make tag VERSION=vX.Y.Z # create annotated git tag (requires clean tree)" \ @@ -52,6 +52,8 @@ release: tag git push origin "$(VERSION)" install-hooks: - @echo "Installing git pre-commit hook..." + @echo "Installing git hooks..." + @mkdir -p .git/hooks @ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit - @echo "Done. Hook installed at .git/hooks/pre-commit" + @ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg + @echo "Done. Hooks installed at .git/hooks/pre-commit and .git/hooks/commit-msg" diff --git a/README.md b/README.md index 684416ad..9f8c57fb 100644 --- a/README.md +++ b/README.md @@ -189,10 +189,14 @@ make install-dev # Format code make fmt -# Install git pre-commit hook (gofmt, go vet, go build on staged files) +# Install git hooks: +# pre-commit runs gofmt/go vet/go build checks +# commit-msg normalizes subjects to type: summary [(td-)] make install-hooks ``` +Regular development commits should use `type: summary (td-)`, for example `feat: add query validation (td-a1b2)`. Automation or release-maintenance commits that are not tied to a td task should use `type: summary`, for example `chore: bump homebrew formula to v0.2.0`. + ## Tests & Quality Checks ```bash diff --git a/docs/guides/releasing-new-version.md b/docs/guides/releasing-new-version.md index ca98e527..6a559ed3 100644 --- a/docs/guides/releasing-new-version.md +++ b/docs/guides/releasing-new-version.md @@ -53,7 +53,7 @@ Add entry at the top of `CHANGELOG.md`: Commit the changelog: ```bash git add CHANGELOG.md -git commit -m "docs: Update changelog for vX.Y.Z" +git commit -m "docs: update changelog for vX.Y.Z" ``` ### 3. Verify Tests Pass @@ -73,7 +73,7 @@ git tag -a vX.Y.Z -m "Release vX.Y.Z: brief description" git push origin vX.Y.Z ``` -Pushing the tag triggers `.github/workflows/release.yml`, which runs GoReleaser to build binaries, create the GitHub release, and update the Homebrew tap. +Pushing the tag triggers `.github/workflows/release.yml`, which runs GoReleaser to build binaries, create the GitHub release, and update the Homebrew tap. Release automation uses the same canonical subject shape as the rest of the repo: `type: summary`. For normal development work, keep using `type: summary (td-)`. ### 5. Verify @@ -137,7 +137,7 @@ go test ./... # Update changelog # (Edit CHANGELOG.md, add entry at top) git add CHANGELOG.md -git commit -m "docs: Update changelog for vX.Y.Z" +git commit -m "docs: update changelog for vX.Y.Z" # Push commits, then tag (tag push triggers automated release) git push origin main diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh new file mode 100755 index 00000000..bd79639d --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# commit-msg hook for td +# Install: make install-hooks (or: ln -sf ../../scripts/commit-msg.sh .git/hooks/commit-msg) +set -euo pipefail + +MESSAGE_FILE="${1:-}" +ALLOWED_TYPES_REGEX='^(feat|fix|chore|docs|refactor|test|ci|build|perf|style|revert)$' + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +normalize_subject() { + local original="$1" + local working prefix raw_type remainder type task_suffix normalized + + working="$(trim "$original")" + prefix="" + + if [[ "$working" =~ ^((fixup|squash)!\ )(.*)$ ]]; then + prefix="${BASH_REMATCH[1]}" + working="${BASH_REMATCH[3]}" + fi + + if [[ "$working" == Merge\ * || "$working" == Revert\ \"* ]]; then + printf '%s%s\n' "$prefix" "$working" + return 0 + fi + + if [[ "$working" =~ ^([[:alpha:]]+)[[:space:]]*:[[:space:]]*(.+)$ ]]; then + raw_type="${BASH_REMATCH[1]}" + remainder="${BASH_REMATCH[2]}" + elif [[ "$working" =~ ^([[:alpha:]]+)[[:space:]]+(.+)$ ]]; then + raw_type="${BASH_REMATCH[1]}" + remainder="${BASH_REMATCH[2]}" + else + return 1 + fi + + type="$(printf '%s' "$raw_type" | tr '[:upper:]' '[:lower:]')" + if ! [[ "$type" =~ $ALLOWED_TYPES_REGEX ]]; then + return 1 + fi + + remainder="$(trim "$remainder")" + task_suffix="" + if [[ "$remainder" =~ ^(.*)[[:space:]]+\(([Tt][Dd]-[[:alnum:]][[:alnum:]-]*)\)$ ]]; then + remainder="$(trim "${BASH_REMATCH[1]}")" + task_suffix="$(printf '%s' "${BASH_REMATCH[2]}" | tr '[:upper:]' '[:lower:]')" + fi + + if [[ -z "$remainder" ]]; then + return 1 + fi + + normalized="${prefix}${type}: ${remainder}" + if [[ -n "$task_suffix" ]]; then + normalized="${normalized} (${task_suffix})" + fi + + printf '%s\n' "$normalized" +} + +if [[ -z "$MESSAGE_FILE" || ! -f "$MESSAGE_FILE" ]]; then + echo "commit-msg: expected the commit message file path as the first argument." >&2 + exit 1 +fi + +lines=() +while IFS= read -r line || [[ -n "$line" ]]; do + lines+=("$line") +done < "$MESSAGE_FILE" +subject_index=-1 +subject_line="" + +for i in "${!lines[@]}"; do + line="${lines[$i]}" + trimmed_line="$(trim "$line")" + if [[ -z "$trimmed_line" || "$trimmed_line" == \#* ]]; then + continue + fi + subject_index="$i" + subject_line="$line" + break +done + +if [[ "$subject_index" -lt 0 ]]; then + exit 0 +fi + +if ! normalized_subject="$(normalize_subject "$subject_line")"; then + cat >&2 <<'EOF' +Commit message must use one of these formats: + type: summary (td-) # normal development work + type: summary # automation/release work without a td task + +Examples: + feat: add session analytics (td-a1b2) + chore: bump homebrew formula to v0.2.0 + +Allowed types: feat, fix, chore, docs, refactor, test, ci, build, perf, style, revert +EOF + exit 1 +fi + +if [[ "$(trim "$subject_line")" != "$normalized_subject" ]]; then + lines[$subject_index]="$normalized_subject" + printf '%s\n' "${lines[@]}" > "$MESSAGE_FILE" + echo "Normalized commit subject to: $normalized_subject" +fi diff --git a/scripts/loop-prompt.md b/scripts/loop-prompt.md index 7f84db2e..e5027ca8 100644 --- a/scripts/loop-prompt.md +++ b/scripts/loop-prompt.md @@ -163,6 +163,7 @@ td review ``` Use `td review`, not `td close` — self-closing is blocked. +For release or automation-only commits without a td task, use `type: ` instead. ## Rules @@ -174,4 +175,4 @@ Use `td review`, not `td close` — self-closing is blocked. - **Don't break sync.** Deterministic IDs, proper event logging, no hard deletes. - **Session isolation is sacred.** Don't bypass review guards. - **If stuck, log and skip.** `td log "Blocked: "` then `td block `. -- **Commit messages reference td.** Format: `feat|fix|chore: (td-)` +- **Commit messages use one canonical subject style.** Prefer `type: (td-)`; release/automation commits without a td task may use `type: `. diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 2b563f26..721e89b0 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # pre-commit hook for td -# Install: make install-hooks (or: ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit) +# Install: make install-hooks (installs both pre-commit and commit-msg hooks) set -euo pipefail PASS=0