Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
78 changes: 78 additions & 0 deletions scripts/check-tauri-versions.sh
Original file line number Diff line number Diff line change
@@ -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-<name> with tauri-plugin-<name>. 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

# "<npm name> <version>" 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."
92 changes: 92 additions & 0 deletions scripts/test-check-tauri-versions.sh
Original file line number Diff line number Diff line change
@@ -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 "<pkg>=<ver>" ... → 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 "<crate>=<ver>" ... → 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 <name> <want-exit> <needle> <npm-lock> <cargo-lock>
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 ]]
4 changes: 2 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading