From 6a3ad2ae3e552a0432862db1c66340ff03d2822b Mon Sep 17 00:00:00 2001 From: mohammad junayd <111487869+m-jay21@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:50:10 +0400 Subject: [PATCH] test(ci): lock release checksum and attestation workflow Assert the build-release job signs artifacts, publishes bundles, and generates SHA256SUMS without hashing provenance (#1267). --- tests/test_build_release_workflow.py | 184 +++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tests/test_build_release_workflow.py diff --git a/tests/test_build_release_workflow.py b/tests/test_build_release_workflow.py new file mode 100644 index 00000000..4ef3ed0d --- /dev/null +++ b/tests/test_build_release_workflow.py @@ -0,0 +1,184 @@ +"""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 + + +@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