Expo + Maestro CI Pipeline Overviews (EAS Custom Builds, Maestro Cloud)

Pipeline overview

  • Push to main triggers a GitHub Action workflow
  • The workflow installs dependencies, installs EAS CLI, then runs an EAS build with a build-and-maestro-test profile
  • Artifacts and logs are uploaded for debugging.

Note: I handled the responsibility of creating our mobile app CI pipeline.

EAS Custom Builds vs Maestro Cloud (cost-driven choice)

I implemented both EAS‑based and Maestro Cloud pipelines and validated they work

We run on EAS Build because it’s significantly cheaper. As of Aug 24, 2025: EAS Starter Plan starts at $19/month; Maestro Cloud is about $212.50/month

EAS Custom Builds Configuration

Here is our build-and-maestro-test configuration (eas.json):

"build-and-maestro-test": {
    "withoutCredentials": true,
    "config": "build-and-maestro-test.yml",
    "android": {
    "buildType": "apk",
    "image": "latest"
    },
    "ios": {
    "simulator": true,
    "image": "latest"
    },
    "channel": "build-and-maestro-test"
}

Here is the corresponding Github Actions workflow file:

name: EAS Build & Maestro Tests

on:
  push:
    branches:
      - "main"
  pull_request:
    branches:
      - "main"

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    timeout-minutes: 120
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Install Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install

      - name: Install EAS CLI
        run: npm install -g eas-cli

      - name: Run EAS Build with Maestro Tests
        run: EXPO_TOKEN=${{ secrets.EXPO_TOKEN }} eas build --platform android --profile build-and-maestro-test --non-interactive

Maestro Cloud Configuration

Here is the Github Actions workflow file for this:

name: EAS Build & Maestro Tests

on:
  push:
    branches:
      - "**"

jobs:
  build_and_test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Install Bun
        uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Install dependencies
        run: bun install

      - name: Install EAS CLI
        run: npm install -g eas-cli

      - name: Fetch Latest EAS Build Artifact
        run: |
          # Export Expo token for authentication
          export EXPO_TOKEN=${{ secrets.EXPO_TOKEN }}
          # Get latest build URL
          BUILDS_JSON=$(eas build:list --platform android --profile production --status finished --json --non-interactive)
          # Validate JSON response
          if [[ -z "$BUILDS_JSON" || "$BUILDS_JSON" == "[]" ]]; then
            echo "No completed builds found!"
            exit 1
          fi
          # Extract the build URL
          BUILD_URL=$(echo "$BUILDS_JSON" | jq -r '.[0].artifacts.buildUrl')
          # Ensure a valid URL was extracted
          if [[ -z "$BUILD_URL" || "$BUILD_URL" == "null" ]]; then
            echo "No valid build URL found!"
            exit 1
          fi
          echo "Downloading APK from $BUILD_URL"
          # Download the APK using Expo authentication
          curl -L -o app-release.apk $BUILD_URL
          echo "APK downloaded to app-release.apk"
          ls -la
          
      - name: Install Maestro CLI
        run: |
          curl -Ls "https://get.maestro.mobile.dev" | bash
          echo "$HOME/.maestro/bin" >> $GITHUB_PATH
          
      - name: Run Maestro Cloud Tests on APK
        uses: mobile-dev-inc/action-maestro-cloud@v1.9.6
        with:
          api-key: ${{ secrets.MAESTRO_API_KEY }}
          project-id: ${{ secrets.PROJECT_ID }}
          app-file: app-release.apk
          workspace: maestro
          android-api-level: 33
          env: |
            MAESTRO_TEST_EMAIL=${{ secrets.MAESTRO_TEST_EMAIL }}
            MAESTRO_TEST_PASSWORD=${{ secrets.MAESTRO_TEST_PASSWORD }}

Tips and tricks from our experience

These tests can be flaky:

  • Add short delays before interacting with components in dialogs
  • Use pixel coordinates when IDs aren’t reliable
  • Use runScript for dynamic data
  • I tested locally on a Pixel 5 emulator (API 33)