process: add mandatory PR body post-check step (#392)
This commit is contained in:
@@ -92,6 +92,25 @@ def validate_testing_doc_has_dynamic_count_guidance(errors: list[str]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def validate_pr_body_postcheck_guidance(errors: list[str]) -> None:
|
||||
required_tokens = {
|
||||
"commands": (
|
||||
"PR Body Post-Check (Mandatory)",
|
||||
"python3 scripts/validate_pr_body.py --pr <PR_NUMBER>",
|
||||
),
|
||||
"workflow": (
|
||||
"PR 생성 직후 본문 무결성 검증(필수)",
|
||||
"python3 scripts/validate_pr_body.py --pr <PR_NUMBER>",
|
||||
),
|
||||
}
|
||||
for key, tokens in required_tokens.items():
|
||||
path = REQUIRED_FILES[key]
|
||||
text = _read(path)
|
||||
for token in tokens:
|
||||
if token not in text:
|
||||
errors.append(f"{path}: missing PR body post-check guidance token -> {token}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
@@ -117,6 +136,7 @@ def main() -> int:
|
||||
validate_summary_docs_reference_core_docs(errors)
|
||||
validate_commands_endpoint_duplicates(errors)
|
||||
validate_testing_doc_has_dynamic_count_guidance(errors)
|
||||
validate_pr_body_postcheck_guidance(errors)
|
||||
|
||||
if errors:
|
||||
print("[FAIL] docs sync validation failed")
|
||||
@@ -128,6 +148,7 @@ def main() -> int:
|
||||
print("[OK] summary docs link to core docs and links resolve")
|
||||
print("[OK] commands endpoint rows have no duplicates")
|
||||
print("[OK] testing doc includes dynamic count guidance")
|
||||
print("[OK] PR body post-check guidance exists in commands/workflow docs")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
94
scripts/validate_pr_body.py
Normal file
94
scripts/validate_pr_body.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate PR body formatting to prevent escaped-newline artifacts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HEADER_PATTERN = re.compile(r"^##\s+\S+", re.MULTILINE)
|
||||
LIST_ITEM_PATTERN = re.compile(r"^\s*(?:-|\*|\d+\.)\s+\S+", re.MULTILINE)
|
||||
|
||||
|
||||
def validate_pr_body_text(text: str) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if "\\n" in text and "\n" not in text:
|
||||
errors.append("body contains escaped newline sequence (\\n)")
|
||||
if text.count("```") % 2 != 0:
|
||||
errors.append("body has unbalanced fenced code blocks (``` count is odd)")
|
||||
if not HEADER_PATTERN.search(text):
|
||||
errors.append("body is missing markdown section headers (e.g. '## Summary')")
|
||||
if not LIST_ITEM_PATTERN.search(text):
|
||||
errors.append("body is missing markdown list items")
|
||||
return errors
|
||||
|
||||
|
||||
def fetch_pr_body(pr_number: int) -> str:
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"tea",
|
||||
"api",
|
||||
"-R",
|
||||
"origin",
|
||||
f"repos/{{owner}}/{{repo}}/pulls/{pr_number}",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||
raise RuntimeError(f"failed to fetch PR #{pr_number}: {exc}") from exc
|
||||
|
||||
try:
|
||||
payload = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise RuntimeError(f"failed to parse PR payload for #{pr_number}: {exc}") from exc
|
||||
|
||||
body = payload.get("body", "")
|
||||
if not isinstance(body, str):
|
||||
raise RuntimeError(f"unexpected PR body type for #{pr_number}: {type(body).__name__}")
|
||||
return body
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate PR body markdown formatting and escaped-newline artifacts."
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--pr", type=int, help="PR number to fetch via `tea api`")
|
||||
group.add_argument("--body-file", type=Path, help="Path to markdown body file")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if args.body_file is not None:
|
||||
if not args.body_file.exists():
|
||||
print(f"[FAIL] body file not found: {args.body_file}")
|
||||
return 1
|
||||
body = args.body_file.read_text(encoding="utf-8")
|
||||
source = f"file:{args.body_file}"
|
||||
else:
|
||||
body = fetch_pr_body(args.pr)
|
||||
source = f"pr:{args.pr}"
|
||||
|
||||
errors = validate_pr_body_text(body)
|
||||
if errors:
|
||||
print("[FAIL] PR body validation failed")
|
||||
print(f"- source: {source}")
|
||||
for err in errors:
|
||||
print(f"- {err}")
|
||||
return 1
|
||||
|
||||
print("[OK] PR body validation passed")
|
||||
print(f"- source: {source}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user