Files
kandev/scripts/pr-walkthrough-render
T
2026-08-22 21:46:57 +01:00

110 lines
3.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Render agent-supplied walkthrough JSON with trusted pull-request identity."""
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
TRUSTED_DIR = Path(".opencode-walkthrough")
OUTPUT_DIR = Path("docs/pr-walkthrough")
REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
def required_env(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
raise ValueError(f"{name} is required")
return value
def trusted_identity() -> dict[str, object]:
number_text = required_env("PR_NUMBER")
if not number_text.isdigit() or int(number_text) < 1:
raise ValueError("PR_NUMBER must be a positive integer")
number = int(number_text)
repo = required_env("PR_REPO")
if not REPO_RE.fullmatch(repo):
raise ValueError("PR_REPO must be an owner/repository slug")
expected_url = f"https://github.com/{repo}/pull/{number}"
url = required_env("PR_URL").rstrip("/")
if url != expected_url:
raise ValueError(f"PR_URL must be {expected_url}")
return {
"number": number,
"title": required_env("PR_TITLE"),
"url": url,
"files_url": f"{url}/files",
"repo": repo,
"base": required_env("PR_BASE"),
"head": required_env("PR_HEAD"),
}
def bind_trusted_metadata(data: object) -> dict[str, object]:
if not isinstance(data, dict):
raise ValueError("walkthrough JSON must be an object")
pr = data.get("pr")
if not isinstance(pr, dict):
pr = {}
data["pr"] = pr
pr.update(trusted_identity())
changes = data.get("changes")
if isinstance(changes, list):
for change in changes:
if isinstance(change, dict):
change.pop("file_url", None)
return data
def render(data: dict[str, object]) -> tuple[Path, Path]:
build_script = TRUSTED_DIR / "build.py"
shell = TRUSTED_DIR / "shell.html"
if not build_script.is_file() or not shell.is_file():
raise ValueError("trusted walkthrough renderer assets are missing")
number = int(data["pr"]["number"])
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
json_path = OUTPUT_DIR / f"pr-{number}.json"
html_path = OUTPUT_DIR / f"pr-{number}.html"
with tempfile.TemporaryDirectory(prefix=".render-", dir=OUTPUT_DIR) as temp_name:
temp_dir = Path(temp_name)
temp_json = temp_dir / json_path.name
temp_html = temp_dir / html_path.name
temp_json.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
result = subprocess.run(
[sys.executable, str(build_script), str(temp_json), str(temp_html)],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
message = result.stderr.strip() or result.stdout.strip() or "renderer failed"
raise ValueError(message)
if not temp_html.is_file() or temp_html.stat().st_size == 0:
raise ValueError("renderer did not produce non-empty HTML")
temp_html.replace(html_path)
temp_json.replace(json_path)
return json_path, html_path
def main() -> int:
try:
data = json.loads(sys.stdin.read())
bound = bind_trusted_metadata(data)
json_path, html_path = render(bound)
except (json.JSONDecodeError, OSError, ValueError) as exc:
print(f"walkthrough render failed: {exc}", file=sys.stderr)
return 1
print(f"rendered {json_path} and {html_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())