strix/tests/test_build_release_workflow.py
mohammad junayd 0d2407e71a docs: add manual release checksum and provenance checks
Document verify steps in quickstart and prepend them to generated
release notes (#1267).
2026-09-14 17:51:44 +04:00

195 lines
6.7 KiB
Python

"""Structural checks for release signing + checksum publishing (#1267)."""
from __future__ import annotations
import hashlib
import re
import subprocess
from pathlib import Path
import pytest
import yaml
WORKFLOW = (
Path(__file__).resolve().parent.parent / ".github" / "workflows" / "build-release.yml"
)
ATTEST_PIN = "actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6"
def _workflow() -> dict[str, object]:
return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
def _job(name: str) -> dict[str, object]:
jobs = _workflow()["jobs"]
assert isinstance(jobs, dict)
job = jobs[name]
assert isinstance(job, dict)
return job
def _steps(job_name: str) -> list[dict[str, object]]:
steps = _job(job_name)["steps"]
assert isinstance(steps, list)
return [step for step in steps if isinstance(step, dict)]
def _step_named(job_name: str, name: str) -> dict[str, object]:
for step in _steps(job_name):
if step.get("name") == name:
return step
raise AssertionError(f"missing step {name!r} in job {job_name!r}")
def test_build_job_requests_attestation_permissions() -> None:
permissions = _job("build")["permissions"]
assert permissions == {
"contents": "read",
"id-token": "write",
"attestations": "write",
}
def test_release_job_requests_attestation_permissions() -> None:
permissions = _job("release")["permissions"]
assert permissions == {
"contents": "write",
"id-token": "write",
"attestations": "write",
}
def test_build_attests_unix_and_windows_archives_with_pinned_action() -> None:
unix = _step_named("build", "Attest Unix archive and wheel")
windows = _step_named("build", "Attest Windows archive and wheel")
assert unix["uses"] == ATTEST_PIN
assert unix["id"] == "attest-unix"
assert unix["if"] == "runner.os != 'Windows'"
assert "dist/release/*.tar.gz" in str(unix["with"]["subject-path"])
assert "dist/*.whl" in str(unix["with"]["subject-path"])
assert windows["uses"] == ATTEST_PIN
assert windows["id"] == "attest-windows"
assert windows["if"] == "runner.os == 'Windows'"
assert "dist/release/*.zip" in str(windows["with"]["subject-path"])
assert "dist/*.whl" in str(windows["with"]["subject-path"])
def test_build_publishes_and_uploads_per_target_attestation_bundle() -> None:
publish = _step_named("build", "Publish attestation bundle")
assert "strix-${{ matrix.target }}.intoto.jsonl" in str(publish["run"])
assert "attest-unix.outputs.bundle-path" in str(publish["env"]["BUNDLE_PATH"])
assert "attest-windows.outputs.bundle-path" in str(publish["env"]["BUNDLE_PATH"])
upload = next(step for step in _steps("build") if "upload-artifact@" in str(step.get("uses", "")))
paths = str(upload["with"]["path"])
assert "dist/release/*.intoto.jsonl" in paths
assert "dist/release/*.tar.gz" in paths
assert "dist/*.whl" in paths
def test_release_generates_sha256sums_excluding_provenance_bundles() -> None:
generate = _step_named("release", "Generate SHA256SUMS")
assert generate["working-directory"] == "release"
script = str(generate["run"])
assert "SHA256SUMS" in script
assert "! -name '*.intoto.jsonl'" in script
assert "sha256sum" in script
def test_release_attests_and_publishes_sha256sums_bundle() -> None:
attest = _step_named("release", "Attest SHA256SUMS")
assert attest["uses"] == ATTEST_PIN
assert attest["id"] == "attest-sums"
assert attest["with"]["subject-path"] == "release/SHA256SUMS"
publish = _step_named("release", "Publish SHA256SUMS attestation bundle")
assert "SHA256SUMS.intoto.jsonl" in str(publish["run"])
assert "attest-sums.outputs.bundle-path" in str(publish["env"]["BUNDLE_PATH"])
create = _step_named("release", "Create Release")
assert create["with"]["files"] == "release/**"
def test_every_action_use_is_sha_pinned() -> None:
text = WORKFLOW.read_text(encoding="utf-8")
uses = re.findall(r"^\s+uses:\s+(\S+)", text, flags=re.MULTILINE)
assert uses
for ref in uses:
assert "@" in ref, ref
digest = ref.rsplit("@", 1)[1]
assert re.fullmatch(r"[0-9a-f]{40}", digest), ref
def test_sha256sums_script_hashes_product_files_only(tmp_path: Path) -> None:
"""Run the release job's checksum recipe against a fixture tree."""
release_dir = tmp_path / "release"
release_dir.mkdir()
(release_dir / "strix-1.0.0-linux-x86_64.tar.gz").write_bytes(b"archive-bytes")
(release_dir / "strix_agent-1.0.0-py3-none-any.whl").write_bytes(b"wheel-bytes")
(release_dir / "strix-linux-x86_64.intoto.jsonl").write_text(
'{"attestation":true}\n',
encoding="utf-8",
)
script = str(_step_named("release", "Generate SHA256SUMS")["run"])
result = subprocess.run( # noqa: S603
["/bin/bash", "-c", script],
cwd=release_dir,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
sums_path = release_dir / "SHA256SUMS"
assert sums_path.is_file()
lines = [
line
for line in sums_path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
names = {line.split()[-1] for line in lines}
assert names == {
"strix-1.0.0-linux-x86_64.tar.gz",
"strix_agent-1.0.0-py3-none-any.whl",
}
assert "strix-linux-x86_64.intoto.jsonl" not in names
assert "SHA256SUMS" not in names
for line in lines:
digest, name = line.split()
expected = hashlib.sha256((release_dir / name).read_bytes()).hexdigest()
assert digest == expected
def test_release_notes_include_manual_verification() -> None:
create = _step_named("release", "Create Release")
body = str(create["with"]["body"])
assert "## Verify this release" in body
assert "sha256sum -c --ignore-missing SHA256SUMS" in body
assert "gh attestation verify" in body
assert "usestrix/strix/.github/workflows/build-release.yml" in body
assert "docs.strix.ai/quickstart#verify-a-downloaded-release" in body
assert create["with"]["generate_release_notes"] is True
@pytest.mark.parametrize(
("job_name", "step_name"),
[
("build", "Attest Unix archive and wheel"),
("build", "Attest Windows archive and wheel"),
("release", "Attest SHA256SUMS"),
],
)
def test_attest_steps_comment_mentions_pinned_version(
job_name: str,
step_name: str,
) -> None:
# Keep the SHA pin and the human version comment in sync with checkout/etc.
text = WORKFLOW.read_text(encoding="utf-8")
assert "actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2" in text
step = _step_named(job_name, step_name)
assert step["uses"] == ATTEST_PIN