diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba89d43..5f52b74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,54 @@ jobs: name: termlens-report-ci-${{ matrix.os }} cli-version: "0.10.1" # checked against the manifest by check-skill-version.sh + # The public API against the last published release. + # + # `--release-type` is FORCED, never inferred, and the reason is measured + # rather than assumed. Removing `Config::kernel` from launchbound-space + # while bumping the manifest to 3.0.0 in the same PR: + # + # inferred -> "0 checks: 0 pass, 254 skip", exit 0 + # --release-type patch -> "1 major and 0 minor checks failed", exit 100 + # + # cargo-semver-checks compares the manifest version to the baseline and + # runs only the lints that bump would not already excuse. Declare the major + # bump in the same commit as the break and it has nothing left to say. That + # is the hole: the gate agrees with whatever the PR claims about itself. + # + # `patch` means "no API change of any kind is excused", so every lint + # fires. It does NOT forbid additions -- cargo-semver-checks only reports + # breaking changes, so a new `pub fn` passes (verified: 223 checks, 223 + # pass, with a `pub use` added). + # + # A deliberate break carries the `breaking` label, which switches this to + # `major`. Three of the eleven published crates are binary-only and have no + # library API; the eight that do are all covered by --workspace. + # + # `baseline-version` is a literal, moved in the release PR after publish -- + # left at the old release it would compare against a version nobody can + # install, and moved before publish it would compare against one that does + # not exist yet. + semver: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install pinned toolchain from rust-toolchain.toml + run: rustup show + # A prebuilt binary, downloaded every run: never built from source, and + # never restored from a cache. A gate that reports on whatever a cache + # held the day it was filled is not reporting on this PR. + - uses: taiki-e/install-action@v2 + with: + tool: cargo-semver-checks + - name: Compare the public API against the published baseline + env: + RELEASE_TYPE: ${{ contains(github.event.pull_request.labels.*.name, 'breaking') && 'major' || 'patch' }} + run: | + echo "release-type: $RELEASE_TYPE" + cargo semver-checks --workspace \ + --baseline-version 2.1.0 \ + --release-type "$RELEASE_TYPE" + # MSRV applies to the crates that do not need the pinned nightly # (CONTRIBUTING.md); checked against the committed lockfile. msrv: diff --git a/Cargo.toml b/Cargo.toml index 9d18737..3ac490b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,14 +33,19 @@ keywords = ["cuda", "gpu", "autotuner", "convergence", "kernel"] categories = ["development-tools", "development-tools::profiling"] [workspace.dependencies] -launchbound-space = { path = "crates/launchbound-space", version = "2.0.0" } -launchbound-prune = { path = "crates/launchbound-prune", version = "2.0.0" } -launchbound-build = { path = "crates/launchbound-build", version = "2.0.0" } -launchbound-bench = { path = "crates/launchbound-bench", version = "2.0.0" } -launchbound-report = { path = "crates/launchbound-report", version = "2.0.0" } -launchbound-search = { path = "crates/launchbound-search", version = "2.0.0" } -launchbound-model = { path = "crates/launchbound-model", version = "2.0.0" } -launchbound-metal = { path = "crates/launchbound-metal", version = "2.0.0" } +# These `version =` pins must equal `workspace.package.version` above. They +# are what a crates.io consumer resolves against, so a stale pin says a +# published crate is compatible with a release it was never built against -- +# harmless for a path build, wrong as a record. They sat at 2.0.0 through the +# whole 2.1.0 line; `just versions` (and a test) now refuse the drift. +launchbound-space = { path = "crates/launchbound-space", version = "2.1.0" } +launchbound-prune = { path = "crates/launchbound-prune", version = "2.1.0" } +launchbound-build = { path = "crates/launchbound-build", version = "2.1.0" } +launchbound-bench = { path = "crates/launchbound-bench", version = "2.1.0" } +launchbound-report = { path = "crates/launchbound-report", version = "2.1.0" } +launchbound-search = { path = "crates/launchbound-search", version = "2.1.0" } +launchbound-model = { path = "crates/launchbound-model", version = "2.1.0" } +launchbound-metal = { path = "crates/launchbound-metal", version = "2.1.0" } anyhow = "1" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } diff --git a/docs/RELEASING.md b/docs/RELEASING.md index ff57f3f..eefe649 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -22,9 +22,13 @@ maintainer moving between them is not relearning the process. gh workflow run stress.yml -f iterations=100 gh run watch # ten shards, both OSes -# 1. Bump the version. It appears once per crate plus the workspace pins. -$EDITOR Cargo.toml # version = "X.Y.Z" +# 1. Bump the version. `workspace.package.version` AND the internal +# `version =` pins in [workspace.dependencies] -- both, or `just +# versions` fails. They are not cosmetic: pins left behind a major +# bump make `cargo metadata` refuse to resolve the workspace at all. +$EDITOR Cargo.toml # version = "X.Y.Z", and the eight pins cargo check --workspace # refreshes Cargo.lock +just versions # the two agree # 2. Move the CHANGELOG section: [Unreleased] -> [X.Y.Z] - YYYY-MM-DD, # leaving an empty [Unreleased] above it. @@ -70,6 +74,20 @@ tag had to be pushed by hand. ```sh gh workflow run install.yml ``` +- **Move the semver baseline, in a PR of its own after the publish.** + `baseline-version` in `ci.yml`'s `semver` job is a literal. Left at the old + release it compares every PR against a version nobody can install any more, + and it would also carry this release's own breaks forward as if they were + new. Moved *before* the publish it names a version that does not exist yet + and the job cannot fetch it. So: publish, confirm the index has it, then + bump the literal. + + ```sh + $EDITOR .github/workflows/ci.yml # baseline-version: X.Y.Z + ``` +- **A break needs the `breaking` label on its PR**, which switches the semver + job from `patch` to `major`. Without it the job fails, which is the point; + with it, the release notes owe the reader a migration note. ## What a version number means here diff --git a/justfile b/justfile index 4205d08..de30a3f 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,7 @@ default: ci # "all required jobs green" aggregator job in ci.yml — this recipe is the # aggregator, and the `ci` job runs it verbatim on both OSes — so a new gate # becomes required by being listed here. -ci: fmt-check clippy test deny schemas pins skill +ci: fmt-check clippy test deny schemas pins versions skill # Cargo errors on a memberless virtual workspace, so the cargo recipes no-op # until the first crate lands in S1. `grep -c` prints 1 when packages is empty. @@ -48,6 +48,13 @@ prune cc="8.6": pins: ./scripts/check-pins.sh +# The workspace version, every crate's version, and the internal `version =` +# pins in [workspace.dependencies] agree. The pins sat at 2.0.0 through the +# whole 2.1.0 line: harmless for a path build, wrong as a record, and fatal +# to the next major bump (`cargo metadata` refuses to resolve). +versions: + ./scripts/check-versions.sh + # Golden + JSON Schema validation of report documents (S4). schemas: cargo test -p launchbound-report --test schema_and_golden diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..71eb78e --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# CI gate: the workspace version, every crate's version, and the internal +# `version =` pins in [workspace.dependencies] all agree. +# +# The pins are what a crates.io consumer resolves against. `launchbound-prune` +# 2.1.0 depending on `launchbound-space = "^2.0.0"` says it works with a +# release it was never built or tested against -- harmless for a path build, +# which is why it went unnoticed through the whole 2.1.0 line, and wrong as a +# record. It is not only cosmetic: a `3.0.0` workspace bump with the pins left +# at `2.0.0` fails `cargo metadata` outright ("candidate versions found which +# didn't match: 3.0.0"), so the drift breaks the next major release rather +# than the current one. +# +# Portability: macOS ships bash 3.2 -- no `declare -A`, no `local -n`, no +# GNU-only sed. (scripts/check-pins.sh, which learned this the hard way.) +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" + +status=0 + +# `workspace.package.version` -- the one everything else must match. Anchored +# to the section so a dependency's `version =` cannot be read by mistake. +workspace_version=$( + sed -n '/^\[workspace\.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml | head -1 +) +if [ -z "$workspace_version" ]; then + echo "VERSION GATE: could not read workspace.package.version from Cargo.toml" >&2 + exit 1 +fi +echo "workspace.package.version: $workspace_version" + +# The internal path pins. +pins=$(sed -n 's/^\(launchbound-[a-z-]*\) = { path = "[^"]*", version = "\([^"]*\)".*/\1 \2/p' Cargo.toml) +if [ -z "$pins" ]; then + echo "VERSION GATE: no internal version pins found -- has [workspace.dependencies] changed shape?" >&2 + exit 1 +fi + +count=0 +while read -r name pin; do + [ -z "$name" ] && continue + count=$((count + 1)) + if [ "$pin" != "$workspace_version" ]; then + echo "VERSION GATE: $name is pinned at $pin, workspace is $workspace_version" >&2 + status=1 + fi +done <&2 + status=1 + fi +done + +if [ "$status" -eq 0 ]; then + echo "every recorded version agrees" +fi +exit "$status"