Pin the versionCode in the config, not in a file the build overwrites
🚴 Build and Test BikeControl / Workspace tests (push) Successful in 13m33s
🚴 Build and Test BikeControl / Android compile check (push) Successful in 3m29s

The Android job died on `tauri.properties not found`. It is not found
because `cargo tauri android init` does not write it: in a clean clone
init leaves gen/android/app/ holding build.gradle.kts, proguard-rules.pro
and src/, nothing else. The generated app/.gitignore names the pattern —
tauri.properties sits with tauri.build.gradle.kts and proguard-tauri.pro,
the tauri.* files that `android build` stamps out on every run.

So the step was wrong twice and the crash was the lucky half. Had the
file existed, the build would have rewritten it from tauri.conf.json and
discarded the sed — a green build shipping the default versionCode, which
is the failure that reaches a rider's phone rather than the log.

Tauri exposes the actual input, so use it:

- tauri.conf.json gains bundle.android.versionCode, committed rather than
  conjured by CI, so the key is greppable and the sed has a fixed target.
- ci-android-version-code.sh edits the config. Formula and the 1000 floor
  are unchanged; the header comment is rewritten, since its premise (init
  writes the file, the default collides) does not hold — Tauri's default
  is major*1000000 + minor*1000 + patch, monotonic, and it puts 0.1.0 at
  exactly 1000, which is where the floor comes from. Missing key or failed
  substitution now exits 1 instead of degrading to a silent no-op.
- The step moves ahead of `android init`, next to ci-set-version.sh, since
  both edit the same config.

Verified against a clean clone: 0.1.0 -> 1100, v0.2.3 -> 1203,
v1.0.0 -> 11000, config still parses at each step. Then a real
`cargo tauri android build`, which wrote versionCode=1100 into
tauri.properties — the value reaches the APK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:39:06 +02:00
co-authored by Claude Opus 5
parent a042814625
commit db974dcac1
3 changed files with 42 additions and 18 deletions
+7 -3
View File
@@ -196,13 +196,17 @@ jobs:
- name: Set app version from tag - name: Set app version from tag
run: ./scripts/ci-set-version.sh run: ./scripts/ci-set-version.sh
# Before init, and before the build: this edits tauri.conf.json, which is
# the input both of them read. `gen/android/app/tauri.properties` is an
# output of `android build` — it does not exist yet, and anything written
# there would be overwritten by the build anyway.
- name: Pin a monotonic Android versionCode
run: ./scripts/ci-android-version-code.sh
- name: Initialise the Android project - name: Initialise the Android project
working-directory: src-tauri working-directory: src-tauri
run: cargo tauri android init run: cargo tauri android init
- name: Pin a monotonic Android versionCode
run: ./scripts/ci-android-version-code.sh
# Manifest, MainActivity, gradle config, and btleplug's Java backend. # Manifest, MainActivity, gradle config, and btleplug's Java backend.
# `tauri android init` above regenerated gen/android and knows about none # `tauri android init` above regenerated gen/android and knows about none
# of it — without this step the APK builds and then finds no trainer. # of it — without this step the APK builds and then finds no trainer.
+32 -15
View File
@@ -1,10 +1,15 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Pin an explicit, monotonic Android versionCode. # Pin an explicit, monotonic Android versionCode.
# #
# `tauri android init` derives one from the semver in a way that is not # Tauri derives one from the semver as
# monotonic across a version series: 0.1.0 and 0.0.10 can collide, and Android #
# refuses to install an APK whose versionCode is not greater than the installed # major * 1000000 + minor * 1000 + patch
# one. A rider who cannot update is a rider who stops updating. #
# which is monotonic but leaves the pre-1.0 series crowded down at the bottom of
# the range: 0.1.0 is versionCode 1000. Android refuses to install an APK whose
# versionCode is not greater than the installed one, and a rider who cannot
# update is a rider who stops updating, so this pins the number rather than
# leaving it to a default that a Tauri upgrade could change under us:
# #
# code = 1000 + major * 10000 + minor * 100 + patch # code = 1000 + major * 10000 + minor * 100 + patch
# #
@@ -12,20 +17,23 @@
# semver order for any minor/patch below 100, which is well past where this # semver order for any minor/patch below 100, which is well past where this
# project will ever get. # project will ever get.
# #
# The 1000 floor is not decoration: `tauri android init` writes versionCode=1000 # The 1000 floor is not decoration: Tauri's default gives 0.1.0 versionCode
# for 0.1.0 today, so anyone carrying a locally-built APK already has that # 1000, so anyone carrying an APK from before this script already has that
# number installed. A bare major/minor/patch code would be 100 — a *downgrade*, # number installed. A bare major/minor/patch code would be 100 — a *downgrade*,
# which Android refuses outright. # which Android refuses outright.
# #
# This writes bundle.android.versionCode in tauri.conf.json, NOT
# gen/android/app/tauri.properties. tauri.properties is generated by
# `cargo tauri android build`, not by `android init` — it does not exist at init
# time, and the build overwrites it from the config on every run, so an edit
# there is either a crash or a no-op. The config is the only durable input.
#
# POSIX sh: the runner's /bin/sh is dash. No here-strings, no \s in sed. # POSIX sh: the runner's /bin/sh is dash. No here-strings, no \s in sed.
set -e set -e
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROPS="$ROOT/src-tauri/gen/android/app/tauri.properties"
CONF="$ROOT/src-tauri/tauri.conf.json" CONF="$ROOT/src-tauri/tauri.conf.json"
[ -f "$PROPS" ] || { echo "$PROPS not found — run 'cargo tauri android init' first" >&2; exit 1; }
VERSION=$(grep '"version"' "$CONF" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') VERSION=$(grep '"version"' "$CONF" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
MAJ=$(echo "$VERSION" | cut -d. -f1) MAJ=$(echo "$VERSION" | cut -d. -f1)
MIN=$(echo "$VERSION" | cut -d. -f2) MIN=$(echo "$VERSION" | cut -d. -f2)
@@ -36,9 +44,18 @@ PAT=$(echo "$VERSION" | cut -d. -f3)
CODE=$(( 1000 + MAJ * 10000 + MIN * 100 + PAT )) CODE=$(( 1000 + MAJ * 10000 + MIN * 100 + PAT ))
echo "version=$VERSION -> versionCode=$CODE" echo "version=$VERSION -> versionCode=$CODE"
if grep -q '^tauri.android.versionCode=' "$PROPS"; then
sed -i "s/^tauri.android.versionCode=.*/tauri.android.versionCode=$CODE/" "$PROPS" # The key is committed to tauri.conf.json, so a missing one means the config was
else # restructured and this sed would otherwise silently do nothing — shipping an
echo "tauri.android.versionCode=$CODE" >> "$PROPS" # APK with a stale versionCode that no device would accept as an update.
fi grep -q '"versionCode"' "$CONF" || {
cat "$PROPS" echo "❌ no \"versionCode\" key in $CONF — add bundle.android.versionCode back" >&2
exit 1
}
sed -i "s/\"versionCode\"[[:space:]]*:[[:space:]]*[0-9]*/\"versionCode\": $CODE/" "$CONF"
grep -q "\"versionCode\": $CODE" "$CONF" || {
echo "❌ failed to set versionCode to $CODE in $CONF" >&2
exit 1
}
grep -A2 '"android"' "$CONF"
+3
View File
@@ -42,6 +42,9 @@
"icons/icon.png" "icons/icon.png"
], ],
"category": "Utility", "category": "Utility",
"android": {
"versionCode": 1100
},
"shortDescription": "Indoor cycling trainer control", "shortDescription": "Indoor cycling trainer control",
"longDescription": "Control a smart trainer over BLE, ride gradient profiles and synthetic waveforms, and record the result." "longDescription": "Control a smart trainer over BLE, ride gradient profiles and synthetic waveforms, and record the result."
} }