From 82386766fdf34dd2812892e807809a80f7040c73 Mon Sep 17 00:00:00 2001 From: mertcan Date: Tue, 15 Sep 2026 19:09:18 +0300 Subject: [PATCH] PRD-25: release evidence that opens Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 56 +++++++++++++++++++++++------ README.md | 1 + SECURITY.md | 4 +++ docs/compatibility.md | 1 + docs/releases/0.3.2.md | 32 +++++++++++++++++ tests/contracts/test_workflows.py | 60 +++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ec79f7..6f430cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -132,11 +132,14 @@ jobs: cp /tmp/shim-cli-evidence-source/tests/corpus/guard-tools-v1.json dist/guard-tools-v1.json - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: - path: dist/packages + path: /tmp/shim-cli-benchmark format: cyclonedx-json output-file: dist/shim-cli.sbom.cdx.json upload-artifact: false upload-release-assets: false + - name: Require the SBOM to list the package and every locked runtime dependency + run: | + python -c 'import json, os, re; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); found = {(re.sub(r"[-_.]+", "-", component["name"]).lower(), component.get("version")) for component in json.loads(Path("dist/shim-cli.sbom.cdx.json").read_text())["components"] if component["type"] == "library"}; locked = {(re.sub(r"[-_.]+", "-", name).lower(), pin) for name, pin in re.findall(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==(\S+) \\$", Path("dist/requirements.lock").read_text(), re.MULTILINE)}; assert ("shim", version) in found; assert locked and locked <= found, sorted(locked - found)' - name: Recheck the package artifact allowlist run: | python -c 'import os; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); expected = {f"shim-{version}-py3-none-any.whl", f"shim-{version}.tar.gz"}; assert {path.name for path in Path("dist/packages").iterdir()} == expected' @@ -150,12 +153,6 @@ jobs: cd .. sha256sum COMPATIBILITY.md shim.pyz guard-v2.json guard-v2-metrics.json guard-tools-v1.json benchmark-hook.json requirements.lock shim-cli.sbom.cdx.json ) > dist/SHA256SUMS - - name: Verify checksums from a flat download directory - run: | - mkdir dist/download - cp "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/shim-cli.sbom.cdx.json dist/SHA256SUMS dist/download/ - cd dist/download - sha256sum -c SHA256SUMS - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-dist @@ -187,18 +184,48 @@ jobs: - name: Verify the package artifact allowlist run: python -c 'import os; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); expected = {f"shim-{version}-py3-none-any.whl", f"shim-{version}.tar.gz"}; assert {path.name for path in Path("dist/packages").iterdir()} == expected' - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + id: provenance with: - subject-path: dist/packages/* + subject-path: | + dist/packages/* + dist/shim.pyz - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + id: sbom with: - subject-path: dist/packages/* + subject-path: | + dist/packages/* + dist/shim.pyz sbom-path: dist/shim-cli.sbom.cdx.json + - name: Attach and hash the attestation bundles + env: + PROVENANCE_BUNDLE: ${{ steps.provenance.outputs.bundle-path }} + SBOM_BUNDLE: ${{ steps.sbom.outputs.bundle-path }} + run: | + cp "$PROVENANCE_BUNDLE" "dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl" + cp "$SBOM_BUNDLE" "dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" + cp "$PROVENANCE_BUNDLE" dist/shim.pyz.sigstore.json + ( + cd dist + sha256sum "shim-${GITHUB_REF_NAME#v}.intoto.jsonl" "shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" shim.pyz.sigstore.json + ) >> dist/SHA256SUMS + - name: Verify checksums from a flat download directory + run: | + mkdir /tmp/shim-cli-download + cp "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/shim-cli.sbom.cdx.json "dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl" "dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" dist/shim.pyz.sigstore.json dist/SHA256SUMS /tmp/shim-cli-download/ + cd /tmp/shim-cli-download + sha256sum -c SHA256SUMS + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: attested-dist + path: dist/ + if-no-files-found: error release: needs: [build, attest] runs-on: ubuntu-latest environment: release permissions: + attestations: read contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 @@ -211,13 +238,20 @@ jobs: test -f "docs/releases/${GITHUB_REF_NAME#v}.md" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: release-dist + name: attested-dist path: dist - name: Verify the package artifact allowlist run: python -c 'import os; from pathlib import Path; version = os.environ["GITHUB_REF_NAME"].removeprefix("v"); expected = {f"shim-{version}-py3-none-any.whl", f"shim-{version}.tar.gz"}; assert {path.name for path in Path("dist/packages").iterdir()} == expected' + - name: Verify the attestations before publishing + env: + GH_TOKEN: ${{ github.token }} + run: | + gh attestation verify "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" --repo "$GITHUB_REPOSITORY" + gh attestation verify "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" --repo "$GITHUB_REPOSITORY" + gh attestation verify dist/shim.pyz --repo "$GITHUB_REPOSITORY" - env: GH_TOKEN: ${{ github.token }} - run: gh release create "$GITHUB_REF_NAME" "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/SHA256SUMS dist/shim-cli.sbom.cdx.json --notes-file "docs/releases/${GITHUB_REF_NAME#v}.md" --repo "$GITHUB_REPOSITORY" + run: gh release create "$GITHUB_REF_NAME" "dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl" "dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz" dist/COMPATIBILITY.md dist/shim.pyz dist/guard-v2.json dist/guard-v2-metrics.json dist/guard-tools-v1.json dist/benchmark-hook.json dist/requirements.lock dist/SHA256SUMS dist/shim-cli.sbom.cdx.json "dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl" "dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json" dist/shim.pyz.sigstore.json --notes-file "docs/releases/${GITHUB_REF_NAME#v}.md" --repo "$GITHUB_REPOSITORY" publish: needs: [build, attest, release] diff --git a/README.md b/README.md index 2c0259c..a24508a 100644 --- a/README.md +++ b/README.md @@ -469,6 +469,7 @@ Every figure here was measured on the released build, not estimated. | Hook cost | **67 ms** median end to end, interpreter start included; **41 ms** for a session summary | | With 32 custom patterns | **+0.8 ms** median against the same prompt with none | | Detector corpus | **570 cases**, graded on exact redacted output rather than category presence | +| Release evidence | SBOM, provenance and Sigstore bundles on the release page from 0.3.2, with the `gh attestation verify` command in [the compatibility record](https://github.com/GetSHIM/shim-cli/blob/main/docs/compatibility.md#030-release-evidence) |

None: assert text.count("persist-credentials: false") == checkouts, ( f"{workflow.name}: a checkout keeps its credentials" ) + + +RELEASE = WORKFLOWS / "release.yml" + + +def _step(marker: str) -> str: + text = RELEASE.read_text(encoding="utf-8") + start = text.rindex("\n - ", 0, text.index(marker)) + end = text.find("\n - ", start + 1) + return text[start:end] + + +def test_the_sbom_scans_the_installed_wheel_and_is_checked() -> None: + text = RELEASE.read_text(encoding="utf-8") + sbom = _step("uses: anchore/sbom-action@") + check = _step("name: Require the SBOM to list") + + assert "path: /tmp/shim-cli-benchmark" in sbom + assert "dist/packages" not in sbom + assert text.index('--no-deps "dist/packages/shim-') < text.index(sbom) + assert text.index(sbom) + len(sbom) == text.index(check) + assert '["components"]' in check and "dist/requirements.lock" in check + + +def test_the_attestation_bundles_are_hashed_and_attached() -> None: + bundles = ( + '"dist/shim-${GITHUB_REF_NAME#v}.intoto.jsonl"', + '"dist/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl.sigstore.json"', + "dist/shim.pyz.sigstore.json", + ) + hashed = _step("name: Attach and hash the attestation bundles") + flat = _step("name: Verify checksums from a flat download directory") + create = _step("gh release create") + + assert all(bundle in flat and bundle in create for bundle in bundles) + assert ">> dist/SHA256SUMS" in hashed + assert all( + name in hashed + for name in (".intoto.jsonl", ".whl.sigstore.json", " shim.pyz.sigstore.json") + ) + attest = [_step("id: provenance"), _step("id: sbom")] + assert all("dist/packages/*" in step and "dist/shim.pyz" in step for step in attest) + + +def test_the_release_verifies_attestations_before_it_publishes() -> None: + text = RELEASE.read_text(encoding="utf-8") + verify = _step("gh attestation verify") + + assert text.index(verify) < text.index("gh release create") + assert ( + "attestations: read" + in text[text.index("\n release:") : text.index("\n publish:")] + ) + assert verify.count("gh attestation verify ") == 3 + for subject in ( + '"dist/packages/shim-${GITHUB_REF_NAME#v}-py3-none-any.whl"', + '"dist/packages/shim-${GITHUB_REF_NAME#v}.tar.gz"', + "dist/shim.pyz", + ): + assert f'gh attestation verify {subject} --repo "$GITHUB_REPOSITORY"' in verify