93 lines
2.8 KiB
Python
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())
|