Running Maestro tests in GitHub Actions: a working setup

The workflow my team actually runs: an Android emulator job, tags for smoke vs regression, parallel shards, and artifacts that make failures debuggable.

Once my first flows passed locally, the real work was getting Maestro tests in GitHub Actions — because a suite that only runs on my laptop is a demo, not automation. This is the setup my team runs today: an Android emulator job, tags that split smoke from regression, parallel shards, and artifacts that turn a red build into something you can actually debug.

Getting Maestro tests running in GitHub Actions

GitHub-hosted runners don't come with a running emulator, so the job has to create one. The reactivecircus/android-emulator-runner action handles booting and snapshot caching; on Linux runners, enable KVM first (two udev lines, documented in the action's README) or the emulator will crawl. The skeleton:

name: maestro-smoke
on: [pull_request]

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Maestro
        run: curl -fsSL "https://get.maestro.mobile.dev" | bash
      - name: Run smoke suite
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          arch: x86_64
          script: |
            adb install app-debug.apk
            $HOME/.maestro/bin/maestro test .maestro/flows --include-tags=smoke
      - name: Upload failure artifacts
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: maestro-failures
          path: ~/.maestro/tests

Build the APK in an earlier step or download it from your build pipeline — the emulator job should install a ready binary, not compile one. And pin the action version and API level explicitly: an emulator image that changes silently underneath you is a flakiness source no test fix will ever cure.

Tags decide what runs when

Every flow carries tags: [smoke] or tags: [regression] in its header. The workflow above runs the smoke set on every pull request — it finishes in minutes and answers "is this build worth testing?". A second workflow, triggered by schedule (nightly) and by release branches, runs with --include-tags=regression. Same flows folder, same tooling, two very different feedback loops. The tag is also documentation: when someone asks what's covered on every PR, the answer is whatever carries the smoke tag.

Sharding for parallel speed

Once the regression set outgrows a single emulator, split it. Maestro shards natively:

maestro test .maestro/flows \
  --include-tags=regression \
  --shard-split 3 \
  --format junit --output report.xml

On one runner, --shard-split distributes flows across several emulator instances; if a single runner is still too slow, use a job matrix where each job boots its own emulator and runs a subset of flows. Two rules make sharding safe: every flow must be independent (start with launchApp and clearState: true), and no flow may depend on data another flow created.

Failure artifacts are the whole point

A red build with no evidence turns into "rerun it and hope." Maestro writes screenshots and screen recordings of failed flows under ~/.maestro/tests, and the if: failure() upload step attaches them to the workflow run. The JUnit report from --format junit feeds any test-report action, so reviewers see which flow failed without opening a single log.

Optional: Slack and status wiring

The commit status check comes free — a red X on the pull request is usually all the smoke suite needs. For the nightly regression run I add one webhook step, guarded by if: failure(), that posts the workflow URL to the team channel. Resist notifying on every green run: alerts people learn to ignore are worse than no alerts at all.

Start with the smoke job on pull requests only. Once it has been boringly green for two weeks, add the nightly regression workflow, then sharding. Each step of this setup should earn trust before the next one adds complexity.