Skip to content
Open
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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ 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 }}

- name: Validate pull request title
run: bash ./scripts/commit-msg.sh --title "${{ github.event.pull_request.title }}"

test:
name: Test
runs-on: ubuntu-latest
Expand Down
12 changes: 8 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Binary name
BINARY=nightshift
PKG=./cmd/nightshift
HOOKS_DIR = $(shell git rev-parse --git-path hooks)

# Build the binary
build:
Expand Down Expand Up @@ -75,10 +76,13 @@ 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 pre-commit and commit-msg hooks
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)"
@ln -sf "$(CURDIR)/scripts/pre-commit.sh" "$(HOOKS_DIR)/pre-commit"
@ln -sf "$(CURDIR)/scripts/commit-msg.sh" "$(HOOKS_DIR)/commit-msg"
@echo "✓ pre-commit hook installed ($(HOOKS_DIR)/pre-commit -> $(CURDIR)/scripts/pre-commit.sh)"
@echo "✓ commit-msg hook installed ($(HOOKS_DIR)/commit-msg -> $(CURDIR)/scripts/commit-msg.sh)"
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
This resolves the active Git hooks directory, including linked worktrees, and symlinks `scripts/pre-commit.sh` plus `scripts/commit-msg.sh` into it.

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

To bypass in a pinch: `git commit --no-verify`
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

Expand Down
92 changes: 92 additions & 0 deletions scripts/commit-msg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/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_PATTERN="^(${CONVENTIONAL_TYPES})(\\([[:alnum:]./_-]+\\))?(!)?: [^[:space:]].*"

usage() {
echo "Usage: scripts/commit-msg.sh <commit-msg-file> | scripts/commit-msg.sh --title \"<subject>\"" >&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
Loading