|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Exercise the public release artifact identity and checksum contract.""" |
| 3 | + |
| 4 | +from __future__ import annotations |
| 5 | + |
| 6 | +import importlib.util |
| 7 | +import gzip |
| 8 | +import hashlib |
| 9 | +import io |
| 10 | +import subprocess |
| 11 | +import sys |
| 12 | +import tarfile |
| 13 | +import tempfile |
| 14 | +from pathlib import Path |
| 15 | + |
| 16 | + |
| 17 | +ROOT = Path(__file__).resolve().parents[1] |
| 18 | +SCRIPT = ROOT / "scripts" / "release_artifacts.py" |
| 19 | +SPEC = importlib.util.spec_from_file_location("release_artifacts", SCRIPT) |
| 20 | +assert SPEC is not None and SPEC.loader is not None |
| 21 | +release_artifacts = importlib.util.module_from_spec(SPEC) |
| 22 | +sys.modules[SPEC.name] = release_artifacts |
| 23 | +SPEC.loader.exec_module(release_artifacts) |
| 24 | + |
| 25 | + |
| 26 | +def run(*args: str) -> subprocess.CompletedProcess[str]: |
| 27 | + return subprocess.run( |
| 28 | + [sys.executable, str(SCRIPT), "--project-root", str(ROOT), *args], |
| 29 | + check=False, |
| 30 | + capture_output=True, |
| 31 | + text=True, |
| 32 | + ) |
| 33 | + |
| 34 | + |
| 35 | +def write_sdist(path: Path, *, mtime: int) -> None: |
| 36 | + with path.open("wb") as raw_stream: |
| 37 | + with gzip.GzipFile(filename="", mode="wb", fileobj=raw_stream, mtime=mtime) as gzip_stream: |
| 38 | + with tarfile.open(fileobj=gzip_stream, mode="w", format=tarfile.PAX_FORMAT) as archive: |
| 39 | + payload = b"fixture package metadata\n" |
| 40 | + member = tarfile.TarInfo("loopx-fixture/PKG-INFO") |
| 41 | + member.size = len(payload) |
| 42 | + member.mtime = mtime |
| 43 | + archive.addfile(member, io.BytesIO(payload)) |
| 44 | + |
| 45 | + |
| 46 | +def digest(path: Path) -> str: |
| 47 | + return hashlib.sha256(path.read_bytes()).hexdigest() |
| 48 | + |
| 49 | + |
| 50 | +def main() -> int: |
| 51 | + identity = release_artifacts.load_identity(ROOT) |
| 52 | + assert identity.name == "loopx", identity |
| 53 | + assert run("expected-tag").stdout.strip() == identity.tag |
| 54 | + assert run("validate-tag", identity.tag).returncode == 0 |
| 55 | + |
| 56 | + invalid_tag = run("validate-tag", "v999.0.0") |
| 57 | + assert invalid_tag.returncode == 2, invalid_tag |
| 58 | + assert "does not match package tag" in invalid_tag.stderr, invalid_tag.stderr |
| 59 | + |
| 60 | + with tempfile.TemporaryDirectory(prefix="loopx-release-artifacts-") as temporary: |
| 61 | + root = Path(temporary) |
| 62 | + dist_dir = root / "packages" |
| 63 | + dist_dir.mkdir() |
| 64 | + wheel = dist_dir / f"loopx-{identity.version}-py3-none-any.whl" |
| 65 | + sdist = dist_dir / f"loopx-{identity.version}.tar.gz" |
| 66 | + wheel.write_bytes(b"wheel fixture\n") |
| 67 | + write_sdist(sdist, mtime=1_700_000_001) |
| 68 | + checksums = root / "SHA256SUMS" |
| 69 | + |
| 70 | + normalized = run( |
| 71 | + "normalize-sdist", |
| 72 | + "--dist-dir", |
| 73 | + str(dist_dir), |
| 74 | + "--source-date-epoch", |
| 75 | + "1700000000", |
| 76 | + ) |
| 77 | + assert normalized.returncode == 0, normalized |
| 78 | + normalized_digest = digest(sdist) |
| 79 | + write_sdist(sdist, mtime=1_700_000_099) |
| 80 | + normalized_again = run( |
| 81 | + "normalize-sdist", |
| 82 | + "--dist-dir", |
| 83 | + str(dist_dir), |
| 84 | + "--source-date-epoch", |
| 85 | + "1700000000", |
| 86 | + ) |
| 87 | + assert normalized_again.returncode == 0, normalized_again |
| 88 | + assert digest(sdist) == normalized_digest |
| 89 | + |
| 90 | + written = run( |
| 91 | + "write-checksums", |
| 92 | + "--dist-dir", |
| 93 | + str(dist_dir), |
| 94 | + "--output", |
| 95 | + str(checksums), |
| 96 | + ) |
| 97 | + assert written.returncode == 0, written |
| 98 | + manifest = checksums.read_text(encoding="ascii") |
| 99 | + lines = manifest.splitlines() |
| 100 | + assert len(lines) == 2, lines |
| 101 | + assert lines == sorted(lines, key=lambda line: line.split(" ", 1)[1]), lines |
| 102 | + assert all(len(line.split(" ", 1)[0]) == 64 for line in lines), lines |
| 103 | + |
| 104 | + verified = run( |
| 105 | + "verify-checksums", |
| 106 | + "--dist-dir", |
| 107 | + str(dist_dir), |
| 108 | + "--checksum-file", |
| 109 | + str(checksums), |
| 110 | + ) |
| 111 | + assert verified.returncode == 0, verified |
| 112 | + |
| 113 | + wheel.write_bytes(b"tampered wheel\n") |
| 114 | + tampered = run( |
| 115 | + "verify-checksums", |
| 116 | + "--dist-dir", |
| 117 | + str(dist_dir), |
| 118 | + "--checksum-file", |
| 119 | + str(checksums), |
| 120 | + ) |
| 121 | + assert tampered.returncode == 2, tampered |
| 122 | + assert "does not match release distributions" in tampered.stderr, tampered.stderr |
| 123 | + |
| 124 | + extra = dist_dir / "unexpected.txt" |
| 125 | + extra.write_text("not a release asset\n", encoding="utf-8") |
| 126 | + rejected = run( |
| 127 | + "write-checksums", |
| 128 | + "--dist-dir", |
| 129 | + str(dist_dir), |
| 130 | + "--output", |
| 131 | + str(checksums), |
| 132 | + ) |
| 133 | + assert rejected.returncode == 2, rejected |
| 134 | + assert "unexpected files" in rejected.stderr, rejected.stderr |
| 135 | + |
| 136 | + workflow = (ROOT / ".github" / "workflows" / "release-artifacts.yml").read_text( |
| 137 | + encoding="utf-8" |
| 138 | + ) |
| 139 | + required_contract = ( |
| 140 | + "release:\n types: [published]", |
| 141 | + "python scripts/release_artifacts.py validate-tag", |
| 142 | + "python scripts/release_artifacts.py normalize-sdist", |
| 143 | + "python scripts/release_artifacts.py write-checksums", |
| 144 | + "python scripts/release_artifacts.py verify-checksums", |
| 145 | + "uses: actions/attest@v4", |
| 146 | + "gh release upload", |
| 147 | + "vars.PYPI_PUBLISH_ENABLED == 'true'", |
| 148 | + "environment:\n name: pypi", |
| 149 | + "pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33", |
| 150 | + ) |
| 151 | + for text in required_contract: |
| 152 | + assert text in workflow, text |
| 153 | + assert "password:" not in workflow |
| 154 | + assert "--clobber" not in workflow |
| 155 | + |
| 156 | + print("release-artifacts-smoke ok") |
| 157 | + return 0 |
| 158 | + |
| 159 | + |
| 160 | +if __name__ == "__main__": |
| 161 | + raise SystemExit(main()) |
0 commit comments