mirror of
https://github.com/usestrix/strix.git
synced 2026-09-24 00:51:20 +00:00
191 lines
6.7 KiB
Python
191 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
|