feat(orquestra): enforce semver and conventional commits
This commit is contained in:
93
scripts/validate_conventional_commit.py
Executable file
93
scripts/validate_conventional_commit.py
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user