Files
BikeControl/.gitea/workflows/build-and-test.yml
T
dtourolleandClaude Opus 5 da56985499
🚴 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
Check every JNI symbol, not just the first one
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>
2026-08-21 12:12:42 +02:00

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"