Skip to content

release: v0.9.5 — polish + multi-SCM comment-suppress parity#22

Merged
Metbcy merged 13 commits intomainfrom
release/v0.9.5
Apr 30, 2026
Merged

release: v0.9.5 — polish + multi-SCM comment-suppress parity#22
Metbcy merged 13 commits intomainfrom
release/v0.9.5

Conversation

@Metbcy
Copy link
Copy Markdown
Owner

@Metbcy Metbcy commented Apr 30, 2026

v0.9.5 — polish + multi-SCM parity

The "v0.9 follow-up backlog" milestone. v0.9.5 ships eight items that
were deferred to v1.0 in the v0.9 changelog, with one headline feature
that's actually a behavior change rather than polish: comment-driven
suppression bridges for Bitbucket Cloud and Azure DevOps
, giving
bomdrift parity across all four major SCMs.

Highlights

  • Per-exception SPDX allow/deny. [license] allow_exceptions /
    deny_exceptions arrays + --allow-exception / --deny-exception
    CLI flags. License expressions like Apache-2.0 WITH LLVM-exception
    now evaluated at the exception level too, not just the base.
  • Bitbucket + Azure DevOps comment-suppress bridges. Cloudflare
    Worker references with the same five guards as the v0.9 GitLab
    bridge. examples/bitbucket-pipelines/comment-bridge/ and
    examples/azure-devops/comment-bridge/. bomdrift now has
    comment-driven suppression on GitHub (action), GitLab, Bitbucket,
    and Azure DevOps.
  • bomdrift::vex::parse_synthetic_id public helper — round-trips
    bomdrift's synthetic finding IDs back to a structured kind. Lets
    external VEX tooling identify which finding a statement targets.
  • spdx crate exact-pinned to =0.10.9. SPDX list updates can
    shift LicenseId.is_gnu() membership and silently change policy
    semantics; pin makes bumps deliberate.
  • BaselineEntry / ExpiredEntry unified internally, public
    alias preserved. No behavior change.
  • CI Rust toolchain pinned to MSRV 1.88. Avoids surprises from
    newer clippy lints (1.94 added several that broke v0.8 until
    handled).
  • Single source of truth for the suppress-comment grammar:
    scripts/parse-suppress-comment.sh plus a CI sync guard
    (scripts/check-suppress-regex-sync.sh) that fails the build if
    the shell + JS copies drift.
  • GitLab note upsert + threading semantics documented in
    docs/src/gitlab-ci.md. Closes the open question from v0.7.

Process

Two parallel background sub-agents ran on isolated git worktrees
with disjoint file ownership (Rust core in one, platform/docs/CI/
bridges in the other), avoiding the cli.rs/lib.rs/markdown.rs
conflict pain that v0.8 and v0.9 had to serialize around. Both
branches merged cleanly into release/v0.9.5 with zero conflicts.

Test status

  • 369 → 389 tests (+20). All green: cargo test --release ubuntu /
    macos / windows.
  • cargo clippy --all-targets --all-features --release -- -D warnings clean.
  • cargo fmt --all -- --check clean.
  • New CI gate: scripts/check-suppress-regex-sync.sh fails the build
    if the shell + JS comment-parser copies drift.

Scope notes

Deferred (still v1.0 candidates):

  • PyPI / crates.io maintainer-set-changed — blocked on upstream APIs.
  • Custom rules / plugin system (WASM-based extensibility).
  • OCI artifact attestation verification.
  • Reachability — explicit non-goal; pair with Endor / Snyk.

Post-merge checklist (maintainer)

  1. Tag v0.9.5 on the merge commit.
  2. Re-point the floating v1 tag to v0.9.5.
  3. Cut the GitHub Release with the cosign-signed archives.
  4. Delete release/v0.9.5, feat/v0.9.5-rust, and
    feat/v0.9.5-platform branches.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Metbcy and others added 11 commits April 29, 2026 18:20
Replace dtolnay/rust-toolchain@stable with @1.88 in ci.yml, release.yml,
and docs.yml so CI tracks the documented MSRV instead of whatever
'stable' resolves to on the runner. Newer clippy lints (e.g.
cloned_ref_to_slice_refs, useless_vec, is_multiple_of) shipped in 1.94
and previously broke the build until source was adapted; pinning here
removes that surprise. Bump deliberately when Cargo.toml rust-version
is bumped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a 'How notes are upserted' subsection to docs/src/gitlab-ci.md
that pins down the previously-unverified v0.7 question of whether
the GitLab Notes API PUT preserves threading.

Documented behaviour:
- POST/PUT against /merge_requests/:iid/notes is a true upsert: the
  note ID is stable, so permalinks survive and the comment doesn't
  move in the MR timeline.
