Appium Python Tests in GitHub Actions — Full Pipeline

7 min read

GitHub Actions provides the infrastructure for running mobile tests on every PR and on a nightly schedule. This lesson covers a working workflow for Android emulator tests, optional BrowserStack integration, and Allure report publishing.

Workflow for Android emulator tests

name: Mobile Smoke Tests
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  android-smoke:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
 
      - name: Cache pip dependencies
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
 
      - name: Install Python dependencies
        run: pip install -r requirements.txt
 
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
 
      - name: Install Appium
        run: |
          npm install -g appium
          appium driver install uiautomator2
 
      - name: Run tests with Android emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          target: google_apis
          arch: x86_64
          profile: pixel_6
          disable-animations: true
          script: |
            appium --base-path / --log appium.log &
            sleep 5
            pytest -m smoke --alluredir=allure-results
 
      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: allure-results
          path: allure-results
 
      - name: Upload Appium log on failure
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: appium-log
          path: appium.log

Programmatic Appium vs background process

Background process (shown above):

appium --base-path / --log appium.log &
sleep 5

The sleep 5 is a blunt wait for server startup. Replace with a health check loop:

appium --base-path / --log appium.log &
timeout 30 bash -c 'until curl -sf http://127.0.0.1:4723/status; do sleep 1; done'

Programmatic (preferred for clean teardown):

Use subprocess in a session-scoped fixture:

@pytest.fixture(scope="session", autouse=True)
def appium_server():
    import subprocess
    import time
    proc = subprocess.Popen(["appium", "--base-path", "/"],
                             stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    time.sleep(4)
    yield
    proc.terminate()

Nightly regression on BrowserStack

name: Nightly Regression
 
on:
  schedule:
    - cron: '0 2 * * 1-5'  # 2am UTC Mon-Fri
 
jobs:
  regression-browserstack:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
 
      - run: pip install -r requirements.txt
 
      - name: Upload app to BrowserStack
        env:
          BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
          BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
        run: |
          APP_URL=$(curl -s -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \
            -X POST https://api-cloud.browserstack.com/app-automate/upload \
            -F "file=@apps/app-debug.apk" | python3 -c "import sys,json; print(json.load(sys.stdin)['app_url'])")
          echo "BROWSERSTACK_APP_URL=$APP_URL" >> $GITHUB_ENV
 
      - name: Run regression suite
        env:
          BROWSERSTACK: "true"
          BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
          BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
          BUILD_NUMBER: ${{ github.run_number }}
        run: pytest -m regression -n 2 --alluredir=allure-results
 
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: allure-results
          path: allure-results

Secrets configuration

Store credentials in GitHub repository settings under Settings > Secrets and variables > Actions:

  • BROWSERSTACK_USERNAME
  • BROWSERSTACK_ACCESS_KEY
  • SLACK_WEBHOOK_URL (optional)

Reference them in workflows as ${{ secrets.SECRET_NAME }}.

Publishing Allure to GitHub Pages

- name: Generate Allure Report
  if: always()
  run: allure generate allure-results -o allure-report --clean
 
- name: Deploy to GitHub Pages
  if: github.ref == 'refs/heads/main' && always()
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: allure-report

The report is available at https://<org>.github.io/<repo>/ after each main branch run.

Disabling animations in CI

Animations cause flake by adding non-deterministic delays to transitions:

# conftest.py
import os
 
@pytest.fixture(scope="session", autouse=True)
def disable_animations(driver):
    """Disable Android animations in CI."""
    if os.getenv("CI"):
        for scale in ["window_animation_scale", "transition_animation_scale",
                      "animator_duration_scale"]:
            driver.execute_script("mobile: shell", {
                "command": f"settings put global {scale} 0"
            })
    yield
    # Restore animations after the session
    if os.getenv("CI"):
        for scale in ["window_animation_scale", "transition_animation_scale",
                      "animator_duration_scale"]:
            driver.execute_script("mobile: shell", {
                "command": f"settings put global {scale} 1"
            })

The reactivecircus/android-emulator-runner action also supports disable-animations: true which achieves the same effect at the runner level.

Slack notification on failure

- name: Notify Slack
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": ":x: Mobile tests failed on ${{ github.ref_name }}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Mobile Tests Failed*\nBranch: `${{ github.ref_name }}`\n<${{ github.run_url }}|View run>"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

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