🚴 Build and Test BikeControl / Workspace tests (push) Successful in 16m55s
Build & Release / Run tests (push) Successful in 6m35s
🚴 Build and Test BikeControl / Android compile check (push) Successful in 3m41s
Build & Release / Build Linux (deb + AppImage) (push) Successful in 15m18s
Build & Release / Build Arch package (push) Successful in 29m19s
Build & Release / Build Android APK (push) Failing after 29s
Build & Release / Create release (push) Skipped
The android-check job failed on a tree that is perfectly correct: android.rs exports three `Java_..._MainActivity_*` symbols, the check compared all three against a single expected name and called it a mismatch. Two bugs, both from assuming one native: - SYM collected every export while EXPECTED was built from one method, so the comparison was three lines against one. - The Kotlin side matched `external fun name()` on empty parens, which quietly skipped nativeSetActivity and nativeBluetoothStateChanged — parameters do not appear in the symbol name, so stop at the paren. Now both sides are collected into sets and diffed, so a native declared in Kotlin with no Rust half fails as loudly as the reverse. Verified against the real files: passes as-is, and fails with a readable diff for a Kotlin-only native and for a renamed Rust export. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
9.0 KiB
YAML
211 lines
9.0 KiB
YAML
name: '🚴 Build and Test BikeControl'
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
RUST_BACKTRACE: 1
|
|
CARGO_TERM_COLOR: always
|
|
# Incremental state is never reused between CI runs -- pure disk cost.
|
|
CARGO_INCREMENTAL: 0
|
|
|
|
jobs:
|
|
test:
|
|
name: Workspace tests
|
|
runs-on: linux/amd64
|
|
container:
|
|
image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Rust dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
# Registry only -- never `target`. That directory is ~30 GB locally
|
|
# (25G debug, 3.1G android, 2.9G release) and was cached under two
|
|
# keys, on a runner that JellyTau's builds share -- the same leak that
|
|
# filled its disk in August 2026. registry/src is left out too: cargo
|
|
# re-extracts it for free from registry/cache (155 MB of .crate
|
|
# tarballs vs 1.1 GB extracted).
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
# One shared key across every job. The host/android split existed to
|
|
# stop the two `target` dirs clobbering each other; with target no
|
|
# longer cached, registry contents are target-independent and every
|
|
# job wants the same crates. First job to finish saves; the rest
|
|
# restore.
|
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-registry-
|
|
|
|
- name: Cache npm downloads
|
|
uses: actions/cache@v3
|
|
with:
|
|
# npm's download cache, not ui/node_modules: `npm ci` deletes
|
|
# node_modules before it installs, so the old cache was restored and
|
|
# then immediately thrown away. ~/.npm is what actually makes the
|
|
# reinstall fast.
|
|
path: ~/.npm
|
|
key: ${{ runner.os }}-npm-${{ hashFiles('ui/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-npm-
|
|
|
|
- name: Install frontend dependencies
|
|
run: npm --prefix ui ci
|
|
|
|
# Advisory, not a gate. The tree predates this workflow and `cargo fmt
|
|
# --all` currently rewrites ~2000 lines across 28 files; making that a
|
|
# blocking check would mean landing a repo-wide reformat as a side effect
|
|
# of adding CI. Run `cargo fmt --all` once, in its own commit, then drop
|
|
# the `continue-on-error` below and this becomes a real gate.
|
|
- name: Check formatting (advisory)
|
|
run: cargo fmt --all --check
|
|
continue-on-error: true
|
|
|
|
- name: Clippy
|
|
run: cargo clippy --workspace --all-targets -- -D warnings
|
|
|
|
- name: Run workspace tests
|
|
run: cargo test --workspace --locked
|
|
|
|
- name: Type-check the frontend
|
|
run: npm --prefix ui run check
|
|
|
|
- name: Frontend tests
|
|
run: npm --prefix ui test
|
|
|
|
- name: Build the frontend
|
|
run: npm --prefix ui run build
|
|
|
|
# Per-commit Android compile check.
|
|
#
|
|
# This deliberately does NOT build an APK. The full signed build runs only on
|
|
# tags (build-release.yml) and takes ~15 min; `cargo check` for the Android
|
|
# target is ~1 min and catches everything that actually breaks here — the JNI
|
|
# shim in src-tauri/src/android.rs, btleplug's droidplug backend, and any
|
|
# desktop-only API that has crept into a shared crate (NFR-5).
|
|
android-check:
|
|
name: Android compile check
|
|
runs-on: linux/amd64
|
|
needs: test
|
|
container:
|
|
image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest
|
|
env:
|
|
ANDROID_HOME: /opt/android-sdk
|
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
|
ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Rust dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
# Registry only -- never `target`. That directory is ~30 GB locally
|
|
# (25G debug, 3.1G android, 2.9G release) and was cached under two
|
|
# keys, on a runner that JellyTau's builds share -- the same leak that
|
|
# filled its disk in August 2026. registry/src is left out too: cargo
|
|
# re-extracts it for free from registry/cache (155 MB of .crate
|
|
# tarballs vs 1.1 GB extracted).
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
# One shared key across every job. The host/android split existed to
|
|
# stop the two `target` dirs clobbering each other; with target no
|
|
# longer cached, registry contents are target-independent and every
|
|
# job wants the same crates. First job to finish saves; the rest
|
|
# restore.
|
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-registry-
|
|
|
|
# Cheap invariants that a compiler cannot see. Both failures are silent:
|
|
# the app builds, installs, launches, and then finds no trainer.
|
|
- name: Check the Android source layout and the JNI symbols
|
|
run: |
|
|
set -e
|
|
# gen/ is generated and must stay untracked, or a `tauri android init`
|
|
# turns into a confusing diff and the sync script becomes optional.
|
|
if git ls-files --error-unmatch src-tauri/gen >/dev/null 2>&1; then
|
|
echo "❌ src-tauri/gen is tracked. It is generated — untrack it and"
|
|
echo " keep hand-written sources in src-tauri/android/."
|
|
exit 1
|
|
fi
|
|
|
|
KT=src-tauri/android/src/main/java/paris/tourolle/bikecontrol/MainActivity.kt
|
|
[ -f "$KT" ] || { echo "❌ $KT is missing"; exit 1; }
|
|
|
|
# JNI symbols are matched by the *runtime*, by name. Rename the
|
|
# Kotlin package, add a native without its Rust half, or drop a Rust
|
|
# export, and nothing fails to compile — the method simply is not
|
|
# there when Java calls it. So compare the two sets, both ways.
|
|
#
|
|
# Sets, not a single pair: there are three natives now, and matching
|
|
# `external fun name()` on empty parens missed the two that take
|
|
# arguments. Parameters do not appear in the symbol name, so stop at
|
|
# the opening paren.
|
|
TMP="${RUNNER_TEMP:-/tmp}"
|
|
PKG=$(sed -n 's/^package \(.*\)$/\1/p' "$KT" | tr -d '\r')
|
|
PREFIX="Java_$(echo "$PKG" | tr '.' '_')_MainActivity_"
|
|
|
|
sed -n 's/.*pub extern "system" fn \(Java_[A-Za-z0-9_]*\).*/\1/p' \
|
|
src-tauri/src/android.rs | sort -u > "$TMP/jni-rust"
|
|
sed -n "s/.*external fun \([A-Za-z0-9_]*\)(.*/$PREFIX\1/p" \
|
|
"$KT" | sort -u > "$TMP/jni-kotlin"
|
|
|
|
[ -s "$TMP/jni-rust" ] || { echo "❌ No JNI exports found in android.rs"; exit 1; }
|
|
|
|
if ! diff -u "$TMP/jni-kotlin" "$TMP/jni-rust" > "$TMP/jni-diff"; then
|
|
echo "❌ JNI symbols differ between MainActivity.kt and android.rs:"
|
|
echo " - declared in MainActivity.kt, not exported by android.rs"
|
|
echo " + exported by android.rs, not declared in MainActivity.kt"
|
|
tail -n +4 "$TMP/jni-diff"
|
|
exit 1
|
|
fi
|
|
echo "✅ $(wc -l < "$TMP/jni-rust") JNI symbols match $PKG.MainActivity:"
|
|
sed 's/^/ /' "$TMP/jni-rust"
|
|
|
|
- name: Cargo check (aarch64-linux-android)
|
|
run: |
|
|
TC="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
|
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TC/aarch64-linux-android24-clang"
|
|
export CC_aarch64_linux_android="$TC/aarch64-linux-android24-clang"
|
|
export AR_aarch64_linux_android="$TC/llvm-ar"
|
|
cargo check -p bikecontrol-app --lib --target aarch64-linux-android --locked
|
|
|
|
# btleplug's Android backend is half Java. That half is copied out of the
|
|
# crate sources at the version Cargo.lock pins, so a bump to btleplug that
|
|
# moved or renamed those sources must fail here — loudly, in a one-minute
|
|
# job — rather than in a fifteen-minute release build, or at the first
|
|
# scan on a phone.
|
|
- name: Verify the BLE Java backend can be sourced
|
|
run: |
|
|
set -e
|
|
cargo fetch --target aarch64-linux-android
|
|
for crate in btleplug jni-utils; do
|
|
VER=$(awk -v pkg="name = \"$crate\"" \
|
|
'$0 == pkg { f = 1; next } f && /^version = / { gsub(/[",]/, "", $3); print $3; exit }' Cargo.lock)
|
|
DIR=$(ls -d "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/"$crate-$VER" 2>/dev/null | head -1)
|
|
[ -n "$DIR" ] || { echo "❌ $crate $VER sources not in the registry"; exit 1; }
|
|
echo "✅ $crate $VER at $DIR"
|
|
done
|
|
test -d "$(ls -d "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/btleplug-*/src/droidplug/java/src/main/java/com | head -1)"
|
|
echo "✅ droidplug Java sources present"
|