Files
playbook/.gitea/ci/commit_message_lint.py
T
cshandClaude Fable 5 7408c532f0 🐛 fix(commit-message): enforce one rule owner across skill and CI
CI ran .gitea/ci/commit_message_lint.py, an independent reimplementation
that parsed the type/emoji mapping out of docs/common/commit_message.md.
Its scope pattern accepted fix(-a), fix(a-), fix(a--b) and fix(_), and it
had no subject length check, so the gate that actually blocks merges
enforced weaker rules than the Validation this skill reports.

The CI entry is now a wrapper that locates the skill validator and
delegates to it with no arguments; commit_policy.json becomes the only
rule source and explanatory docs stop being a machine policy input.

The validator also learns the workflow_run event, whose payload carries
neither a PR title nor a before/after range. An upstream pull_request now
validates <integration-branch>..head_sha instead of HEAD alone, taking
the branch name from COMMIT_LINT_MAIN_BRANCH, and degrades to the
upstream head commit with a WARN rather than guessing a base.

Alongside: --help now documents the no-argument CI mode it had always
supported silently, CI wiring detail moves to references/ci-wiring.md,
and the description gains negative boundaries.

test/test_commit_message_policy.py asserts policy/spec-table equality and
uses ast to assert the CI entry imports no regex and reads no file, so a
second implementation cannot reappear unnoticed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 15:21:37 +08:00

81 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""CI entry point for commit-message lint.
This file only locates and delegates. Every rule, the type/emoji mapping and
the policy schema belong to the commit-message skill validator, which reads
`skills/commit-message/references/commit_policy.json`.
Do not reimplement validation here and do not parse rules out of
`docs/common/commit_message.md`: that document is human guidance, not a machine
policy source. See `skills/commit-message/references/ci-wiring.md`.
"""
from __future__ import annotations
import os
import pathlib
import subprocess
import sys
VALIDATOR_CANDIDATES = (
("skills", "commit-message", "scripts", "validate_commit_message.py"),
(
"docs",
"standards",
"playbook",
"skills",
"commit-message",
"scripts",
"validate_commit_message.py",
),
)
def _eprint(*args: object) -> None:
print(*args, file=sys.stderr)
def _repo_root() -> pathlib.Path:
output = subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
)
return pathlib.Path(output.strip())
def _find_validator(root: pathlib.Path) -> pathlib.Path:
tried: list[pathlib.Path] = []
for parts in VALIDATOR_CANDIDATES:
path = root.joinpath(*parts)
tried.append(path)
if path.is_file():
return path
raise FileNotFoundError(
"commit-message skill validator not found; expected one of:\n"
+ "\n".join(f"- {path}" for path in tried)
)
def main() -> int:
try:
root = _repo_root()
except Exception as exc:
_eprint(f"ERROR: not a git repository: {exc}")
return 2
os.chdir(root)
try:
validator = _find_validator(root)
except FileNotFoundError as exc:
_eprint(f"ERROR: {exc}")
return 2
print(f"validator: {validator}")
sys.stdout.flush()
# No arguments: the validator selects its CI input mode from the event
# payload. Exit codes pass through unchanged (0 pass / 1 lint / 2 input).
return subprocess.run([sys.executable, str(validator)], cwd=root).returncode
if __name__ == "__main__":
raise SystemExit(main())