#!/usr/bin/env bash # # commit-msg — enforce Conventional Commits 1.0.0 # https://www.conventionalcommits.org/en/v1.0.0/ # # Subject format: ()!: # - must be one of the angular types (feat, fix, ...) # - () is optional # - ! is optional and marks a breaking change # - is mandatory and must be non-empty # # Bypass: commit with --no-verify. # Merge, revert, fixup!, squash!, and amend! messages are passed through. set -eu COMMIT_MSG_FILE="${1:-}" if [ -z "$COMMIT_MSG_FILE" ] || [ ! -f "$COMMIT_MSG_FILE" ]; then echo "commit-msg: missing commit message file" >&2 exit 1 fi # First non-comment, non-empty line is the subject. subject="" while IFS= read -r line || [ -n "$line" ]; do case "$line" in ''|'#'*) continue ;; esac subject="$line" break done < "$COMMIT_MSG_FILE" if [ -z "$subject" ]; then echo "commit-msg: empty commit message" >&2 exit 1 fi # Pass-through commits generated by git itself. case "$subject" in "Merge "*|"Revert \""*|"fixup! "*|"squash! "*|"amend! "*) exit 0 ;; esac ALLOWED_TYPES="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" # Description must not start with an uppercase letter — kept in sync with the # subjectPattern in .github/workflows/conventional-commits.yml so the local # hook is the strictly tighter of the two gates. (Without this guard, a commit # like "feat: Add thing" passes locally but fails the PR-title CI check.) PATTERN="^(${ALLOWED_TYPES})(\([^)]+\))?!?: [^A-Z].*" if printf '%s' "$subject" | grep -Eq "$PATTERN"; then exit 0 fi cat >&2 <()!: (description must start with a lowercase letter) Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert Examples: feat(router): add weighted round-robin strategy fix(bedrock): decouple STS region from aws_region_name chore(deps): bump black to 26.3.1 refactor!: drop Python 3.8 support See https://www.conventionalcommits.org/en/v1.0.0/ To bypass (use sparingly): git commit --no-verify EOF exit 1