fix: 将 ARM64 wheel 纳入发布校验和上传流程

补齐全部架构的运行证据、GitHub 发布说明和 PyPI 逐文件下载核验。已通过 161 项测试、代码和文档门禁、九个 wheel 的本地索引下载核验,以及 ARM 三个 ABI 的 40 帧 CPU 检查。
This commit is contained in:
ykcheungab
2026-10-15 21:39:31 +08:00
parent bb15b750fb
commit 9c951e9d3c
10 changed files with 496 additions and 47 deletions
+110 -2
View File
@@ -41,6 +41,9 @@ def _report_payload(version: str, wheels: list[Path], **overrides) -> dict:
{"name": wheel.name, "sha256": publish.sha256_file(wheel), "size": wheel.stat().st_size}
for wheel in wheels
],
"steps": [{"name": "wheel-runtime", "ok": True, "data": {
"records": [{"wheel": wheel.name, "sha256": publish.sha256_file(wheel)} for wheel in wheels]
}}],
}
payload.update(overrides)
return payload
@@ -89,6 +92,21 @@ def test_gate_rejects_changed_wheel(tmp_path):
assert any("changed after preflight" in problem for problem in problems)
def test_gate_rejects_arm_wheel_without_runtime_evidence(tmp_path):
wheel = _wheel(tmp_path, "orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl")
payload = _report_payload("1.2.3", [wheel], steps=[])
problems, _ = publish.evaluate_gate(payload, "1.2.3", {wheel.name: wheel})
assert any("runtime" in problem and wheel.name in problem for problem in problems)
def test_gate_rejects_runtime_evidence_for_different_bytes(tmp_path):
wheel = _wheel(tmp_path, "orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl")
payload = _report_payload("1.2.3", [wheel])
payload["steps"][0]["data"]["records"][0]["sha256"] = "stale"
problems, _ = publish.evaluate_gate(payload, "1.2.3", {wheel.name: wheel})
assert any("runtime" in problem for problem in problems)
def test_gate_rejects_uncertified_and_missing_wheels(tmp_path):
wheels = [_wheel(tmp_path, "orisys-1.2.3-cp310-cp310-win_amd64.whl")]
payload = _report_payload("1.2.3", wheels)
@@ -186,9 +204,12 @@ def test_github_steps_include_notes_file(tmp_path):
def test_release_notes_list_every_wheel():
notes = github.build_release_notes(
"1.2.3", [Path("a.whl"), Path("b.whl")], "c" * 40
"1.2.3", [Path("orisys-1.2.3-cp311-cp311-win_amd64.whl"),
Path("orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl")], "c" * 40
)
assert "a.whl" in notes and "b.whl" in notes
assert "win_amd64.whl" in notes and "aarch64.whl" in notes
assert "Linux ARM64" in notes
assert "Linux x86_64" not in notes
assert "cccccccccccc" in notes
@@ -622,3 +643,90 @@ def test_publish_aborts_on_a_conflicting_existing_release(tmp_path, monkeypatch,
def test_index_json_urls_cover_both_indexes():
assert pypi.INDEX_JSON["testpypi"].startswith("https://test.pypi.org/")
assert pypi.INDEX_JSON["pypi"].startswith("https://pypi.org/")
@pytest.mark.parametrize("abi", ["cp310", "cp311", "cp312"])
@pytest.mark.parametrize("platform", ["win_amd64", "manylinux_2_28_x86_64", "manylinux_2_28_aarch64"])
def test_fetch_back_targets_every_abi_and_platform(tmp_path, monkeypatch, abi, platform):
wheel = _wheel(tmp_path, f"orisys-1.2.3-{abi}-{abi}-{platform}.whl")
monkeypatch.setenv("PIP_EXTRA_INDEX_URL", "https://fallback.invalid/simple")
def run(argv, **kwargs):
assert "PIP_EXTRA_INDEX_URL" not in kwargs["env"]
assert kwargs["env"]["PIP_CONFIG_FILE"] == pypi.os.devnull
assert argv[argv.index("--python-version") + 1] == abi[2:]
assert argv[argv.index("--abi") + 1] == abi
assert argv[argv.index("--platform") + 1] == platform
assert argv[argv.index("--index-url") + 1] == pypi.INDEX_SIMPLE["testpypi"]
assert "--extra-index-url" not in argv
target = Path(argv[argv.index("--dest") + 1])
target.mkdir()
(target / wheel.name).write_bytes(wheel.read_bytes())
return subprocess.CompletedProcess(argv, 0)
monkeypatch.setattr(pypi.subprocess, "run", run)
assert pypi._fetch_back("testpypi", "1.2.3", wheel) == 0
def test_fetch_back_rejects_modified_download(tmp_path, monkeypatch):
wheel = _wheel(tmp_path, "orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl")
def run(argv, **kwargs):
target = Path(argv[argv.index("--dest") + 1])
target.mkdir()
(target / wheel.name).write_bytes(b"tampered")
return subprocess.CompletedProcess(argv, 0)
monkeypatch.setattr(pypi.subprocess, "run", run)
with pytest.raises(publish.PublishGateError, match="different bytes"):
pypi._fetch_back("pypi", "1.2.3", wheel)
def test_pypi_upload_and_fetch_include_arm_wheels(tmp_path, monkeypatch):
sdk, original = _synthetic_report_layout(tmp_path, "1.2.3")
wheels = [original, _wheel(original.parent, "orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl")]
report = sdk / "build/release/reports/1.2.3/preflight.json"
report.write_text(json.dumps(_report_payload("1.2.3", wheels)), encoding="utf-8")
monkeypatch.setattr(pypi, "REPO_ROOT", sdk)
monkeypatch.setattr(pypi, "_ensure_credentials", lambda *a, **k: True)
monkeypatch.setattr(pypi, "index_files", lambda *a, **k: (None, None))
monkeypatch.setattr(pypi, "index_state", lambda *a, **k: pypi.IndexState(versions=[]))
monkeypatch.setattr(pypi, "wait_for_release", lambda *a, **k: ([w.name for w in wheels], None))
uploaded, fetched = [], []
monkeypatch.setattr(pypi, "_upload", lambda files, index: uploaded.extend(files) or 0)
monkeypatch.setattr(pypi, "_fetch_back", lambda index, version, wheel: fetched.append(wheel) or 0)
assert pypi.publish(pypi.Options(version="1.2.3", apply=True, pypi_only=True, skip_check=True)) == 0
assert set(uploaded) == set(fetched) == set(wheels)
def test_github_dry_run_includes_arm_asset_and_generated_notes(tmp_path, monkeypatch, capsys):
sdk, release, original = _synthetic_publish_layout(tmp_path, "1.2.3", tag_exists=False)
wheels = [original, _wheel(original.parent, "orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl")]
report = sdk / "build/release/reports/1.2.3/preflight.json"
report.write_text(json.dumps(_report_payload("1.2.3", wheels)), encoding="utf-8")
monkeypatch.setattr(github, "REPO_ROOT", sdk)
monkeypatch.setattr(github, "MANIFEST_PATH", sdk / "tools/downstream_sync_manifest.json")
monkeypatch.setattr(github, "_check_gh", lambda repo: [])
monkeypatch.setattr(github, "_release_exists", lambda repo, tag: False)
assert github.publish(github.Options(version="1.2.3", skip_fetch=True)) == 0
output = capsys.readouterr().out
assert wheels[1].name in output
assert "Linux ARM64" in output
assert "--notes-file" in output
assert not (release / "release").exists()
assert not (report.parent / "github-release-notes.md").exists()
def test_inconclusive_testpypi_download_stops_before_production(tmp_path, monkeypatch, capsys):
sdk, wheel = _synthetic_report_layout(tmp_path, "1.2.3")
monkeypatch.setattr(pypi, "REPO_ROOT", sdk)
monkeypatch.setattr(pypi, "_ensure_credentials", lambda *a, **k: True)
monkeypatch.setattr(pypi, "index_files", lambda *a, **k: (None, None))
monkeypatch.setattr(pypi, "index_state", lambda *a, **k: pypi.IndexState(versions=[]))
monkeypatch.setattr(pypi, "wait_for_release", lambda *a, **k: ([wheel.name], None))
uploaded = []
monkeypatch.setattr(pypi, "_upload", lambda files, index: uploaded.append(index) or 0)
monkeypatch.setattr(pypi, "_fetch_back", lambda *a: 1)
assert pypi.publish(pypi.Options(version="1.2.3", apply=True, confirm_pypi=True, skip_check=True)) == 0
assert uploaded == ["testpypi"]
assert "later indexes were not uploaded" in capsys.readouterr().out
+148
View File
@@ -0,0 +1,148 @@
"""Release verification must cover each staged architecture and Python ABI."""
import json
import subprocess
from pathlib import Path
from types import SimpleNamespace
import pytest
from release import preflight, runtime
def test_all_wheel_pythons_selects_all_nine_wheels(tmp_path, monkeypatch):
check = preflight.Preflight(
preflight.PreflightOptions(version="1.2.3", all_wheel_pythons=True),
repo_root=tmp_path,
)
check.wheels = [
tmp_path / f"orisys-1.2.3-{abi}-{abi}-{platform}.whl"
for abi in ("cp310", "cp311", "cp312")
for platform in ("win_amd64", "manylinux_2_28_x86_64", "manylinux_2_28_aarch64")
]
monkeypatch.setattr(preflight.sys, "platform", "win32")
monkeypatch.setattr(
preflight.version_mod, "python_for_tag", lambda tag: Path(f"{tag}/python.exe")
)
assert {wheel for _, wheel, _ in check._runtime_targets()} == set(check.wheels)
@pytest.mark.parametrize(
"architecture,platform,image",
[
("x86_64", "linux/amd64", "orisys-manylinux"),
("aarch64", "linux/arm64", "orisys-manylinux-aarch64"),
],
)
def test_manylinux_verifier_uses_matching_container(
tmp_path, monkeypatch, architecture, platform, image
):
wheel = tmp_path / f"orisys-1.2.3-cp311-cp311-manylinux_2_28_{architecture}.whl"
wheel.touch()
video = tmp_path / "sample.mp4"
video.touch()
record = {
"wheel": wheel.name,
"assets": "OK",
"imports": "OK",
"frames": 40,
"flow_shape": [1, 2],
}
calls = []
def run(argv, **kwargs):
calls.append(argv)
return subprocess.CompletedProcess(
argv, 0, "progress\nORISYS_RUNTIME_RESULT=" + json.dumps(record), ""
)
monkeypatch.setattr(runtime.subprocess, "run", run)
assert (
runtime.verify_manylinux_wheel(
wheel, repo_root=tmp_path, video=video, config_name="box", frames=40
)
== record
)
argv = calls[0]
assert argv[argv.index("--platform") + 1] == platform
assert image in argv
assert "/opt/python/cp311-cp311/bin/python" in argv
assert any(
str(tmp_path.resolve()) in arg and arg.endswith(":/src:ro") for arg in argv
)
def test_manylinux_verifier_propagates_container_failure(tmp_path, monkeypatch):
monkeypatch.setattr(
runtime.subprocess,
"run",
lambda argv, **kwargs: subprocess.CompletedProcess(
argv, 1, "", "ARM import failed"
),
)
with pytest.raises(RuntimeError, match="ARM import failed"):
runtime.verify_manylinux_wheel(
tmp_path / "orisys-1.2.3-cp312-cp312-manylinux_2_28_aarch64.whl",
repo_root=tmp_path,
video=tmp_path / "sample.mp4",
config_name="box",
frames=40,
)
def test_runtime_step_records_hashes_for_windows_and_arm(tmp_path, monkeypatch):
video = tmp_path / "sample.mp4"
video.touch()
check = preflight.Preflight(
preflight.PreflightOptions(
version="1.2.3", all_wheel_pythons=True, video=video
),
repo_root=tmp_path,
)
check.wheels = [
tmp_path / f"orisys-1.2.3-cp311-cp311-{target}.whl"
for target in ("win_amd64", "manylinux_2_28_aarch64")
]
for wheel in check.wheels:
wheel.write_bytes(b"wheel")
monkeypatch.setattr(preflight.sys, "platform", "win32")
monkeypatch.setattr(
preflight.version_mod, "python_for_tag", lambda tag: Path("python.exe")
)
seen = []
def verify(wheel, **kwargs):
seen.append(wheel)
return {"wheel": wheel.name, "assets": "OK", "frames": 40, "flow_shape": [1, 2]}
monkeypatch.setattr(preflight, "verify_wheel", verify)
monkeypatch.setattr(preflight, "verify_manylinux_wheel", verify)
step = check._wheel_runtime()
assert step.ok, step.detail
assert set(seen) == set(check.wheels)
assert all(
record["sha256"] == preflight._sha256_file(check.wheels[0])
for record in step.data["records"]
)
@pytest.mark.parametrize(
"name",
[
"orisys-1.2.3-cp311-cp311-manylinux_2_28_aarch64.whl",
"orisys-1.2.3-cp311-cp311-manylinux_2_28_x86_64.whl",
],
)
def test_native_linux_selection_matches_machine(tmp_path, monkeypatch, name):
check = preflight.Preflight(
preflight.PreflightOptions(version="1.2.3"), repo_root=tmp_path
)
check.wheels = [tmp_path / name]
monkeypatch.setattr(preflight.sys, "platform", "linux")
monkeypatch.setattr(preflight.sys, "version_info", SimpleNamespace(major=3, minor=11))
monkeypatch.setattr(preflight.platform, "machine", lambda: "aarch64")
if "aarch64" in name:
assert check._runtime_targets()[0][1].name == name
else:
with pytest.raises(RuntimeError, match="no cp311 aarch64 wheel"):
check._runtime_targets()