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 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: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-host-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-host- - name: Cache Node dependencies uses: actions/cache@v3 with: path: ui/node_modules 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: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-android-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-android- # 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 symbol 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; } # The JNI symbol in android.rs is matched by the *runtime*, by name. # Rename the Kotlin package or the method and nothing fails to # compile — btleplug simply never gets initialised. SYM=$(sed -n 's/.*pub extern "system" fn \(Java_[A-Za-z0-9_]*\).*/\1/p' src-tauri/src/android.rs) PKG=$(sed -n 's/^package \(.*\)$/\1/p' "$KT" | tr -d '\r') METHOD=$(sed -n 's/.*external fun \([A-Za-z0-9_]*\)().*/\1/p' "$KT") EXPECTED="Java_$(echo "$PKG" | tr '.' '_')_MainActivity_$METHOD" if [ "$SYM" != "$EXPECTED" ]; then echo "❌ JNI symbol mismatch:" echo " android.rs exports: $SYM" echo " MainActivity needs: $EXPECTED" exit 1 fi echo "✅ JNI symbol $SYM matches $PKG.MainActivity.$METHOD" - 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"