Repository navigation
feat(release): attest a CycloneDX bill of materials for each tag - #288
Conversation
Build provenance attestation already shipped; the bill-of-materials half did not. The tag workflow now installs an exact pinned cargo-cyclonedx, generates a CycloneDX SBOM from the locked dependency graph into dist/ so it publishes as a release asset, and attests it alongside the Homebrew formula, VSIX, and Zed source archive so it carries the same GitHub/Sigstore provenance as the bytes it describes. Ordering is load-bearing and enforced: generation runs after the native archives are downloaded, so the dependency graph ships beside the artifacts it covers, and before attestation, so it cannot be published unattested. check-release-distribution gains DIST-9 topology enforcement. Deterministic mutations reject a removed generation step, an unpinned tool, output written outside dist/, a step reordered before the native download, and an attestation omitting the SBOM. Two existing cases asserted the previous attestation message and are updated deliberately, since the attested set changed. The v0.4.0 packet is amended to carry the SBOM slice in both Must ship and Scoped slices; the validator's symmetric inventory now admits 34 scoped issues. ROADMAP moves the slice from parked to delivered: it was parked pending the distribution and signing authorities, which #245 and #251 established. Evidence: 33/33 release-distribution cases; live checker passes 3 native platforms, 2 Homebrew platforms, 3 editor registries; zizmor clean across 5 workflows; workflow-security and dependency-update policies satisfied; packet admitted at 4 goalposts and 34 scoped issues; full release-prep passed with 80 mutants and zero survivors. Refs #227.
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Summary by CodeRabbit
WalkthroughThe release pipeline now generates CycloneDX SBOMs for ChangesRelease SBOM provenance
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0894209f51
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… command Three review findings, all valid. Register the SBOM in the release verification authorities. The workflow produced and attested an asset that no post-publication procedure checked, so a missing or invalid public attestation would have passed manual verification despite the new claim. .continuum/release.yml now carries the SBOM in the required-artifact inventory, docs/RELEASING.md verifies it is well-formed JSON and attestation-verified alongside the formula, and the v0.4.0 publication witness gains a bill-of-materials row. Validate the executable command rather than token presence. The previous check tested for 'cargo cyclonedx', '--locked', and a dist/ path independently, which an inert stand-in such as echo plus touch would have satisfied, letting an arbitrary empty file be attested and published as the SBOM. Generation is now matched as an exact normalized command sequence, mirroring the Homebrew check, which also binds --format json and the dist/ destination to the invocation. A new mutation case rejects inert commands, an unbound format, an output written outside dist/, and an extra command smuggled after the reviewed sequence. Restore DIST-8a's evidence. Inserting DIST-9a orphaned the trailing Evidence and Status fields onto it, which named only the Homebrew generator, leaving DIST-8a without its implemented evidence and DIST-9a with irrelevant evidence. The Homebrew block returns to DIST-8a and DIST-9a gains its own. Evidence: 34/34 release-distribution cases; live checker passes; release profile OK; packet admitted at 4 goalposts and 34 scoped issues; Markdown clean. Refs #227.
Code Lawyer self-audit — P0: the SBOM step cannot executeSelf-audit of This would surface only at tag time, on an immutable tag.
Root causeI bound the policy to an exact command sequence — which was the right hardening, and Codex was Proposed fixGenerate for each shipped binary and ship both: set -euo pipefail
cargo cyclonedx --format json --all --manifest-path crates/colorful-cli/Cargo.toml --override-filename sbom
cargo cyclonedx --format json --all --manifest-path crates/colorful-lsp/Cargo.toml --override-filename sbom
cp crates/colorful-cli/sbom.json "dist/colorful-language-${GITHUB_REF_NAME}-colorful-sbom.cdx.json"
cp crates/colorful-lsp/sbom.json "dist/colorful-language-${GITHUB_REF_NAME}-colorful-lsp-sbom.cdx.json"Requires updating Merge gate: LOCKED. cc @codex for a second opinion on the two-artifact design and on whether |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9e6d6d0bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Self-audit P0. The Generate SBOM step could not run. Verified by downloading the
pinned cargo-cyclonedx 0.5.9 release binary and executing it against this
workspace:
1. cargo cyclonedx rejects --locked
'error: unexpected argument --locked found', exit 2. Under
set -euo pipefail the release job aborts.
2. Output is nine files, one per crate, written beside each crate's manifest
and named sbom.json. No repo-root sbom.cdx.json is ever produced, so the
cp failed on filename, path, and cardinality at once.
3. The attestation glob dist/*sbom.cdx.json therefore matched nothing.
4. A single crate's SBOM does not cover both shipped binaries: colorful-cli
resolves 47 components without tower-lsp, which is colorful-lsp's
dependency.
None of this was reachable before a tag, because nothing in CI executes the
release workflow.
Root cause: the previous commit hardened the gate to require an exact command
sequence, which was the correct review outcome, but exactness was verified while
executability was assumed.
The fix generates one SBOM per shipped binary, since cargo-cyclonedx emits
per-package documents and has no aggregate-workspace mode. The replacement
sequence was executed end to end before being admitted: all four commands
succeed, both assets land in dist/, and together they cover the shipped graph
(colorful 47 components; colorful-lsp 112 including tower-lsp).
The release profile, runbook verification commands, DIST-9a, the v0.4.0
publication witness, and CHANGELOG all move to the two-asset shape. The packet
validator additionally rejected a first draft of the witness row for asserting
completed evidence inside a section declared unavailable; the row now states
required evidence rather than a claim.
Evidence: 34/34 release-distribution cases; 49/49 release-packet cases; live
distribution checker, release profile, packet, zizmor, and dependency-update
policy all pass; Markdown clean.
Refs #227.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/RELEASING.md`:
- Around line 731-735: Update docs/RELEASING.md lines 731-735 so the SBOM
verification procedure records both SBOM filenames, JSON validation outcomes,
and attestation verification outcomes. Update
docs/topics/distribution/test-plan.md lines 144-146 to assign public SBOM
evidence to DIST-9a or add a dedicated SBOM evidence row; do not assign it to
DIST-7a.
In `@scripts/check-release-distribution.mjs`:
- Line 88: The mutable cargo-cyclonedx version is duplicated outside the
canonical policy gate. In scripts/check-release-distribution.mjs:88-88, remove
the local EXPECTED_SBOM_TOOL ownership and expose or reuse the policy script’s
canonical pin; update the validator to read that value. In
scripts/check-release-distribution.test.mjs:20-20, replace the hard-coded
cargo-cyclonedx@0.5.9 fixture with the same canonical policy value so future pin
updates remain synchronized.
- Around line 85-105: Update validateReleaseDistribution and its validSnapshot
contract to include the .continuum/release.yml publish.artifacts inventory,
requiring both expected SBOM asset paths defined by EXPECTED_SBOM_ASSET or the
reviewed SBOM commands. Add failure tests covering removal or renaming of each
required SBOM artifact entry so either inventory change is rejected.
- Around line 431-445: The SBOM installer validation in the checks around
sbomInstall must also require sbomInstall.with?.fallback to equal "none",
alongside the existing tool and command checks. Add a negative test configuring
fallback as "cargo-binstall" and assert that validation rejects it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 8a973bd6-d465-4f89-917c-3dc962192e1f
📒 Files selected for processing (11)
.continuum/release.yml.github/workflows/release.ymlCHANGELOG.mdROADMAP.mddocs/RELEASING.mddocs/goalposts/v0.4.0/release.mddocs/goalposts/v0.4.0/verification.mddocs/topics/distribution/README.mddocs/topics/distribution/test-plan.mdscripts/check-release-distribution.mjsscripts/check-release-distribution.test.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
- GitHub Check: Generated IR and vocabulary drift
- GitHub Check: Rust coverage
- GitHub Check: Editor integrations (compile)
- GitHub Check: Downstream discovery version-compat matrix
- GitHub Check: Cargo package witness
- GitHub Check: Rust (fmt, clippy, test)
- GitHub Check: CodeQL (rust)
- GitHub Check: CodeQL (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (10)
.continuum/release.yml
📄 CodeRabbit inference engine (AGENTS.md)
Inspect .continuum/release.yml for repository-local release versions, signposts, validation entrypoints, workflows, crates, and artifacts.
Files:
.continuum/release.yml
docs/**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
New durable documentation pages must be linked from docs/README.md and follow the documentation corpus standard, including one primary reader job.
Files:
docs/RELEASING.mddocs/goalposts/v0.4.0/release.mddocs/topics/distribution/README.mddocs/topics/distribution/test-plan.mddocs/goalposts/v0.4.0/verification.md
docs/RELEASING.md
📄 CodeRabbit inference engine (AGENTS.md)
For every release, version, tag, publish, or readiness request, read docs/RELEASING.md and follow its complete release gate rather than a remembered subset.
Files:
docs/RELEASING.md
**/*.md
📄 CodeRabbit inference engine (AGENTS.md)
**/*.md: Use one logical change per commit and Conventional Commit prefixes such as feat, fix, docs, refactor, test, or chore.
Use runnable examples where practical, separate commands from expected output, omit shell prompts from copyable command blocks, warn before destructive or privileged commands, and provide useful visual alt text or nearby equivalents.
Treat prose metrics as editorial signals, not universal merge gates; hard gates concern links, examples, generated references, evidence, Markdown, whitespace, and contract coverage.
For documentation changes, run markdownlint-cli2, diff whitespace checks, internal-link checks, and documentation-citation checks.
Files:
docs/RELEASING.mddocs/goalposts/v0.4.0/release.mddocs/topics/distribution/README.mddocs/topics/distribution/test-plan.mddocs/goalposts/v0.4.0/verification.mdROADMAP.mdCHANGELOG.md
**/*
📄 CodeRabbit inference engine (AGENTS.md)
**/*: Do not force-push, rebase, squash, or amend shared branches; make a new commit instead.
Do not claim work or verification is complete unless it was actually performed; report failures with their output.
Do not casually regenerate golden fixtures; golden changes must be deliberate, reviewable, and tied to a contract change.
Files:
docs/RELEASING.mddocs/goalposts/v0.4.0/release.mddocs/topics/distribution/README.mddocs/topics/distribution/test-plan.mddocs/goalposts/v0.4.0/verification.mdROADMAP.mdCHANGELOG.mdscripts/check-release-distribution.mjsscripts/check-release-distribution.test.mjs
docs/topics/**/README.md
📄 CodeRabbit inference engine (AGENTS.md)
Topic README files must describe only implemented current truth; planned verification belongs in test-plan.md.
Files:
docs/topics/distribution/README.md
docs/topics/**/test-plan.md
📄 CodeRabbit inference engine (AGENTS.md)
Before implementation, record planned cases with stable IDs, requirements, explicit oracles, evidence types, and status; later record actual evidence and mark implemented.
Files:
docs/topics/distribution/test-plan.md
.github/workflows/*.yml
📄 CodeRabbit inference engine (AGENTS.md)
Validate every modified GitHub Actions workflow with actionlint before pushing.
Files:
.github/workflows/release.yml
ROADMAP.md
📄 CodeRabbit inference engine (AGENTS.md)
Keep roadmap anchors synchronized with goalpost and issue status, and do not describe unbuilt goalposts as existing.
Files:
ROADMAP.md
CHANGELOG.md
📄 CodeRabbit inference engine (AGENTS.md)
Update the changelog for release-visible changes.
Files:
CHANGELOG.md
🧠 Learnings (1)
📚 Learning: 2026-07-30T04:29:11.102Z
Learnt from: flyingrobots
Repo: flyingrobots/colorful-language PR: 279
File: scripts/check-coverage-policy.mjs:36-41
Timestamp: 2026-07-30T04:29:11.102Z
Learning: In this repository’s GitHub Actions workflow validation code, treat `scripts/check-dependency-update-policy.mjs` as the *only* source of mutable pin data. It should own full commit SHA or Docker digest pins, any release/comment text it depends on, and the identity→pin mapping across the workflow family. For `scripts/check-coverage-policy.mjs`, `scripts/check-repository-maintenance.mjs`, and any other `scripts/check-*.mjs` validators, only verify action identity, enforce full-SHA pin syntax, and apply their specific security/topology semantics—without duplicating mutable release SHA constants or identity→pin mappings. This prevents Dependabot/updates from becoming unsatisfiable across the workflow family.
Applied to files:
scripts/check-release-distribution.mjsscripts/check-release-distribution.test.mjs
🪛 LanguageTool
docs/topics/distribution/test-plan.md
[uncategorized] ~137-~137: The official name of this software platform is spelled with a capital “H”.
Context: ...s admitted, not merely pattern-matched; .github/workflows/release.yml; `scripts/chec...
(GITHUB)
🔇 Additional comments (8)
docs/topics/distribution/README.md (1)
36-39: LGTM!docs/topics/distribution/test-plan.md (1)
26-29: LGTM!CHANGELOG.md (1)
12-28: LGTM!ROADMAP.md (1)
162-167: LGTM!docs/goalposts/v0.4.0/release.md (1)
73-81: LGTM!Also applies to: 136-136
docs/goalposts/v0.4.0/verification.md (1)
49-49: 📐 Maintainability & Code QualityRun the required documentation checks before merge.
The supplied context does not include results for
markdownlint-cli2, diff whitespace checks, internal-link checks, or documentation-citation checks. Run all four checks over the changed Markdown files and report any failure.Source: Coding guidelines
.continuum/release.yml (1)
134-138: LGTM!.github/workflows/release.yml (1)
238-254: 📐 Maintainability & Code QualityNo workflow change needed.
Code Lawyer — Activity SummaryAll P0/P1 findings from the self-audit are resolved in
Finding 5 — accepted, not fixed
Bonus: the packet validator caught me mid-fixA first draft of the witness row said the SBOMs were "attestation-verified" — a completed claim Residual riskThe release workflow still has no dry-run; a tag remains its first execution. That is the Gates: 34/34 release-distribution · 49/49 release-packet · live distribution checker, |
The validator pinned taiki-e/install-action's tool but ignored its fallback, so a workflow could switch to cargo-binstall or a source build and still satisfy the release gate, changing how the pinned SBOM tool is obtained without review. Require fallback: none, matching the intent of the existing pin. New case rejects cargo-binstall, build, and an omitted fallback. Refs #227.
The pin cargo-cyclonedx@0.5.9 was owned independently by the policy script and its fixture, so bumping one could leave the release gate asserting a version the workflow no longer installs, with the suite still green. Export EXPECTED_SBOM_TOOL from the canonical policy script and have the fixture read it, matching how the other expected-policy constants are shared. Refs #227.
The runbook verified the SBOMs but never required recording the outcome, and DIST-9a deferred public evidence to DIST-7a, which covers editor and server artifacts. An SBOM could therefore be generated, attested, and verified while the release witness stayed silent about it. DIST-9a now owns its public evidence and names what must be recorded: both asset filenames, each JSON validation result, and each attestation verification result. The runbook states that running the commands is not the evidence; the recorded outcome is. Refs #227.
The release profile parser returned only document.distribution, so publish.artifacts was never validated. The SBOM entries added to .continuum/release.yml were therefore decorative: deleting or renaming either one passed every gate, while the runbook and witness continued to claim both assets ship. Add an exported EXPECTED_SBOM_ARTIFACT contract, parse publish.artifacts into the snapshot, and require an entry whose name, platform, and contents match both asset paths. Scoped to the SBOM entry rather than the whole inventory, since the native, editor, and formula artifacts are already constrained by EXPECTED_PLATFORMS and the workflow topology checks. Five mutations reject a missing entry, either asset dropped, a renamed destination, and empty contents. Verified against the real profile as well as the fixture: deleting the entry from .continuum/release.yml makes the live checker fail, and restoring it passes. Refs #227.
What changed
Build provenance attestation already shipped (
release.ymlactions/attest);the bill-of-materials half did not. The tag workflow now generates an attested
CycloneDX SBOM for every release.
Closes #227
Evidence
Workflow.
Install SBOM toolpinscargo-cyclonedx@0.5.9through theestablished
taiki-e/install-actionpattern (same reviewed SHA already used forcargo-mutants,cargo-llvm-cov,cargo-deny, andzizmor).Generate SBOMbuilds a CycloneDX document from the locked dependency graph into
dist/,so it publishes as a release asset via the existing
gh release create … dist/*.The SBOM is added to the
Attest Homebrew and editor artifactssubject-path, soit carries the same GitHub/Sigstore provenance as the bytes it describes.
Ordering is load-bearing and enforced. Generation runs after the native
archives are downloaded, so the dependency graph ships beside the artifacts it
covers, and before attestation, so it cannot be published unattested. Both
constraints are expressed in
REVIEWED_RELEASE_STEP_ORDERrather than left toconvention.
Policy.
check-release-distribution.mjsgains DIST-9 enforcement.Deterministic mutations reject each refused shape:
cargo-cyclonedxwith no version)dist/Two existing cases asserted the previous attestation message and are updated
deliberately, since the attested set genuinely changed — flagged rather than
quietly edited.
Packet.
docs/goalposts/v0.4.0/release.mdcarries #227 in both Must shipand Scoped slices; the validator's symmetric inventory admits 34 scoped
issues (was 33). ROADMAP moves #227 parked → delivered: it was parked pending
"the distribution and signing authorities", which #245 and #251 established.
Gates.
node --test scripts/check-release-distribution.test.mjs— 33/33, 0 fail.node scripts/check-release-distribution.mjs— 3 native platforms, 2 Homebrewplatforms, 3 editor registries.
node scripts/check-release-packet.mjs— 4 goalposts, 34 scoped issues.node scripts/check-workflow-security.mjs— zizmor 1.28.0, 5 workflows clean.node scripts/check-dependency-update-policy.mjs— satisfied (pin authority).mise exec node@22.23.1 -- bash scripts/release-prep.sh— RELEASE PREPPASSED on the pushed tree: 80 mutants with 63 caught, 17 unviable, zero
survivors; packaged editor smoke; 74 Markdown files, 0 errors.
Why now
The tag is immutable. An SBOM absent at tag time cannot be added to that
release, so this had to land before v0.4.0 rather than after.
Checklist
main—docs/topics/distribution/README.mddescribes the mechanism, and makes nopublic-URL or availability claim.
DIST-9a case added to
docs/topics/distribution/test-plan.md.CHANGELOG.md/ROADMAP.mdupdated.cargo fmt,cargo clippy -D warnings,cargo testpass locally.