Skip to content

Attach an SBOM to every release - #86

Open
Maximilian-Nesslauer wants to merge 3 commits into
release-workflowfrom
sbom
Open

Attach an SBOM to every release#86
Maximilian-Nesslauer wants to merge 3 commits into
release-workflowfrom
sbom

Conversation

@Maximilian-Nesslauer

Copy link
Copy Markdown
Member

Closes #81. Stacked on #78, so the base branch is release-workflow.

Every release now ships a software bill of materials next to the builds.
An SBOM is a list of every third-party package inside a program, with the exact version, the license and a hash of each package. With it, anyone can answer "does this Borea build carry the vulnerable version of package X" by reading one file instead of rebuilding Borea. SHA256SUMS.txt says the files are unchanged, the provenance attestation says which workflow run built them, and the SBOM says what is inside them.

How it works:

  1. A new sbom job restores Borea.App the way the builds do and runs the CycloneDX .NET tool on the restored package graph. The tool is pinned in .config/dotnet-tools.json, which Dependabot keeps current with the other NuGet packages.
  2. The job checks the result: Avalonia, CommunityToolkit.Mvvm, Tomlyn and one transitive package are listed, nothing test-only or Debug-only is, and every package sits in the dependency graph.
  3. The release job adds the file to the checksum table and the release assets, and attests it twice: build provenance like every other file, and an SBOM attestation that binds the list to each archive.

What the user gets: one more release asset, Borea-<version>.cdx.json, one more row in the checksum table, and a verify command in the release text and the README.

The version is now named once in the test job and read by every later job, so a bad tag fails before the tests run.

Two things differ from the issue text. actions/attest-sbom is deprecated and only wraps actions/attest, so the workflow uses actions/attest directly. The optional dependency submission is left out: it needs contents: write in a job that restores packages, and once #82 lands a lock file, GitHub's dependency graph reads the transitive closure from it anyway. #82 itself is not needed first, because the SBOM command does not change when the lock file arrives.

Tested locally: the tool run on Borea.App (31 packages, each with license and hash, the Debug-only DiagnosticsSupport excluded), the check script with a failing case, the release body step, and actionlint. GitHub's own dependency graph of the repository lists the same packages plus exactly the 13 test packages and the Debug-only one. Not tested yet: the attestation and the upload, which only run on a real tag, same as #78.

One limit: the SBOM lists the NuGet packages, not the .NET runtime that the self-contained builds carry. The runtime version is in Borea.App.deps.json inside each archive.

LLM usage disclosure: Claude Fable 5.1 (and Opus 5 for the code review workflow) was used collaboratively with me to create parts of this. I read, reviewed and tested all of it myself and like it.

- The publish matrix computed the version in every leg, and the SBOM job needs the same value.
- The test job computes it once and exposes it as an output, so every later job reads one value.
- A tag that is not a version fails there, before the tests run.
- A new sbom job restores Borea.App in Release and runs the CycloneDX .NET tool on the restored graph, so the list matches the builds and leaves the Debug-only package out.
- The tool is pinned in .config/dotnet-tools.json, which Dependabot updates like the other NuGet packages.
- The job checks the result: four known packages are in, nothing test-only or Debug-only is, and every package sits in the dependency graph.
- The release job adds the file to the checksums and the assets, and actions/attest binds it to each archive; actions/attest-sbom is deprecated and only wraps it.
- The release text names the file, says what it lists, and gives the verify command.
- What the file lists, and the verify command with the signer workflow pinned, next to the provenance command.
@Maximilian-Nesslauer Maximilian-Nesslauer added this to the Release milestone Sep 5, 2026
@Maximilian-Nesslauer Maximilian-Nesslauer added the area:infra CI, build, release label Sep 5, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Borea Sep 5, 2026
@Maximilian-Nesslauer Maximilian-Nesslauer moved this from Backlog to In review in Borea Sep 5, 2026
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 33956664485

Warning

No base build found for commit 53837b6 on release-workflow.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 92.638%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 2353
Covered Lines: 2248
Line Coverage: 95.54%
Relevant Branches: 1206
Covered Branches: 1049
Branch Coverage: 86.98%
Branches in Coverage %: Yes
Coverage Strength: 56.07 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra CI, build, release

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Attach an SBOM to every release

2 participants