Skip to content

ci: attach the SBOMs to the GitHub release - #17

Merged
CaffeinatedCoder merged 1 commit into
mainfrom
ci/release-assets-sbom
Aug 16, 2026
Merged

CaffeinatedCoder merged 1 commit into
mainfrom
ci/release-assets-sbom

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Why

SECURITY.md says each release ships a CycloneDX SBOM. It did — as a workflow artifact, which expires after 90 days (the current ones on 2026-11-14), and nuget.org has no slot for one. The GitHub releases for v5.0.0–v5.0.3 carry no assets. So the statement holds for one quarter per release and then goes quietly false — the exact shape of claim the security policy warns against overstating.

What

A third job in release.yml, release, runs after publish:

  • Creates the GitHub release if none exists, with the README's ## What changed in <version> section as the notes — the same text ChangelogConsistencyTests already requires for the shipped version. An empty extraction fails the job rather than publishing a blank release. A release created by hand before the tag was pushed is left as written; only the assets are added.
  • Attaches the per-package *.cdx.json files (--clobber, so a re-run is safe).
  • Appends a one-line footer to generated notes linking the workflow run that built and published the version.

Runs after the push, so what is attached describes what is on nuget.org; a failure here is loud and costs a manual upload, nothing more.

Only the SBOMs are attached, deliberately. nuget.org repository-signs every package on ingestion, so a .nupkg downloaded from there never matches ours byte-for-byte. Attaching ours would invite a hash comparison that fails for a benign reason, and nuget.org is already the immutable store for the packages. The SBOM has no other home.

Permissions: release holds contents: write and no id-token; publish holds id-token: write and contents: read. No job holds both — the split that was already the security boundary is kept.

Docs updated to match: SECURITY.md now says where the SBOM lives; CLAUDE.md describes three jobs.

Verified

  • actionlint clean.
  • The two run: blocks were extracted verbatim from the YAML and executed locally with gh release create/upload stubbed and gh release view live: existing release (v5.0.3) → leaves notes alone, exit 0; missing release → creates with the extracted section; view failing for a reason other than "release not found" (401) → fails loudly instead of attempting a create; no README section for the version → fails; prerelease version → --prerelease; no .cdx.json in the artifact → fails.
  • The section extraction was checked against every shipped version (5.0.0–5.0.3): starts at the heading, stops at the next H2.
  • PackagingConventionTests, ClaudeMdConsistencyTests, ChangelogConsistencyTests pass.

Not exercised: an actual tag push. The first real run of the job is the next release; if it fails, the packages are already on nuget.org and the SBOMs are still in the run's artifact for a manual upload.

🤖 Generated with Claude Code

SECURITY.md says each release ships a CycloneDX SBOM. It did — as a workflow
artifact, which expires after 90 days, and nuget.org has no slot for one. The
releases for v5.0.0 through v5.0.3 carry no assets, so the statement was true
for one quarter per release and then quietly false.

A third job, `release`, now runs after `publish`: it creates the GitHub
release if none exists (notes taken from the README's "What changed in
<version>" section, which ChangelogConsistencyTests already requires for the
shipped version — an empty extraction fails the job rather than publishing a
blank release) and attaches the per-package `.cdx.json` files. A release
created by hand before the tag is pushed is left as written; only the assets
are added. It runs after the push so what is attached describes what is on
nuget.org, and a failure here costs nothing but a manual upload.

Only the SBOMs are attached, deliberately. nuget.org repository-signs every
package on ingestion, so a .nupkg downloaded from there never matches ours
byte-for-byte; attaching ours would invite a hash comparison that fails for
a benign reason, and nuget.org is already the immutable store for the
packages. The SBOM has no other home.

The job holds contents: write and no id-token; publish holds id-token: write
and contents: read. No job holds both.

Verified: actionlint clean; the two run blocks executed locally, extracted
verbatim from the YAML, with `gh release create/upload` stubbed and `gh
release view` live — existing release (v5.0.3) leaves notes alone and exits 0,
missing release creates with the extracted section, a view failure that is
not "release not found" (401) fails loudly instead of attempting a create,
a version with no README section fails, a prerelease version gets
--prerelease, and a missing SBOM glob fails. The extraction was checked
against every shipped version's section. Not exercised: an actual tag push —
the first real run is the next release.

Co-Authored-By: Claude Fable 5 <[email protected]>
@CaffeinatedCoder
CaffeinatedCoder merged commit a8f90ea into main Aug 16, 2026
4 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the ci/release-assets-sbom branch August 16, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant