diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f89857ed..ebe88a6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,18 @@ env: RUSTFLAGS: -Dwarnings jobs: + versions: + name: Version drift + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Check version drift + run: ./scripts/release/check-versions.sh + lint: name: Lint runs-on: ubuntu-latest diff --git a/docs/RELEASE_RUNBOOK.md b/docs/RELEASE_RUNBOOK.md index 2c8e76a8..df678e96 100644 --- a/docs/RELEASE_RUNBOOK.md +++ b/docs/RELEASE_RUNBOOK.md @@ -42,6 +42,7 @@ Current packaging note: Run these from the repository root before cutting a tag: ```bash +./scripts/release/check-versions.sh # version drift between workspace, npm, lockfile cargo fmt --all -- --check cargo check --workspace --all-targets --locked cargo clippy --workspace --all-targets --all-features --locked -- -D warnings @@ -50,6 +51,11 @@ cargo publish --dry-run --locked --allow-dirty -p deepseek-tui ./scripts/release/publish-crates.sh dry-run ``` +`check-versions.sh` also runs in CI on every push/PR (the `versions` job in +`.github/workflows/ci.yml`), so drift between `Cargo.toml`, the per-crate +manifests, `npm/deepseek-tui/package.json`, and `Cargo.lock` is caught before +release time rather than at it. + `publish-crates.sh dry-run` performs a full `cargo publish --dry-run` for crates without unpublished workspace dependencies and a packaging preflight for dependent workspace crates. That avoids false negatives from crates.io not yet containing the diff --git a/scripts/release/check-versions.sh b/scripts/release/check-versions.sh new file mode 100755 index 00000000..cab67183 --- /dev/null +++ b/scripts/release/check-versions.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Fails CI if version state is inconsistent across the workspace, npm +# wrapper, and Cargo.lock. Run on every push/PR so silent drift can't ship. +# +# Checks performed: +# 1. No `crates/*/Cargo.toml` carries a literal `version = "x.y.z"`; every +# crate must inherit `version.workspace = true`. +# 2. `npm/deepseek-tui/package.json` `version` matches the workspace +# `version` in the root `Cargo.toml`. +# 3. `Cargo.lock` is in sync with the manifests (`cargo metadata --locked` +# fails if not). +set -euo pipefail + +cd "$(dirname "$0")/../.." + +fail=0 + +# 1) Literal versions in crate manifests. +literals="$(grep -nE '^version = "' crates/*/Cargo.toml || true)" +if [[ -n "${literals}" ]]; then + echo "::error::Crate manifests must use 'version.workspace = true', not literal versions:" >&2 + echo "${literals}" >&2 + fail=1 +fi + +# 2) Workspace ↔ npm package.json. +workspace_version="$(grep -E '^version = "' Cargo.toml | head -n1 | sed -E 's/^version = "([^"]+)".*/\1/')" +npm_version="$(node -p "require('./npm/deepseek-tui/package.json').version")" +if [[ "${workspace_version}" != "${npm_version}" ]]; then + echo "::error::npm/deepseek-tui/package.json version (${npm_version}) does not match workspace Cargo.toml (${workspace_version})." >&2 + fail=1 +fi + +# 3) Cargo.lock in sync. +if ! cargo metadata --locked --format-version 1 --no-deps >/dev/null 2>&1; then + echo "::error::Cargo.lock is out of sync with the manifests. Run 'cargo update -p deepseek-tui' or 'cargo build' and commit the result." >&2 + fail=1 +fi + +if [[ "${fail}" -eq 0 ]]; then + echo "Version state OK: workspace=${workspace_version}, npm=${npm_version}, lockfile in sync." +fi + +exit "${fail}"