feat(release): add CycloneDX SBOMs and npm provenance attestations (#66)
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-<triple>` 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.
This commit is contained in:
@@ -79,6 +79,30 @@ jobs:
|
|||||||
name: crate-files
|
name: crate-files
|
||||||
path: target/package/*.crate
|
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
|
# PyPI: cross-platform wheels + sdist
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -208,6 +232,14 @@ jobs:
|
|||||||
needs: node-build
|
needs: node-build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: release
|
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:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
@@ -266,13 +298,13 @@ jobs:
|
|||||||
# scripts during publish (npm runs prepublishOnly/prepare/etc. from
|
# scripts during publish (npm runs prepublishOnly/prepare/etc. from
|
||||||
# the package being published — a malicious or stray script would
|
# the package being published — a malicious or stray script would
|
||||||
# execute with the npm token in the environment).
|
# 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=$?
|
local rc=$?
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
if [ "$rc" -ne 0 ]; then
|
if [ "$rc" -ne 0 ]; then
|
||||||
echo "::warning::first attempt of $pkgname failed (rc=$rc); retrying after 30s"
|
echo "::warning::first attempt of $pkgname failed (rc=$rc); retrying after 30s"
|
||||||
sleep 30
|
sleep 30
|
||||||
(cd "$dir" && npm publish --access public --ignore-scripts)
|
(cd "$dir" && npm publish --access public --ignore-scripts --provenance)
|
||||||
rc=$?
|
rc=$?
|
||||||
fi
|
fi
|
||||||
if [ "$rc" -ne 0 ]; then
|
if [ "$rc" -ne 0 ]; then
|
||||||
@@ -313,12 +345,12 @@ jobs:
|
|||||||
# --ignore-scripts so any leftover prepublish hooks (which would
|
# --ignore-scripts so any leftover prepublish hooks (which would
|
||||||
# otherwise try to republish the already-published platform
|
# otherwise try to republish the already-published platform
|
||||||
# subpackages) can't sabotage the main publish.
|
# subpackages) can't sabotage the main publish.
|
||||||
npm publish --access public --ignore-scripts
|
npm publish --access public --ignore-scripts --provenance
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ "$rc" -ne 0 ]; then
|
if [ "$rc" -ne 0 ]; then
|
||||||
echo "::warning::first attempt failed (rc=$rc); retrying after 30s"
|
echo "::warning::first attempt failed (rc=$rc); retrying after 30s"
|
||||||
sleep 30
|
sleep 30
|
||||||
npm publish --access public --ignore-scripts
|
npm publish --access public --ignore-scripts --provenance
|
||||||
rc=$?
|
rc=$?
|
||||||
fi
|
fi
|
||||||
exit $rc
|
exit $rc
|
||||||
@@ -346,10 +378,18 @@ jobs:
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# WASM: wasm-pack build + npm publish (as `wickra-wasm`)
|
# 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:
|
wasm-publish:
|
||||||
name: Publish wickra-wasm to npm
|
name: Publish wickra-wasm to npm
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: release
|
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:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
@@ -405,7 +445,7 @@ jobs:
|
|||||||
- name: Publish wickra-wasm to npm (idempotent)
|
- name: Publish wickra-wasm to npm (idempotent)
|
||||||
working-directory: bindings/wasm/pkg
|
working-directory: bindings/wasm/pkg
|
||||||
run: |
|
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" | grep -q "You cannot publish over" && echo "skip: version already on npm" \
|
||||||
|| (echo "$out"; exit 1))
|
|| (echo "$out"; exit 1))
|
||||||
env:
|
env:
|
||||||
@@ -459,6 +499,8 @@ jobs:
|
|||||||
find artifacts -type f -name "wickra-*.tgz" -exec cp {} release-assets/ \;
|
find artifacts -type f -name "wickra-*.tgz" -exec cp {} release-assets/ \;
|
||||||
# Cargo .crate files (one per workspace member).
|
# Cargo .crate files (one per workspace member).
|
||||||
find artifacts -type f -name "*.crate" -exec cp {} release-assets/ \;
|
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/
|
ls -lh release-assets/
|
||||||
echo "asset-count=$(ls release-assets/ | wc -l)"
|
echo "asset-count=$(ls release-assets/ | wc -l)"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user