fix: 将 ARM64 wheel 纳入发布校验和上传流程
补齐全部架构的运行证据、GitHub 发布说明和 PyPI 逐文件下载核验。已通过 161 项测试、代码和文档门禁、九个 wheel 的本地索引下载核验,以及 ARM 三个 ABI 的 40 帧 CPU 检查。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user