Skip to content

feat(release): attest a CycloneDX bill of materials for each tag - #287

Closed
flyingrobots wants to merge 1 commit into
mainfrom
feat/release-sbom-provenance
Closed

flyingrobots wants to merge 1 commit into
mainfrom
feat/release-sbom-provenance

Conversation

@flyingrobots

Copy link
Copy Markdown
Owner

What changed

Build provenance attestation already shipped (release.yml actions/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 tool pins cargo-cyclonedx@0.5.9 through the
established taiki-e/install-action pattern (same reviewed SHA already used for
cargo-mutants, cargo-llvm-cov, cargo-deny, and zizmor). Generate SBOM
builds 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 artifacts subject-path, 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. Both
constraints are expressed in REVIEWED_RELEASE_STEP_ORDER rather than left to
convention.

Policy. check-release-distribution.mjs gains DIST-9 enforcement.
Deterministic mutations reject each refused shape:

Mutation Rejected because
generation step removed no SBOM is produced
tool unpinned (cargo-cyclonedx with no version) release inputs must be exact
output written outside dist/ asset would not publish
step reordered before the native download graph would not describe shipped bytes
attestation omitting the SBOM unattested artifact

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.md carries #227 in both Must ship
and 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 Homebrew
    platforms, 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 PREP
    PASSED
    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

  • Living references describe only what is true on main —
    docs/topics/distribution/README.md describes the mechanism, and makes no
    public-URL or availability claim.
  • Planned cases marked implemented with evidence — DIST-9 requirement and
    DIST-9a case added to docs/topics/distribution/test-plan.md.
  • CHANGELOG.md / ROADMAP.md updated.
  • cargo fmt, cargo clippy -D warnings, cargo test pass locally.

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 #227 in both Must ship and Scoped slices;
the validator's symmetric inventory now admits 34 scoped issues. ROADMAP moves
#227 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; Markdown clean.

Closes #227.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Release packages now include a CycloneDX software bill of materials (SBOM) generated from locked dependencies.
    • SBOMs are attested alongside other release artifacts, supporting provenance and release-content auditing.
  • Documentation

    • Updated release planning, distribution guidance, changelog, and test-plan documentation to describe SBOM availability and verification.
  • Tests

    • Added validation for SBOM generation, placement, tool pinning, ordering, and attestation inclusion.

Walkthrough

The tagged release workflow now installs a pinned cargo-cyclonedx, generates a locked CycloneDX JSON SBOM in dist, publishes it with a tag-specific name, and includes it in artifact attestations. Release validation, tests, and documentation enforce this flow.

Changes

Release SBOM

Layer / File(s) Summary
Generate and attest the release SBOM
.github/workflows/release.yml
The release job generates a locked JSON SBOM after native archive downloads and includes the SBOM in attestation subjects.
Validate SBOM topology and attestation
scripts/check-release-distribution.mjs, scripts/check-release-distribution.test.mjs
Validation and tests require pinned installation, correct ordering, dist output, and SBOM attestation.
Document release provenance coverage
CHANGELOG.md, ROADMAP.md, docs/goalposts/v0.4.0/release.md, docs/topics/distribution/*
Project documentation records SBOM generation, publication, ordering, and attestation requirements.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseWorkflow
  participant CargoCycloneDX
  participant DistributionValidator
  participant ArtifactAttestation
  ReleaseWorkflow->>CargoCycloneDX: Install pinned tool
  ReleaseWorkflow->>CargoCycloneDX: Generate locked CycloneDX SBOM
  CargoCycloneDX->>ReleaseWorkflow: Write SBOM to dist
  DistributionValidator->>ReleaseWorkflow: Verify order, output, and pinning
  ReleaseWorkflow->>ArtifactAttestation: Attest release artifacts and SBOM
Loading

Possibly related PRs

Poem

Locked graphs assemble,
CycloneDX takes flight,
dist holds the record,
Attestations shine bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary release change: attesting a CycloneDX bill of materials for each tag.
Description check ✅ Passed The description follows the template, documents the implementation and evidence, links issue #227, and completes the checklist.
Linked Issues check ✅ Passed The changes satisfy the core objectives of issue #227 by generating, publishing, and attesting a CycloneDX SBOM for releases.
Out of Scope Changes check ✅ Passed The workflow, validation, tests, documentation, changelog, roadmap, and release packet changes all support issue #227.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@flyingrobots

Copy link
Copy Markdown
Owner Author

Superseded by the branch feat/release-sbom-attestation with identical content.

The commit message on this branch carried Closes #227, which the issue-closure contract correctly rejects: CONTRIBUTING reserves closing keywords for the PR body, because a merge commit carrying one would close the issue the moment it lands on main, bypassing the PR gate. Commits must use Refs #NN.

Amending or force-pushing would rewrite pushed history, so the replacement is a fresh branch with an identical tree and a corrected commit message.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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 @.github/workflows/release.yml:
- Around line 250-251: Update the SBOM generation and validation flow so every
workspace member’s BOM is emitted with a unique filename and published. In
.github/workflows/release.yml lines 250-251, replace the single shared
override/copy with per-member generation or collection. Update
scripts/check-release-distribution.mjs lines 417-422 to discover and validate
all generated SBOMs, and adjust the expectations and fixtures in
scripts/check-release-distribution.test.mjs lines 154-160 and 964-1006 to cover
every workspace manifest and unique output.
- Line 250: Generate target-complete SBOMs by adding target coverage to the
cargo cyclonedx command in .github/workflows/release.yml:250. Update the
validation in scripts/check-release-distribution.mjs:420-422 and the valid
command fixture in scripts/check-release-distribution.test.mjs:154-160 to
require the same strategy, then add the mutation case in
scripts/check-release-distribution.test.mjs:964-1006 that removes target
coverage and asserts validation rejects it.

In `@scripts/check-release-distribution.mjs`:
- Around line 414-423: Update the SBOM installation validation in
scripts/check-release-distribution.mjs around requiredStep and
requirePinnedAction to require both tool "cargo-cyclonedx@0.5.9" and fallback
"none", rejecting any other fallback value. In
scripts/check-release-distribution.test.mjs around the existing SBOM validation
mutations, add a test mutation that changes fallback away from "none" and assert
validation fails.
🪄 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: e6bda206-a0b4-4fcf-b545-97dfade3f970

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbbc85 and f48dc91.

📒 Files selected for processing (8)
  • .github/workflows/release.yml
  • CHANGELOG.md
  • ROADMAP.md
  • docs/goalposts/v0.4.0/release.md
  • docs/topics/distribution/README.md
  • docs/topics/distribution/test-plan.md
  • scripts/check-release-distribution.mjs
  • scripts/check-release-distribution.test.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (8)
  • GitHub Check: CodeQL (rust)
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: Rust (fmt, clippy, test)
  • GitHub Check: Generated IR and vocabulary drift
  • GitHub Check: Editor integrations (compile)
  • GitHub Check: Rust coverage
  • GitHub Check: Downstream discovery version-compat matrix
  • GitHub Check: Cargo package witness
🧰 Additional context used
📓 Path-based instructions (8)
.github/workflows/*.yml

📄 CodeRabbit inference engine (AGENTS.md)

Validate every modified GitHub Actions workflow with actionlint before pushing.

Files:

  • .github/workflows/release.yml
**/*.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:

  • CHANGELOG.md
  • docs/topics/distribution/test-plan.md
  • ROADMAP.md
  • docs/topics/distribution/README.md
  • docs/goalposts/v0.4.0/release.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:

  • CHANGELOG.md
  • docs/topics/distribution/test-plan.md
  • ROADMAP.md
  • docs/topics/distribution/README.md
  • scripts/check-release-distribution.mjs
  • docs/goalposts/v0.4.0/release.md
  • scripts/check-release-distribution.test.mjs
CHANGELOG.md

📄 CodeRabbit inference engine (AGENTS.md)

Update the changelog for release-visible changes.

Files:

  • CHANGELOG.md
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/topics/distribution/test-plan.md
  • docs/topics/distribution/README.md
  • docs/goalposts/v0.4.0/release.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
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
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
🧠 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.mjs
  • scripts/check-release-distribution.test.mjs
🔇 Additional comments (1)
CHANGELOG.md (1)

12-24: 📐 Maintainability & Code Quality

Run the required documentation checks.

Run markdownlint-cli2, whitespace checks, internal-link checks, and documentation-citation checks for these documentation changes. Report any failures with their output. As per coding guidelines, “For documentation changes, run markdownlint-cli2, diff whitespace checks, internal-link checks, and documentation-citation checks.”

Source: Coding guidelines

- name: Generate SBOM
run: |
set -euo pipefail
cargo cyclonedx --locked --format json --override-filename sbom

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Changed workflow/stat:"
git diff --stat || true

echo
echo "Locate files:"
git ls-files | grep -E '(^\.github/workflows/release\.yml$|scripts/check-release-distribution\.mjs$|scripts/check-release-distribution\.test\.mjs$)' || true

echo
echo "Relevant release workflow lines around L230-L270:"
if [ -f .github/workflows/release.yml ]; then
  nl -ba .github/workflows/release.yml | sed -n '220,270p'
fi

echo
echo "Relevant validator lines L390-L435:"
if [ -f scripts/check-release-distribution.mjs ]; then
  nl -ba scripts/check-release-distribution.mjs | sed -n '390,435p'
fi

echo
echo "Relevant valid fixture and tests lines:"
if [ -f scripts/check-release-distribution.test.mjs ]; then
  nl -ba scripts/check-release-distribution.test.mjs | sed -n '140,170p'
  nl -ba scripts/check-release-distribution.test.mjs | sed -n '950,1015p'
fi

echo
echo "Search for cargo-cyclonedx invocation/options:"
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'cargo-cyclonedx|cargo cyclonedx|--target|Generate SBOM|sbom\.cdx\.json' .github scripts 2>/dev/null || true

Repository: flyingrobots/colorful-language

Length of output: 160


🌐 Web query:

cargo-cyclonedx --target all default target docs.rs

💡 Result:

In the cargo-cyclonedx tool, the --target flag is used to specify the platform (target triple) for which the Software Bill of Materials (SBOM) should be generated [1][2]. When you set --target to all, it instructs the tool to include dependencies for all possible targets [1][2]. By default, if the --target flag is not specified, the tool records dependencies for the host platform, as determined by rustc -vV [1][2]. Regarding docs.rs, the documentation available there for cargo-cyclonedx provides the same command-line interface information as the project's README [3][4]. It confirms that the --target flag expects a target triple string (e.g., x86_64-unknown-linux-gnu) or the keyword all to encompass all possible targets [1][3]. Summary of --target behavior: - Default: Records dependencies for the current host target only [1][5]. - --target : Records dependencies for the specified target platform [1][2]. - --target all: Includes dependencies for all possible targets [1][2]. This functionality is useful for ensuring that an SBOM accurately reflects the dependencies across various platforms if your project supports multiple target architectures [5].

Citations:


🌐 Web query:

GitHub Actions ubuntu job runs on runner arm64 host architecture cargo-cyclonedx default target

💡 Result:

When running a GitHub Actions job on an arm64 Ubuntu runner, the default behavior of cargo-cyclonedx depends on the host architecture of the runner itself [1][2]. 1. GitHub Actions Arm64 Runners: GitHub provides arm64 hosted runners for Linux (Ubuntu) that can be selected using labels such as ubuntu-24.04-arm or ubuntu-26.04-arm [3][4][5]. When your job runs on one of these machines, the host architecture is aarch64 (ARM64) [5]. 2. cargo-cyclonedx Default Target: According to the cargo-cyclonedx documentation, if the --target flag is not explicitly provided, the tool defaults to the host target, which is determined by the output of rustc -vV [1][2]. Consequently, when executed on a GitHub-hosted arm64 runner, cargo-cyclonedx will default to targeting the arm64 architecture (e.g., aarch64-unknown-linux-gnu) [1][2]. If you need to generate a Software Bill of Materials (SBOM) for a different architecture while running on an arm64 host, you should explicitly specify the desired target using the --target flag (e.g., --target x86_64-unknown-linux-gnu) [1][2].

Citations:


Generate SBOMs for each shipped release target.

The release jobs publish Linux, macOS, and Windows binaries. cargo-cyclonedx records dependencies only for the current runner target unless --target is set, so this command can miss the other shipped targets.

  • .github/workflows/release.yml#L250: generate a target-complete SBOM by using cargo cyclonedx --locked --format json --target all --override-filename sbom, or generate and publish one SBOM per shipped archive.
  • scripts/check-release-distribution.mjs#L420-L422: require the repository’s chosen target-complete generation strategy.
  • scripts/check-release-distribution.test.mjs#L154-L160: update the valid cargo cyclonedx fixture to use the same target-complete command.
  • scripts/check-release-distribution.test.mjs#L964-L1006: add a mutation that removes target coverage and expects validation rejection.
📍 Affects 3 files
  • .github/workflows/release.yml#L250-L250 (this comment)
  • scripts/check-release-distribution.mjs#L420-L422
  • scripts/check-release-distribution.test.mjs#L154-L160
  • scripts/check-release-distribution.test.mjs#L964-L1006
🤖 Prompt for 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.

In @.github/workflows/release.yml at line 250, Generate target-complete SBOMs by
adding target coverage to the cargo cyclonedx command in
.github/workflows/release.yml:250. Update the validation in
scripts/check-release-distribution.mjs:420-422 and the valid command fixture in
scripts/check-release-distribution.test.mjs:154-160 to require the same
strategy, then add the mutation case in
scripts/check-release-distribution.test.mjs:964-1006 that removes target
coverage and asserts validation rejects it.

Comment on lines +250 to +251
cargo cyclonedx --locked --format json --override-filename sbom
cp sbom.cdx.json "dist/colorful-language-${GITHUB_REF_NAME}-sbom.cdx.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant workflow/test/check files =="
git ls-files .github/workflows/release.yml scripts/check-release-distribution.mjs scripts/check-release-distribution.test.mjs Cargo.toml

echo
echo "== release workflow around Generate SBOM =="
nl -ba .github/workflows/release.yml | sed -n '220,265p'

echo
echo "== check-release-distribution around sbom validation =="
nl -ba scripts/check-release-distribution.mjs | sed -n '380,455p'

echo
echo "== test snapshots around Generate SBOM =="
nl -ba scripts/check-release-distribution.test.mjs | sed -n '120,180p'
nl -ba scripts/check-release-distribution.test.mjs | sed -n '930,1025p'

echo
echo "== workspace metadata =="
python3 - <<'PY'
import json, pathlib, re
for p in [pathlib.Path(name) for name in ["Cargo.toml","Cargo.lock"]]:
    print(f"\n{p.exists()} {p}")
cm=json.loads(pathlib.Path("Cargo.toml").read_text())
print(cm.get("workspace",{}))
print("members:", cm.get("workspace",{}).get("members",[]))
for pat in ["Cargo.toml","Cargo.lock"]:
    for f in pathlib.Path(".").rglob(pat):
        if any(part in {".git","target","node_modules"} for part in f.parts):
            continue
        print(f)
        txt=f.read_text()
        print("  has[workspace]:", "[workspace]" in txt)
        if "[package]" in txt:
            print("  package name/version:", dict(re.findall(r'(?m)^(name|version)\s*=\s*"([^"]+)"', txt)))
PY

Repository: flyingrobots/colorful-language

Length of output: 418


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== release workflow around Generate SBOM =="
awk '{printf "%6d\t%s\n", NR, $0}' .github/workflows/release.yml | sed -n '220,265p'

echo
echo "== check-release-distribution around sbom validation =="
awk '{printf "%6d\t%s\n", NR, $0}' scripts/check-release-distribution.mjs | sed -n '380,455p'

echo
echo "== test snapshots around Generate SBOM =="
awk '{printf "%6d\t%s\n", NR, $0}' scripts/check-release-distribution.test.mjs | sed -n '120,180p'
awk '{printf "%6d\t%s\n", NR, $0}' scripts/check-release-distribution.test.mjs | sed -n '930,1025p'

echo
echo "== workspace manifests =="
python3 - <<'PY'
import json, pathlib, re
p=next(pathlib.Path(".").glob("Cargo.toml"))
txt=p.read_text()
data=json.loads(txt)
print("Cargo.toml workspace:", data.get("workspace", {}))
print("members:", data.get("workspace", {}).get("members", []))
print("Cargo.lock members/packages:")
lock=json.loads(p.parent.joinpath("Cargo.lock").read_text())
for pkg in lock.get("package", []):
    print(pkg.get("name"), pkg.get("version"), "workspace=", pkg.get("workspace"))
for f in p.parent.rglob("Cargo.toml"):
    if any(part in {".git","target","node_modules"} for part in f.parts):
        continue
    c=json.loads(f.read_text())
    name=c.get("package",{}).get("name")
    has_ws="[workspace]" in f.read_text()
    print(f"{f}: package={name} has_workspace={has_ws}")
PY

echo
echo "== lockfiles =="
git ls-files '**/Cargo.lock' '**/Cargo.toml'
python3 - <<'PY'
import json, pathlib
for f in pathlib.Path(".").rglob("Cargo.lock"):
    p=f.as_posix()
    if any(part in {".git","target","node_modules"} for part in f.parts):
        continue
    print(p)
PY

Repository: flyingrobots/colorful-language

Length of output: 11832


🌐 Web query:

cargo-cyclonedx workspace Cargo.lock Cargo.toml --override-filename BOM default filename sbom.cdx.json

💡 Result:

The command you provided has incorrect syntax for the cargo-cyclonedx tool. The tool does not accept positional arguments like Cargo.lock or Cargo.toml [1][2][3]. Instead, it automatically detects these files when run from within a Rust project directory [4]. To generate an SBOM with a specific filename while working with a workspace, you should use the following command structure: cargo cyclonedx --format json --override-filename sbom.cdx.json Explanation of the flags: --format json: Specifies that the output should be in JSON format [1][2]. --override-filename sbom.cdx.json: Sets the output file to your desired name, overriding the default behavior [1][2][5]. Important details: Workspace Behavior: When run in a workspace, cargo-cyclonedx by default produces an SBOM file adjacent to every Cargo.toml file in the workspace [2][3]. Manifest Path: If you need to specify a different path to a specific Cargo.toml (rather than running it at the workspace root), use the --manifest-path flag instead of providing it as a positional argument [2][3]. For example, to target a specific member of a workspace: cargo cyclonedx --manifest-path path/to/member/Cargo.toml --override-filename sbom.cdx.json

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files =="
git ls-files '**/Cargo.toml' '**/Cargo.lock' '**/Cargo.lock' | sed -n '1,80p'

echo
echo "== workspace/Cargo files =="
for f in $(git ls-files '**/Cargo.toml' '**/Cargo.lock' | sed -n '1,40p'); do
  echo "--- $f"
  cat "$f"
done

echo
echo "== cargo-cyclonedx source/config references in repo =="
rg -n "generate|bom|CycloneDX|sbom|cdx|cargo-cyclonedx|override-filename|workspace" README.md Cargo.toml Cargo.lock .github scripts -S | sed -n '1,220p'

Repository: flyingrobots/colorful-language

Length of output: 50386


🌐 Web query:

cargo cyclonedx 0.5.9 --override-filename sbom Cargo.toml workspace output filename sbom.cdx.json

💡 Result:

To generate a CycloneDX SBOM with a specific filename using cargo-cyclonedx 0.5.9, you should use the --override-filename flag [1][2][3]. The command you provided has a minor syntax error regarding how the filename is specified. The --override-filename flag takes the desired filename as its argument. Note that if you provide a specific filename (e.g., sbom.cdx.json), the tool will use that exact string [1][2]. The correct command structure is: cargo cyclonedx --override-filename sbom.cdx.json Explanation: - The --override-filename flag is specifically designed to set a custom string for the output filename [1][2]. - You do not need to specify the output format (e.g., JSON) in the filename itself if you are also using the -f json flag, but including the correct file extension in the override string is recommended for clarity [1][3]. - Ensure you are running this from the directory containing your Cargo.toml, or use the --manifest-path flag if targeting a different location [4]. If you intended to process a workspace, cargo cyclonedx will typically generate an SBOM for each member of the workspace [4]. Using --override-filename will apply that name to the generated files [1].

Citations:


🌐 Web query:

cargo-cyclonedx source generate_workspace_sboms override_filename Cargo.toml

💡 Result:

The --override-filename flag is a command-line option for the cargo-cyclonedx tool used to specify a custom string for the output filename of the generated SBOM [1][2]. When running cargo cyclonedx in a workspace, the tool by default generates an SBOM file adjacent to every Cargo.toml file in the workspace [1][3]. Because of this default behavior, using --override-filename can lead to collisions or difficulties in identifying which SBOM corresponds to which crate if the same override filename is applied across the entire workspace [4]. Important technical considerations include: 1. Scope: Configuration of cargo-cyclonedx is strictly command-line based; support for configuration via Cargo.toml was explicitly removed in past updates [5]. 2. Alternative Approaches: For workspaces, users often prefer using individual invocations of the command with --manifest-path and --override-filename to ensure each crate's SBOM is uniquely identified and located correctly [4]. 3. Flag purpose: The --override-filename flag replaces older options like --output-prefix and --output-pattern [5]. For more information, you can view the tool's help output by running cargo cyclonedx --help [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Extract relevant Cargo.toml/workspace metadata only.
python3 - <<'PY'
import json, pathlib, re
for f in pathlib.Path(".").glob("Cargo.toml"):
    txt=f.read_text()
    if "[workspace]" not in txt:
        continue
    data=json.loads(txt)
    print(f"CARGO_WORKSPACE={f}")
    print(data.get("workspace"))
    if "members" in data.get("workspace", {}):
        for m in data["workspace"]["members"]:
            mf=f.parent/m
            print(f"WORKSPACE_MEMBER={mf}")
            for lf in (mf, mf/"Cargo.toml"):
                if lf.exists():
                    try:
                        print("WORKSPACE_MEMBER_FILE=", lf, "workspace=", json.load(lf.open()).get("package",{}).get("name"))
                    except Exception:
                        pass
PY

echo
echo "== lockfile package counts =="
python3 - <<'PY'
import json
lock=json.load(open("Cargo.lock"))
pkgs=[p for p in lock["package"] if p["name"] in ["colorful-cli","colorful-core","colorful-language-fuzz","colorful-language-zed"]]
for p in pkgs:
    print(p["name"], p.get("version"), p.get("dependencies"))
PY

echo
echo "== root workspace manifest summary =="
python3 - <<'PY'
import json, pathlib
all_packages=[]
for f in pathlib.Path(".").rglob("Cargo.toml"):
    if any(part in {".git","target","node_modules"} for part in f.parts):
        continue
    txt=f.read_text()
    if "[workspace]" in txt:
        print(f"WORKSPACE_ROOT_OR_LOCAL={f}")
        all_packages.extend((f,x) for x in txt.splitlines() if x.startswith("name = "))
    try:
        pkg=json.load(f.open()).get("package",{})
        all_packages.append((f,pkg.get("name")))
    except Exception as e:
        all_packages.append((f,None))
for f,n in all_packages[:120]:
    print(f"{n}@{f}")
print("TOTAL_PACKAGE_MANIFESTS=", len(all_packages))
PY

Repository: flyingrobots/colorful-language

Length of output: 679


🌐 Web query:

crates.io cargo-cyclonedx 0.5.9 changelog override filename workspace README

💡 Result:

The cargo-cyclonedx version 0.5.9 was released on March 19, 2026 [1][2][3]. Regarding the use of --override-filename in a workspace context: 1. Changelog: The 0.5.9 release focused on additions like support for SOURCE_DATE_EPOCH and CARGO_BUILD_TARGET, as well as several fixes and changes including an increased MSRV to 1.85 [1][3]. It did not introduce new features specifically for workspace filename overriding [1][3]. 2. Override Filename Behavior: The --override-filename flag is a standard option available in cargo-cyclonedx [4][5]. It allows you to specify a custom string for the output filename [4][5]. 3. Workspace Usage: By default, when running cargo cyclonedx in a workspace, the tool produces a bom.xml (or.json) file adjacent to every Cargo.toml file in the workspace [5]. If you use the --override-filename flag during a workspace-wide execution, it applies that same filename to the SBOM generated for every member crate [6]. Because this results in filename collisions (every crate trying to write to the same filename in their respective directories), users often work around this by identifying individual Cargo.toml files first and iterating over them, applying a unique --override-filename for each [6]. As of version 0.5.9, there is no built-in mechanism to automatically generate unique filenames for workspace members based on crate names when using the override flag [6].

Citations:


Publish every generated workspace SBOM.

cargo cyclonedx emits a BOM for each workspace Cargo.toml. With --override-filename sbom, .cargo/config.toml can still make the next copy collision into sbom.cdx.json. cp sbom.cdx.json ... then either fails or publishes only the root member’s BOM. Use per-workspace-members manifests with unique --override-filename values, or collect renamed BOMs from each member directory.

  • .github/workflows/release.yml#L250-L251
  • scripts/check-release-distribution.mjs#L417-L422
  • scripts/check-release-distribution.test.mjs#L154-L160
  • scripts/check-release-distribution.test.mjs#L964-L1006
📍 Affects 3 files
  • .github/workflows/release.yml#L250-L251 (this comment)
  • scripts/check-release-distribution.mjs#L417-L422
  • scripts/check-release-distribution.test.mjs#L154-L160
  • scripts/check-release-distribution.test.mjs#L964-L1006
🤖 Prompt for 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.

In @.github/workflows/release.yml around lines 250 - 251, Update the SBOM
generation and validation flow so every workspace member’s BOM is emitted with a
unique filename and published. In .github/workflows/release.yml lines 250-251,
replace the single shared override/copy with per-member generation or
collection. Update scripts/check-release-distribution.mjs lines 417-422 to
discover and validate all generated SBOMs, and adjust the expectations and
fixtures in scripts/check-release-distribution.test.mjs lines 154-160 and
964-1006 to cover every workspace manifest and unique output.

Comment on lines +414 to +423
const sbomInstall = requiredStep(steps, "Install SBOM tool", context);
requirePinnedAction(sbomInstall, "taiki-e/install-action", context);
const sbomGenerate = requiredStep(steps, "Generate SBOM", context);
const sbomSource = String(sbomGenerate.run ?? "");
if (
sbomInstall.with?.tool !== EXPECTED_SBOM_TOOL ||
!sbomSource.includes("cargo cyclonedx") ||
!sbomSource.includes("--locked") ||
!/dist\/[^"'\s]*sbom\.cdx\.json/u.test(sbomSource)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'scripts/(check-release-distribution|check-release-distribution\.test)\.mjs$' || true

echo "== relevant validator lines =="
sed -n '380,435p' scripts/check-release-distribution.mjs

echo "== relevant test lines =="
sed -n '930,1025p' scripts/check-release-distribution.test.mjs

echo "== fallback occurrences =="
rg -n "fallback|Install SBOM tool|Generate SBOM|cargo-cyclonedx|sbom" scripts/check-release-distribution.mjs scripts/check-release-distribution.test.mjs

Repository: flyingrobots/colorful-language

Length of output: 7951


🌐 Web query:

taiki-e install-action fallback cargo-binstall fallback none documentation

💡 Result:

In the taiki-e/install-action GitHub Action, the fallback input controls how the action attempts to install tools that are not explicitly supported or when standard installation methods fail [1][2]. Setting fallback: none explicitly disables all fallback installation mechanisms [1][2]. By default, the fallback input is set to cargo-binstall [1][3]. The available options for this input are: - none: Disables all fallback options [1][2]. - cargo-binstall: Uses cargo-binstall as a fallback, which may include installing prebuilt binaries or building from source [1][2]. - cargo-install: Uses cargo install as a fallback [1][2]. Using fallback: none is recommended if you want to ensure that no fallback mechanism is used, which also enhances security by preventing the action from passing the GitHub token to cargo-binstall [1][2]. When cargo-binstall is used as a fallback, the action may pass the ${{ github.token }} to it to bypass API rate limits; setting fallback: none prevents this behavior [1][2]. Example usage: - uses: taiki-e/install-action@v2 with: tool: cargo-hack fallback: none

Citations:


Enforce the no-fallback installation policy.

The Generate SBOM install step does not require fallback: none, so the validator passes a later workflow change that restores the action’s default cargo-bbuild fallback path, which can use the action token. Reject any Install SBOM tool step that is not { tool: "cargo-cyclonedx@0.5.9", fallback: "none" }, and add a test mutation that changes fallback away from none.

📍 Affects 2 files
  • scripts/check-release-distribution.mjs#L414-L423 (this comment)
  • scripts/check-release-distribution.test.mjs#L964-L1006
🤖 Prompt for 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.

In `@scripts/check-release-distribution.mjs` around lines 414 - 423, Update the
SBOM installation validation in scripts/check-release-distribution.mjs around
requiredStep and requirePinnedAction to require both tool
"cargo-cyclonedx@0.5.9" and fallback "none", rejecting any other fallback value.
In scripts/check-release-distribution.test.mjs around the existing SBOM
validation mutations, add a test mutation that changes fallback away from "none"
and assert validation fails.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f48dc91143

ℹ️ 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".

run: |
set -euo pipefail
cargo cyclonedx --locked --format json --override-filename sbom
cp sbom.cdx.json "dist/colorful-language-${GITHUB_REF_NAME}-sbom.cdx.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Generate a workspace SBOM before copying it

On every tag, this cp looks for sbom.cdx.json at the repository root, but this is a virtual workspace whose packages live under crates/*; cargo-cyclonedx 0.5.9 documents that workspace mode creates one BOM for every crate next to that crate's Cargo.toml, so --override-filename sbom produces files such as crates/colorful-cli/sbom.cdx.json, not this source path (cargo-cyclonedx 0.5.9 documentation). Because the step uses set -euo pipefail, the missing source aborts the release before attestation or publication; generate a single aggregate workspace BOM or explicitly combine/copy the per-package outputs.

Useful? React with 👍 / 👎.

deterministic generator, archive-integrity, and release-policy mutation
tests. *Tracking:*
[#251](https://github.com/flyingrobots/colorful-language/issues/251).
- **DIST-9a** — *Requirement:* DIST-9. *Behavior:* the tag workflow installs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep DIST-8a evidence under its own case

This new bullet starts before DIST-8a's existing Evidence and Status continuation, so Markdown now assigns the Homebrew generator evidence and tap-specific status on lines 117–131 to DIST-9a instead. Consequently DIST-8a has no recorded executable evidence/status, while DIST-9a records unrelated evidence rather than the newly added SBOM tests; move DIST-9a after the completed DIST-8a block and give it its own actual evidence and implemented status.

AGENTS.md reference: AGENTS.md:L115-L124

Useful? React with 👍 / 👎.

Comment on lines +420 to +422
!sbomSource.includes("cargo cyclonedx") ||
!sbomSource.includes("--locked") ||
!/dist\/[^"'\s]*sbom\.cdx\.json/u.test(sbomSource)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-executing SBOM generation steps

The new release-policy check only searches the step's source text, so a workflow mutation that leaves this body intact but adds if: false, or replaces it with echo 'cargo cyclonedx --locked dist/fake-sbom.cdx.json', still passes check-release-distribution.mjs without generating any SBOM. In that scenario the supposedly fail-closed admission gate succeeds and the tag workflow fails only later when the copy or attestation cannot find the asset; validate that the step is unconditional and match an executable reviewed command sequence rather than independent substrings.

Useful? React with 👍 / 👎.

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.

Idea: emit a signed SBOM/build-provenance attestation at release using existing canonical-hash infra

1 participant