Skip to content

Instantly share code, notes, and snippets.

@jonsamp
Last active August 1, 2025 19:12
Show Gist options
  • Save jonsamp/a58a5a6afbf25ba66c05888c205ffb5b to your computer and use it in GitHub Desktop.
Save jonsamp/a58a5a6afbf25ba66c05888c205ffb5b to your computer and use it in GitHub Desktop.
maestro-workflow-example.yml
name: Maestro Test Workflow
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
build_id:
description: 'Build ID to test (optional if using build job)'
required: false
type: string
platform:
description: 'Platform to test on'
required: true
default: 'ios'
type: choice
options:
- ios
- android
flow_path:
description: 'Path to Maestro test file(s)'
required: true
type: string
record_screen:
description: 'Record screen during tests'
required: false
default: false
type: boolean
shards:
description: 'Number of test shards'
required: false
type: number
retries:
description: 'Number of retries on failure'
required: false
default: 1
type: number
include_tags:
description: 'Tags to include (comma-separated)'
required: false
type: string
exclude_tags:
description: 'Tags to exclude (comma-separated)'
required: false
type: string
maestro_version:
description: 'Maestro version to use'
required: false
type: string
output_format:
description: 'Test report format'
required: false
default: 'junit'
type: choice
options:
- junit
android_system_image_package:
description: 'Android emulator system image package'
required: false
type: string
device_identifier:
description: 'Device identifier (e.g., "iPhone 16" for iOS, "pixel_6" for Android)'
required: false
type: string
jobs:
# Optional: Build the app first
build:
name: Build App
type: build
if: ${{ !inputs.build_id }}
params:
platform: ${{ inputs.platform || 'ios' }}
profile: production
outputs:
build_id: ${{ outputs.build_id }}
# Main Maestro test job
maestro-test:
name: Maestro Tests
type: custom
needs: [build]
if: always() && (needs.build.result == 'success' || needs.build.result == 'skipped')
params:
build_id: ${{ inputs.build_id || needs.build.outputs.build_id }}
platform: ${{ inputs.platform || 'ios' }}
flow_path: ${{ inputs.flow_path }}
record_screen: ${{ inputs.record_screen || false }}
shards: ${{ inputs.shards || 1 }}
retries: ${{ inputs.retries || 1 }}
include_tags: ${{ inputs.include_tags }}
exclude_tags: ${{ inputs.exclude_tags }}
maestro_version: ${{ inputs.maestro_version || 'latest' }}
output_format: ${{ inputs.output_format || 'junit' }}
android_system_image_package: ${{ inputs.android_system_image_package || 'system-images;android-35-ext15;google_apis_playstore;x86_64' }}
device_identifier: ${{ inputs.device_identifier || (inputs.platform == 'ios' ? 'iPhone 16' : 'pixel_6') }}
image: ${{ inputs.platform == 'android' ? 'ubuntu-latest' : 'macos-latest' }}
env:
MAESTRO_TESTS_DIR: '$HOME/.maestro/tests'
steps:
- uses: eas/checkout
- name: Install additional tools
if: ${{ params.platform == 'ios' }}
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
run: /opt/homebrew/bin/brew install jq
- name: Install additional tools
if: ${{ params.platform == 'android' }}
run: sudo apt-get -y install jq
- name: Download build
uses: eas/download_build
id: prepare_application
with:
build_id: ${{ params.build_id }}
extensions: ${{ params.platform == 'android' ? 'apk' : 'app' }}
- name: Install Maestro
uses: eas/install_maestro
with:
maestro_version: ${{ params.maestro_version }}
- name: Start iOS Simulator
if: ${{ params.platform == 'ios' }}
uses: eas/start_ios_simulator
with:
count: ${{ params.shards }}
device_identifier: ${{ params.device_identifier }}
- name: Start Android Emulator
if: ${{ params.platform == 'android' }}
uses: eas/start_android_emulator
with:
count: ${{ params.shards }}
system_image_package: ${{ params.android_system_image_package }}
device_identifier: ${{ params.device_identifier }}
- name: Install application on the Simulator
if: ${{ params.platform == 'ios' }}
run: |
SIMULATOR_UDIDS=$(xcrun simctl list -v devices booted -j | jq --raw-output '.devices | .[] | .[] | .udid')
for UDID in $(printf '%s\n' $SIMULATOR_UDIDS); do
echo "Installing app on simulator $UDID..."
xcrun simctl install "$UDID" "${{ steps.prepare_application.outputs.artifact_path }}"
done
- name: Install application on the Emulator
if: ${{ params.platform == 'android' }}
run: |
EMULATOR_SERIALS=$(adb devices -l | grep emulator | cut -d ' ' -f 1)
for SERIAL in $(printf '%s\n' $EMULATOR_SERIALS); do
echo "Installing app on emulator $SERIAL..."
adb -s $SERIAL install "${{ steps.prepare_application.outputs.artifact_path }}"
done
- name: Start screen recording
if: ${{ params.record_screen && params.platform == 'ios' }}
run: |
SIMULATOR_UDIDS=$(xcrun simctl list -v devices booted -j | jq --raw-output '.devices | .[] | .[] | .udid')
mkdir -p "$HOME/screen-recordings"
for UDID in $(printf '%s\n' $SIMULATOR_UDIDS); do
echo -n "Starting screen recording of simulator $UDID..."
LOG_FILE="$HOME/screen-recordings/$UDID.log"
PID_FILE="$HOME/screen-recordings/$UDID.pid"
xcrun simctl io "$UDID" recordVideo -f ~/screen-recordings/$UDID.mov > "$LOG_FILE" 2>&1 < /dev/null &
RECORDING_PID=$!
echo $RECORDING_PID > "$PID_FILE"
i=0
while [ $i -lt 20 ]; do
if grep -q "Recording started" "$LOG_FILE"; then
echo " done."
break
else
sleep 1
i=$((i+1))
fi
done
if [ $i -eq 20 ]; then
echo " failed (recording not started)."
fi
done
- name: Start screen recording
if: ${{ params.record_screen && params.platform == 'android' }}
run: |
EMULATOR_SERIALS=$(adb devices -l | grep emulator | cut -d ' ' -f 1)
mkdir -p "$HOME/screen-recordings"
for SERIAL in $(printf '%s\n' $EMULATOR_SERIALS); do
echo -n "Starting screen recording of $SERIAL..."
i=0
while [ $i -lt 10 ]; do
if adb -s $SERIAL shell touch /sdcard/.expo-recording-ready > /dev/null 2>&1; then
break
fi
sleep 1
i=$((i + 1))
done
if [ $i -ge 10 ]; then
echo " failed (filesystem not ready)."
continue
fi
LOG_FILE="$HOME/screen-recordings/$SERIAL.log"
PID_FILE="$HOME/screen-recordings/$SERIAL.pid"
if adb -s $SERIAL shell screenrecord --help 2>&1 | grep -q "remove the time limit"; then
adb -s $SERIAL shell screenrecord --verbose --time-limit 0 /sdcard/expo-recording.mp4 > "$LOG_FILE" 2>&1 < /dev/null &
RECORDING_PID=$!
else
adb -s $SERIAL shell screenrecord --verbose /sdcard/expo-recording.mp4 > "$LOG_FILE" 2>&1 < /dev/null &
RECORDING_PID=$!
fi
echo $RECORDING_PID > "$PID_FILE"
echo " done."
done
- name: Run Maestro Tests
run: |
# Build Maestro command
FLOW_PATHS="${{ params.flow_path }}"
INCLUDE_TAGS="${{ params.include_tags }}"
EXCLUDE_TAGS="${{ params.exclude_tags }}"
SHARDS="${{ params.shards }}"
OUTPUT_FORMAT="${{ params.output_format }}"
PLATFORM="${{ params.platform }}"
MAESTRO_CMD="maestro test"
if [ -n "$INCLUDE_TAGS" ]; then
MAESTRO_CMD="$MAESTRO_CMD --include-tags=\"$INCLUDE_TAGS\""
fi
if [ -n "$EXCLUDE_TAGS" ]; then
MAESTRO_CMD="$MAESTRO_CMD --exclude-tags=\"$EXCLUDE_TAGS\""
fi
if [ -n "$SHARDS" ]; then
MAESTRO_CMD="$MAESTRO_CMD --shard-split=$SHARDS"
fi
if [ "$OUTPUT_FORMAT" = "junit" ]; then
MAESTRO_CMD="$MAESTRO_CMD --format=JUNIT --output=\"$HOME/.maestro/tests/${PLATFORM}-maestro-junit.xml\""
fi
MAESTRO_CMD="$MAESTRO_CMD \"$FLOW_PATHS\""
# Run tests with retries
for i in $(seq 1 ${{ params.retries }}); do
if eval $MAESTRO_CMD; then
break
fi
if [ $i -eq ${{ params.retries }} ]; then
exit 1
fi
echo ""
echo "Test failed, retrying…"
sleep 2
echo ""
done
- name: Stop Screen Recording
if: ${{ params.record_screen && params.platform == 'ios' }}
run: |
SIMULATOR_UDIDS=$(xcrun simctl list -v devices booted -j | jq --raw-output '.devices | .[] | .[] | .udid')
for UDID in $(printf '%s\n' $SIMULATOR_UDIDS); do
echo -n "Stopping screen recording of simulator $UDID..."
LOG_FILE="$HOME/screen-recordings/$UDID.log"
PID_FILE="$HOME/screen-recordings/$UDID.pid"
kill -2 $(cat $PID_FILE)
i=0
while [ $i -lt 20 ]; do
if grep -q "Wrote video" "$LOG_FILE"; then
break
else
sleep 1
i=$((i+1))
fi
done
if [ $i -eq 20 ]; then
echo " failed (recording file busy)."
continue
fi
echo " done."
rm $LOG_FILE
rm $PID_FILE
done
- name: Stop Screen Recording
if: ${{ params.record_screen && params.platform == 'android' }}
run: |
EMULATOR_SERIALS=$(adb devices -l | grep emulator | cut -d ' ' -f 1)
for SERIAL in $(printf '%s\n' $EMULATOR_SERIALS); do
echo -n "Stopping screen recording of $SERIAL..."
PID_FILE="$HOME/screen-recordings/$SERIAL.pid"
LOG_FILE="$HOME/screen-recordings/$SERIAL.log"
RECORDING_PID=$(cat $PID_FILE)
rm $PID_FILE
if kill -1 $RECORDING_PID 2> /dev/null; then
echo " done."
else
echo " failed (recording already stopped)."
fi
echo -n "Ensuring recording file is ready..."
i=0
while [ $i -lt 10 ]; do
if adb -s $SERIAL shell "lsof -t /sdcard/expo-recording.mp4 | wc -l" | grep -q "0"; then
echo "done."
break
fi
sleep 1
i=$((i + 1))
done
if [ $i -ge 10 ]; then
echo " failed (recording file busy)."
continue
fi
echo -n "Pulling recording file..."
if ! adb -s $SERIAL pull /sdcard/expo-recording.mp4 "$HOME/screen-recordings/$SERIAL.mp4" 2> /dev/null; then
echo " failed."
continue
fi
echo " done."
rm $LOG_FILE
done
- name: Capture Simulator logs
if: ${{ params.platform == 'ios' }}
run: |
SIMULATOR_UDIDS=$(xcrun simctl list -v devices booted -j | jq --raw-output '.devices | .[] | .[] | .udid')
for UDID in $(printf '%s\n' $SIMULATOR_UDIDS); do
echo "Capturing logs of Simulator $UDID..."
xcrun simctl spawn "$UDID" log collect --output "$HOME/.maestro/tests/$UDID.logarchive" || true
done
- name: Capture Emulator logs
if: ${{ params.platform == 'android' }}
run: |
EMULATOR_SERIALS=$(adb devices -l | grep emulator | cut -d ' ' -f 1)
for SERIAL in $(printf '%s\n' $EMULATOR_SERIALS); do
echo "Capturing logs of Emulator $SERIAL..."
adb -s $SERIAL logcat -d > "$HOME/.maestro/tests/$SERIAL.log" || true
done
- name: Upload Test Results
uses: eas/upload_artifact
with:
key: Maestro Test Results
path: ${{ env.HOME }}/.maestro/tests
- name: Upload Test Report
if: ${{ params.output_format == 'junit' }}
uses: eas/upload_artifact
with:
key: ${{ params.platform == 'ios' ? 'iOS' : 'Android' }} Maestro Test Report (junit)
path: ${{ env.HOME }}/.maestro/tests/${{ params.platform }}-maestro-junit.xml
- name: Prepare Screen Recordings
if: ${{ params.record_screen }}
id: prepare_screen_recordings
run: |
RECORDINGS_DIR="$HOME/screen-recordings"
RECORDING_FILES=$(find "$RECORDINGS_DIR" -type f -print)
set-output recording_files "$RECORDING_FILES"
echo "Found:"
echo "$RECORDING_FILES"
- name: Upload Screen Recordings
if: ${{ params.record_screen }}
uses: eas/upload_artifact
with:
key: Screen Recording
path: ${{ steps.prepare_screen_recordings.outputs.recording_files }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment