Skip to content

ci(release): tolerate npm-ls dev peer warning in SBOM step (--ignore-… #5

ci(release): tolerate npm-ls dev peer warning in SBOM step (--ignore-…

ci(release): tolerate npm-ls dev peer warning in SBOM step (--ignore-… #5

Workflow file for this run

name: Release (SBOM · SLSA provenance · Sigstore)
# Triggered on version tags. Produces a CycloneDX SBOM, SLSA build provenance,
# and a cosign keyless signature for supply-chain verifiability.
on:
push:
tags: ["v*"]
permissions:
contents: read
jobs:
build:
name: Build · test · SBOM
runs-on: ubuntu-latest
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run build
- run: npm run test:coverage
- name: CycloneDX 1.6 SBOM
# --ignore-npm-errors: `npm ls` exits non-zero on a harmless dev-only peer
# mismatch (madge→precinct wants TypeScript 5.x while the project pins 6.x);
# the installed tree is valid, so the SBOM is still complete and accurate.
# (Mirrors the same flag in ci.yml's Security job.)
run: npx --yes @cyclonedx/cyclonedx-npm@latest --ignore-npm-errors --output-format JSON --spec-version 1.6 --output-file sbom.json
- name: Pack tarball (from a staging dir → Apache-2.0 LICENSE, no GPL leak)
# The published library is Apache-2.0 but the repo root LICENSE is GPL-3.0
# (the app/aggregate). `npm pack` force-includes the root LICENSE; pack from
# a clean staging dir whose only LICENSE is LICENSE-APACHE. See docs/RELEASE.md.
run: bash scripts/pack-staging.sh
- name: Subject hashes (for provenance)
id: hash
run: echo "hashes=$(sha256sum *.tgz sbom.json | base64 -w0)" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v7
with:
name: release-artifacts
path: |
*.tgz
sbom.json
provenance:
name: SLSA provenance
needs: [build]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: ${{ needs.build.outputs.hashes }}
upload-assets: true
sign:
name: Cosign keyless signing
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: release-artifacts
- uses: sigstore/cosign-installer@v3
- name: Sign artifacts (keyless OIDC)
run: |
for f in *.tgz sbom.json; do
cosign sign-blob --yes "$f" \
--output-signature "$f.sig" --output-certificate "$f.pem"
done
- uses: softprops/action-gh-release@v2
with:
files: |
*.tgz
sbom.json
*.sig
*.pem
image:
name: Build · push · sign container image
needs: [build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=ref,event=tag
- id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- uses: sigstore/cosign-installer@v3
- name: Sign the image (keyless OIDC)
env:
DIGEST: ${{ steps.build.outputs.digest }}
# Pass the (multi-line) tag list via env, not inline interpolation, so a
# second tag doesn't break the shell. Iterate line-by-line.
TAGS: ${{ steps.meta.outputs.tags }}
run: |
echo "$TAGS" | while read -r tag; do
[ -n "$tag" ] && cosign sign --yes "${tag}@${DIGEST}"
done