Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6bbf3d2
fix(release): classify releases via cargo-semver-checks vs crates.io …
Vaiz Jul 9, 2026
568dfdb
refactor(ci): compute publishing set via reusable pwsh script
Vaiz Jul 9, 2026
61003f5
fix(release): make entry point throw instead of Exit so it is testable
Vaiz Jul 9, 2026
25cbc39
test(release): mock tool-presence check in scenario harness
Vaiz Jul 9, 2026
21f522b
refactor(release): address PR review feedback
Vaiz Jul 9, 2026
b2d5167
feat(ci): richer semver PR report — table, stop sign, per-push comment
Vaiz Jul 9, 2026
6b081f6
refactor(release): registry-controlled semver baseline + review fixes
Vaiz Jul 9, 2026
071cfa0
fix(ci): robust registry baseline lookup for semver check
Vaiz Jul 9, 2026
ee2ee46
fix(ci): parse ANSI-colorized cargo info output
Vaiz Jul 9, 2026
faf4752
fix(ci): do not post semver comment when no crates are publishing
Vaiz Jul 10, 2026
3feea1c
fix(ci): distinguish under-increment (fail) from unknown baseline (warn)
Vaiz Jul 10, 2026
8ebbd42
refactor(ci): semver baseline from previous version-bump commit (git …
Vaiz Jul 10, 2026
776575c
feat(ci): show resolved baseline commit per crate in semver report
Vaiz Jul 13, 2026
6e77530
fix(ci): compare on-disk version against required minimum in semver r…
Vaiz Jul 13, 2026
3bab117
refactor(release): drop unreachable registry-absence branch in semver…
Vaiz Jul 13, 2026
e65a632
ci: use unambiguous ${env:...} form in semver fetch refspec
Vaiz Jul 13, 2026
318f777
fix(release): surface baseline lookup failures instead of silent 'none'
Vaiz Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 40 additions & 33 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,17 @@ jobs:
- name: Check Spelling
run: just spellcheck

# Runs cargo-semver-checks against each crate's PREVIOUS VERSION-BUMP COMMIT in
# git history (the most recent commit on the base branch that changed the
# crate's `[package] version`, passed as --baseline-rev), scoped to the crates
# whose `version =` is bumped in this PR (the "publishing set"). The baseline
# rustdoc is rebuilt from that commit's source, so no registry access is needed
# and the check behaves identically for OSS and enterprise/offline consumers.
# The job is informational only: it never fails the build and posts a fresh PR
# comment on every push with a per-crate table (baseline vs this-PR vs the
# minimum required version) plus a link to the run. It is a no-op on PRs that
# do not bump any crate version, since only version-bumping PRs trigger a
# publish and thus need a compatibility check against the previous version.
semver:
needs: [delta]
if: github.event_name == 'pull_request' && needs.delta.outputs.skip != 'true'
Expand All @@ -215,41 +226,37 @@ jobs:
rust-toolchain: RUST_LATEST
cargo-tools: CARGO_SEMVER_CHECKS_VERSION

# execute
- name: Semver Compatibility
id: semver
# Compute the publishing set (version-bumped published crates), run
# cargo-semver-checks per crate against its previous version-bump commit in
# git history (--baseline-rev, no registry), and render a Markdown report
# (scripts/ci/semver-report.ps1 — reuses the release library's version-diff
# / next-version helpers). Sets outputs:
# publishing = true|false (any crate version bumped)
# status = pass|warn|fail (fail = a crate is under-incremented for
# its API changes; warn = a baseline could
# not be determined so a check is
# incomplete; pass = all sufficient)
# Kept non-failing so a git/tooling hiccup never blocks the PR.
- name: Semver Report
id: report
continue-on-error: true
shell: pwsh
env:
BASE_REF: ${{ github.base_ref }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -o pipefail
cargo semver-checks --all-features --workspace ${{ needs.delta.outputs.exclude_not_affected }} --color never --baseline-rev origin/${{ github.base_ref }} 2>&1 | tee semver-output.txt

# report
- name: Prepare Semver Comment
if: steps.semver.outcome == 'failure'
run: |
echo "## ⚠️ Breaking Changes Detected" > semver-comment.txt
echo "" >> semver-comment.txt
echo '```' >> semver-comment.txt
grep -v "^ *Cloning " semver-output.txt | grep -v "^ *Building " | grep -v "^ *Built \[" | grep -v "^ *Parsing " | grep -v "^ *Parsed \[" | grep -v "^ *Checking" | grep -v "^ *Checked \[" | grep -v "^ *Finished \[" | grep -v "^ *Summary " >> semver-comment.txt
echo '```' >> semver-comment.txt
echo "" >> semver-comment.txt
echo "If the breaking changes are intentional then everything is fine - this message is merely informative." >> semver-comment.txt
echo "" >> semver-comment.txt
echo "Remember to apply a version number bump with the correct severity when publishing a version with breaking changes (\`1.x.x -> 2.x.x\` or \`0.1.x -> 0.2.x\`)." >> semver-comment.txt

- name: Post Semver Failure Comment
if: steps.semver.outcome == 'failure' && github.event.pull_request.head.repo.full_name == github.repository
uses: marocchino/sticky-pull-request-comment@v3.0.4
with:
header: semver-check
path: semver-comment.txt

- name: Remove Semver Comment on Success
if: steps.semver.outcome == 'success' && github.event.pull_request.head.repo.full_name == github.repository
uses: marocchino/sticky-pull-request-comment@v3.0.4
with:
header: semver-check
delete: true
$ErrorActionPreference = 'Stop'
git fetch --no-tags origin "+refs/heads/${env:BASE_REF}:refs/remotes/origin/${env:BASE_REF}"
./scripts/ci/semver-report.ps1 -BaseRef "origin/${env:BASE_REF}" -ReportPath semver-comment.md -RunUrl "${env:RUN_URL}"

# Post a NEW comment on every push (not a sticky update) so each run's
# result is visible in the PR timeline. Only fires when the PR actually
# publishes something and is not from a fork (forks lack a write token).
- name: Post Semver Comment
if: steps.report.outputs.publishing == 'true' && github.event.pull_request.head.repo.full_name == github.repository
env:
GH_TOKEN: ${{ github.token }}
run: gh pr comment ${{ github.event.pull_request.number }} --body-file semver-comment.md
Comment thread
Vaiz marked this conversation as resolved.

extended-analysis:
name: extended-analysis (${{ matrix.os }})
Expand Down
96 changes: 78 additions & 18 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,31 +203,91 @@ Examples:

After parsing the tokens, the planner walks the workspace dependency
graph forward from every user-source release and adds each transitive
published dependent as a cascade-source release. The cascade's change
type for each dependent is derived from whether the user-source release
exposes (in its public API) the cascaded-from package — exposing
cascades propagate the source's change type, non-exposing cascades drop
to `patch`.
published dependent as a cascade-source release. The change type for
every release in the plan — both the directly-requested (user-source)
crates and the cascade-pulled dependents — is derived by running
[`cargo semver-checks`](https://crates.io/crates/cargo-semver-checks)
against each crate's **previous version-bump commit in git history** —
the most recent commit that changed the crate's `[package] version`,
supplied to the tool as `--baseline-rev <sha>`. cargo-semver-checks
rebuilds the baseline rustdoc from the crate's source at that commit, so
**no registry access is required** and the check behaves identically for
Comment thread
Vaiz marked this conversation as resolved.
Comment thread
Vaiz marked this conversation as resolved.
open-source (crates.io) and enterprise/offline consumers. The current
working-tree API is analysed, so a coordinated release's in-progress
edits — including a dependency whose public types a dependent re-exports
— are reflected in the dependent's own API diff.

Versioning is treated as a **source-level** concern: the baseline is the
version the repository last *declared*, regardless of whether it was ever
published anywhere. This is what lets one workflow serve both public and
private/enterprise environments (which cannot reach crates.io and whose
published content lags the source), and it means an aborted release that
bumped a crate to `4.0.0` without publishing is still the baseline the
next change is measured against.

This replaces the former
`[package.metadata.cargo_check_external_types]` allowlist heuristic. That
allowlist is a hand-maintained list of the external types a crate is
*permitted* to expose; it was repurposed as a proxy for the types a
crate *actually* re-exports. When the two drift apart — an entry missing
or stale — the heuristic misjudged whether a dependent re-exports a
changed dependency, so a breaking change in an exposed dependency could
be cascaded as `patch` instead of `breaking` (the motivating defect: a
breaking change in `bytesbuf` was not propagated to `bytesbuf_io`, which
re-exports `bytesbuf` types). Analysing the real API with
`cargo semver-checks` removes the proxy entirely.

**How the change type is determined.** `cargo semver-checks` is invoked
as a CLI (not as a library) and its textual result is parsed into one of
our change types. The mapping mirrors the tool's own
[`required_bump`](https://docs.rs/cargo-semver-checks/latest/cargo_semver_checks/struct.CrateReport.html#method.required_bump)
notion (major / minor / none); the exact parsing lives in
`ConvertFrom-SemverChecksOutput` (`scripts/lib/releasing.ps1`):

| `cargo semver-checks` result | change type |
|---|---|
| a major-level change is required | `breaking` |
| only a minor-level change is required | `non-breaking` |
| compatible / no update required | `patch` |
| no prior version-bump commit (new crate) | no constraint |

Cascade dependents are floored at `patch` (they must re-release to pick
up the new dependency version even when their own public API is
unchanged), then raised to whatever their own `cargo semver-checks`
result requires.

**Baseline semantics.** The baseline is the crate's previous
version-bump commit — the most recent commit (before the change under
review) that altered the crate's `[package] version`. Because it comes
from git history rather than a registry, a version that was committed but
never published *is* the baseline: an aborted release that bumped
`bytesbuf` to `4.0.0` without publishing means the next change is
compared against `4.0.0`, not a stale published `3.3.3`. A brand-new
crate with no prior version-bump commit has no baseline and imposes no
constraint. This works offline and in enterprise environments with no
crates.io access, since the baseline API is rebuilt from the crate's own
source at the baseline commit.

The planner enforces **topological consistency**: if a user-supplied
change type for a package is *weaker* than the cascade would compute,
the planner auto-upgrades it and notes the upgrade in the review output.
The caller's `-Packages` token is therefore a *lower bound*, not a
guarantee — the caller can always elevate further on the next iteration
of the review, but cannot suppress a cascade-imposed change type.
change type for a package is *weaker* than `cargo semver-checks`
requires (for that package or via a cascade), the planner auto-upgrades
it and notes the upgrade in the review output. The caller's `-Packages`
token is therefore a *lower bound*, not a guarantee — the caller can
always elevate further on the next iteration of the review, but cannot
suppress a change type the API analysis requires.

### Errors the planner rejects

- An explicit semver that is not strictly greater than the package's
current on-disk version. (Always fatal — `-Force` does not relax this.)
- A user-supplied change type that pins the package *below* what the
cascade computes for it. (The planner can auto-upgrade ordinary
change-type tokens, but treats an explicit semver token as a hard
pin — if the explicit version is below what the cascade requires the
planner errors instead of silently overriding the caller. Pass
`-Force` to override: the pin is honored verbatim, the package's
effective change-type tag is still upgraded so further cascade
decisions are correct, and a warning is printed flagging that
- A user-supplied change type that pins the package *below* what
`cargo semver-checks` (or the cascade) computes for it. (The planner
can auto-upgrade ordinary change-type tokens, but treats an explicit
semver token as a hard pin — if the explicit version is below what the
analysis requires the planner errors instead of silently overriding
the caller. Pass `-Force` to override: the pin is honored verbatim, the
package's effective change-type tag is still upgraded so further
cascade decisions are correct, and a warning is printed flagging that
consumers may break.)

---
Expand Down
Loading
Loading