- PUT does not refire Note Hook webhooks (so a comment-bridge wired
  to Note Hook does not loop on bomdrift's own edits).
- Threaded replies live under the parent discussion, not the note,
  so reviewer replies stay attached across upserts -- matching the
  GitHub upsert shape.
- Author/signing caveat: edits surface under the project access
  token's bot identity, not the MR author.

Also explains why the diff path uses the Notes API rather than the
Discussions API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… + CI sync guard

Three places previously held copies of the /bomdrift suppress
grammar:
  - comment-suppress/entrypoint.sh (GitHub Action shell)
  - examples/gitlab-ci/comment-bridge/worker.js (Cloudflare Worker JS)
  - src/baseline.rs::parse_comment_directive (Rust, already documented
    as the canonical-via-doc-comment in v0.7)

Promote the shell side to a sourced library in
scripts/parse-suppress-comment.sh, exposing parse_bomdrift_suppress()
with documented return codes. comment-suppress/entrypoint.sh now
sources it instead of inlining the regex + ID validator.

The Cloudflare Worker bridges can't source bash (different runtime),
so worker.js declares the regex with a comment pointing at the
canonical bash file. scripts/check-suppress-regex-sync.sh extracts
both regexes, normalizes [[:space:]]<->\\s and the JS / escapes,
and fails if they disagree. The new shell-bridges CI job runs:
  - comment-suppress/test.sh (8 unit tests on the bash parser)
  - check-suppress-regex-sync.sh
  - bash -n on all shell scripts
  - node --check on every bridge worker.js

The Rust regex stays as-is — Rust regex syntax differs slightly from
POSIX/JS so the parsers can't literally share bytes. The existing
doc comment on baseline::parse_comment_directive already references
the shell counterpart; the new CI guard keeps the two flavours
that CAN share grammar (shell + JS) in lockstep.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pre-v0.9.5, ExpiredEntry duplicated four fields of BaselineEntry. They
now share one struct; expired_entries is Vec<BaselineEntry> with the
invariant that expires.is_some() and the date is strictly before today
at load time. ExpiredEntry remains a #[deprecated] type alias for
back-compat with external consumers.

The stderr warning text emitted by lib.rs is unchanged byte-for-byte;
a new regression test (expired_entry_warning_text_is_stable) pins that
format string against future drift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ft finding ids

Adds bomdrift::parse_synthetic_id() and SyntheticFindingKind, re-exported
from the crate root so external VEX tooling can decode the synthetic
finding ids bomdrift emits (e.g. as VEX statement vulnerability names)
without re-implementing string-splitting against an undocumented format.

Format remains 'bomdrift.<kind>:<purl>[:<extra>]'. The parser handles
purls (one ':' from the 'pkg:' scheme) and the bare-component-name
fallback the emitters use when component.purl is None.

Synthetic-id emitters added for parity with the SARIF rule taxonomy:
license-change, recently-published, deprecated, maintainer-set-changed.
These are pure helpers — vex::apply / vex::emit are not yet wired to
the new finding kinds (a v1.0+ scope item).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…orker bridges

Mirror the v0.9 GitLab Cloudflare Worker bridge for Bitbucket Cloud
and Azure DevOps so /bomdrift suppress <ID> comments now work as a
zero-click suppression UX on all four major SCMs.

Each bridge ships:
  - worker.js  (Cloudflare Worker, ~150 lines, plain JS, no deps)
  - README.md  (architecture diagram, threat model, deploy guide,
                platform-specific gotchas, troubleshooting table)
  - vercel-equivalent.md (port notes for Vercel / Netlify / Lambda)

The same five-guard security model as the GitLab bridge, adapted to
each platform's webhook + identity model:

Bitbucket Cloud (examples/bitbucket-pipelines/comment-bridge/):
  1. HMAC-SHA256 X-Hub-Signature against byte-exact body
  2. Event-type filter: pullrequest:comment_created only
  3. Repo-full-name allowlist (org/repo)
  4. Commenter permission: write|admin|owner via /workspaces
     /<ws>/permissions
  5. PR-context: state=OPEN AND source.full_name===destination.full_name
     (rejects fork-PR comment-suppress)
  → triggers a custom 'bomdrift-comment-suppress' pipeline.

Azure DevOps (examples/azure-devops/comment-bridge/):
  1. X-Bomdrift-Bridge-Secret custom header (constant-time compare)
  2. Event-type: ms.vss-code.git-pullrequest-comment-event
  3. Project-UUID allowlist
  4. Commenter is a member of the project's Contributors team
  5. PR-context: status=active AND targetRefName===MAIN_BRANCH
  → triggers POST /_apis/pipelines/<id>/runs with BOMDRIFT_NOTE_BODY
    as a templateParameter.

Both pipeline templates updated:
  - bitbucket-pipelines.yml gains a 'custom: bomdrift-comment-suppress'
    step (only fires when the bridge triggers it).
  - azure-pipelines.yml restructured into stages with a new
    bomdrift_suppress stage gated on the BOMDRIFT_NOTE_BODY parameter
    (normal PR builds leave it empty so only the diff stage runs).

Both new bridge worker.js files declare the canonical
BOMDRIFT_SUPPRESS_REGEX (with comment pointing at
scripts/parse-suppress-comment.sh) and are now picked up by
scripts/check-suppress-regex-sync.sh.

Docs:
  - docs/src/bitbucket.md: 'Comment-driven suppression (advanced)'
    section mirroring the GitLab equivalent.
  - docs/src/azure-devops.md: same.
  - STATUS.md: Bitbucket + Azure DevOps rows note v0.9.5 bridge support.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The v0.9 SPDX evaluator treated the right-hand side of a 'WITH' clause
as informational only — Apache-2.0 WITH LLVM-exception was permitted by
allow=[Apache-2.0] regardless of which exception applied. v0.9.5 adds
per-exception allow/deny:

  [license]
  allow_exceptions = ["LLVM-exception", "Classpath-exception-2.0"]
  deny_exceptions  = ["GCC-exception-3.1"]

  --allow-exception ID,ID  (repeatable + comma-split)
  --deny-exception  ID,ID

Semantics:

  * Base-license deny check stays conservative (any required atomic in
    the deny list → violation).
  * Base allow + exception checks share Expression::evaluate, so OR
    branches resolve correctly: (Apache-2.0 WITH LLVM-exception) OR
    BSD-3-Clause with deny_exceptions=[LLVM-exception] permits via the
    BSD-3-Clause path.
  * Both exception lists empty → exceptions are permitted (preserves
    v0.9 behavior; back-compat).

LicenseViolation::matched_rule cites the precise exception identifier
('exception:LLVM-exception denied' or 'exception:LLVM-exception not in
allow list'). The SARIF synthetic id encodes the full license string
including the WITH suffix, so partialFingerprints differ between
exception-driven and base-license violations on the same component
(asserted in a new SARIF test).

The --debug-calibration license row now surfaces matched_rule directly
(instead of the bare kind tag) so operators tuning policy see the why,
not just deny/not-allowed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Bump Cargo.toml + Cargo.lock 0.9.0 → 0.9.5.
- CHANGELOG.md: v0.9.5 entry covering per-exception SPDX allow/deny,
  Bitbucket + Azure DevOps comment-suppress bridges, public
  parse_synthetic_id helper, spdx crate exact-pin, BaselineEntry
  unification, CI Rust pin, suppress-regex single source of truth,
  GitLab threading docs.
- docs/src/roadmap.md: add "Shipped (v0.9.5 — polish + multi-SCM
  parity)" section; remove per-exception SPDX from future candidates;
  add reachability cross-reference to non-goals.
- Bump example v0.9.0 pins → v0.9.5 in README, quickstart,
  action-broke issue template.

389 tests pass, clippy + fmt clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 30, 2026

SBOM diff

Change Count
Added 1
Removed 1
Version changed 1
License changed 0

Added (1)

Show details
Ecosystem Name Version
github dtolnay/rust-toolchain 1.88

Removed (1)

Show details
Ecosystem Name Version
github dtolnay/rust-toolchain stable

Version changed (1)

Show details
Ecosystem Name Before After
cargo bomdrift 0.9.0 0.9.5

False positive? Report it · Suppress a finding? Comment /bomdrift suppress <ID> (requires the comment-suppress sub-action) · Docs

Metbcy and others added 2 commits April 29, 2026 18:43
Pinning CI Rust to 1.88 (this release) surfaces the
clippy::uninlined_format_args lint that newer toolchains had
already accepted. Inline the four offending sites:
- src/clock.rs::is_expired_iso8601 (anyhow! macro)
- src/render/markdown.rs::section_open (writeln + write)
- src/render/sarif.rs test (assert! macro)

Verified locally with rustup 1.88 toolchain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Promote clock::tests::env_lock to clock::test_env_lock (pub(crate),
#[cfg(test)]). Have baseline::tests::lock_today, vex::tests::pin_clock,
and enrich::registry::tests::days_since_zero_for_now all acquire the
same crate-wide mutex.

Before this fix, each module had a local mutex (or no mutex at all),
so parallel `cargo test` threads in different modules could race on
SOURCE_DATE_EPOCH — manifesting as an intermittent ubuntu-latest CI
failure on baseline::tests::expired_object_entry_warns_and_does_not_
suppress (PR #22 first run). macOS and Windows happened to schedule
the relevant tests in non-conflicting order.

Verified: 3 consecutive `cargo test --release --all-features` runs
green with default parallelism (was previously failing 1 in N).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Metbcy Metbcy merged commit e8853b0 into main Apr 30, 2026
9 checks passed
@Metbcy Metbcy deleted the release/v0.9.5 branch April 30, 2026 01:53
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.

2 participants