-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify_release.py
More file actions
executable file
·145 lines (120 loc) · 4.29 KB
/
Copy pathverify_release.py
File metadata and controls
executable file
·145 lines (120 loc) · 4.29 KB
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/usr/bin/env python3
"""Run every release gate locally, mirroring CI (Story 5.1, qs-02/03/09).
This is the maintainer's single green-bar command: it reproduces the gates that
`.github/workflows/ci.yml` enforces, so a release candidate can be validated on
a workstation before merging a qualifying Conventional Commit PR. Each gate runs
in sequence; the script reports a per-gate PASS/FAIL summary and exits non-zero
if ANY gate fails.
Gates, in order:
1. Ruff lint (E/W/F/B/SIM/RUF on src + tests + scripts)
2. mypy --strict (the shippable package)
3. pytest + coverage (full suite; project floor 85%)
4. evaluation/ floor (evaluation modules >= 95%)
5. parity suite (tests/parity/ must pass 100% — release-blocking)
6. uv build (wheel + sdist build cleanly)
Usage:
python scripts/verify_release.py
python scripts/verify_release.py --skip-build # skip the slow uv build
Exit codes:
0 all gates passed
1 one or more gates failed
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
EVALUATION_COVERAGE_FLOOR = 95
@dataclass
class GateResult:
name: str
passed: bool
detail: str = ""
@dataclass
class Runner:
"""Sequences gates and accumulates results."""
results: list[GateResult] = field(default_factory=list)
def run(self, name: str, cmd: list[str]) -> bool:
print(f"\n=== {name} ===\n$ {' '.join(cmd)}", flush=True)
completed = subprocess.run(cmd, cwd=REPO_ROOT)
passed = completed.returncode == 0
self.results.append(
GateResult(name, passed, "" if passed else f"exit {completed.returncode}")
)
return passed
def _coverage_gate(runner: Runner) -> None:
"""Run the full suite under coverage (project floor enforced by --cov-fail-under)."""
runner.run(
"pytest + coverage (project floor 85%)",
[
sys.executable,
"-m",
"pytest",
"-p",
"no:cacheprovider",
"--cov=convert_sdk",
"--cov-report=term-missing",
"--cov-fail-under=85",
],
)
def _evaluation_coverage_gate(runner: Runner) -> None:
"""Enforce the evaluation/ 95% floor against the coverage data just collected."""
print(
f"\n=== evaluation/ coverage (floor {EVALUATION_COVERAGE_FLOOR}%) ===",
flush=True,
)
cmd = [
sys.executable,
"-m",
"coverage",
"report",
"--include=*/convert_sdk/evaluation/*",
f"--fail-under={EVALUATION_COVERAGE_FLOOR}",
]
print(f"$ {' '.join(cmd)}", flush=True)
completed = subprocess.run(cmd, cwd=REPO_ROOT)
passed = completed.returncode == 0
runner.results.append(
GateResult(
f"evaluation/ coverage >= {EVALUATION_COVERAGE_FLOOR}%",
passed,
"" if passed else f"exit {completed.returncode}",
)
)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--skip-build",
action="store_true",
help="Skip the (slow) uv build gate.",
)
args = parser.parse_args(argv)
runner = Runner()
runner.run("Ruff lint", ["ruff", "check", "src", "tests", "scripts"])
runner.run("mypy --strict", ["mypy", "--strict"])
_coverage_gate(runner)
_evaluation_coverage_gate(runner)
runner.run(
"parity suite (release-blocking)",
[sys.executable, "-m", "pytest", "-p", "no:cacheprovider", "tests/parity", "-x"],
)
if not args.skip_build:
runner.run("uv build (wheel + sdist)", ["uv", "build"])
print("\n" + "=" * 60)
print("Release gate summary")
print("=" * 60)
all_passed = True
for result in runner.results:
status = "PASS" if result.passed else "FAIL"
suffix = f" ({result.detail})" if result.detail else ""
print(f" [{status}] {result.name}{suffix}")
all_passed = all_passed and result.passed
if all_passed:
print("\nAll release gates passed.")
return 0
print("\nOne or more release gates FAILED.")
return 1
if __name__ == "__main__":
raise SystemExit(main())