File size: 4,354 Bytes
fe0c99f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de8ccff
fe0c99f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de8ccff
fe0c99f
de8ccff
fe0c99f
 
 
de8ccff
fe0c99f
de8ccff
fe0c99f
 
 
de8ccff
fe0c99f
 
 
 
de8ccff
fe0c99f
 
de8ccff
fe0c99f
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/usr/bin/env python3
"""Release readiness runner + checklist report generator.

Usage:
  python run_release_checklist.py
  python run_release_checklist.py --skip-e2e
  python run_release_checklist.py --skip-perf
  python run_release_checklist.py --quick
"""

from __future__ import annotations

import subprocess
import sys
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
REPORTS = ROOT / "reports"
REPORTS.mkdir(exist_ok=True)


@dataclass
class StepResult:
    label: str
    command: list[str]
    returncode: int
    duration_s: float

    @property
    def ok(self) -> bool:
        return self.returncode == 0


def _venv_python() -> Path:
    if sys.platform.startswith("win"):
        return ROOT / ".venv" / "Scripts" / "python.exe"
    return ROOT / ".venv" / "bin" / "python"


def _run(label: str, cmd: list[str]) -> StepResult:
    t0 = time.perf_counter()
    code = subprocess.call(cmd, cwd=str(ROOT))
    dt = time.perf_counter() - t0
    return StepResult(label=label, command=cmd, returncode=code, duration_s=dt)


def _write_report(results: list[StepResult], quick: bool, skip_perf: bool, skip_e2e: bool) -> Path:
    ts = datetime.now(UTC).strftime("%Y%m%d_%H%M%SZ")
    latest = REPORTS / "release_checklist_latest.md"
    stamped = REPORTS / f"release_checklist_{ts}.md"

    lines: list[str] = []
    lines.append("# Release Checklist Report")
    lines.append("")
    lines.append(f"- Generated (UTC): `{datetime.now(UTC).isoformat().replace('+00:00', 'Z')}`")
    lines.append(f"- Mode: `quick={quick}`, `skip_perf={skip_perf}`, `skip_e2e={skip_e2e}`")
    lines.append("")
    lines.append("## Automated Gates")
    lines.append("")
    for r in results:
        status = "PASS" if r.ok else "FAIL"
        lines.append(
            f"- [{status}] **{r.label}** (`{r.duration_s:.1f}s`)  \n"
            f"  Command: `{ ' '.join(r.command) }`"
        )
    lines.append("")

    all_ok = all(r.ok for r in results)
    lines.append("## Summary\n")
    lines.append(f"- Overall automated status: **{'PASS' if all_ok else 'FAIL'}**")
    lines.append("")
    lines.append("## Manual Release Checks")
    lines.append("")
    lines.append("- [ ] Launch app and verify home screen loads")
    lines.append("- [ ] Problem Solver: solve 3 representative inputs (derivative, limit, integral)")
    lines.append("- [ ] Problem Solver: verify graph renders + legend labels are readable")
    lines.append("- [ ] Learning: open pathway, navigate next/previous, open/close notes")
    lines.append("- [ ] Learning: verify quiz placeholder progression works")
    lines.append("- [ ] Verify copy buttons (expression/visual/text output) work as expected")
    lines.append("- [ ] Package/open distribution build (`dist/`) on target OS")
    lines.append("")

    content = "\n".join(lines) + "\n"
    latest.write_text(content, encoding="utf-8")
    stamped.write_text(content, encoding="utf-8")
    return latest


def main() -> int:
    args = set(sys.argv[1:])
    skip_e2e = "--skip-e2e" in args
    skip_perf = "--skip-perf" in args
    quick = "--quick" in args

    py = _venv_python()
    interpreter = str(py if py.exists() else Path(sys.executable))

    steps: list[StepResult] = []
    steps.append(_run("Quality Gate (ruff + mypy + tests)", [interpreter, "scripts/run_quality.py"]))
    if not steps[-1].ok:
        _write_report(steps, quick=quick, skip_perf=skip_perf, skip_e2e=skip_e2e)
        return 1

    if not skip_perf:
        steps.append(_run("Performance Smoke Tests", [interpreter, "scripts/run_tests.py", "--perf"]))
        if not steps[-1].ok:
            _write_report(steps, quick=quick, skip_perf=skip_perf, skip_e2e=skip_e2e)
            return 1

    if not skip_e2e:
        e2e_cmd = [interpreter, "scripts/run_tests.py", "--e2e"]
        if quick:
            e2e_cmd = [interpreter, "-m", "pytest", "-q", "-m", "e2e", "tests/test_e2e_backend_smoke.py"]
        steps.append(_run("E2E Smoke Tests", e2e_cmd))
        if not steps[-1].ok:
            _write_report(steps, quick=quick, skip_perf=skip_perf, skip_e2e=skip_e2e)
            return 1

    _write_report(steps, quick=quick, skip_perf=skip_perf, skip_e2e=skip_e2e)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())