From 2bfaf3d7300cf900ac40822cc8189eaf1c967622 Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Sat, 11 Apr 2026 04:06:36 -0700 Subject: [PATCH 1/2] chore: normalize commit message enforcement Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- .github/workflows/ci.yml | 53 +++++++++++++++++++++++ Makefile | 14 ++++-- README.md | 46 ++++++++++++++++---- scripts/commit-msg.sh | 93 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 12 deletions(-) create mode 100755 scripts/commit-msg.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fc5686..c69dfe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,59 @@ on: branches: [main] jobs: + commit-format: + name: Commit Format + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout base branch validator + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: base + + - name: Validate pull request title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + set -euo pipefail + + if [[ -f base/scripts/commit-msg.sh ]]; then + bash base/scripts/commit-msg.sh --title "$PR_TITLE" + exit 0 + fi + + echo "Base branch has no commit validator yet; using bootstrap rules." + conventional_types='build|chore|ci|docs|feat|fix|perf|refactor|style|test' + conventional_pattern="^(${conventional_types})(\\([[:alnum:]#./_-]+\\))?(!)?: [^[:space:]].*" + + if [[ "$PR_TITLE" =~ ^(Merge|Revert)\ ]]; then + exit 0 + fi + + if [[ "$PR_TITLE" =~ $conventional_pattern ]]; then + exit 0 + fi + + echo "Invalid pull request title: $PR_TITLE" >&2 + cat >&2 <<'EOF' + Pull request title must use Conventional Commits: + type: summary + type(scope): summary + type!: summary + type(scope)!: summary + + Accepted types: build, chore, ci, docs, feat, fix, perf, refactor, style, test + Allowed exceptions: Merge ..., Revert ... + + Examples: + feat(run): add pause command + feat!: drop legacy API + fix(config): preserve provider YAML keys + docs(readme): explain hook installation + EOF + exit 1 + test: name: Test runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 088be01..e2cb23b 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ # Binary name BINARY=nightshift PKG=./cmd/nightshift +HOOKS_DIR := $(shell git rev-parse --git-path hooks) # Build the binary build: @@ -75,10 +76,15 @@ help: @echo " check - Run tests and lint" @echo " install - Build and install to Go bin directory" @echo " calibrate-providers - Compare local Claude/Codex session usage for calibration" - @echo " install-hooks - Install git pre-commit hook" + @echo " install-hooks - Install git hooks for pre-commit and commit-msg checks" @echo " help - Show this help" -# Install git pre-commit hook +# Install git hooks with wrappers that resolve the active worktree at runtime. install-hooks: - @ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit - @echo "✓ pre-commit hook installed (.git/hooks/pre-commit → scripts/pre-commit.sh)" + @mkdir -p "$(HOOKS_DIR)" + @printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' '' 'repo_root="$$(git rev-parse --show-toplevel)"' 'exec "$$repo_root/scripts/pre-commit.sh" "$$@"' > "$(HOOKS_DIR)/pre-commit" + @chmod +x "$(HOOKS_DIR)/pre-commit" + @printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' '' 'repo_root="$$(git rev-parse --show-toplevel)"' 'exec "$$repo_root/scripts/commit-msg.sh" "$$@"' > "$(HOOKS_DIR)/commit-msg" + @chmod +x "$(HOOKS_DIR)/commit-msg" + @echo "✓ pre-commit hook installed ($(HOOKS_DIR)/pre-commit -> worktree-aware scripts/pre-commit.sh)" + @echo "✓ commit-msg hook installed ($(HOOKS_DIR)/commit-msg -> worktree-aware scripts/commit-msg.sh)" diff --git a/README.md b/README.md index 84f92cd..60c846e 100644 --- a/README.md +++ b/README.md @@ -258,20 +258,50 @@ Each task has a default cooldown interval to prevent the same task from running ## Development -### Pre-commit hooks +### Git hooks and commit messages -Install the git pre-commit hook to catch formatting and vet issues before pushing: +Install the local git hooks before pushing: ```bash make install-hooks ``` -This symlinks `scripts/pre-commit.sh` into `.git/hooks/pre-commit`. The hook runs: -- **gofmt** — flags any staged `.go` files that need formatting -- **go vet** — catches common correctness issues -- **go build** — ensures the project compiles - -To bypass in a pinch: `git commit --no-verify` +This installs worktree-aware wrappers into the active Git hooks directory, including linked worktrees, and dispatches to both `scripts/pre-commit.sh` and `scripts/commit-msg.sh`. + +The `pre-commit` hook runs: +- **gofmt** - flags any staged `.go` files that need formatting +- **go vet** - catches common correctness issues +- **go build** - ensures the project compiles + +The `commit-msg` hook validates the first non-comment line of each commit message. Use Conventional Commits: +- `type: summary` +- `type(scope): summary` +- `type!: summary` +- `type(scope)!: summary` + +Accepted types: +- `build` +- `chore` +- `ci` +- `docs` +- `feat` +- `fix` +- `perf` +- `refactor` +- `style` +- `test` + +Examples: +- `feat(run): add pause command` +- `feat!: drop legacy API` +- `fix(config): preserve provider YAML keys` +- `docs(readme): explain hook installation` + +Git-generated `Merge ...` and `Revert ...` subjects are allowed automatically. + +Pull request titles are validated in CI with the same rules so squash-merge commits on `main` stay consistent even when local hooks are skipped. + +To bypass local hooks in a pinch: `git commit --no-verify` ## Uninstalling diff --git a/scripts/commit-msg.sh b/scripts/commit-msg.sh new file mode 100755 index 0000000..9898aa4 --- /dev/null +++ b/scripts/commit-msg.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# commit-msg hook for nightshift +# Install: make install-hooks +set -euo pipefail + +readonly CONVENTIONAL_TYPES='build|chore|ci|docs|feat|fix|perf|refactor|style|test' +readonly CONVENTIONAL_SCOPE='[[:alnum:]#./_-]+' +readonly CONVENTIONAL_PATTERN="^(${CONVENTIONAL_TYPES})(\\(${CONVENTIONAL_SCOPE}\\))?(!)?: [^[:space:]].*" + +usage() { + echo "Usage: scripts/commit-msg.sh | scripts/commit-msg.sh --title \"\"" >&2 +} + +read_subject() { + if [[ $# -eq 2 && "$1" == "--title" ]]; then + awk ' + { + line = $0 + sub(/\r$/, "", line) + sub(/^[[:space:]]+/, "", line) + sub(/[[:space:]]+$/, "", line) + if (line != "" && line !~ /^#/) { + print line + exit + } + } + ' <<<"$2" + return + fi + + if [[ $# -eq 1 ]]; then + awk ' + { + line = $0 + sub(/\r$/, "", line) + sub(/^[[:space:]]+/, "", line) + sub(/[[:space:]]+$/, "", line) + if (line != "" && line !~ /^#/) { + print line + exit + } + } + ' "$1" + return + fi + + usage + exit 2 +} + +print_failure() { + cat >&2 <<'EOF' +Commit subject must use Conventional Commits: + type: summary + type(scope): summary + type!: summary + type(scope)!: summary + +Accepted types: build, chore, ci, docs, feat, fix, perf, refactor, style, test +Allowed exceptions: Merge ..., Revert ... + +Examples: + feat(run): add pause command + feat!: drop legacy API + fix(config): preserve provider YAML keys + docs(readme): explain hook installation +EOF +} + +if [[ $# -eq 1 && ( "$1" == "-h" || "$1" == "--help" ) ]]; then + usage + exit 0 +fi + +subject="$(read_subject "$@")" + +if [[ -z "$subject" ]]; then + echo "Commit subject is empty." >&2 + print_failure + exit 1 +fi + +if [[ "$subject" =~ ^(Merge|Revert)\ ]]; then + exit 0 +fi + +if [[ "$subject" =~ $CONVENTIONAL_PATTERN ]]; then + exit 0 +fi + +echo "Invalid commit subject: $subject" >&2 +print_failure +exit 1 From e5c029fd4b2a2b7f0b34bbca405357a3481ccb56 Mon Sep 17 00:00:00 2001 From: Marcus Vorwaller Date: Sat, 11 Apr 2026 04:08:49 -0700 Subject: [PATCH 2/2] fix: unlink existing hook paths before install Nightshift-Task: commit-normalize Nightshift-Ref: https://github.com/marcus/nightshift --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index e2cb23b..9446eb7 100644 --- a/Makefile +++ b/Makefile @@ -82,6 +82,7 @@ help: # Install git hooks with wrappers that resolve the active worktree at runtime. install-hooks: @mkdir -p "$(HOOKS_DIR)" + @rm -f "$(HOOKS_DIR)/pre-commit" "$(HOOKS_DIR)/commit-msg" @printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' '' 'repo_root="$$(git rev-parse --show-toplevel)"' 'exec "$$repo_root/scripts/pre-commit.sh" "$$@"' > "$(HOOKS_DIR)/pre-commit" @chmod +x "$(HOOKS_DIR)/pre-commit" @printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' '' 'repo_root="$$(git rev-parse --show-toplevel)"' 'exec "$$repo_root/scripts/commit-msg.sh" "$$@"' > "$(HOOKS_DIR)/commit-msg"