"""Shared publish gate: certify a release from its preflight report. Both publish commands (``release github`` and ``release pypi``) refuse to act unless a passing ``release preflight`` report exists for the exact version and every wheel still hashes to what that report recorded. The report is the contract between "we verified this" and "we shipped this"; re-deriving the checks here would just be a second, weaker opinion. """ from __future__ import annotations import hashlib import json from dataclasses import dataclass, field from pathlib import Path # Options that weaken the gate. A release must not be certified by a run that # waived any of them: re-run preflight on a clean commit instead. WAIVED_GUARD_KEYS = ("skip_tests", "allow_dirty", "force", "no_runtime", "no_assets") class PublishGateError(RuntimeError): """The preflight report does not certify these wheels.""" @dataclass class WheelRecord: name: str sha256: str size: int = 0 @dataclass class GateResult: version: str git_commit: str | None report_path: Path wheels: list[WheelRecord] = field(default_factory=list) warnings: list[str] = field(default_factory=list) def sha256_file(path: Path) -> str: digest = hashlib.sha256() with Path(path).open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def evaluate_gate( payload: dict, version: str, wheels: dict[str, Path], *, head_commit: str | None = None, ) -> tuple[list[str], list[str]]: """Return ``(problems, warnings)`` for a report payload and the wheels on disk. Pure function: no I/O beyond hashing the wheels, so it is cheap to test. """ problems: list[str] = [] warnings: list[str] = [] if str(payload.get("version")) != version: problems.append( f"report is for version {payload.get('version')!r}, not {version!r}" ) if not payload.get("ok"): problems.append( "report result is FAIL - run `release preflight --keep-going` and fix " "everything before publishing" ) if payload.get("aborted"): problems.append("report stopped early (aborted); it certifies nothing") options = payload.get("options") or {} waived = sorted(key for key in WAIVED_GUARD_KEYS if options.get(key)) if waived: problems.append( "report waived gate options " + ", ".join(f"--{key.replace('_', '-')}" for key in waived) + "; commit the tree and re-run `release preflight` without them" ) recorded = {item.get("name"): item.get("sha256") for item in payload.get("wheels") or []} if not recorded: problems.append("report lists no wheels") missing = sorted(set(recorded) - set(wheels)) if missing: problems.append(f"wheels in the report are missing from disk: {missing}") extra = sorted(set(wheels) - set(recorded)) if extra: problems.append(f"wheels on disk are not covered by the report: {extra}") for name in sorted(set(recorded) & set(wheels)): actual = sha256_file(wheels[name]) if actual != recorded[name]: problems.append( f"{name} changed after preflight: report {recorded[name][:12]}, " f"disk {actual[:12]} - rebuild and re-run preflight" ) commit = payload.get("git", {}).get("commit") if isinstance(payload.get("git"), dict) else None if head_commit and commit and commit != head_commit: warnings.append( f"report certifies commit {commit[:12]} but HEAD is {head_commit[:12]}; " "the wheels are unchanged, so this is usually just a later commit" ) return problems, warnings def load_gate(repo_root: Path, version: str) -> GateResult: """Load and enforce the preflight report for ``version``. Raises :class:`PublishGateError` with every problem found, so one run reports all of them instead of stopping at the first. """ repo_root = Path(repo_root) report_path = repo_root / "build" / "release" / "reports" / version / "preflight.json" if not report_path.is_file(): raise PublishGateError( f"no preflight report at {report_path}. Run:\n" f" python build/run.py release preflight\n" f"(the publish commands only ship what preflight certified)" ) try: payload = json.loads(report_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise PublishGateError(f"cannot read {report_path}: {exc}") from exc wheel_dir = repo_root / "SDK" / version wheels = {path.name: path for path in sorted(wheel_dir.glob("*.whl"))} if not wheels: raise PublishGateError( f"no wheels in {wheel_dir}; build first (`python build/run.py wheel-win` / " "`manylinux`) and re-run preflight" ) problems, warnings = evaluate_gate(payload, version, wheels) if problems: raise PublishGateError( f"the preflight report at {report_path} does not certify orisys {version}:\n " + "\n ".join(problems) ) git_payload = payload.get("git") if isinstance(payload.get("git"), dict) else {} return GateResult( version=version, git_commit=git_payload.get("commit"), report_path=report_path, wheels=[ WheelRecord(name=name, sha256=sha256_file(path), size=path.stat().st_size) for name, path in wheels.items() ], warnings=warnings, )