#!/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())