#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Scaffold a deploy / QA note from a git commit range. Usage (from repo root): python scripts/generate_deploy_test_plan.py HEAD~5..HEAD python scripts/generate_deploy_test_plan.py abc1234..def5678 --out docs/deploy/20260801_example.md The script lists commits and touched files. You (or the agent) still fill test steps and expected results after reading the diffs. """ from __future__ import annotations import argparse import subprocess import sys from datetime import date from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def run(args: list[str]) -> str: r = subprocess.run( args, cwd=ROOT, capture_output=True, text=True, encoding="utf-8", errors="replace", ) if r.returncode != 0: raise SystemExit(r.stderr or r.stdout or f"command failed: {args}") return r.stdout.strip() def main() -> None: p = argparse.ArgumentParser(description="Scaffold deploy test plan from git range") p.add_argument( "range", help="Git revision range, e.g. HEAD~5..HEAD or origin/production..HEAD", ) p.add_argument( "--out", type=Path, default=None, help="Optional output path under docs/deploy/", ) p.add_argument("--title", default="Deploy note (draft)", help="Document title") args = p.parse_args() log = run(["git", "log", "--oneline", args.range]) if not log: print("No commits in range.", file=sys.stderr) sys.exit(1) stat = run(["git", "diff", "--stat", args.range]) name_status = run(["git", "diff", "--name-status", args.range]) commits = [ln for ln in log.splitlines() if ln.strip()] commit_bullets = "\n".join(f"- `{c[:7]}` — {c[8:]}" for c in commits) body = f"""# {args.title} Date: {date.today().isoformat()} Branch / build: (fill) Range: `{args.range}` Author: (fill) > Auto-scaffolded from git. **Replace the Test plan with real steps** after reviewing the diff. ## Summary - (TODO: 1–3 bullets — user-facing impact) ## Scope - Backend: (see files below) - Frontend: (check paired repo if UI) - DB / Liquibase: (none / list changelog files) - Config / ops: (none / list) ## Commits {commit_bullets} ## Files touched ``` {name_status} ``` ### Diffstat ``` {stat} ``` ## Test plan | # | Steps (who / where / data) | Expected result | |---|----------------------------|-----------------| | 1 | TODO — happy path | TODO | | 2 | TODO — edge / failure case | TODO | | 3 | TODO — regression on related screen | TODO | ## Out of scope / not tested - (TODO) ## Rollback - Revert range `{args.range}` / redeploy previous build """ if args.out: out = args.out if args.out.is_absolute() else ROOT / args.out out.parent.mkdir(parents=True, exist_ok=True) out.write_text(body, encoding="utf-8") print(f"Wrote {out.relative_to(ROOT)}") else: sys.stdout.reconfigure(encoding="utf-8") print(body) if __name__ == "__main__": main()