Two builder images, because the jobs genuinely need two distributions: Ubuntu for the tests, the Linux bundle and the Android APK, and Arch for the .pkg.tar.zst, since makepkg is Arch-specific. Both are built from this repo (NFR-12) and pinned by tag in the workflows, so a Dockerfile change only reaches CI once it has been pushed. libdbus-1-dev is not incidental in the Ubuntu image: btleplug's Linux backend is bluez-async over the dbus crate, so without it the workspace does not build at all. The per-commit Android job is a cargo check, not an APK. The full signed build is ~15 minutes and runs only on tags; a one-minute check catches what actually breaks — the JNI shim, droidplug, and any desktop-only API that has crept into a shared crate. Two invariants a compiler cannot see are checked there too, because both fail silently: the app builds, installs, launches, and finds no trainer. The JNI symbol in android.rs is matched by the runtime by name, so renaming the Kotlin package compiles fine and simply never initialises btleplug; and gen/ must stay untracked or the sync script quietly becomes optional. The Android versionCode carries a 1000 floor. `tauri android init` writes 1000 for 0.1.0 today, so anyone holding a locally built APK already has that number installed, and a bare major/minor/patch code would be 100 — a downgrade, which Android refuses outright. The fmt check is advisory for now. The tree predates this workflow and `cargo fmt --all` currently rewrites ~2000 lines across 28 files; making it a gate here would mean landing a repo-wide reformat as a side effect of adding CI. Run fmt in its own commit, then drop the continue-on-error. The two clippy warnings that stood between the tree and a real `-D warnings` gate are fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
168 lines
6.3 KiB
YAML
168 lines
6.3 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
|
|
|
|
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"
|