Files
grm/scripts/validate_commit_msg.py
T
Emil Simeonov 3048d7ade7
CI / lint (push) Has been cancelled
CI / unit-tests (push) Has been cancelled
CI / molecule-tests (push) Has been cancelled
GRM-20: fix: enforce GRM-N: conventional on master commits and PR titles
2026-06-19 15:10:32 +02:00

78 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Validate commit messages for GRM.
Rules:
- On feature branches: conventional commits ONLY, must NOT include GRM-N prefix.
- On master branch: must follow '<task-id>: <conventional commit>' pattern,
e.g. 'GRM-24: fix: resolve timeout'.
"""
import re
import subprocess
import sys
CONVENTIONAL_RE = re.compile(
r"^(feat|fix|chore|docs|style|refactor|perf|test|ci|build|revert|BREAKING CHANGE)(\(.+\))?: .+"
)
TASK_ID_RE = re.compile(r"^GRM-\d+:")
def first_line(text: str) -> str:
return text.split("\n")[0]
def get_branch() -> str:
try:
result = subprocess.run(
["git", "symbolic-ref", "--short", "HEAD"],
capture_output=True, text=True, check=True,
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return ""
def main(args=None) -> None:
argv = args if args is not None else sys.argv
if len(argv) < 2:
print("Usage: validate_commit_msg.py <commit-msg-file>")
sys.exit(1)
with open(argv[1]) as f:
msg = f.read().strip()
branch = get_branch()
subject = first_line(msg)
if branch == "master":
if not TASK_ID_RE.match(subject):
print("ERROR: Master branch commits must start with a task ID.")
print(" Expected: GRM-N: <conventional commit message>")
print(f" Got: {subject}")
sys.exit(1)
# Strip task-id prefix and validate the remainder as conventional
remainder = TASK_ID_RE.sub("", subject).strip()
if not CONVENTIONAL_RE.match(remainder):
print("ERROR: Master branch commit message must follow conventional format after task ID.")
print(" Expected: GRM-N: <type>: <description>")
print(f" Got: {subject}")
sys.exit(1)
return
if TASK_ID_RE.match(subject):
print("ERROR: Do not include task ID (GRM-N) in feature branch commits.")
print(" Task ID will be added automatically on merge via CI.")
sys.exit(1)
if not CONVENTIONAL_RE.match(subject):
print("ERROR: Commit message must follow conventional commit format.")
print(" Expected: <type>: <description>")
print(f" Got: {subject}")
print(" Allowed types: feat, fix, chore, docs, style, refactor,")
print(" perf, test, ci, build, revert, BREAKING CHANGE")
sys.exit(1)
if __name__ == "__main__": # pragma: no cover
main()