Skip to content

Commit d593d92

Browse files
authored
ci: gate the public API against the published release, and stop the pins drifting (#63)
Eleven crates are on crates.io at 2.1.0 — the issue said ten — and no workflow ran cargo-semver-checks. Why the release type is forced, measured rather than assumed. Removing `Config::kernel` from launchbound-space while bumping the manifest to 3.0.0 in the same PR: inferred gives "0 checks: 0 pass, 254 skip" and exit 0; `--release-type patch` gives "1 major and 0 minor checks failed" and exit 100. cargo-semver-checks runs only the lints the declared bump would not already excuse, so a PR that declares its own major bump silences the gate. `patch` excuses nothing. It does not forbid additions — verified at 223 checks, 223 pass, against a branch carrying two new public items. A deliberate break carries the new `breaking` label, which switches the job to `major`. The pins were not only cosmetic. `[workspace.dependencies]` pinned the path crates at 2.0.0 while the workspace was 2.1.0. Harmless for a path build, which is why it survived the whole 2.1.0 line — but with the pins stale, bumping to 3.0.0 makes `cargo metadata` refuse to resolve the workspace at all. The drift breaks the release after next. `scripts/check-versions.sh` (in `just ci`, bash-3.2-portable) asserts the workspace version, the eight internal pins, and that no crate sets a literal version of its own. Both failure shapes were exercised. RELEASING.md gains the pin bump in step 1 and `baseline-version` as its own step after publish. Note: `main` has no branch protection, so this gate reports rather than blocks. Closes #56 Signed-off-by: Vyncint Ng <115854244+vyncint@users.noreply.github.com>
1 parent b44fd77 commit d593d92

5 files changed

Lines changed: 156 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,54 @@ jobs:
4444
name: termlens-report-ci-${{ matrix.os }}
4545
cli-version: "0.10.1" # checked against the manifest by check-skill-version.sh
4646

47+
# The public API against the last published release.
48+
#
49+
# `--release-type` is FORCED, never inferred, and the reason is measured
50+
# rather than assumed. Removing `Config::kernel` from launchbound-space
51+
# while bumping the manifest to 3.0.0 in the same PR:
52+
#
53+
# inferred -> "0 checks: 0 pass, 254 skip", exit 0
54+
# --release-type patch -> "1 major and 0 minor checks failed", exit 100
55+
#
56+
# cargo-semver-checks compares the manifest version to the baseline and
57+
# runs only the lints that bump would not already excuse. Declare the major
58+
# bump in the same commit as the break and it has nothing left to say. That
59+
# is the hole: the gate agrees with whatever the PR claims about itself.
60+
#
61+
# `patch` means "no API change of any kind is excused", so every lint
62+
# fires. It does NOT forbid additions -- cargo-semver-checks only reports
63+
# breaking changes, so a new `pub fn` passes (verified: 223 checks, 223
64+
# pass, with a `pub use` added).
65+
#
66+
# A deliberate break carries the `breaking` label, which switches this to
67+
# `major`. Three of the eleven published crates are binary-only and have no
68+
# library API; the eight that do are all covered by --workspace.
69+
#
70+
# `baseline-version` is a literal, moved in the release PR after publish --
71+
# left at the old release it would compare against a version nobody can
72+
# install, and moved before publish it would compare against one that does
73+
# not exist yet.
74+
semver:
75+
runs-on: ubuntu-latest
76+
steps:
77+
- uses: actions/checkout@v4
78+
- name: Install pinned toolchain from rust-toolchain.toml
79+
run: rustup show
80+
# A prebuilt binary, downloaded every run: never built from source, and
81+
# never restored from a cache. A gate that reports on whatever a cache
82+
# held the day it was filled is not reporting on this PR.
83+
- uses: taiki-e/install-action@v2
84+
with:
85+
tool: cargo-semver-checks
86+
- name: Compare the public API against the published baseline
87+
env:
88+
RELEASE_TYPE: ${{ contains(github.event.pull_request.labels.*.name, 'breaking') && 'major' || 'patch' }}
89+
run: |
90+
echo "release-type: $RELEASE_TYPE"
91+
cargo semver-checks --workspace \
92+
--baseline-version 2.1.0 \
93+
--release-type "$RELEASE_TYPE"
94+
4795
# MSRV applies to the crates that do not need the pinned nightly
4896
# (CONTRIBUTING.md); checked against the committed lockfile.
4997
msrv:

Cargo.toml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,19 @@ keywords = ["cuda", "gpu", "autotuner", "convergence", "kernel"]
3333
categories = ["development-tools", "development-tools::profiling"]
3434

3535
[workspace.dependencies]
36-
launchbound-space = { path = "crates/launchbound-space", version = "2.0.0" }
37-
launchbound-prune = { path = "crates/launchbound-prune", version = "2.0.0" }
38-
launchbound-build = { path = "crates/launchbound-build", version = "2.0.0" }
39-
launchbound-bench = { path = "crates/launchbound-bench", version = "2.0.0" }
40-
launchbound-report = { path = "crates/launchbound-report", version = "2.0.0" }
41-
launchbound-search = { path = "crates/launchbound-search", version = "2.0.0" }
42-
launchbound-model = { path = "crates/launchbound-model", version = "2.0.0" }
43-
launchbound-metal = { path = "crates/launchbound-metal", version = "2.0.0" }
36+
# These `version =` pins must equal `workspace.package.version` above. They
37+
# are what a crates.io consumer resolves against, so a stale pin says a
38+
# published crate is compatible with a release it was never built against --
39+
# harmless for a path build, wrong as a record. They sat at 2.0.0 through the
40+
# whole 2.1.0 line; `just versions` (and a test) now refuse the drift.
41+
launchbound-space = { path = "crates/launchbound-space", version = "2.1.0" }
42+
launchbound-prune = { path = "crates/launchbound-prune", version = "2.1.0" }
43+
launchbound-build = { path = "crates/launchbound-build", version = "2.1.0" }
44+
launchbound-bench = { path = "crates/launchbound-bench", version = "2.1.0" }
45+
launchbound-report = { path = "crates/launchbound-report", version = "2.1.0" }
46+
launchbound-search = { path = "crates/launchbound-search", version = "2.1.0" }
47+
launchbound-model = { path = "crates/launchbound-model", version = "2.1.0" }
48+
launchbound-metal = { path = "crates/launchbound-metal", version = "2.1.0" }
4449
anyhow = "1"
4550
clap = { version = "4", features = ["derive"] }
4651
serde = { version = "1", features = ["derive"] }

docs/RELEASING.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ maintainer moving between them is not relearning the process.
2222
gh workflow run stress.yml -f iterations=100
2323
gh run watch # ten shards, both OSes
2424

25-
# 1. Bump the version. It appears once per crate plus the workspace pins.
26-
$EDITOR Cargo.toml # version = "X.Y.Z"
25+
# 1. Bump the version. `workspace.package.version` AND the internal
26+
# `version =` pins in [workspace.dependencies] -- both, or `just
27+
# versions` fails. They are not cosmetic: pins left behind a major
28+
# bump make `cargo metadata` refuse to resolve the workspace at all.
29+
$EDITOR Cargo.toml # version = "X.Y.Z", and the eight pins
2730
cargo check --workspace # refreshes Cargo.lock
31+
just versions # the two agree
2832

2933
# 2. Move the CHANGELOG section: [Unreleased] -> [X.Y.Z] - YYYY-MM-DD,
3034
# leaving an empty [Unreleased] above it.
@@ -70,6 +74,20 @@ tag had to be pushed by hand.
7074
```sh
7175
gh workflow run install.yml
7276
```
77+
- **Move the semver baseline, in a PR of its own after the publish.**
78+
`baseline-version` in `ci.yml`'s `semver` job is a literal. Left at the old
79+
release it compares every PR against a version nobody can install any more,
80+
and it would also carry this release's own breaks forward as if they were
81+
new. Moved *before* the publish it names a version that does not exist yet
82+
and the job cannot fetch it. So: publish, confirm the index has it, then
83+
bump the literal.
84+
85+
```sh
86+
$EDITOR .github/workflows/ci.yml # baseline-version: X.Y.Z
87+
```
88+
- **A break needs the `breaking` label on its PR**, which switches the semver
89+
job from `patch` to `major`. Without it the job fails, which is the point;
90+
with it, the release notes owe the reader a migration note.
7391

7492
## What a version number means here
7593

justfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ default: ci
66
# "all required jobs green" aggregator job in ci.yml — this recipe is the
77
# aggregator, and the `ci` job runs it verbatim on both OSes — so a new gate
88
# becomes required by being listed here.
9-
ci: fmt-check clippy test deny schemas pins skill
9+
ci: fmt-check clippy test deny schemas pins versions skill
1010

1111
# Cargo errors on a memberless virtual workspace, so the cargo recipes no-op
1212
# until the first crate lands in S1. `grep -c` prints 1 when packages is empty.
@@ -48,6 +48,13 @@ prune cc="8.6":
4848
pins:
4949
./scripts/check-pins.sh
5050

51+
# The workspace version, every crate's version, and the internal `version =`
52+
# pins in [workspace.dependencies] agree. The pins sat at 2.0.0 through the
53+
# whole 2.1.0 line: harmless for a path build, wrong as a record, and fatal
54+
# to the next major bump (`cargo metadata` refuses to resolve).
55+
versions:
56+
./scripts/check-versions.sh
57+
5158
# Golden + JSON Schema validation of report documents (S4).
5259
schemas:
5360
cargo test -p launchbound-report --test schema_and_golden

scripts/check-versions.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
# CI gate: the workspace version, every crate's version, and the internal
3+
# `version =` pins in [workspace.dependencies] all agree.
4+
#
5+
# The pins are what a crates.io consumer resolves against. `launchbound-prune`
6+
# 2.1.0 depending on `launchbound-space = "^2.0.0"` says it works with a
7+
# release it was never built or tested against -- harmless for a path build,
8+
# which is why it went unnoticed through the whole 2.1.0 line, and wrong as a
9+
# record. It is not only cosmetic: a `3.0.0` workspace bump with the pins left
10+
# at `2.0.0` fails `cargo metadata` outright ("candidate versions found which
11+
# didn't match: 3.0.0"), so the drift breaks the next major release rather
12+
# than the current one.
13+
#
14+
# Portability: macOS ships bash 3.2 -- no `declare -A`, no `local -n`, no
15+
# GNU-only sed. (scripts/check-pins.sh, which learned this the hard way.)
16+
set -euo pipefail
17+
18+
ROOT=$(cd "$(dirname "$0")/.." && pwd)
19+
cd "$ROOT"
20+
21+
status=0
22+
23+
# `workspace.package.version` -- the one everything else must match. Anchored
24+
# to the section so a dependency's `version =` cannot be read by mistake.
25+
workspace_version=$(
26+
sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml | head -1
27+
)
28+
if [ -z "$workspace_version" ]; then
29+
echo "VERSION GATE: could not read workspace.package.version from Cargo.toml" >&2
30+
exit 1
31+
fi
32+
echo "workspace.package.version: $workspace_version"
33+
34+
# The internal path pins.
35+
pins=$(sed -n 's/^\(launchbound-[a-z-]*\) = { path = "[^"]*", version = "\([^"]*\)".*/\1 \2/p' Cargo.toml)
36+
if [ -z "$pins" ]; then
37+
echo "VERSION GATE: no internal version pins found -- has [workspace.dependencies] changed shape?" >&2
38+
exit 1
39+
fi
40+
41+
count=0
42+
while read -r name pin; do
43+
[ -z "$name" ] && continue
44+
count=$((count + 1))
45+
if [ "$pin" != "$workspace_version" ]; then
46+
echo "VERSION GATE: $name is pinned at $pin, workspace is $workspace_version" >&2
47+
status=1
48+
fi
49+
done <<EOF
50+
$pins
51+
EOF
52+
echo " internal pins checked: $count"
53+
54+
# Every member crate inherits the workspace version rather than setting its
55+
# own; a literal here would drift silently.
56+
for manifest in crates/*/Cargo.toml; do
57+
own=$(sed -n '/^\[package\]/,/^\[/s/^version = "\(.*\)"/\1/p' "$manifest" | head -1)
58+
if [ -n "$own" ]; then
59+
echo "VERSION GATE: $manifest sets version = \"$own\" instead of version.workspace = true" >&2
60+
status=1
61+
fi
62+
done
63+
64+
if [ "$status" -eq 0 ]; then
65+
echo "every recorded version agrees"
66+
fi
67+
exit "$status"

0 commit comments

Comments
 (0)