name: E2E Android
on:
push:
branches: [main, develop]
paths:
- 'packages/**'
- 'examples/demo-app/**'
- 'e2e/**'
- '.github/workflows/e2e-android.yml'
pull_request:
branches: [main, develop]
paths:
- 'packages/**'
- 'examples/demo-app/**'
- 'e2e/**'
- '.github/workflows/e2e-android.yml'
jobs:
check:
name: Run check
runs-on: ubuntu-latest
outputs:
should-run: ${{ steps.changes.outputs.e2e }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
e2e:
- 'packages/**'
- 'examples/demo-app/**'
- 'e2e/**'
- '.github/workflows/e2e-android.yml'
e2e-android:
name: E2E Android
needs: check
if: needs.check.outputs.should-run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free disk space
run: |
sudo apt-get remove -y '^dotnet-.*' 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri
sudo apt-get autoremove -y
sudo apt-get clean
sudo rm -rf /usr/share/dotnet/ /usr/local/graalvm/ /usr/local/.ghcup/ /usr/local/share/powershell /usr/local/share/chromium
- name: Enable KVM
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: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Bun cache
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install dependencies
run: bun install
- name: Build MCP server
run: bun run build
- name: Build MCP client
run: bun run --filter @ohah/react-native-mcp-server/client build
- name: Gradle cache
uses: gradle/actions/setup-gradle@v4
- name: Debug keystore cache
uses: actions/cache@v4
id: keystore-cache
with:
path: examples/demo-app/android/app/debug.keystore
key: android-debug-keystore
- name: Generate debug keystore (CI)
if: steps.keystore-cache.outputs.cache-hit != 'true'
run: |
keytool -genkey -v -keystore app/debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "C=US, O=Android, CN=Android Debug"
working-directory: examples/demo-app/android
- name: Android APK cache
id: app-cache
uses: actions/cache@v4
with:
path: ${{ runner.temp }}/e2e-app-cache/android
key: ${{ runner.os }}-android-apk-${{ hashFiles('examples/demo-app/android/build.gradle', 'examples/demo-app/android/settings.gradle', 'examples/demo-app/android/app/build.gradle', 'examples/demo-app/android/app/src/**', 'examples/demo-app/package.json', 'examples/demo-app/index.js', 'examples/demo-app/metro.config.js', 'examples/demo-app/babel.config.js', 'examples/demo-app/app.json', 'examples/demo-app/tsconfig.json', 'examples/demo-app/src/**', 'examples/demo-app/react-native.config.js', 'packages/react-native-mcp-server/package.json', 'packages/react-native-mcp-server/src/**') }}
restore-keys: |
${{ runner.os }}-android-apk-
- name: Build demo app Release APK
if: steps.app-cache.outputs.cache-hit != 'true'
env:
REACT_NATIVE_MCP_ENABLED: 'true'
run: ./gradlew assembleRelease
working-directory: examples/demo-app/android
- name: Save Android APK to cache
if: steps.app-cache.outputs.cache-hit != 'true'
run: |
mkdir -p "${{ runner.temp }}/e2e-app-cache/android"
cp examples/demo-app/android/app/build/outputs/apk/release/app-release.apk "${{ runner.temp }}/e2e-app-cache/android/"
- name: Copy APK to build output (cache hit)
if: steps.app-cache.outputs.cache-hit == 'true'
run: |
mkdir -p examples/demo-app/android/app/build/outputs/apk/release
cp "${{ runner.temp }}/e2e-app-cache/android/app-release.apk" examples/demo-app/android/app/build/outputs/apk/release/
- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-34
- name: Create AVD snapshot (cache miss)
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
avd-name: avd-34
api-level: 34
target: google_apis
arch: x86_64
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Run E2E YAML tests
id: e2e-test
uses: reactivecircus/android-emulator-runner@v2
with:
avd-name: avd-34
api-level: 34
target: google_apis
arch: x86_64
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
ls -la examples/demo-app/android/app/build/outputs/apk/release/app-release.apk
adb install -r examples/demo-app/android/app/build/outputs/apk/release/app-release.apk
adb reverse tcp:12300 tcp:12300
node packages/react-native-mcp-server/dist/test/cli.js run examples/demo-app/e2e/ -p android -o e2e-artifacts/yaml-results --no-auto-launch
- name: Save screenshot and logs on failure
if: failure()
run: |
mkdir -p e2e-artifacts
adb exec-out screencap -p > e2e-artifacts/failure-screenshot.png 2>/dev/null || true
adb logcat -d 2>/dev/null | tail -n 3000 > e2e-artifacts/logcat.txt || true
- name: Upload artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-android-failure-artifacts
path: e2e-artifacts/
retention-days: 14
if-no-files-found: ignore