GitHub Viewer
name: E2E
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
coverage:
# Runs first; fast static check that gates the long device jobs so
# missing demos / flows fail before the emulator boots.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Check E2E coverage of pythonnative.__all__
run: python scripts/check-e2e-coverage.py
e2e-android:
needs: coverage
runs-on: ubuntu-latest
timeout-minutes: 30
# GitHub-hosted Android emulators grow unstable after ~15 minutes of
# sustained Maestro driving and start reporting "device offline" /
# "device not found" mid-run (the emulator VM, not the app, dies).
# The full suite reliably crosses that threshold around the ~35th
# flow. Sharding the run into a few balanced groups, each on its
# own freshly booted emulator, keeps every session well under the
# limit, and parallelizes the Android pass as a bonus. Groups are
# sized so none exceeds ~15 flows; ``components`` (28 flows after
# the gesture/animated additions) crossed the ceiling at ~flow 26,
# so it runs as two halves, and ``hooks`` (22 flows after the async
# hook additions) now takes ~10 minutes of driving on its own, so
# it gets a dedicated emulator. ``fail-fast: false`` so one shard's
# failure still lets the others report.
strategy:
fail-fast: false
matrix:
include:
- name: components-a
suites: components-a
- name: components-b
suites: components-b
- name: hooks
suites: hooks
- name: nav-gestures-layout-styling
suites: navigation gestures layout styling
- name: anim-misc
suites: animations misc
name: e2e-android (${{ matrix.name }})
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install PythonNative
run: pip install -e .
- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Enable KVM
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: Build, install, and run E2E tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 31
arch: x86_64
script: bash -lc "./scripts/run-e2e.sh android ${{ matrix.suites }}"
# Maestro writes per-flow debug output (command log, view-hierarchy
# dumps, failure screenshots) under ~/.maestro/tests. Surface it when
# a shard fails so CI-only failures are diagnosable without guesswork.
- name: Upload Maestro debug output
if: failure()
uses: actions/upload-artifact@v4
with:
name: maestro-debug-android-${{ matrix.name }}
path: ~/.maestro/tests/
if-no-files-found: ignore
retention-days: 7
e2e-ios:
needs: coverage
# Pinned to macos-15 rather than macos-latest: the macos-26 image
# (default since mid-July 2026) sporadically delivers a single
# XCTest-synthesized tap as TWO UIControl action sends (~25% of
# taps; the app's unified log shows one UIEvent followed by a
# doubled "send control actions" burst ~1ms apart). It reproduces
# on both the iOS 26.5 and 26.2 simulator runtimes on that image,
# but never on a macOS 15 host, so the host-side simulator/XCTest
# event-injection stack is the trigger. Doubled taps break every
# non-idempotent press handler: reducer counters jump by two,
# alerts present twice, pickers reopen after selecting. This suite
# ran green on the macos-15 image for months.
runs-on: macos-15
# Budget for the worst case: ~8 min build/boot plus two full suite
# attempts (run-e2e.sh retries the whole suite once after a driver
# flake) at up to ~20 min each on a slow runner. 40 was enough for
# a clean pass but cut the retry off mid-suite.
timeout-minutes: 55
# iOS simulators are stable for the full suite (unlike the Android
# emulator; see the e2e-android note), so this split is purely a
# speed optimization: the iOS Maestro run is ~22 minutes and is the
# critical path for the whole E2E workflow. Sharding it across three
# simulators (same balanced groups as Android) runs them in parallel
# and roughly halves wall-clock time. macOS minutes are free on this
# public repo, so the extra per-shard build/boot overhead is fine.
# ``fail-fast: false`` so one shard's failure still lets the others
# report.
strategy:
fail-fast: false
matrix:
include:
- name: components
suites: components
- name: hooks-nav-gestures
suites: hooks navigation gestures
- name: layout-styling-anim-misc
suites: layout styling animations misc
name: e2e-ios (${{ matrix.name }})
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PythonNative
run: pip install -e .
# Maestro drives iOS simulators with its own bundled XCTest
# runner; idb-companion (once installed here) hasn't been needed
# since Maestro dropped idb, and its formula no longer installs
# on macos-15 anyway (no bottle, and building needs Xcode 26).
- name: Install Maestro
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Build and run E2E tests
run: ./scripts/run-e2e.sh ios ${{ matrix.suites }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Maestro writes per-flow debug output (command log, view-hierarchy
# dumps, failure screenshots) under ~/.maestro/tests. Surface it when
# a shard fails so CI-only failures are diagnosable without guesswork.
- name: Upload Maestro debug output
if: failure()
uses: actions/upload-artifact@v4
with:
name: maestro-debug-ios-${{ matrix.name }}
path: ~/.maestro/tests/
if-no-files-found: ignore
retention-days: 7