diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7523425..02c97b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,6 +163,9 @@ jobs: - name: Test the cargo-audit gate run: scripts/test-cargo-audit-check.sh + - name: Test the Tauri version check + run: scripts/test-check-tauri-versions.sh + # ARCHITECTURE section 5 is the reference for the Tauri command surface, and # it had drifted into describing ten commands that were never written — a # reader following it wrote calls that fail at runtime with "command not @@ -269,6 +272,12 @@ jobs: exit 1 fi echo "All versions match: $jsver" + # tauri build refuses to start when a Tauri npm package and its crate are + # on different major.minor releases, but only the release workflow runs + # it. Dependabot bumps npm and cargo in separate PRs, so a one-sided + # Tauri bump otherwise passes here and breaks the next release. + - name: Check Tauri npm packages match their crates + run: scripts/check-tauri-versions.sh lint-ts: name: Lint (TypeScript) diff --git a/justfile b/justfile index e9cc56b..aa09515 100644 --- a/justfile +++ b/justfile @@ -95,6 +95,7 @@ lint: cd src-tauri && cargo clippy -p mas-agent -p mas-core -p mas-export -p mas-admin -p mas-mcp -- -D warnings npx tsc --noEmit npx dprint check + ./scripts/check-tauri-versions.sh # actionlint covers syntax, expressions and the shell inside `run:` blocks; # the script covers the two things it does not — whether a pinned action SHA @@ -106,6 +107,7 @@ lint-workflows: ./scripts/check-action-pins.sh ./scripts/test-check-action-pins.sh ./scripts/test-cargo-audit-check.sh + ./scripts/test-check-tauri-versions.sh # Format Rust and everything dprint owns. fmt: diff --git a/scripts/check-tauri-versions.sh b/scripts/check-tauri-versions.sh new file mode 100755 index 0000000..dbffaa6 --- /dev/null +++ b/scripts/check-tauri-versions.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# +# Fail when a Tauri npm package and its Rust crate are on different +# major.minor releases. +# +# `tauri build` refuses to start when they disagree ("Found version mismatched +# Tauri packages"), but nothing on a pull request runs `tauri build`: it runs +# only in the release workflow. So a dependency PR that moves one side alone +# passes CI and breaks the next release, which is how +# @tauri-apps/plugin-updater 2.12 shipped against tauri-plugin-updater 2.11. +# +# The rule is the CLI's own: @tauri-apps/api pairs with the `tauri` crate, and +# @tauri-apps/plugin- with tauri-plugin-. Versions are the +# resolved ones, from package-lock.json and src-tauri/Cargo.lock, because that +# is what the CLI compares. A package with no counterpart on the other side +# (@tauri-apps/cli, say) is not a pair and is skipped. +# +# Usage: check-tauri-versions.sh [repo-root] + +set -euo pipefail + +root="${1:-$(cd "$(dirname "$0")/.." && pwd)}" +npm_lock="$root/package-lock.json" +cargo_lock="$root/src-tauri/Cargo.lock" + +command -v jq >/dev/null 2>&1 || { echo "check-tauri-versions: jq is not installed" >&2; exit 2; } +for f in "$npm_lock" "$cargo_lock"; do + [[ -f "$f" ]] || { echo "check-tauri-versions: $f not found" >&2; exit 2; } +done + +# " " for every top-level @tauri-apps package. +npm_versions="$(jq -r ' + .packages | to_entries[] + | select(.key | test("^node_modules/@tauri-apps/[^/]+$")) + | "\(.key | sub("^node_modules/"; "")) \(.value.version)" +' "$npm_lock")" + +# Version of crate `$1` in Cargo.lock, empty when absent. +crate_version() { + awk -v want="$1" ' + $0 == "[[package]]" { name = "" } + $1 == "name" { gsub(/"/, "", $3); name = $3 } + $1 == "version" && name == want { gsub(/"/, "", $3); print $3; exit } + ' "$cargo_lock" +} + +major_minor() { echo "$1" | cut -d. -f1,2; } + +checked=0 +mismatches=0 +while read -r pkg version; do + [[ -n "$pkg" ]] || continue + case "$pkg" in + @tauri-apps/api) crate=tauri ;; + @tauri-apps/plugin-*) crate="tauri-plugin-${pkg#@tauri-apps/plugin-}" ;; + *) continue ;; + esac + crate_ver="$(crate_version "$crate")" + [[ -n "$crate_ver" ]] || continue + checked=$((checked + 1)) + if [[ "$(major_minor "$version")" != "$(major_minor "$crate_ver")" ]]; then + echo "::error::$pkg is $version but the $crate crate is $crate_ver; tauri build needs the same major.minor on both." + mismatches=$((mismatches + 1)) + else + echo "ok $pkg $version = $crate $crate_ver" + fi +done <<< "$npm_versions" + +if [[ $checked -eq 0 ]]; then + echo "::error::check-tauri-versions: found no Tauri package pairs to compare; the lockfiles are not what this script expects." >&2 + exit 2 +fi +if [[ $mismatches -gt 0 ]]; then + echo "" + echo "Bump the other side to the same major.minor. Dependabot updates npm and cargo in separate PRs, so a Tauri bump on one side needs its partner." + exit 1 +fi +echo "All $checked Tauri package pairs agree." diff --git a/scripts/test-check-tauri-versions.sh b/scripts/test-check-tauri-versions.sh new file mode 100755 index 0000000..f494b72 --- /dev/null +++ b/scripts/test-check-tauri-versions.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# scripts/test-check-tauri-versions.sh +# +# Tests for check-tauri-versions.sh. Each case builds a minimal +# package-lock.json and Cargo.lock in a temporary repo root and points the +# checker at it. + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +checker="$script_dir/check-tauri-versions.sh" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +passed=0 +failed=0 +ok() { printf ' ok %s\n' "$1"; passed=$((passed + 1)); } +bad() { printf ' FAIL %s\n %s\n' "$1" "$2"; failed=$((failed + 1)); } + +# npm_lock "=" ... → a package-lock.json body +npm_lock() { + local entries="" sep="" pair + for pair in "$@"; do + entries+="$sep\"node_modules/${pair%%=*}\": {\"version\": \"${pair#*=}\"}" + sep="," + done + printf '{"lockfileVersion": 3, "packages": {"": {"name": "app"}%s%s}}' "${entries:+,}" "$entries" +} + +# cargo_lock "=" ... → a Cargo.lock body +cargo_lock() { + local pair + printf 'version = 4\n' + for pair in "$@"; do + printf '\n[[package]]\nname = "%s"\nversion = "%s"\nsource = "registry+https://github.com/rust-lang/crates.io-index"\ndependencies = [\n "serde",\n]\n' \ + "${pair%%=*}" "${pair#*=}" + done +} + +# expect +expect() { + local name="$1" want="$2" needle="$3" dir="$work/$1" output status + mkdir -p "$dir/src-tauri" + printf '%s' "$4" > "$dir/package-lock.json" + printf '%s' "$5" > "$dir/src-tauri/Cargo.lock" + output=$(bash "$checker" "$dir" 2>&1) && status=0 || status=$? + if [[ $status -ne $want ]]; then + bad "$name" "expected exit $want, got $status: $output" + elif [[ "$output" != *"$needle"* ]]; then + bad "$name" "expected the output to mention '$needle', got: $output" + else + ok "$name" + fi +} + +echo "check-tauri-versions.sh" + +expect "matching pairs pass" 0 "All 2 Tauri package pairs agree" \ + "$(npm_lock @tauri-apps/api=2.11.1 @tauri-apps/plugin-updater=2.12.0)" \ + "$(cargo_lock tauri=2.11.5 tauri-plugin-updater=2.12.3)" + +expect "a plugin a minor ahead fails" 1 "@tauri-apps/plugin-updater is 2.12.0 but the tauri-plugin-updater crate is 2.11.0" \ + "$(npm_lock @tauri-apps/api=2.11.1 @tauri-apps/plugin-updater=2.12.0)" \ + "$(cargo_lock tauri=2.11.5 tauri-plugin-updater=2.11.0)" + +expect "the api package pairs with the tauri crate" 1 "@tauri-apps/api is 2.12.0 but the tauri crate is 2.11.5" \ + "$(npm_lock @tauri-apps/api=2.12.0)" \ + "$(cargo_lock tauri=2.11.5)" + +expect "patch differences are fine" 0 "All 1 Tauri package pairs agree" \ + "$(npm_lock @tauri-apps/plugin-shell=2.3.9)" \ + "$(cargo_lock tauri-plugin-shell=2.3.6)" + +expect "a package with no crate is skipped" 0 "All 1 Tauri package pairs agree" \ + "$(npm_lock @tauri-apps/api=2.11.1 @tauri-apps/cli=2.12.0)" \ + "$(cargo_lock tauri=2.11.5)" + +expect "a crate named like a prefix is not confused" 1 "tauri-plugin-shell crate is 2.3.6" \ + "$(npm_lock @tauri-apps/plugin-shell=2.4.0)" \ + "$(cargo_lock tauri-plugin-shell-extra=2.4.0 tauri-plugin-shell=2.3.6)" + +expect "nested copies are ignored" 0 "All 1 Tauri package pairs agree" \ + "$(npm_lock @tauri-apps/api=2.11.1 some-dep/node_modules/@tauri-apps/api=1.6.0)" \ + "$(cargo_lock tauri=2.11.5)" + +expect "no pairs at all fails" 2 "found no Tauri package pairs" \ + "$(npm_lock react=19.3.0)" \ + "$(cargo_lock serde=1.0.0)" + +echo "" +echo "$passed passed, $failed failed" +[[ $failed -eq 0 ]] diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ba646b9..4e7fd7b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5772,9 +5772,9 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28d8cabdeb0564f03ae261963de4bc3d98321cd3d213e76a81b7d344e5df606" +checksum = "7a5cad8ed5948d988e1018ecd31e27cacedbf72ce3fb972940c3e4bf51639e4b" dependencies = [ "base64 0.22.1", "dirs", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 14ccc7c..eb1e4a8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -69,7 +69,7 @@ specta-typescript = { workspace = true } tauri = { workspace = true } tauri-plugin-process = "2.3.1" tauri-plugin-shell = "2" -tauri-plugin-updater = "2.10.1" +tauri-plugin-updater = "2.12.0" tauri-specta = { workspace = true } tokio = { workspace = true } tracing = { workspace = true }