Skip to content

CI Integration

This page covers running AutoMobile JUnit tests in GitHub Actions using an emulator.wtf ADB session. The auto-mobile-tests job shown here is the reference workflow used in this repository.

Overview

AutoMobile tests need:

  1. A built and installed app APK on a connected device
  2. A running AutoMobile daemon reachable over the Unix socket
  3. The CtrlProxy accessibility service installed on the device
  4. A Gradle test task that can reach the daemon and the device via ADB

The sections below walk through each step.

Full workflow job

auto-mobile-tests:
  name: "AutoMobile JUnit Tests"
  runs-on: ubuntu-latest
  # Requires EW_API_TOKEN for the emulator.wtf ADB session.
  # Contact support@emulator.wtf to request access to start-session.
  if: github.secret_source == 'Actions'
  needs:
    - build-apk
  steps:
    - name: "Git Checkout"
      uses: actions/checkout@v4

    - name: "Setup Bun"
      uses: oven-sh/setup-bun@v2

    - name: "Install auto-mobile"
      shell: bash
      run: npm install -g @kaeawc/auto-mobile --ignore-scripts

    - name: "Download app APK"
      uses: actions/download-artifact@v4.1.8
      with:
        name: apk

    - name: "Install ew-cli"
      shell: bash
      run: |
        curl -fsSL \
          https://github.com/emulator-wtf/ew-cli/releases/latest/download/ew-cli-linux \
          -o /usr/local/bin/ew-cli
        chmod +x /usr/local/bin/ew-cli

    - name: "Start emulator.wtf ADB session"
      shell: bash
      env:
        EW_API_TOKEN: ${{ secrets.EW_API_TOKEN }}
      run: |
        ew-cli start-session \
          --device model=Pixel6,version=33,gpu=auto \
          --adb \
          --max-time-limit 20m &
        echo "EW_SESSION_PID=$!" >> "$GITHUB_ENV"
        adb wait-for-device
        adb shell 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 2; done'

    - name: "Install app APK on emulator"
      shell: bash
      run: adb install app-debug.apk

    - name: "Start auto-mobile daemon"
      shell: bash
      run: |
        auto-mobile --daemon-mode &
        echo "AUTOMOBILE_DAEMON_PID=$!" >> "$GITHUB_ENV"
        until [ -S "/tmp/auto-mobile-daemon-$(id -u).sock" ]; do sleep 1; done

    - name: "Pre-install CtrlProxy via observe"
      shell: bash
      run: auto-mobile --cli observe --platform android

    - name: "Install JDK"
      uses: actions/setup-java@v5
      with:
        distribution: zulu
        java-version: '21'

    - name: "Setup Gradle"
      uses: gradle/actions/setup-gradle@v4
      with:
        cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
        cache-cleanup: on-success
        validate-wrappers: true

    - name: "Restore Android SDK cache"
      id: cache-android-sdk
      uses: actions/cache/restore@v4
      with:
        path: |
          ~/.android
          ~/.config
        key: v1-${{ runner.os }}-android-sdk

    - name: "Setup Android SDK"
      if: steps.cache-android-sdk.outputs.cache-hit != 'true'
      uses: android-actions/setup-android@v3

    - name: "Save Android SDK cache"
      if: steps.cache-android-sdk.outputs.cache-hit != 'true'
      uses: actions/cache/save@v4
      with:
        path: |
          ~/.android
          ~/.config
        key: v1-${{ runner.os }}-android-sdk

    - name: "Run AutoMobile JUnit Tests"
      shell: bash
      run: >-
        ./gradlew :app:testDebugUnitTest
        --tests 'com.example.automobiletest.*'
        --continue
        --stacktrace

    - name: "Stop emulator.wtf session and daemon"
      if: always()
      shell: bash
      run: |
        kill "$AUTOMOBILE_DAEMON_PID" 2>/dev/null || true
        kill "$EW_SESSION_PID" 2>/dev/null || true

    - name: "Publish Test Report"
      uses: mikepenz/action-junit-report@v4
      if: always()
      with:
        check_name: "AutoMobile Test Report"
        report_paths: 'app/build/test-results/**/*.xml'

Step-by-step breakdown

1. Bun and auto-mobile

The daemon is a Node/Bun application. setup-bun ensures the Bun runtime is available on the runner so that auto-mobile --daemon-mode works correctly.

- uses: oven-sh/setup-bun@v2

- run: npm install -g @kaeawc/auto-mobile --ignore-scripts

--ignore-scripts skips the optional native post-install hooks in the npm package, which are not needed on CI and can fail on headless runners.

2. emulator.wtf ADB session

The start-session --adb flag from ew-cli provisions a managed emulator and bridges its ADB connection to the runner’s local ADB server, making the device visible to all subsequent adb and ./gradlew commands.

ew-cli start-session \
  --device model=Pixel6,version=33,gpu=auto \
  --adb \
  --max-time-limit 20m &

Closed alpha feature

start-session --adb is a closed alpha feature. Contact support@emulator.wtf to request access.

After starting the session in the background, the step waits for the device to finish booting:

adb wait-for-device
adb shell 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 2; done'

Device configuration options

Flag Example values Notes
model Pixel6, Pixel9Pro, GalaxyS22 Physical model to emulate
version 3035 Android API level
gpu auto, swiftshader_indirect, host GPU rendering mode; auto picks the best available
--max-time-limit 10m, 20m, 30m Hard session time limit; the session ends and billing stops after this

3. App APK installation

AutoMobile tests run against an already-installed app. The APK is downloaded from a prior build-apk job artifact and installed onto the emulator:

- name: "Download app APK"
  uses: actions/download-artifact@v4.1.8
  with:
    name: apk

- name: "Install app APK on emulator"
  run: adb install app-debug.apk

4. Daemon startup

Start the daemon in the background and capture its PID so it can be cleanly shut down at the end:

auto-mobile --daemon-mode &
echo "AUTOMOBILE_DAEMON_PID=$!" >> "$GITHUB_ENV"
until [ -S "/tmp/auto-mobile-daemon-$(id -u).sock" ]; do sleep 1; done

The until loop polls for the Unix socket file instead of using a fixed sleep, so it proceeds as soon as the daemon is ready regardless of machine speed.

5. CtrlProxy pre-installation

The --cli observe command instructs the daemon to take a screenshot and capture the accessibility hierarchy. As a side effect it installs the CtrlProxy accessibility service APK if it is not already present:

auto-mobile --cli observe --platform android

Running this once before the test task ensures the CtrlProxy is ready before observe steps in test plans execute, avoiding installation delays mid-test.

6. Gradle test task

The reference workflow above uses an internal composite action. If you are setting this up in your own repository, expand it to these steps:

- name: "Install JDK"
  uses: actions/setup-java@v5
  with:
    distribution: zulu
    java-version: '21'

- name: "Setup Gradle"
  uses: gradle/actions/setup-gradle@v4
  with:
    cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
    cache-cleanup: on-success
    validate-wrappers: true

- name: "Restore Android SDK cache"
  id: cache-android-sdk
  uses: actions/cache/restore@v4
  with:
    path: |
      ~/.android
      ~/.config
    key: v1-${{ runner.os }}-android-sdk

- name: "Setup Android SDK"
  if: steps.cache-android-sdk.outputs.cache-hit != 'true'
  uses: android-actions/setup-android@v3

- name: "Save Android SDK cache"
  if: steps.cache-android-sdk.outputs.cache-hit != 'true'
  uses: actions/cache/save@v4
  with:
    path: |
      ~/.android
      ~/.config
    key: v1-${{ runner.os }}-android-sdk

- name: "Run AutoMobile JUnit Tests"
  shell: bash
  run: >-
    ./gradlew :app:testDebugUnitTest
    --tests 'com.example.automobiletest.*'
    --continue
    --stacktrace

gradle/actions/setup-gradle handles Gradle wrapper caching and the build cache automatically. Environment variables like AUTOMOBILE_CTRL_PROXY_APK_PATH are safe with the configuration cache as long as they are declared via providers.environmentVariable() in your Gradle build — which is exactly what the Project Setup example does. Gradle tracks the provider as a configuration input and invalidates the cache entry automatically when the value changes.

7. Cleanup

The if: always() ensures cleanup runs even when tests fail:

kill "$AUTOMOBILE_DAEMON_PID" 2>/dev/null || true
kill "$EW_SESSION_PID" 2>/dev/null || true

Killing the ew-cli background process ends the emulator.wtf session and stops billing.

8. Test report publishing

- uses: mikepenz/action-junit-report@v4
  if: always()
  with:
    check_name: "AutoMobile Test Report"
    report_paths: 'app/build/test-results/**/*.xml'

The JUnit XML reports written to app/build/test-results/ are picked up and published as a GitHub check, making failures visible in the PR Checks tab.

Required secrets

Secret Used by How to obtain
EW_API_TOKEN ew-cli start-session emulator.wtf dashboard → API tokens
GRADLE_ENCRYPTION_KEY Gradle configuration cache encryption Generate a random 256-bit base64 key; store in repository secrets

Set both secrets under Settings → Secrets and variables → Actions in your GitHub repository.

Job dependencies

The auto-mobile-tests job should depend on build-apk so it downloads the APK artifact once it is ready:

needs:
  - build-apk

Conditional execution

The if: github.secret_source == 'Actions' guard prevents the job from running on forks or for pull requests from contributors who do not have access to repository secrets. This avoids spurious failures when EW_API_TOKEN is unavailable.

To also skip on draft pull requests:

if: github.secret_source == 'Actions' && github.event.pull_request.draft != true

Running without emulator.wtf

If you have a self-hosted runner with an emulator already running, omit the ew-cli steps and replace the adb wait-for-device block with a step that starts your emulator. Everything else (daemon startup, CtrlProxy pre-install, Gradle task) remains the same.

- name: "Start AVD emulator"
  uses: reactivecircus/android-emulator-runner@v2
  with:
    api-level: 33
    target: google_apis
    arch: x86_64
    profile: pixel_6
    script: |
      adb wait-for-device
      adb shell 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 2; done'

See also

  • Project Setup — Gradle config, SNAPSHOT dependency, local dev
  • Writing Tests@AutoMobileTest parameters, YAML plan reference
  • CtrlProxy — Accessibility service setup and version management