litellm/.githooks/commit-msg
ryan-crabbe-berri 0d120de785
chore(hooks): enforce Conventional Commits and Conventional Branches (#30174)
* chore(hooks): enforce Conventional Commits and Conventional Branches

Adds opt-in local git hooks plus a CI PR-title check:

- .githooks/commit-msg validates commit subjects against Conventional
  Commits 1.0.0 (feat|fix|docs|style|refactor|perf|test|build|ci|
  chore|revert)(scope)!: subject. Merge/revert/fixup!/squash!/amend!
  messages pass through; --no-verify still works.
- .githooks/pre-push validates branch names against Conventional
  Branches (feature|bugfix|hotfix|release|chore)/desc. Bypasses
  main, litellm_internal_staging, dependabot/*, gh-readonly-queue/*.
  Tag pushes and deletions are skipped.
- scripts/install_git_hooks.sh sets core.hooksPath=.githooks and is
  wired up as 'make install-hooks'. Opt-in — not chained into
  install-dev.
- .github/workflows/conventional-commits.yml validates PR titles via
  amannn/action-semantic-pull-request pinned to v6.1.1's SHA. This is
  the actual gate since squash-merge uses the PR title as the commit
  subject.
- tests/test_litellm/test_git_hooks.py exercises both hooks via
  subprocess for accept / reject / bypass / git-generated-message
  cases.
- CONTRIBUTING.md documents the conventions, the install step, the
  bypass list, and the --no-verify escape hatch.

Resolves LIT-3306

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(hooks): address Greptile review on PR #28703

Resolves two findings from the automated code review:

1. CONTRIBUTING.md: shrink the new Conventional Commits / Branches
   section to a 2-line pointer at docs.litellm.ai. Per the team
   convention, the full documentation lives in the litellm-docs
   repo — see BerriAI/litellm-docs#208 for the companion change that
   adds the section to docs/extras/contributing_code.md.

2. .githooks/commit-msg: tighten the subject regex to also reject an
   uppercase first letter in the description. CI's subjectPattern is
   ^(?![A-Z]).+$ so the previous local hook would accept 'feat: Add
   thing' which would then fail the PR-title check. The local hook is
   now the strictly tighter of the two gates. Test cases extended to
   cover both the new rejection and the digit/symbol-start cases that
   remain allowed.

Resolves LIT-3306

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: trigger ci after branch rename

* fix(ci): rerun pr title check when bypass label changes

amannn/action-semantic-pull-request only honors ignoreLabels if the
workflow retriggers on labeled/unlabeled events; without them a red
check stays red after a maintainer applies the bypass label.

Also point the CONTRIBUTING.md workflow comments at the conventions
section, which now sits above the Development Workflow section.

---------

Co-authored-by: Yassin Kortam <yassinkortam@Yassins-MBP.localdomain>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 10:00:23 -07:00

76 lines
2.2 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# commit-msg — enforce Conventional Commits 1.0.0
# https://www.conventionalcommits.org/en/v1.0.0/
#
# Subject format: <type>(<scope>)!: <description>
# - <type> must be one of the angular types (feat, fix, ...)
# - (<scope>) is optional
# - ! is optional and marks a breaking change
# - <description> 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 <<EOF
✗ Commit message does not follow Conventional Commits.
Got: $subject
Expected: <type>(<scope>)!: <description>
(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