Appium in GitHub Actions without an emulator
Run an existing Appium or WebdriverIO suite on a hosted Android runner from GitHub Actions, with secrets, JUnit reporting and guaranteed teardown.
A GitHub-hosted runner has no Android emulator and no hardware acceleration. SpawnDroid supplies the device and the Appium server; the workflow supplies the build and the test command.
The short version
Use the official action when the workflow owns the session:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- name: Run UI tests on Android
uses: kl3jvi/spawndroid/actions/android-test@v1
with:
api-key: ${{ secrets.SPAWNDROID_API_KEY }}
app-path: app/build/outputs/apk/debug/app-debug.apk
profile: android-14-phone
command: pnpm test
report-path: reports/junit.xmlThe action uploads the APK, starts a runner, waits for the device, runs your
command with SPAWNDROID_WD_URL, SPAWNDROID_UDID, SPAWNDROID_APP_PACKAGE
and SPAWNDROID_SESSION_ID in the environment, attaches the JUnit XML, reports
pass or fail, and always tears the device down.
The explicit four-call flow
Use this when you already manage the Appium session yourself:
name: UI tests
on: pull_request
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build APK
run: ./gradlew assembleDebug
- name: Start device
id: session
env:
SPAWNDROID_API_KEY: ${{ secrets.SPAWNDROID_API_KEY }}
run: |
APK_ID=$(curl -sS -X POST https://app.spawndroid.io/api/apps \
-H "Authorization: Bearer $SPAWNDROID_API_KEY" \
-F file=@app/build/outputs/apk/debug/app-debug.apk | jq -r '.app.slug')
RESPONSE=$(curl -sS -X POST https://app.spawndroid.io/api/v1/sessions \
-H "Authorization: Bearer $SPAWNDROID_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"artifactId\":\"$APK_ID\"}")
echo "session_id=$(echo "$RESPONSE" | jq -r '.sessionId')" >> "$GITHUB_OUTPUT"
echo "wd_url=$(echo "$RESPONSE" | jq -r '.webdriverUrl')" >> "$GITHUB_OUTPUT"
echo "udid=$(echo "$RESPONSE" | jq -r '.device.serial')" >> "$GITHUB_OUTPUT"
echo "package=$(echo "$RESPONSE" | jq -r '.app.package')" >> "$GITHUB_OUTPUT"
- name: Wait for ready
env:
SPAWNDROID_API_KEY: ${{ secrets.SPAWNDROID_API_KEY }}
run: |
for attempt in $(seq 1 60); do
STATE=$(curl -sS https://app.spawndroid.io/api/v1/sessions/${{ steps.session.outputs.session_id }} \
-H "Authorization: Bearer $SPAWNDROID_API_KEY" | jq -r '.state')
[ "$STATE" = "ready" ] && exit 0
[ "$STATE" = "failed" ] && exit 1
sleep 5
done
exit 1
- name: Run tests
env:
SPAWNDROID_WD_URL: ${{ steps.session.outputs.wd_url }}
SPAWNDROID_UDID: ${{ steps.session.outputs.udid }}
SPAWNDROID_APP_PACKAGE: ${{ steps.session.outputs.package }}
run: pnpm test
- name: Report result
if: always()
env:
SPAWNDROID_API_KEY: ${{ secrets.SPAWNDROID_API_KEY }}
run: |
RESULT=${{ job.status == 'success' && 'passed' || 'failed' }}
curl -sS -X DELETE https://app.spawndroid.io/api/v1/sessions/${{ steps.session.outputs.session_id }} \
-H "Authorization: Bearer $SPAWNDROID_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"result\":\"$RESULT\"}"Secrets and keys
Store the key as a GitHub Actions secret. Give the pipeline its own key with
upload scope: keys are workspace-scoped, not per project, so a key shared
between repositories gives every workflow the same access. Revoke it
independently when a pipeline is retired. Never echo the key; GitHub masks it,
but a set -x shell can still leak it into a log.
Reporting and artifacts
report-path accepts JUnit XML from Jest (jest-junit), pytest
(--junitxml), Gradle/Espresso, Maestro (--format junit) and most other
runners. The run page shows total/failed tests and keeps the raw XML.
Personal and above also retain the final logcat and the screen recording.
Run it on SpawnDroid
Hobby includes one parallel runner and 200 device-minutes a month, no card. Start free or read the CI tests reference for the full API.
Was this page helpful?
Run Appium tests on a cloud Android device
Upload a build, start a hosted Appium runner, and point an existing W3C WebDriver suite at the session URL. No local emulator or SDK.
Run Android tests in parallel
Shard an Appium or WebdriverIO suite across parallel SpawnDroid runners, size the job matrix to the plan, and calculate the device-minute cost.