infra: enforce governance sync and TASK-REQ mapping in CI (#330)
This commit is contained in:
@@ -5,9 +5,16 @@ from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
REQUIREMENTS_REGISTRY = "docs/ouroboros/01_requirements_registry.md"
|
||||
TASK_WORK_ORDERS_DOC = "docs/ouroboros/30_code_level_work_orders.md"
|
||||
TASK_DEF_LINE = re.compile(r"^-\s+`(?P<task_id>TASK-[A-Z0-9-]+-\d{3})`(?P<body>.*)$")
|
||||
REQ_ID_IN_LINE = re.compile(r"\bREQ-[A-Z0-9-]+-\d{3}\b")
|
||||
TASK_ID_IN_TEXT = re.compile(r"\bTASK-[A-Z0-9-]+-\d{3}\b")
|
||||
TEST_ID_IN_TEXT = re.compile(r"\bTEST-[A-Z0-9-]+-\d{3}\b")
|
||||
|
||||
|
||||
def must_contain(path: Path, required: list[str], errors: list[str]) -> None:
|
||||
@@ -75,8 +82,45 @@ def validate_registry_sync(changed_files: list[str], errors: list[str]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def validate_task_req_mapping(errors: list[str], *, task_doc: Path | None = None) -> None:
|
||||
path = task_doc or Path(TASK_WORK_ORDERS_DOC)
|
||||
if not path.exists():
|
||||
errors.append(f"missing file: {path}")
|
||||
return
|
||||
|
||||
text = path.read_text(encoding="utf-8")
|
||||
found_task = False
|
||||
for line in text.splitlines():
|
||||
m = TASK_DEF_LINE.match(line.strip())
|
||||
if not m:
|
||||
continue
|
||||
found_task = True
|
||||
if not REQ_ID_IN_LINE.search(m.group("body")):
|
||||
errors.append(
|
||||
f"{path}: TASK without REQ mapping -> {m.group('task_id')}"
|
||||
)
|
||||
if not found_task:
|
||||
errors.append(f"{path}: no TASK definitions found")
|
||||
|
||||
|
||||
def validate_pr_traceability(warnings: list[str]) -> None:
|
||||
title = os.getenv("GOVERNANCE_PR_TITLE", "").strip()
|
||||
body = os.getenv("GOVERNANCE_PR_BODY", "").strip()
|
||||
if not title and not body:
|
||||
return
|
||||
|
||||
text = f"{title}\n{body}"
|
||||
if not REQ_ID_IN_LINE.search(text):
|
||||
warnings.append("PR text missing REQ-ID reference")
|
||||
if not TASK_ID_IN_TEXT.search(text):
|
||||
warnings.append("PR text missing TASK-ID reference")
|
||||
if not TEST_ID_IN_TEXT.search(text):
|
||||
warnings.append("PR text missing TEST-ID reference")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
changed_files = load_changed_files(sys.argv[1:], errors)
|
||||
|
||||
pr_template = Path(".gitea/PULL_REQUEST_TEMPLATE.md")
|
||||
@@ -141,6 +185,8 @@ def main() -> int:
|
||||
errors.append(f"missing file: {handover_script}")
|
||||
|
||||
validate_registry_sync(changed_files, errors)
|
||||
validate_task_req_mapping(errors)
|
||||
validate_pr_traceability(warnings)
|
||||
|
||||
if errors:
|
||||
print("[FAIL] governance asset validation failed")
|
||||
@@ -149,6 +195,10 @@ def main() -> int:
|
||||
return 1
|
||||
|
||||
print("[OK] governance assets validated")
|
||||
if warnings:
|
||||
print(f"[WARN] governance advisory: {len(warnings)}")
|
||||
for warn in warnings:
|
||||
print(f"- {warn}")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user