Appium Java Tests in GitHub Actions — Full Pipeline

7 min read

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-results

Android emulator in CI

GitHub Actions' ubuntu-latest runners support Android emulators via hardware acceleration (KVM). The reactivecircus/android-emulator-runner action handles:

  1. Starting the AVD
  2. Waiting for it to boot
  3. Running your script command
  4. 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=true

Caching 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.xml

disable-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-plugin

The 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 }}

// tip to track lessons you complete and pick up where you left off across devices.