# ABOUTME: CI workflow for Pierre Mobile app (React Native/Expo)
# ABOUTME: Runs unit tests, type checking, linting, and Maestro E2E tests on push/PR
name: Mobile Tests
on:
push:
branches: [ "main", "debug/*", "feature/*", "claude/*" ]
paths:
- 'frontend-mobile/**'
- 'packages/**'
- 'src/routes/auth.rs'
- 'src/routes/coaches.rs'
- 'src/routes/chat.rs'
- 'src/models/**'
- '.github/workflows/mobile-tests.yml'
pull_request:
branches: [ main ]
paths:
- 'frontend-mobile/**'
- 'packages/**'
- 'src/routes/auth.rs'
- 'src/routes/coaches.rs'
- 'src/routes/chat.rs'
- 'src/models/**'
workflow_dispatch: # Allow manual triggering
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Security: Explicit permissions following principle of least privilege
permissions:
contents: read
jobs:
mobile-unit-tests:
name: Mobile Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.4"
- name: Install shared-types dependencies
working-directory: packages/shared-types
run: bun install
- name: Install api-client dependencies
working-directory: packages/api-client
run: bun install
- name: Install mobile dependencies
working-directory: frontend-mobile
run: bun install --frozen-lockfile
- name: Type check
working-directory: frontend-mobile
run: bun run typecheck
- name: Run unit tests
working-directory: frontend-mobile
run: bun run test -- --coverage
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: mobile-coverage
path: frontend-mobile/coverage/
retention-days: 7
mobile-integration-tests:
name: Mobile Integration Tests (Real Server)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.4"
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-mobile-integration-1.92.0-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-mobile-integration-1.92.0-
${{ runner.os }}-cargo-
- name: Build Pierre server (release for performance)
run: cargo build --release
env:
CARGO_INCREMENTAL: 0
- name: Install shared-types dependencies
working-directory: packages/shared-types
run: bun install
- name: Install api-client dependencies
working-directory: packages/api-client
run: bun install
- name: Install mobile dependencies
working-directory: frontend-mobile
run: bun install --frozen-lockfile
- name: Create data directory
run: mkdir -p data
- name: Start Pierre server
run: |
./target/release/pierre-mcp-server &
echo "Waiting for server to start..."
for i in {1..30}; do
if curl -s http://localhost:8081/health > /dev/null; then
echo "Server is healthy!"
break
fi
echo "Attempt $i: Server not ready yet..."
sleep 2
done
env:
DATABASE_URL: "sqlite:${{ github.workspace }}/data/mobile-integration-test.db"
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_RSA_KEY_SIZE: "2048"
HTTP_PORT: "8081"
RUST_LOG: "warn"
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8081/auth/strava/callback"
- name: Run mobile integration tests
working-directory: frontend-mobile
run: bun run e2e:integration
env:
CI: true
BACKEND_URL: "http://localhost:8081"
- name: Upload integration test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: mobile-integration-test-results
path: frontend-mobile/integration/
retention-days: 7
mobile-e2e-maestro:
name: Mobile E2E Tests (Maestro - iOS/Expo Go)
runs-on: macos-latest
# Run on main, claude/* branches, or when explicitly triggered
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/claude/') || github.event_name == 'workflow_dispatch'
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-maestro-1.92.0-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-maestro-1.92.0-
${{ runner.os }}-cargo-
- name: Build Pierre server (release for performance)
run: cargo build --release
env:
CARGO_INCREMENTAL: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.4"
- name: Install shared-types dependencies
working-directory: packages/shared-types
run: bun install
- name: Install api-client dependencies
working-directory: packages/api-client
run: bun install
- name: Remove expo-dev-client for E2E build
working-directory: frontend-mobile
run: |
# Remove expo-dev-client from package.json BEFORE install
# This package interferes with Maestro testing - its native code
# tries to connect to a dev server that doesn't exist in CI
cat package.json | jq 'del(.dependencies["expo-dev-client"])' > package.json.tmp
mv package.json.tmp package.json
- name: Install mobile dependencies
working-directory: frontend-mobile
run: bun install
- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Create data directory
run: mkdir -p data
- name: Start Pierre server
run: |
./target/release/pierre-mcp-server &
echo "Waiting for server to start..."
for i in {1..30}; do
if curl -s http://localhost:8081/health > /dev/null; then
echo "Server is healthy!"
break
fi
echo "Attempt $i: Server not ready yet..."
sleep 2
done
env:
DATABASE_URL: "sqlite:${{ github.workspace }}/data/maestro-test.db"
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_RSA_KEY_SIZE: "2048"
HTTP_PORT: "8081"
RUST_LOG: "warn"
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8081/auth/strava/callback"
- name: Seed test user for Maestro tests
run: |
./target/release/pierre-cli user create --email mobiletest@pierre.dev --password MobileTest123!
env:
DATABASE_URL: "sqlite:${{ github.workspace }}/data/maestro-test.db"
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
RUST_LOG: "warn"
- name: List available simulators
run: xcrun simctl list devices available
- name: Boot iOS Simulator
run: |
SIMULATOR_UDID=$(xcrun simctl list devices available | grep "iPhone 16 Pro (" | head -1 | grep -oE '[0-9A-Fa-f-]{36}')
if [ -z "$SIMULATOR_UDID" ]; then
SIMULATOR_UDID=$(xcrun simctl list devices available | grep "iPhone 16 (" | head -1 | grep -oE '[0-9A-Fa-f-]{36}')
fi
if [ -z "$SIMULATOR_UDID" ]; then
SIMULATOR_UDID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | grep -oE '[0-9A-Fa-f-]{36}')
fi
if [ -z "$SIMULATOR_UDID" ]; then
echo "No iPhone simulator found!"
exit 1
fi
echo "Booting simulator: $SIMULATOR_UDID"
xcrun simctl boot "$SIMULATOR_UDID" || echo "Simulator may already be booted"
echo "SIMULATOR_UDID=$SIMULATOR_UDID" >> $GITHUB_ENV
- name: Adapt Maestro tests for Expo Go
run: |
# Replace native app ID with Expo Go app ID in all Maestro YAML files
find frontend-mobile/.maestro -name "*.yaml" -exec sed -i '' 's/appId: com.pierre.fitness/appId: host.exp.Exponent/g' {} +
# Override launch helper for Expo Go deep linking
cat > frontend-mobile/.maestro/helpers/launch-app.yaml << 'LAUNCH_EOF'
# ABOUTME: Helper flow to launch the Pierre app via Expo Go
# ABOUTME: Uses deep link to connect Expo Go to Metro bundler
appId: host.exp.Exponent
---
- clearState
- launchApp
- openLink: "exp://localhost:8082"
- extendedWaitUntil:
visible:
id: "login-screen"
timeout: 30000
LAUNCH_EOF
- name: Start Metro and install Expo Go
working-directory: frontend-mobile
run: |
# Start Metro in Expo Go mode; --ios flag installs Expo Go and opens the app
npx expo start --go --ios --port 8082 &
echo "METRO_PID=$!" >> $GITHUB_ENV
echo "Waiting for Metro bundler to be ready..."
for i in {1..90}; do
if curl -s http://localhost:8082 > /dev/null 2>&1; then
echo "Metro is ready!"
break
fi
if [ "$i" -eq 90 ]; then
echo "Metro failed to start within timeout!"
exit 1
fi
echo "Attempt $i: Metro not ready yet..."
sleep 2
done
# Give Expo Go time to install and load the initial bundle
sleep 15
env:
CI: "true"
EXPO_PUBLIC_API_URL: "http://localhost:8081"
- name: Run Maestro tests
working-directory: frontend-mobile
run: |
$HOME/.maestro/bin/maestro test .maestro/ --format junit --output maestro-results.xml
env:
TEST_EMAIL: "mobiletest@pierre.dev"
TEST_PASSWORD: "MobileTest123!"
BACKEND_URL: "http://localhost:8081"
- name: Upload Maestro test results
uses: actions/upload-artifact@v4
if: always()
with:
name: maestro-test-results-ios
path: |
frontend-mobile/maestro-results.xml
frontend-mobile/.maestro/
retention-days: 7
mobile-e2e-maestro-android:
name: Mobile E2E Tests - Android (Maestro)
runs-on: ubuntu-latest
# Run on main, claude/* branches, or when explicitly triggered
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/claude/') || github.event_name == 'workflow_dispatch'
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@1.92.0
- name: Cache Rust dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-maestro-android-1.92.0-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-maestro-android-1.92.0-
${{ runner.os }}-cargo-
- name: Build Pierre server (release for performance)
run: cargo build --release
env:
CARGO_INCREMENTAL: 0
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.4"
- name: Install shared-types dependencies
working-directory: packages/shared-types
run: bun install
- name: Install api-client dependencies
working-directory: packages/api-client
run: bun install
- name: Remove expo-dev-client for E2E build
working-directory: frontend-mobile
run: |
# Remove expo-dev-client from package.json BEFORE install
# This package interferes with Maestro testing - its native code
# tries to connect to a dev server that doesn't exist in CI
cat package.json | jq 'del(.dependencies["expo-dev-client"])' > package.json.tmp
mv package.json.tmp package.json
- name: Install mobile dependencies
working-directory: frontend-mobile
run: bun install
- name: Install Maestro CLI
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Free disk space for Android build
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android/sdk/ndk
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
echo "Disk space after cleanup:"
df -h
- name: Create data directory
run: mkdir -p data
- name: Start Pierre server
run: |
./target/release/pierre-mcp-server &
echo "Waiting for server to start..."
for i in {1..30}; do
if curl -s http://localhost:8081/health > /dev/null; then
echo "Server is healthy!"
break
fi
echo "Attempt $i: Server not ready yet..."
sleep 2
done
env:
DATABASE_URL: "sqlite:${{ github.workspace }}/data/maestro-android-test.db"
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
PIERRE_RSA_KEY_SIZE: "2048"
HTTP_PORT: "8081"
RUST_LOG: "warn"
STRAVA_CLIENT_ID: "test_client_id_ci"
STRAVA_CLIENT_SECRET: "test_client_secret_ci"
STRAVA_REDIRECT_URI: "http://localhost:8081/auth/strava/callback"
- name: Seed test user for Maestro tests
run: |
./target/release/pierre-cli user create --email mobiletest@pierre.dev --password MobileTest123!
env:
DATABASE_URL: "sqlite:${{ github.workspace }}/data/maestro-android-test.db"
PIERRE_MASTER_ENCRYPTION_KEY: "rEFe91l6lqLahoyl9OSzum9dKa40VvV5RYj8bHGNTeo="
RUST_LOG: "warn"
- name: Enable KVM for Android emulator
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Prebuild Android
working-directory: frontend-mobile
run: bunx expo prebuild --platform android --clean
- name: Build Android APK
working-directory: frontend-mobile
run: |
cd android
./gradlew assembleDebug
env:
GRADLE_OPTS: "-Xmx4g -XX:+UseParallelGC"
- name: Run Maestro tests on Android
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
target: google_apis
arch: x86_64
avd-name: maestro_test_emulator
force-avd-creation: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
# Install the APK
adb install frontend-mobile/android/app/build/outputs/apk/debug/app-debug.apk
# Run Maestro tests (cd and test must be chained - each line runs in separate shell)
cd frontend-mobile && $HOME/.maestro/bin/maestro test .maestro/ --format junit --output maestro-results-android.xml
env:
TEST_EMAIL: "mobiletest@pierre.dev"
TEST_PASSWORD: "MobileTest123!"
BACKEND_URL: "http://10.0.2.2:8081"
- name: Upload Maestro test results
uses: actions/upload-artifact@v4
if: always()
with:
name: maestro-test-results-android
path: |
frontend-mobile/maestro-results-android.xml
frontend-mobile/.maestro/
retention-days: 7