From 2945b47e1a28c8ecfe5750f1dab31e6b6e3c37d4 Mon Sep 17 00:00:00 2001 From: kingchenc Date: Sat, 30 May 2026 18:23:47 +0200 Subject: [PATCH] feat(release): add CycloneDX SBOMs and npm provenance attestations (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two modern supply-chain-trust additions to the release pipeline, neither of which changes what gets published — only adds verifiable signals attached to existing releases. 1. **CycloneDX SBOMs.** `cargo-cyclonedx` is installed in the cargo-publish job after the .crate files are built, and runs once per published crate (`wickra-core`, `wickra-data`, `wickra`). The resulting `*.cdx.json` files are uploaded as the `sboms` artifact, then attached to the GitHub Release alongside the existing wheels, tarballs, .node binaries and .crate files. Future security advisories can answer "is my version of crate X transitive in wickra Y.Z?" by reading the SBOM directly instead of resolving the lockfile. 2. **npm `--provenance` flag** on every npm publish call: - main `wickra` package (node-publish, first + retry) - per-platform `wickra-` subpackages (node-publish loop) - `wickra-wasm` (wasm-publish) Provenance attestations are generated server-side by npm from the GitHub Actions OIDC token. The publishing jobs gain `permissions: id-token: write` so the runner can exchange that token. The npm page for each published version will then carry the "Verified provenance" badge, which proves the tarball was built by *this* workflow run and not by an arbitrary local laptop with the NPM_TOKEN. Skipped deliberately (to keep this PR focused, possibly follow-ups): - Sigstore cosign signing of artefacts (different audit story; can be layered on after npm-provenance lands). - SLSA build-provenance attestations via `actions/attest-build-provenance` (would target every artefact uniformly; the npm-provenance flag is the more pragmatic first cut). YAML structure validated (8 jobs intact). No production code touched. This PR conflicts with PR #59 only in line-by-line URL substitutions on release.yml — rebase after #59 should be clean. --- .github/workflows/release.yml | 52 +++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 484f26c9..6af322c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,6 +79,30 @@ jobs: name: crate-files path: target/package/*.crate + # CycloneDX SBOM per published crate. Attached to the GitHub Release + # alongside the .crate / .whl / .tgz artefacts so downstream + # consumers can audit the published dependency tree without + # re-resolving Cargo.lock. + - name: Install cargo-cyclonedx + uses: taiki-e/install-action@6c1f7cf125e42770ff087ea443901b487cc5471a # v2.79.5 + with: + tool: cargo-cyclonedx + + - name: Generate CycloneDX SBOMs + run: | + cargo cyclonedx --format json --top-level -p wickra-core + cargo cyclonedx --format json --top-level -p wickra-data + cargo cyclonedx --format json --top-level -p wickra + mkdir -p sboms + find . -name "*.cdx.json" -not -path "./target/*" -exec cp {} sboms/ \; + ls -lh sboms/ + + - name: Upload SBOMs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sboms + path: sboms/*.cdx.json + # -------------------------------------------------------------------------- # PyPI: cross-platform wheels + sdist # -------------------------------------------------------------------------- @@ -208,6 +232,14 @@ jobs: needs: node-build runs-on: ubuntu-latest environment: release + # `id-token: write` lets npm publish embed a Sigstore provenance + # attestation generated from the GitHub Actions OIDC token. The npm + # registry then shows a "Verified provenance" badge and lets + # consumers verify the package was built from this exact workflow + # run. + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -266,13 +298,13 @@ jobs: # scripts during publish (npm runs prepublishOnly/prepare/etc. from # the package being published — a malicious or stray script would # execute with the npm token in the environment). - (cd "$dir" && npm publish --access public --ignore-scripts) + (cd "$dir" && npm publish --access public --ignore-scripts --provenance) local rc=$? echo "::endgroup::" if [ "$rc" -ne 0 ]; then echo "::warning::first attempt of $pkgname failed (rc=$rc); retrying after 30s" sleep 30 - (cd "$dir" && npm publish --access public --ignore-scripts) + (cd "$dir" && npm publish --access public --ignore-scripts --provenance) rc=$? fi if [ "$rc" -ne 0 ]; then @@ -313,12 +345,12 @@ jobs: # --ignore-scripts so any leftover prepublish hooks (which would # otherwise try to republish the already-published platform # subpackages) can't sabotage the main publish. - npm publish --access public --ignore-scripts + npm publish --access public --ignore-scripts --provenance rc=$? if [ "$rc" -ne 0 ]; then echo "::warning::first attempt failed (rc=$rc); retrying after 30s" sleep 30 - npm publish --access public --ignore-scripts + npm publish --access public --ignore-scripts --provenance rc=$? fi exit $rc @@ -346,10 +378,18 @@ jobs: # -------------------------------------------------------------------------- # WASM: wasm-pack build + npm publish (as `wickra-wasm`) # -------------------------------------------------------------------------- + # Note: this job's npm publish call uses `--provenance` (see below), + # which requires the `id-token: write` permission set at the job level. wasm-publish: name: Publish wickra-wasm to npm runs-on: ubuntu-latest environment: release + # `id-token: write` lets npm publish embed a Sigstore provenance + # attestation generated from the GitHub Actions OIDC token (same + # mechanism as the node-publish job above). + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -405,7 +445,7 @@ jobs: - name: Publish wickra-wasm to npm (idempotent) working-directory: bindings/wasm/pkg run: | - out=$(npm publish --access public 2>&1) && echo "$out" \ + out=$(npm publish --access public --provenance 2>&1) && echo "$out" \ || (echo "$out" | grep -q "You cannot publish over" && echo "skip: version already on npm" \ || (echo "$out"; exit 1)) env: @@ -459,6 +499,8 @@ jobs: find artifacts -type f -name "wickra-*.tgz" -exec cp {} release-assets/ \; # Cargo .crate files (one per workspace member). find artifacts -type f -name "*.crate" -exec cp {} release-assets/ \; + # CycloneDX SBOMs (one per published crate). + find artifacts -type f -name "*.cdx.json" -exec cp {} release-assets/ \; ls -lh release-assets/ echo "asset-count=$(ls release-assets/ | wc -l)"