|
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.13' |
|
- 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 roughly ten |
|
# minutes of sustained Maestro driving and start reporting "device |
|
# offline" / "device not found" mid-run (the emulator VM, not the |
|
# app, dies). Sharding the run into a few balanced groups, each on |
|
# its own freshly booted emulator, keeps every session under the |
|
# limit, and parallelizes the Android pass as a bonus. Groups are |
|
# sized to about nine minutes of driving or less: ``components`` |
|
# (39 flows) runs as three parts; ``hooks`` (26 flows) dropped |
|
# offline at ~11.5 minutes in one session, so it runs as two parts; |
|
# the remaining categories pair up to similar totals, and ``misc`` |
|
# (14 flows) runs alone. |
|
# ``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: components-c |
|
suites: components-c |
|
- name: hooks-a |
|
suites: hooks-a |
|
- name: hooks-b |
|
suites: hooks-b |
|
- name: nav-gestures |
|
suites: navigation gestures |
|
- name: layout-styling |
|
suites: layout styling |
|
- name: anim-assets |
|
suites: animations assets |
|
- name: misc |
|
suites: misc |
|
- name: inbox |
|
suites: inbox |
|
|
|
name: e2e-android (${{ matrix.name }}) |
|
|
|
steps: |
|
- name: Checkout |
|
uses: actions/checkout@v4 |
|
|
|
- name: Set up uv |
|
uses: astral-sh/setup-uv@v10.0.1 |
|
with: |
|
enable-cache: true |
|
python-version: '3.13' |
|
|
|
- name: Set up Java 17 |
|
uses: actions/setup-java@v4 |
|
with: |
|
distribution: 'temurin' |
|
java-version: '17' |
|
|
|
# Restores ~/.gradle (wrapper distribution and dependency caches) |
|
# so the staged project's ./gradlew doesn't download Gradle from |
|
# services.gradle.org on every shard; a reset connection there |
|
# fails the build before Maestro even starts. |
|
- name: Set up Gradle |
|
uses: gradle/actions/setup-gradle@v4 |
|
|
|
- name: Install PythonNative |
|
run: uv sync --locked |
|
|
|
- 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 |
|
|
|
# The emulator step runs its script through a login shell, which |
|
# sources /etc/profile and can rewrite PATH. Confirm uv survives that |
|
# here, in seconds, rather than after a multi-minute emulator boot. |
|
- name: Verify uv is reachable in a login shell |
|
run: bash -lc 'command -v uv && uv --version' |
|
|
|
- name: Build, install, and run E2E tests |
|
uses: reactivecircus/android-emulator-runner@v2 |
|
with: |
|
api-level: 31 |
|
arch: x86_64 |
|
script: bash -lc "uv run ./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 |
|
env: |
|
DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer |
|
# 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). The test step gets its own, shorter timeout so a hung |
|
# attempt still leaves time for the debug-output upload below; |
|
# a job-level timeout kills the runner before that step can run. |
|
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 |
|
# 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. |
|
# Shards are sized so a single attempt is ~10-12 minutes of driving: |
|
# ``components`` alone was ~21 minutes per attempt on a hosted |
|
# runner, so one retry (build + two attempts) overran the job budget. |
|
# ``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: components-c |
|
suites: components-c |
|
- name: hooks |
|
suites: hooks |
|
- name: nav-gestures-layout-styling |
|
suites: navigation gestures layout styling |
|
- name: anim-misc |
|
suites: animations assets misc |
|
- name: inbox |
|
suites: inbox |
|
|
|
name: e2e-ios (${{ matrix.name }}) |
|
|
|
steps: |
|
- name: Checkout |
|
uses: actions/checkout@v4 |
|
|
|
- name: Set up uv |
|
uses: astral-sh/setup-uv@v10.0.1 |
|
with: |
|
enable-cache: true |
|
python-version: '3.13' |
|
|
|
- name: Install PythonNative |
|
run: uv sync --locked |
|
|
|
# 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 |
|
timeout-minutes: 48 |
|
run: uv 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 (or hits the step timeout above) 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 |