mirror of
https://github.com/usestrix/strix.git
synced 2026-09-24 00:51:20 +00:00
test(ci): lock release checksum and attestation workflow
Assert the build-release job signs artifacts, publishes bundles, and generates SHA256SUMS without hashing provenance (#1267).
This commit is contained in:
parent
a079b6c418
commit
6a3ad2ae3e
1 changed files with 184 additions and 0 deletions
184
tests/test_build_release_workflow.py
Normal file
184
tests/test_build_release_workflow.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue