CI is where mobile suites earn their keep. A push to a feature branch should trigger an automated check that catches regressions before code review. This lesson covers a practical GitHub Actions workflow for an Appium Java suite targeting Android emulators, with optional BrowserStack integration for the full device matrix.
Workflow structure
name: Mobile Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
android-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Appium
run: |
npm install -g appium
appium driver install uiautomator2
- name: Start Android Emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
target: google_apis
arch: x86_64
profile: pixel_6
script: mvn test -DsuiteFile=testng-smoke.xml
- name: Upload Allure Results
uses: actions/upload-artifact@v4
if: always()
with:
name: allure-results
path: target/allure-resultsAndroid emulator in CI
GitHub Actions' ubuntu-latest runners support Android emulators via hardware acceleration (KVM). The reactivecircus/android-emulator-runner action handles:
- Starting the AVD
- Waiting for it to boot
- Running your
scriptcommand - Shutting down
The script parameter is the Maven command to run. It runs after the emulator is fully booted — no sleep or wait-for-adb logic needed.
Starting Appium in CI
Appium must be running before tests start. Two approaches:
Programmatic (recommended): Use AppiumServiceBuilder in your suite's @BeforeSuite. No separate process management needed.
Background process:
- name: Start Appium Server
run: appium --base-path / --log target/appium.log &
- name: Wait for Appium
run: |
timeout 30 bash -c 'until curl -s http://127.0.0.1:4723/status; do sleep 1; done'The & backgrounds Appium. The wait loop polls until the status endpoint responds.
Environment variables for secrets
Store BrowserStack credentials as GitHub repository secrets:
- name: Run on BrowserStack
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
BUILD_NUMBER: ${{ github.run_number }}
run: |
APP_URL=$(curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \
-X POST https://api-cloud.browserstack.com/app-automate/upload \
-F "file=@target/app-debug.apk" | jq -r '.app_url')
mvn test \
-DsuiteFile=testng-browserstack.xml \
-Dbrowserstack=true \
-DBROWSERSTACK_APP_URL="$APP_URL"Smoke on PR, full regression nightly
name: PR Smoke Check
on:
pull_request:
branches: [main, develop]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
# ... setup steps ...
- name: Run smoke suite
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
script: mvn test -DsuiteFile=testng-smoke.xml
---
name: Nightly Regression
on:
schedule:
- cron: '0 2 * * 1-5' # 2am Mon-Fri
jobs:
regression:
runs-on: ubuntu-latest
steps:
# ... setup steps ...
- name: Run full regression on BrowserStack
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
run: mvn test -DsuiteFile=testng-regression.xml -Dbrowserstack=trueCaching emulator snapshots
Emulator cold boots take 2–4 minutes. The android-emulator-runner action supports snapshot-based boot which reduces this to ~30 seconds after the first run:
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
emulator-options: -no-snapshot-save
disable-animations: true
script: mvn test -DsuiteFile=testng-smoke.xmldisable-animations: true sets developer option animation scales to 0 — this removes animation delays from test execution and reduces flake significantly.
Publishing Allure to GitHub Pages
- name: Generate Allure Report
if: always()
run: mvn allure:report
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: target/site/allure-maven-pluginThe report is then available at https://{org}.github.io/{repo}/ after each main branch run.
Slack notification on failure
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Mobile tests failed on ${{ github.ref_name }} — ${{ github.run_url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}