158 lines
4.8 KiB
Python
158 lines
4.8 KiB
Python
import hashlib
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PACKAGER = ROOT / "scripts" / "build_release.py"
|
|
VERIFIER = ROOT / "scripts" / "verify_release.py"
|
|
|
|
|
|
def _fixture(root: Path) -> None:
|
|
(root / "src").mkdir(parents=True)
|
|
(root / "frontend").mkdir()
|
|
(root / "src" / "main.py").write_text("print('ready')\n")
|
|
(root / "frontend" / "index.html").write_text("<h1>Stackchain</h1>\n")
|
|
(root / "requirements.txt").write_text("fastapi==1.0\n")
|
|
(root / "README.md").write_text("# Stackchain\n")
|
|
|
|
|
|
def _commit_fixture(source: Path) -> str:
|
|
subprocess.run(["git", "init", "-q", str(source)], check=True)
|
|
subprocess.run(["git", "-C", str(source), "config", "user.name", "Release Test"], check=True)
|
|
subprocess.run(
|
|
["git", "-C", str(source), "config", "user.email", "release-test@example.invalid"],
|
|
check=True,
|
|
)
|
|
subprocess.run(["git", "-C", str(source), "add", "."], check=True)
|
|
subprocess.run(["git", "-C", str(source), "commit", "-qm", "fixture"], check=True)
|
|
return subprocess.run(
|
|
["git", "-C", str(source), "rev-parse", "HEAD"],
|
|
text=True,
|
|
capture_output=True,
|
|
check=True,
|
|
).stdout.strip()
|
|
|
|
|
|
def _build(source: Path, output: Path, commit: str) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(PACKAGER),
|
|
"--root",
|
|
str(source),
|
|
"--output-dir",
|
|
str(output),
|
|
"--commit",
|
|
commit,
|
|
"--source-date-epoch",
|
|
"1720000000",
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
|
|
|
|
def test_release_bundle_is_reproducible_from_declared_commit(tmp_path):
|
|
source = tmp_path / "source"
|
|
_fixture(source)
|
|
commit = _commit_fixture(source)
|
|
|
|
first = _build(source, tmp_path / "first", commit)
|
|
assert first.returncode == 0, first.stderr
|
|
|
|
(source / "src" / "__pycache__").mkdir()
|
|
(source / "src" / "__pycache__" / "main.cpython-311.pyc").write_bytes(b"bytecode")
|
|
(source / "src" / "untracked-secret.txt").write_text("must not ship")
|
|
(source / "src" / "main.py").write_text("print('dirty worktree')\n")
|
|
second = _build(source, tmp_path / "second", commit)
|
|
|
|
assert second.returncode == 0, second.stderr
|
|
first_archive = next((tmp_path / "first").glob("*.tar.gz"))
|
|
second_archive = next((tmp_path / "second").glob("*.tar.gz"))
|
|
assert first_archive.read_bytes() == second_archive.read_bytes()
|
|
|
|
manifest = json.loads(next((tmp_path / "first").glob("*.manifest.json")).read_text())
|
|
checksum = next((tmp_path / "first").glob("*.sha256")).read_text().split()[0]
|
|
assert manifest["commit"] == commit
|
|
assert manifest["artifact"]["sha256"] == checksum
|
|
assert checksum == hashlib.sha256(first_archive.read_bytes()).hexdigest()
|
|
assert sorted(manifest["files"]) == [
|
|
"README.md",
|
|
"frontend/index.html",
|
|
"requirements.txt",
|
|
"src/main.py",
|
|
]
|
|
with tarfile.open(first_archive, "r:gz") as archive:
|
|
names = archive.getnames()
|
|
embedded = json.load(archive.extractfile("release-manifest.json"))
|
|
assert names == [
|
|
"README.md",
|
|
"frontend/index.html",
|
|
"release-manifest.json",
|
|
"requirements.txt",
|
|
"src/main.py",
|
|
]
|
|
assert embedded["commit"] == commit
|
|
assert embedded["files"] == manifest["files"]
|
|
|
|
|
|
def test_release_bundle_rejects_unknown_commit(tmp_path):
|
|
source = tmp_path / "source"
|
|
_fixture(source)
|
|
_commit_fixture(source)
|
|
|
|
built = _build(source, tmp_path / "dist", "a" * 40)
|
|
|
|
assert built.returncode != 0
|
|
assert "declared commit is unavailable" in built.stderr
|
|
|
|
|
|
def test_release_bundle_verifier_enforces_integrity(tmp_path):
|
|
source = tmp_path / "source"
|
|
output = tmp_path / "dist"
|
|
_fixture(source)
|
|
commit = _commit_fixture(source)
|
|
built = _build(source, output, commit)
|
|
assert built.returncode == 0, built.stderr
|
|
|
|
valid = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(VERIFIER),
|
|
"--input-dir",
|
|
str(output),
|
|
"--commit",
|
|
commit,
|
|
"--repository",
|
|
str(source),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert valid.returncode == 0, valid.stderr
|
|
|
|
archive = next(output.glob("*.tar.gz"))
|
|
archive.write_bytes(archive.read_bytes() + b"tampered")
|
|
tampered = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(VERIFIER),
|
|
"--input-dir",
|
|
str(output),
|
|
"--commit",
|
|
commit,
|
|
"--repository",
|
|
str(source),
|
|
],
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
assert tampered.returncode != 0
|