Files
opensquilla--opensquilla/.github/scripts/validate_pr_body.py
T
2026-07-13 13:12:33 +08:00

93 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Warn when a pull request body misses OpenSquilla governance fields."""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from typing import NamedTuple
class RequiredField(NamedTuple):
name: str
pattern: re.Pattern[str]
REQUIRED_FIELDS = (
RequiredField("Scope boundary", re.compile(r"(?im)^\s*Scope boundary\s*:")),
RequiredField("Base branch", re.compile(r"(?im)^\s*Base branch\s*:")),
RequiredField(
"Target exception",
re.compile(r"(?im)^\s*(?:Target exception|Main exception)\s*:"),
),
RequiredField("Linked issue", re.compile(r"(?im)^\s*Linked issue\s*:")),
RequiredField("Release note", re.compile(r"(?im)^\s*Release note\s*:")),
RequiredField("Tests", re.compile(r"(?im)^\s*(?:#+\s*)?Tests\s*:?\s*$")),
RequiredField("Maintainer live check", re.compile(r"(?im)^\s*Maintainer live check\s*:")),
RequiredField("Third-party origin", re.compile(r"(?im)^\s*Third-party origin\s*:")),
)
def missing_fields(body: str | None) -> list[str]:
text = body or ""
return [field.name for field in REQUIRED_FIELDS if field.pattern.search(text) is None]
def _annotation_escape(value: str) -> str:
return value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
def _warning(title: str, message: str) -> None:
print(f"::warning title={_annotation_escape(title)}::{_annotation_escape(message)}")
def _is_strict() -> bool:
return os.environ.get("PR_BODY_LINT_STRICT", "0").strip().lower() in {
"1",
"true",
"yes",
}
def _load_pr_body(event_path: str) -> str | None:
with Path(event_path).open(encoding="utf-8") as event_file:
event = json.load(event_file)
return (event.get("pull_request") or {}).get("body")
def main() -> int:
strict = _is_strict()
event_path = os.environ.get("GITHUB_EVENT_PATH")
if not event_path:
_warning("Missing PR event", "GITHUB_EVENT_PATH is not set; skipping PR body lint.")
return 1 if strict else 0
try:
body = _load_pr_body(event_path)
except (OSError, json.JSONDecodeError) as exc:
_warning("Unreadable PR event", f"Unable to read pull request event: {exc}")
return 1 if strict else 0
missing = missing_fields(body)
if not missing:
print("PR body includes required governance fields.")
return 0
for field in missing:
_warning(
"Missing PR template field",
f"Add the `{field}:` field to make review intent explicit.",
)
print(
"PR body lint is warning-only. Maintainers may harden it by setting "
"PR_BODY_LINT_STRICT=1 after existing pull requests are migrated."
)
return 1 if strict else 0
if __name__ == "__main__":
raise SystemExit(main())