Skip to main content
GitHub Actions is a popular CI/CD platform, especially for open-source projects. This guide shows you how to run Patrol tests effectively in GitHub Actions.

Quick start

Here’s a minimal workflow to run Patrol tests on Android:
.github/workflows/test.yml
name: Run Patrol tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    
    steps:
      - name: Clone repository
        uses: actions/checkout@v4

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: 3.32.x
          channel: stable
          cache: true

      - name: Install Patrol CLI
        run: dart pub global activate patrol_cli

      - name: Build test APKs
        run: patrol build android

      - name: Run tests on emulator.wtf
        env:
          EW_API_TOKEN: ${{ secrets.EW_API_TOKEN }}
        run: |
          curl "https://maven.emulator.wtf/releases/ew-cli" -o ew-cli
          chmod +x ew-cli
          ./ew-cli \
            --app build/app/outputs/apk/debug/app-debug.apk \
            --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
            --device model=Pixel7,version=34

Platform-specific configurations

Android emulator testing

Running Android emulators directly on GitHub Actions is slow and unstable. We recommend using emulator.wtf or Firebase Test Lab instead.
If you must run emulators directly:
name: Android Emulator Tests

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
          
      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: 3.32.x
          channel: stable
          
      - name: Enable KVM group perms
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm
          
      - name: Gradle cache
        uses: gradle/gradle-build-action@v3
        
      - name: Install Patrol CLI
        run: dart pub global activate patrol_cli
        
      - name: Run tests on emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          target: google_apis
          arch: x86_64
          script: patrol test --wait

With emulator.wtf

emulator.wtf provides fast, stable Android emulators:
- name: Install ew-cli
  run: |
    mkdir -p "$HOME/bin"
    curl "https://maven.emulator.wtf/releases/ew-cli" -o "$HOME/bin/ew-cli"
    chmod +x "$HOME/bin/ew-cli"
    echo "$HOME/bin" >> $GITHUB_PATH

- name: Build APKs
  run: patrol build android --verbose

- name: Run tests on emulator.wtf
  env:
    EW_API_TOKEN: ${{ secrets.EW_API_TOKEN }}
  run: |
    ew-cli \
      --app build/app/outputs/apk/debug/app-debug.apk \
      --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
      --device model=Pixel7,version=34 \
      --device model=Pixel7,version=33

With Firebase Test Lab

See the Firebase Test Lab integration guide for complete setup instructions.

Complete example from Patrol repository

Here’s a real workflow from Patrol’s own CI:
name: test android emulator

on:
  pull_request:
  schedule:
    - cron: "0 */12 * * *"

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  run_tests:
    name: Flutter ${{ matrix.flutter-version }} on API ${{ matrix.api-level }}
    runs-on: ubuntu-latest
    timeout-minutes: 30

    strategy:
      fail-fast: false
      matrix:
        flutter-version: ["3.32.x"]
        flutter-channel: ["stable"]
        api-level: [36, 35, 34, 33, 32]

    defaults:
      run:
        working-directory: dev/e2e_app

    steps:
      - name: Clone repository
        uses: actions/checkout@v4

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Gradle cache
        uses: gradle/gradle-build-action@v3

      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: ${{ matrix.flutter-version }}
          channel: ${{ matrix.flutter-channel }}

      - name: Preload Flutter artifacts
        run: flutter precache --android

      - name: Install dependencies
        run: flutter pub get

      - name: Install Patrol CLI
        run: dart pub global activate patrol_cli

      - name: Build test APKs
        run: |
          patrol build android \
            --tags='android && emulator' \
            --dart-define-from-file=defines.json \
            --verbose

      - name: Install ew-cli
        run: |
          mkdir -p "$HOME/bin"
          curl "https://maven.emulator.wtf/releases/ew-cli" -o "$HOME/bin/ew-cli"
          chmod +x "$HOME/bin/ew-cli"
          echo "$HOME/bin" >> $GITHUB_PATH

      - name: Upload to emulator.wtf and run tests
        env:
          EW_API_TOKEN: ${{ secrets.EW_API_TOKEN }}
        run: |
          ew-cli \
            --app build/app/outputs/apk/debug/app-debug.apk \
            --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
            --device model=Pixel7,version=${{ matrix.api-level }} \
            --display-name "Patrol tests API ${{ matrix.api-level }}"

Optimization strategies

Caching

Cache Flutter SDK and dependencies:
- name: Set up Flutter
  uses: subosito/flutter-action@v2
  with:
    flutter-version: 3.32.x
    channel: stable
    cache: true  # Enable caching

- name: Cache Gradle dependencies
  uses: gradle/gradle-build-action@v3
  with:
    cache-read-only: ${{ github.ref != 'refs/heads/main' }}

Parallel testing

Run tests across multiple configurations:
strategy:
  fail-fast: false
  matrix:
    api-level: [34, 33, 32, 31]
    device: [Pixel7, NexusLowRes]

Conditional execution

Run tests only when relevant files change:
on:
  pull_request:
    paths:
      - 'lib/**'
      - 'test/**'
      - 'integration_test/**'
      - 'android/**'
      - 'ios/**'
      - 'pubspec.yaml'

Artifact collection

Always collect test artifacts for debugging:
- name: Upload test results
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: |
      build/app/outputs/
      **/*.log
      **/*.mp4
    retention-days: 7

Troubleshooting

Enable KVM acceleration:
- name: Enable KVM
  run: |
    echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
    sudo udevadm control --reload-rules
    sudo udevadm trigger --name-match=kvm
  • Increase the timeout-minutes value (default is 360)
  • Use faster device labs like emulator.wtf
  • Reduce the number of tests or split into multiple jobs
  • Check if your app is hanging during startup
Make sure you’re using the latest macOS runner:
runs-on: macos-14  # or macos-latest
And install CocoaPods dependencies:
- name: Install pods
  working-directory: ios
  run: pod install
Make sure executables have proper permissions:
- name: Make script executable
  run: chmod +x ./scripts/run_tests.sh

Best practices

Use device labs

Prefer emulator.wtf or Firebase Test Lab over running emulators directly on CI runners.

Cache aggressively

Cache Flutter SDK, pub cache, and Gradle dependencies to speed up builds.

Run in parallel

Test multiple device configurations simultaneously using matrix strategies.

Collect artifacts

Always upload test videos, logs, and reports for debugging failures.

Next steps

Firebase Test Lab

Learn how to use Firebase Test Lab with GitHub Actions

CI/CD Overview

Back to CI/CD overview

Build docs developers (and LLMs) love