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:
37
.github/workflows/release-please.yml
vendored
37
.github/workflows/release-please.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user