Last active
August 1, 2025 19:12
-
-
Save jonsamp/a58a5a6afbf25ba66c05888c205ffb5b to your computer and use it in GitHub Desktop.
maestro-workflow-example.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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