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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: googleapis/release-please-action@v4
|
- uses: googleapis/release-please-action@v4
|
||||||
|
id: release
|
||||||
with:
|
with:
|
||||||
config-file: release-please-config.json
|
config-file: release-please-config.json
|
||||||
manifest-file: .release-please-manifest.json
|
manifest-file: .release-please-manifest.json
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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
|
This template's own consumption of the stack — when it's eventually
|
||||||
pushed to a GitHub remote — uses the same configurations unchanged.
|
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
|
## Alternatives considered
|
||||||
|
|
||||||
- **Dependabot for everything instead of Renovate.** Rejected. Less
|
- **Dependabot for everything instead of Renovate.** Rejected. Less
|
||||||
|
|||||||
Reference in New Issue
Block a user