94 lines
2.6 KiB
Python
Executable File
94 lines
2.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate Conventional Commit messages for Orquestra.
|
|
|
|
Allowed format:
|
|
<type>[optional scope][!]: <description>
|
|
|
|
Examples:
|
|
feat(orders): add awaiting-payment notifications
|
|
fix(DEPLOY-MONOLITH-CLEANUP): clean stale deployments
|
|
chore(release)!: v1.0.0
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ALLOWED_TYPES = {
|
|
'build',
|
|
'chore',
|
|
'ci',
|
|
'docs',
|
|
'feat',
|
|
'fix',
|
|
'perf',
|
|
'refactor',
|
|
'revert',
|
|
'style',
|
|
'test',
|
|
}
|
|
|
|
HEADER_RE = re.compile(
|
|
r'^(?P<type>build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)'
|
|
r'(?:\((?P<scope>[A-Za-z0-9._/-]+)\))?'
|
|
r'(?P<breaking>!)?: (?P<description>\S.{0,119})$'
|
|
)
|
|
|
|
|
|
def validate(message: str) -> tuple[bool, str | None]:
|
|
lines = message.strip('\n').splitlines()
|
|
if not lines or not lines[0].strip():
|
|
return False, 'empty commit message'
|
|
|
|
header = lines[0].strip()
|
|
match = HEADER_RE.match(header)
|
|
if not match:
|
|
return (
|
|
False,
|
|
"first line must be Conventional Commit: "
|
|
"<type>[optional scope][!]: <description>",
|
|
)
|
|
|
|
commit_type = match.group('type')
|
|
if commit_type not in ALLOWED_TYPES:
|
|
return False, f"unsupported type '{commit_type}'"
|
|
|
|
description = match.group('description').strip()
|
|
if description.endswith('.'):
|
|
return False, 'description must not end with a period'
|
|
|
|
if any(line.rstrip() != line for line in lines):
|
|
return False, 'commit message contains trailing whitespace'
|
|
|
|
if len(lines) > 1 and lines[1].strip():
|
|
return False, 'second line must be blank before body/footer'
|
|
|
|
return True, None
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description='Validate Conventional Commit message')
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
group.add_argument('--file', type=Path, help='commit message file')
|
|
group.add_argument('--message', help='commit message string')
|
|
args = parser.parse_args()
|
|
|
|
message = args.message if args.message is not None else args.file.read_text(encoding='utf-8')
|
|
ok, error = validate(message)
|
|
if ok:
|
|
return 0
|
|
|
|
print(f'[FAIL] Invalid commit message: {error}', file=sys.stderr)
|
|
print('', file=sys.stderr)
|
|
print('Examples:', file=sys.stderr)
|
|
print(' feat(orders): add awaiting-payment notifications', file=sys.stderr)
|
|
print(' fix(monolith): clean stale deployments before start', file=sys.stderr)
|
|
print(' chore(release): v0.2.0', file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|