feat(release): score dependency floors and cargo features in semver-level - #2506
feat(release): score dependency floors and cargo features in semver-level#2506iunanua wants to merge 3 commits into
Conversation
…evel
semver-level.sh combined cargo-semver-checks and cargo-public-api, which both
read rustdoc and neither of which reads a manifest. Two semver-relevant
manifest changes therefore came out as patch:
- a raised dependency requirement floor (`http = "1"` -> `"1.1"`), after which
a consumer pinned to the old version can no longer resolve this crate;
- an added cargo feature, for which cargo-semver-checks has no lint at all and
which adds no rustdoc item unless it happens to gate one.
Add two passes that read both facts through `cargo metadata`, so a requirement
behind `{ workspace = true }` is compared as the version it resolves to and the
implicit feature of an `optional = true` dependency is counted. Requirements
are keyed by `.rename` as well as name, kind and target, so a crate aliasing
two versions of one package does not match the second alias against the first
alias's baseline. Exclusive bounds carry a flag, so `>=1.2.3` -> `>1.2.3` is
seen as the narrowing it is.
Feature removals and default-set removals are scored as a backstop: for a
library crate cargo-semver-checks gets there first, but for a crate with no
library target it is skipped entirely and nothing else would notice.
A raised major floor is capped at minor here on purpose --
release-version-major-bumps.sh owns that decision and max_level() lets it win.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
semver-level.sh now scores a raised dependency requirement floor as a minor, so `build(deps): bump foo from 1.0 to 1.1` -- the conventional title for a dependency change -- would start failing the PR type rule. Conventional Commits defines `build` as changes to the build system or external dependencies, so a dependency bump under its own type has to be allowed to be a minor. Both `build:` commits in the last six months raised a real floor. ci/docs/style/test stay restricted; none of them raised a non-dev dependency floor over the same period. The breaking-change rule is untouched, so a `build` PR that breaks the API still needs `build!:`. Also correct the two messages that described the restriction as covering "API changes" only, now that a level can come from a manifest change with no API delta at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
✅ All CI checks and tests passed. 🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: c49fb4f | Docs | View more details | Give us feedback! |
Artifact Size Benchmark Reportaarch64-alpine-linux-musl
aarch64-unknown-linux-gnu
libdatadog-x64-windows
libdatadog-x86-windows
x86_64-alpine-linux-musl
x86_64-unknown-linux-gnu
|
BenchmarksComparisonBenchmark execution time: 2026-09-11 12:05:50 Comparing candidate commit c49fb4f in PR branch Found 19 performance improvements and 5 performance regressions! Performance is the same for 132 metrics, 0 unstable metrics.
|
detect-changes selected a crate only when a file under its own directory changed, so a PR editing just the root manifest's [workspace.dependencies] selected nothing: has_rust_changes came out false and both the semver-check and validate jobs were skipped, leaving any PR title accepted. Since this workspace declares dependencies version-only at the root and members inherit them with `workspace = true`, that is the ordinary shape of a floor raise here, and the level semver-level.sh scores for it -- a minor -- was unreachable from CI. semver-level.sh grows a --list-affected mode naming every member whose dependency requirements or feature surface moved between two revisions, and detect-changes adds those crates to the ones it finds by path. It reuses manifest_facts_at_rev, now able to read the whole workspace in one extraction, so the facts the detector considers cannot drift from the facts the passes score -- a detector that considered fewer would silently reselect nothing. The mode compares from `git merge-base`, matching the `base...HEAD` the changed-file search uses: measured from the baseline tip instead, a branch behind it reports every crate that moved on the baseline since the fork, putting another branch's changes on this one's report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What does this PR do?
semver-level.shcombined cargo-semver-checks and cargo-public-api. Both read rustdoc and neither reads a manifest, so two semver-relevant manifest changes came out aspatch:http = "1"→"1.1"): every rustdoc signature is byte-identical, yet a consumer pinned to the old version can no longer resolve the crate — conventionally a minor;Two passes are added, both reading the manifest through
cargo metadata:minorwhen the lowest version a requirement admits goes upminoron an added feature;majoron one removed or dropped fromdefaultThe second commit is a consequence of the first:
buildmoves into the permissive PR-title type list, because scoring a dependency floor as minor would otherwise makebuild(deps): bump foo from 1.0 to 1.1— the conventional title for a dependency change — fail the type rule.The third commit is what lets CI reach any of it.
detect-changesselected a crate only when a file under its own directory changed, so a PR editing just the root manifest's[workspace.dependencies]selected nothing:has_rust_changescame outfalse, both thesemver-checkandvalidatejobs were skipped, and every PR title was accepted — adocs:title included. Since this workspace declares dependencies version-only at the root and members inherit them withworkspace = true, that is the ordinary shape of a floor raise here, which left the pass above unreachable from CI.semver-level.shgrows a--list-affectedmode naming every member whose dependency requirements or feature surface moved between two revisions, anddetect-changesadds those crates to the ones it finds by path.Motivation
Reviewing release proposal #2482 surfaced
libdd-capabilitiesproposed aspatch, when #2350 had moved itshttprequirement from^1to the workspace entry at^1.1. That is a minor, and nothing in the pipeline could see it: the version had moved into[workspace.dependencies], so even a textual diff of the crate's ownCargo.tomlshows only{ workspace = true }with no version in it.Additional Notes
Points reviewers may want to weigh:
cargo metadata, notCargo.toml. That is what resolves{ workspace = true }to the version the root manifest declares, and what surfaces the implicit feature anoptional = truedependency creates..renameas well as name/kind/target. A crate aliasing two versions of one package emits an identical name, kind and target for both; keyed without the alias, the second matches the first's baseline requirement and an unchanged pair reads as a raise.>=1.2.3→>1.2.3is seen as the narrowing it is rather than comparing equal.minoron purpose. Whether that forces the dependent to major isrelease-version-major-bumps.sh's decision, made with the dependency graph this script cannot see;max_level()lets it win from there.feature_missingandfeature_not_enabled_by_defaultget there first; both were verified to fail the run rather than merely warn, using a two-crate repro against the pinned invocation.*_enables_featurelints' job).On the detection commit:
--list-affectedlives insemver-level.shrather than a script of its own, because it reusesmanifest_facts_at_rev— now able to read the whole workspace in one extraction — so what the detector considers a manifest fact cannot drift from what the passes score. A detector considering fewer facts silently selects nothing and reports "no change", which is the failure this commit fixes; there are no shell tests to catch that drift. 19 lines are new, the 51 they lean on are shared.git merge-base, matching thebase...HEADthe changed-file search uses. Measured from the baseline tip instead, a branch behind the baseline reports every crate that moved on the baseline since the fork, putting another branch's changes on this one's report — and since scoring uses the tip, a feature added on the baseline would read as a removal, i.e. a spurious major.detect-changeskeeps its ownpublishfilter, so apublish = falsemember such aslibdd-agent-clientshows up in the list and is dropped where that filter already lives.skip-pr-title-semver-check, and each member'sversionis literal, so the path search already finds them.commits-since-release.shselects a crate's commits withgit log "$COMMIT_RANGE" -- "$CRATE_PATH", so a root-only floor raise leaves every inheriting crate with no commits andrelease-version-bumps.shdefers it. That is the right outcome, not a second instance of the bug above: the crate's code is unchanged, the requirement its published version states is still true of that code, and requirements are minimums, so a consumer combining it with a freshly published sibling that asks for the raised floor resolves the newer dependency anyway. Deferring moves neither the version nor the tag, so the raise stays in range and the crate's next release is still scored a minor for it — late, not wrong — while the case where a crate's code does need the raised version always arrives with a change to a file of its own, which the path search already finds. What is missing there is only the saying so: a deferred crate is recorded as levelnoneand reads as "nothing happened". A follow-up reports those moves at the deferral point instead of releasing 12 crates that needed nothing.Verified on a one-line root bump of
bytesfrom1.11to1.12(12 members inherit the entry):Both manifest passes produce byte-identical output after the field shift in the shared reader (
bytes (normal): ^1.11 -> ^1.12;Cargo feature added / added: new-thing), run underRUSTUP_TOOLCHAIN=1.92.0as the workflow does.shellcheckon the script and on the extractedrun:block (actionlint's-e SC2086profile) reports nothing that was not already there.🤖 Generated with Claude Code