ci(release): attach CycloneDX SBOM to every GitHub release

Amends release-please.yml with conditional steps that run only when
release-please cuts a release:
- checkout + pnpm install to give @cyclonedx/cyclonedx-npm the full
  resolved workspace graph
- pnpm dlx @cyclonedx/cyclonedx-npm generates a CycloneDX 1.6 JSON SBOM
  named sbom-<tag>.cdx.json; --ignore-npm-errors is required because
  npm ls exits non-zero for dev-deps-of-dev-deps pnpm correctly elides
- softprops/action-gh-release@<SHA> (v3.0.0, Renovate-managed) attaches
  the file to the GitHub release as a downloadable asset

Adds ADR-023 §9 amendment documenting the step shape, rationale for
pnpm dlx (avoids lockfile per ADR-022), --ignore-npm-errors behaviour,
SHA pinning per ADR-023 §1, and the extended failure-mode table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 11:31:08 +00:00
parent 224a5d78c8
commit 08bc19293a
2 changed files with 114 additions and 0 deletions

View File

@@ -40,7 +40,44 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
token: ${{ secrets.GITHUB_TOKEN }}
# The steps below run only when release-please actually cut a release.
# pnpm dlx avoids adding @cyclonedx/cyclonedx-npm to the lockfile (CI-only
# tool per ADR-022); SHA-pinned action follows ADR-023 §1 Renovate pattern.
- uses: actions/checkout@v4
if: ${{ steps.release.outputs.releases_created == 'true' }}
- uses: pnpm/action-setup@v4
if: ${{ steps.release.outputs.releases_created == 'true' }}
with:
version: 9
- uses: actions/setup-node@v4
if: ${{ steps.release.outputs.releases_created == 'true' }}
with:
node-version: 22
cache: pnpm
- name: Install dependencies
if: ${{ steps.release.outputs.releases_created == 'true' }}
run: pnpm install --frozen-lockfile
- name: Generate CycloneDX SBOM
if: ${{ steps.release.outputs.releases_created == 'true' }}
run: >
pnpm dlx @cyclonedx/cyclonedx-npm
--output-file sbom-${{ steps.release.outputs.tag_name }}.cdx.json
--output-format json
--ignore-npm-errors
- name: Attach SBOM to GitHub release
if: ${{ steps.release.outputs.releases_created == 'true' }}
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ steps.release.outputs.tag_name }}
files: sbom-${{ steps.release.outputs.tag_name }}.cdx.json

View File

@@ -288,6 +288,83 @@ GitHub repo. Configurations (`renovate.json`, `.socket.json`, `codeql.yml`,
This template's own consumption of the stack — when it's eventually
pushed to a GitHub remote — uses the same configurations unchanged.
### 9. Amendment — SBOM release artifact (CycloneDX)
**Added:** 2026-05-20 (story `10-sbom-ci-workflow`)
`.github/workflows/release-please.yml` is amended to generate a
[CycloneDX](https://cyclonedx.org/) SBOM and attach it to every GitHub
release cut by release-please.
**Concrete step shape:**
```yaml
- name: Generate CycloneDX SBOM
if: ${{ steps.release.outputs.releases_created == 'true' }}
run: >
pnpm dlx @cyclonedx/cyclonedx-npm
--output-file sbom-${{ steps.release.outputs.tag_name }}.cdx.json
--output-format json
--ignore-npm-errors
- name: Attach SBOM to GitHub release
if: ${{ steps.release.outputs.releases_created == 'true' }}
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ steps.release.outputs.tag_name }}
files: sbom-${{ steps.release.outputs.tag_name }}.cdx.json
```
**Prerequisites** (also conditional on `releases_created == 'true'`):
`actions/checkout@v4` → `pnpm/action-setup@v4` → `actions/setup-node@v4`
→ `pnpm install --frozen-lockfile`, providing the installed workspace
graph that `@cyclonedx/cyclonedx-npm` analyses.
**Rationale:**
- **Compliance surface.** Consumers pursuing SOC 2 / ISO 27001 /
FedRAMP / EU CRA must produce an inventory of every version that
shipped. A CycloneDX JSON SBOM attached to each GitHub release gives
auditors a machine-readable, per-release artifact without requiring
them to inspect or re-run the repo.
- **`pnpm dlx`, not `package.json`.** `@cyclonedx/cyclonedx-npm` is
a release-time audit tool, not a runtime or build dependency; adding
it to the lockfile would violate ADR-022's spirit (library evaluation
required for runtime deps in feature/core packages). `pnpm dlx`
fetches and discards it within the CI step.
- **`--ignore-npm-errors`.** `@cyclonedx/cyclonedx-npm` internally
invokes `npm ls` to traverse the dependency graph. In a pnpm
workspace, `npm ls` exits non-zero when it encounters dev-deps-of-
dev-deps that pnpm correctly elides from the install tree; the SBOM
content is unaffected. Without this flag the step exits 254 and
produces no file. `--ignore-npm-errors` instructs the tool to treat
those `npm ls` warnings as non-fatal and emit the SBOM regardless.
- **SHA-pinned action.** `softprops/action-gh-release` is pinned to a
40-character commit SHA (`# v3.0.0` trailing comment) per §1's
Renovate `pinGitHubActionDigests` preset. Renovate will open a bump
PR when a newer release is available.
- **Conditional execution.** The SBOM steps run only when
`releases_created == 'true'` — every non-release push to `main`
skips them entirely. This keeps the workflow fast for the common case
(release-please just updating its rolling PR).
- **Root SBOM covers all workspace packages.** Running
`@cyclonedx/cyclonedx-npm` from the workspace root after
`pnpm install --frozen-lockfile` captures the full resolved
dependency graph across all packages. Per-package SBOMs are out of
scope (see story `10-sbom-ci-workflow` §Out of scope).
**Failure-mode table row** (extends §5):
| Gate | Layer | Hard block? |
| ------------------------------------------- | ------- | ----------------------- |
| SBOM generation (`@cyclonedx/...`) | release | Yes — release job fails |
| SBOM upload (`softprops/action-gh-release`) | release | Yes — release job fails |
SBOM absence blocks the release job; it does **not** gate main CI
(the `ci.yml` `validate` job is unaffected). This matches the
principle that release assets are part of the release job, not part
of the per-PR validation loop.
## Alternatives considered
- **Dependabot for everything instead of Renovate.** Rejected. Less