From 017bcc1b56d802d1df55cf6bbaffd536c4a3f43c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 27 Sep 2026 09:40:06 +0000 Subject: [PATCH] ci: fail release PR integrity on a lagging Cargo.lock --- scripts/check-release-pr-integrity.sh | 45 +++++++++++++++++++++++++++ tests/release_pr_integrity_test.sh | 28 ++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/scripts/check-release-pr-integrity.sh b/scripts/check-release-pr-integrity.sh index 021e29d89e..0af8d6256c 100755 --- a/scripts/check-release-pr-integrity.sh +++ b/scripts/check-release-pr-integrity.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Validates a release PR: only release metadata changed, and Cargo.lock matches +# the bumped manifests. Before merging a release PR by hand, run it with the PR +# head checked out: scripts/check-release-pr-integrity.sh origin/master HEAD set -euo pipefail usage() { @@ -70,3 +73,45 @@ if ((${#unexpected[@]})); then echo "release PR integrity: explicitly approved extra paths:" >&2 printf ' %s\n' "${unexpected[@]}" >&2 fi + +# Release-please rewrites the release branch on every master push, dropping +# the lockfile commit, and every `--locked` build fails once a lagging lock +# merges. Compare each local package's manifest version with its lock entry. +python3 - <<'PY' +import glob +import sys +import tomllib +from pathlib import Path + + +def load(path): + return tomllib.loads(Path(path).read_text()) + + +root = load("Cargo.toml") +workspace = root.get("workspace", {}) +workspace_version = workspace.get("package", {}).get("version") +packages = [root["package"]] if "package" in root else [] +for pattern in workspace.get("members", []): + for member in sorted(glob.glob(pattern)): + packages.append(load(Path(member, "Cargo.toml"))["package"]) + +locked = { + entry["name"]: entry["version"] + for entry in load("Cargo.lock").get("package", []) + if "source" not in entry +} +stale = [] +for package in packages: + version = package.get("version", "0.0.0") + if version == {"workspace": True}: + version = workspace_version + if locked.get(package["name"]) != version: + stale.append(f"{package['name']}: Cargo.toml {version}, Cargo.lock {locked.get(package['name'], 'missing')}") + +if stale: + print("release PR integrity: Cargo.lock disagrees with the manifest versions:", file=sys.stderr) + print("\n".join(f" {line}" for line in stale), file=sys.stderr) + print("Refresh Cargo.lock; on a release branch run scripts/update-release-pr-lockfile.sh.", file=sys.stderr) + sys.exit(1) +PY diff --git a/tests/release_pr_integrity_test.sh b/tests/release_pr_integrity_test.sh index edd5b766ed..22abeaf3ce 100755 --- a/tests/release_pr_integrity_test.sh +++ b/tests/release_pr_integrity_test.sh @@ -14,7 +14,7 @@ new_repo() { git -C "$repo" config user.name "Release Guard Test" git -C "$repo" config user.email "release-guard@example.com" printf '[package]\nname = "fixture"\nversion = "0.1.0"\n' >"$repo/Cargo.toml" - printf 'version = 3\n' >"$repo/Cargo.lock" + printf 'version = 3\n\n[[package]]\nname = "fixture"\nversion = "0.1.0"\n' >"$repo/Cargo.lock" printf '# Changelog\n' >"$repo/CHANGELOG.md" printf '0.1.0\n' >"$repo/version.txt" printf '{"version":"0.1.0"}\n' >"$repo/server.json" @@ -46,6 +46,7 @@ new_repo base=$(head_sha) printf '\n## 0.2.0\n' >>"$repo/CHANGELOG.md" printf '[package]\nname = "fixture"\nversion = "0.2.0"\n' >"$repo/Cargo.toml" +printf 'version = 3\n\n[[package]]\nname = "fixture"\nversion = "0.2.0"\n' >"$repo/Cargo.lock" printf '0.2.0\n' >"$repo/version.txt" printf '{"version":"0.2.0"}\n' >"$repo/server.json" printf '{".":"0.2.0"}\n' >"$repo/.release-please-manifest.json" @@ -83,3 +84,28 @@ head=$(head_sha) run_guard "$head" "$head" --allow-extra-files gate_expect_failure "tracked ignored files must fail even with extra-file approval" gate_output_contains "tracked ignored file" "tracked.tmp" + +# The master break after the 1.0.0-beta.56 release merge: the workspace +# version moved but the lock still recorded the inheriting members at beta.55. +write_workspace_release() { + local lock_version=$1 + mkdir -p "$repo/crates/tracedecay" "$repo/crates/tracedecay-api" + printf '[workspace]\nmembers = ["crates/tracedecay", "crates/tracedecay-api"]\n\n[workspace.package]\nversion = "1.0.0-beta.56"\n' >"$repo/Cargo.toml" + printf '[package]\nname = "tracedecay"\nversion.workspace = true\n' >"$repo/crates/tracedecay/Cargo.toml" + printf '[package]\nname = "tracedecay-api"\nversion = "0.1.0"\n' >"$repo/crates/tracedecay-api/Cargo.toml" + printf 'version = 4\n\n[[package]]\nname = "serde"\nversion = "1.0.0"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\n\n[[package]]\nname = "tracedecay"\nversion = "%s"\n\n[[package]]\nname = "tracedecay-api"\nversion = "0.1.0"\n' "$lock_version" >"$repo/Cargo.lock" + commit_all "workspace release with lock at $lock_version" +} + +new_repo +base=$(head_sha) +write_workspace_release 1.0.0-beta.55 +run_guard "$base" "$(head_sha)" --allow-extra-files +gate_expect_failure "a lock lagging the workspace version must fail" +gate_output_contains "lagging lock" "tracedecay: Cargo.toml 1.0.0-beta.56, Cargo.lock 1.0.0-beta.55" + +new_repo +base=$(head_sha) +write_workspace_release 1.0.0-beta.56 +run_guard "$base" "$(head_sha)" --allow-extra-files +gate_expect_success "a lock synced to the workspace version"