Appium in CI/CD — GitHub Actions and Jenkins

9 min read

Appium tests that only run on a developer's laptop are not CI tests — they are manual tests with extra steps. The value of mobile automation comes from running it automatically on every pull request, every merge to main, and every nightly release candidate. This lesson shows how to run Appium tests in GitHub Actions and Jenkins, covering the environment setup that trips most teams up.

The CI challenge for mobile

Web automation in CI is straightforward: install a browser, run Selenium. Mobile automation has more moving parts:

  • Android tests need a JDK, Android SDK, an emulator, and Appium
  • iOS tests need macOS, Xcode, a Simulator or a connected device, and Appium
  • Emulators/Simulators must be booted before tests start
  • Boot time varies (2–4 minutes) and must be waited on reliably

GitHub Actions provides Ubuntu and macOS hosted runners. Android tests work on both (emulators run on Ubuntu). iOS tests require macOS runners.

GitHub Actions: Android emulator workflow

Create .github/workflows/android-tests.yml:

name: Android Appium Tests
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  android-tests:
    runs-on: ubuntu-latest
 
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
 
      - name: Set up Android SDK
        uses: android-actions/setup-android@v3
 
      - name: Install Android emulator
        run: |
          sdkmanager "system-images;android-34;google_apis;x86_64"
          sdkmanager "emulator"
          sdkmanager "platform-tools"
          echo "no" | avdmanager create avd \
            --name "test_avd" \
            --package "system-images;android-34;google_apis;x86_64" \
            --device "pixel_6"
 
      - name: Start Android emulator
        run: |
          emulator -avd test_avd -no-window -no-audio -no-snapshot-load &
          adb wait-for-device
          until [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" = "1" ]; do
            sleep 3
          done
          echo "Emulator is ready"
 
      - 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 Appium server
        run: appium --log appium.log &
 
      - name: Wait for Appium to be ready
        run: |
          until curl -s http://127.0.0.1:4723/status | grep -q '"ready":true'; do
            sleep 2
          done
 
      - name: Run tests
        run: mvn test -Dtest=AndroidSmokeTest
 
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: |
            target/surefire-reports/
            target/screenshots/
            appium.log

Key points:

  • emulator ... & starts the emulator in the background
  • The until loop polls sys.boot_completed rather than sleeping a fixed time
  • Appium server also starts in the background; the curl loop waits for the status endpoint
  • if: always() on the upload step ensures artifacts are saved even when tests fail

GitHub Actions: iOS Simulator workflow

name: iOS Appium Tests
 
on:
  push:
    branches: [main]
 
jobs:
  ios-tests:
    runs-on: macos-14   # M1/M2 runners support Simulators
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
 
      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_15.4.app
 
      - name: Boot iOS Simulator
        run: |
          xcrun simctl boot "iPhone 15 Pro" || true
          xcrun simctl list devices | grep "Booted"
 
      - name: Install Appium
        run: |
          npm install -g appium
          appium driver install xcuitest
 
      - name: Start Appium server
        run: appium --log appium.log &
 
      - name: Run tests
        run: mvn test -Dtest=IOSSmokeTest
 
      - name: Upload artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: ios-test-results
          path: target/surefire-reports/

macOS GitHub Actions runners are more expensive than Ubuntu. Keep iOS CI lean by running only smoke tests on every PR and the full suite on nightly builds.

Jenkins pipeline

For a Jenkins pipeline, create a Jenkinsfile in the project root:

pipeline {
    agent { label 'android-agent' }   // agent with Android SDK pre-installed
 
    environment {
        ANDROID_HOME = '/opt/android-sdk'
        PATH = "${ANDROID_HOME}/emulator:${ANDROID_HOME}/platform-tools:${env.PATH}"
        JAVA_HOME = tool 'JDK17'
    }
 
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
 
        stage('Start Emulator') {
            steps {
                sh '''
                    emulator -avd ci_avd -no-window -no-audio -no-snapshot-load &
                    adb wait-for-device
                    until [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" = "1" ]; do
                        sleep 3
                    done
                '''
            }
        }
 
        stage('Start Appium') {
            steps {
                sh 'appium --log ${WORKSPACE}/appium.log &'
                sh '''
                    until curl -s http://127.0.0.1:4723/status | grep -q "ready"; do
                        sleep 2
                    done
                '''
            }
        }
 
        stage('Run Tests') {
            steps {
                sh 'mvn test'
            }
        }
    }
 
    post {
        always {
            junit 'target/surefire-reports/*.xml'
            archiveArtifacts artifacts: 'target/screenshots/**, appium.log', allowEmptyArchive: true
        }
        failure {
            echo 'Tests failed — check archived screenshots and Appium log'
        }
    }
}

Jenkins HTML report issue

When you archive JMeter or HTML reports in Jenkins, the Content Security Policy blocks JavaScript. The same applies to any HTML report (Extent Reports, Surefire HTML). To allow scripts in archived reports:

// In Jenkins script console (Manage Jenkins → Script Console)
System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "")

Or add it to JAVA_OPTS in Jenkins startup configuration.

Connecting CI to cloud providers

For cloud-based tests in CI, store credentials as secrets:

# GitHub Actions — use repository secrets
- name: Run BrowserStack tests
  env:
    BS_USERNAME: ${{ secrets.BS_USERNAME }}
    BS_ACCESS_KEY: ${{ secrets.BS_ACCESS_KEY }}
  run: mvn test -Dtarget=browserstack

In Jenkins, use the Credentials plugin and inject with withCredentials:

withCredentials([
    string(credentialsId: 'bs-username', variable: 'BS_USERNAME'),
    string(credentialsId: 'bs-access-key', variable: 'BS_ACCESS_KEY')
]) {
    sh 'mvn test -Dtarget=browserstack'
}

Test selection strategy in CI

Not every test should run on every trigger. A practical tiering:

TriggerTests to run
Pull requestSmoke tests (5–10 tests, ~5 min)
Merge to mainFull regression on emulator (~30 min)
NightlyFull regression on real device cloud (multi-device matrix)
Pre-release tagFull suite + manual exploratory

Implement with Maven Surefire groups:

# Smoke tests only
mvn test -Dgroups=smoke
 
# Full regression
mvn test -Dgroups=smoke,regression

Tag tests with TestNG groups:

@Test(groups = {"smoke", "regression"})
public void loginTest() { ... }
 
@Test(groups = {"regression"})
public void advancedFeatureTest() { ... }

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