Files
ykcheungab 9c951e9d3c fix: 将 ARM64 wheel 纳入发布校验和上传流程
补齐全部架构的运行证据、GitHub 发布说明和 PyPI 逐文件下载核验。已通过 161 项测试、代码和文档门禁、九个 wheel 的本地索引下载核验,以及 ARM 三个 ABI 的 40 帧 CPU 检查。
2026-10-15 21:39:31 +08:00

170 lines
6.1 KiB
Python

"""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")
runtime_records = {}
for step in payload.get("steps") or []:
if step.get("name") == "wheel-runtime" and step.get("ok"):
runtime_records.update({item.get("wheel"): item.get("sha256")
for item in (step.get("data") or {}).get("records", [])})
uncovered = sorted(name for name, digest in recorded.items()
if runtime_records.get(name) != digest)
if uncovered:
problems.append(f"wheels lack matching runtime verification: {uncovered}; "
"re-run preflight --all-wheel-pythons")
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,
)