feat(release): attest a CycloneDX bill of materials for each tag - #287
flyingrobots wants to merge 1 commit into
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 #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.
Summary by CodeRabbit
WalkthroughThe tagged release workflow now installs a pinned ChangesRelease SBOM
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
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 |
|
Superseded by the branch The commit message on this branch carried Amending or force-pushing would rewrite pushed history, so the replacement is a fresh branch with an identical tree and a corrected commit message. |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
.github/workflows/release.ymlCHANGELOG.mdROADMAP.mddocs/goalposts/v0.4.0/release.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: 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.mddocs/topics/distribution/test-plan.mdROADMAP.mddocs/topics/distribution/README.mddocs/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.mddocs/topics/distribution/test-plan.mdROADMAP.mddocs/topics/distribution/README.mdscripts/check-release-distribution.mjsdocs/goalposts/v0.4.0/release.mdscripts/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.mddocs/topics/distribution/README.mddocs/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.mjsscripts/check-release-distribution.test.mjs
🔇 Additional comments (1)
CHANGELOG.md (1)
12-24: 📐 Maintainability & Code QualityRun 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 |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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:
- 1: https://crates.io/crates/cargo-cyclonedx
- 2: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/README.md
- 3: https://docs.rs/crate/cargo-cyclonedx/latest
- 4: https://docs.rs/cargo-cyclonedx
- 5: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/CHANGELOG.md
🌐 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:
- 1: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/README.md
- 2: https://docs.rs/crate/cargo-cyclonedx/latest
- 3: https://docs.github.com/en/actions/reference/runners/github-hosted-runners
- 4: https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/write-workflows/choose-where-workflows-run/choose-the-runner-for-a-job
- 5: https://github.blog/changelog/2026-01-29-arm64-standard-runners-are-now-available-in-private-repositories/
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 usingcargo 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 validcargo cyclonedxfixture 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-L422scripts/check-release-distribution.test.mjs#L154-L160scripts/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.
| cargo cyclonedx --locked --format json --override-filename sbom | ||
| cp sbom.cdx.json "dist/colorful-language-${GITHUB_REF_NAME}-sbom.cdx.json" |
There was a problem hiding this comment.
🗄️ 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)))
PYRepository: 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)
PYRepository: 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:
- 1: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/README.md
- 2: https://crates.io/crates/cargo-cyclonedx
- 3: https://github.com/CycloneDX/cyclonedx-rust-cargo/tree/main/cargo-cyclonedx
- 4: https://github.com/CycloneDX/cyclonedx-rust-cargo
- 5: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/CHANGELOG.md
🏁 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:
- 1: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/README.md
- 2: https://docs.rs/crate/cargo-cyclonedx/latest
- 3: https://crates.io/crates/cargo-cyclonedx/0.5.9
- 4: https://crates.io/crates/cargo-cyclonedx
🌐 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:
- 1: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/README.md
- 2: https://docs.rs/crate/cargo-cyclonedx/latest
- 3: https://crates.io/crates/cargo-cyclonedx
- 4: Add --workspace-sboms flag for workspace SBOMs CycloneDX/cyclonedx-rust-cargo#870
- 5: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/CHANGELOG.md
🏁 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))
PYRepository: 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:
- 1: https://github.com/CycloneDX/cyclonedx-rust-cargo/releases/tag/cargo-cyclonedx-0.5.9
- 2: https://crates.io/crates/cargo-cyclonedx/0.5.9
- 3: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/CHANGELOG.md
- 4: https://github.com/CycloneDX/cyclonedx-rust-cargo/blob/main/cargo-cyclonedx/README.md
- 5: https://crates.io/crates/cargo-cyclonedx
- 6: Add --workspace-sboms flag for workspace SBOMs CycloneDX/cyclonedx-rust-cargo#870
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-L251scripts/check-release-distribution.mjs#L417-L422scripts/check-release-distribution.test.mjs#L154-L160scripts/check-release-distribution.test.mjs#L964-L1006
📍 Affects 3 files
.github/workflows/release.yml#L250-L251(this comment)scripts/check-release-distribution.mjs#L417-L422scripts/check-release-distribution.test.mjs#L154-L160scripts/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.
| 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) | ||
| ) { |
There was a problem hiding this comment.
🔒 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.mjsRepository: 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:
- 1: https://github.com/taiki-e/install-action
- 2: https://github.com/taiki-e/install-action/blob/c30ba1cbe2face7f695d394a2c95af204d417b29/README.md
- 3: https://github.com/taiki-e/install-action/blob/c2838c6a47dbab3621e06138438374a93da5ce88/action.yml
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.
There was a problem hiding this comment.
💡 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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 👍 / 👎.
| !sbomSource.includes("cargo cyclonedx") || | ||
| !sbomSource.includes("--locked") || | ||
| !/dist\/[^"'\s]*sbom\.cdx\.json/u.test(sbomSource) |
There was a problem hiding this comment.
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 👍 / 👎.
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.