diff --git a/.github/workflows/release-crate.yml b/.github/workflows/release-crate.yml index 8364dea2..ef5269f0 100644 --- a/.github/workflows/release-crate.yml +++ b/.github/workflows/release-crate.yml @@ -34,6 +34,10 @@ jobs: name: build + test + publish dry-run runs-on: ubuntu-latest steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (2026-03-27) with: @@ -57,6 +61,10 @@ jobs: contents: read actions: read steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - name: assert ci.yml is green for this commit env: GH_TOKEN: ${{ github.token }} @@ -84,11 +92,51 @@ jobs: permissions: contents: read id-token: write + # Required by actions/attest-build-provenance to write the SLSA + # provenance attestation for the published .crate. + attestations: write steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable (2026-03-27) with: toolchain: stable + # Supply-chain attestation + SBOM, run BEFORE the publish so a failed + # attestation fails the release closed (the .crate is never pushed if we + # can't attest it). crates.io does not host SBOMs, so the CycloneDX SBOM + # is uploaded as a build artifact rather than shipped with the crate; the + # attestation below is GitHub-issued SLSA build provenance bound to the + # exact published .crate. + - name: Package the crate (produces the artifact to attest) + # Emits target/package/ordvec-.crate — the precise byte-for-byte + # tarball that `cargo publish` uploads, so the provenance covers the + # published artifact. + run: cargo package -p ordvec --locked + - name: Generate CycloneDX SBOM for the crate + # cargo-cyclonedx writes ordvec.cdx.json (named .cdx.json) at the + # repo root for the core crate. Verified locally: the command rejects + # `-p`; package scoping is via --manifest-path against the root manifest, + # which is the `ordvec` package. + run: | + cargo install cargo-cyclonedx --version 0.5.9 --locked + cargo cyclonedx --manifest-path Cargo.toml --format json + - name: Attest build provenance for the .crate + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: target/package/*.crate + - name: Upload SBOM as a build artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom-crate + path: ordvec.cdx.json + if-no-files-found: error + # Mint the crates.io OIDC credential LAST — only here, immediately before + # publish, so the short-lived token is never live during the + # `cargo install cargo-cyclonedx` step (third-party code) or the + # attestation/upload steps. Minimises the token's exposure window. - name: Mint a short-lived crates.io credential (OIDC) id: auth uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 diff --git a/.github/workflows/release-python.yml b/.github/workflows/release-python.yml index caf409e8..b9ee77cc 100644 --- a/.github/workflows/release-python.yml +++ b/.github/workflows/release-python.yml @@ -40,6 +40,10 @@ jobs: - { runner: macos-latest, target: aarch64, manylinux: auto } - { runner: windows-latest, target: x64, manylinux: auto } steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build abi3 wheel (covers CPython 3.9+) uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 @@ -76,6 +80,10 @@ jobs: name: sdist runs-on: ubuntu-latest steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build the sdist uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 @@ -107,6 +115,24 @@ jobs: with: name: sdist path: ordvec-python/dist + # CycloneDX SBOM — generated ONCE here, not per-wheel. The wheel is the + # compiled Rust extension, so the binding crate's Cargo dependency tree is + # the meaningful, platform-independent bill of materials. cargo-cyclonedx + # walks the manifest and writes `.cdx.json` next to it, i.e. + # `ordvec-python/ordvec-python.cdx.json`. The dtolnay toolchain above + # provides cargo. + - name: Install cargo-cyclonedx + shell: bash + run: cargo install cargo-cyclonedx --version 0.5.9 --locked + - name: Generate CycloneDX SBOM for the binding crate + shell: bash + run: cargo cyclonedx --manifest-path ordvec-python/Cargo.toml --format json + - name: Upload the SBOM + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom-python + path: ordvec-python/ordvec-python.cdx.json + if-no-files-found: error require-ci-green: # The wheel must not ship from a commit whose core (`ci.yml`) or binding @@ -120,6 +146,10 @@ jobs: contents: read actions: read steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - name: assert ci.yml and python.yml are green for this commit env: GH_TOKEN: ${{ github.token }} @@ -151,10 +181,25 @@ jobs: permissions: contents: read id-token: write + attestations: write steps: + - name: Harden the runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true + # GitHub SLSA build provenance for the wheels + sdist, complementary to the + # PyPI-side PEP 740 attestations the pypa publish step emits below. Attest + # runs BEFORE publish so a failed attestation fails the release closed — + # nothing is pushed to PyPI without provenance recorded first. + - name: Attest build provenance for the wheels + sdist + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: | + dist/*.whl + dist/*.tar.gz - name: Publish to PyPI (Trusted Publishing; PEP 740 attestations on by default) uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0