Files
self/.github/workflows/mobile-e2e.yml
Javier Cortejoso 09e5ea1bf7 chore: enhance mobile E2E workflow for iOS simulator verification (#1532)
- Added checks for simctl availability and ensured necessary simulator directories exist.
- Improved app installation verification with graceful error handling and fallback checks.
- Enhanced Maestro test execution with detailed output handling and cleanup error suppression.
- Verified the existence of the Maestro test file before execution to prevent failures.
2025-12-26 15:36:32 +01:00

636 lines
27 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Mobile E2E
env:
# Build environment versions
JAVA_VERSION: 17
ANDROID_API_LEVEL: 33
ANDROID_NDK_VERSION: 27.0.12077973
XCODE_VERSION: 16.4
# Cache versions
GH_CACHE_VERSION: v2 # Global cache version - bumped to invalidate caches
GH_GEMS_CACHE_VERSION: v1 # Ruby gems cache version
# Performance optimizations
GRADLE_OPTS: -Dorg.gradle.workers.max=4 -Dorg.gradle.parallel=true -Dorg.gradle.caching=true
CI: true
# Disable Maestro analytics in CI
MAESTRO_CLI_NO_ANALYTICS: true
MAESTRO_VERSION: 1.41.0
on:
push:
branches:
- dev
- staging
- main
paths:
- "app/**"
- "packages/mobile-sdk-alpha/**"
- ".github/workflows/mobile-e2e.yml"
pull_request:
branches:
- dev
- staging
- main
paths:
- "app/**"
- "packages/mobile-sdk-alpha/**"
- ".github/workflows/mobile-e2e.yml"
workflow_dispatch:
jobs:
android-build-test:
# Currently build-only for Android with private repos. E2E steps are preserved but skipped (if: false).
# To re-enable full E2E: change `if: false` to `if: true` on Maestro and emulator steps.
concurrency:
group: ${{ github.workflow }}-android-${{ github.ref }}
cancel-in-progress: true
timeout-minutes: 120
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then
echo "❌ .nvmrc is missing or empty"; exit 1;
fi
VERSION="$(tr -d '\r\n' < .nvmrc)"
VERSION="${VERSION#v}"
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "Invalid .nvmrc content: '$VERSION'"; exit 1;
fi
echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: corepack enable
- run: corepack prepare yarn@4.12.0 --activate
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 5
command: yarn install --immutable --silent
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 5
command: yarn install --immutable --silent
- name: Validate Maestro test file
if: false # Skip for build-only test - keep logic for future E2E
run: |
[ -f app/tests/e2e/launch.android.flow.yaml ] || { echo "❌ Android E2E test file missing"; exit 1; }
- name: Cache Maestro
if: false # Skip for build-only test - keep logic for future E2E
id: cache-maestro
uses: actions/cache@v4
with:
path: ~/.maestro
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
- name: Install Maestro
if: false # Skip for build-only test - keep logic for future E2E
run: curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Add Maestro to path
if: false # Skip for build-only test - keep logic for future E2E
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Free up disk space
uses: ./.github/actions/free-disk-space
- name: Setup Java environment
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Cache Gradle packages
uses: ./.github/actions/cache-gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v3
with:
accept-android-sdk-licenses: true
- name: Install NDK
uses: nick-fields/retry@v3
with:
timeout_minutes: 15
max_attempts: 3
retry_wait_seconds: 10
command: sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"
- name: Build dependencies (outside emulator)
run: |
echo "Building dependencies..."
# Ensure Yarn 4.12.0 is active
corepack enable
corepack prepare yarn@4.12.0 --activate
yarn workspace @selfxyz/mobile-app run build:deps || { echo "❌ Dependency build failed"; exit 1; }
echo "✅ Dependencies built successfully"
- name: Setup Android private modules
run: |
cd app
PLATFORM=android node scripts/setup-private-modules.cjs
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true
- name: Build Android APK
run: |
echo "Building Android APK..."
chmod +x app/android/gradlew
(cd app/android && ./gradlew assembleDebug --quiet --parallel --build-cache --no-configuration-cache) || { echo "❌ Android build failed"; exit 1; }
echo "✅ Android build succeeded"
- name: Clean up Gradle build artifacts
uses: ./.github/actions/cleanup-gradle-artifacts
- name: Verify APK and android-passport-nfc-reader integration
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
run: |
echo "🔍 Verifying build artifacts..."
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
[ -f "$APK_PATH" ] || { echo "❌ APK not found at $APK_PATH"; exit 1; }
echo "✅ APK found at $APK_PATH"
# Check APK size
APK_SIZE=$(stat -f%z "$APK_PATH" 2>/dev/null || stat -c%s "$APK_PATH" 2>/dev/null || echo "unknown")
echo "📱 APK size: $APK_SIZE bytes"
# Verify private modules were properly integrated (skip for forks)
if [ -z "${SELFXYZ_APP_TOKEN:-}" ]; then
echo "🔕 No SELFXYZ_APP_TOKEN available — skipping private module verification"
else
# Verify android-passport-nfc-reader
if [ -d "app/android/android-passport-nfc-reader" ]; then
echo "✅ android-passport-nfc-reader directory exists"
echo "📁 android-passport-nfc-reader contents:"
ls -la app/android/android-passport-nfc-reader/ | head -10
else
echo "❌ android-passport-nfc-reader directory not found"
exit 1
fi
# Verify react-native-passport-reader
if [ -d "app/android/react-native-passport-reader" ]; then
echo "✅ react-native-passport-reader directory exists"
echo "📁 react-native-passport-reader contents:"
ls -la app/android/react-native-passport-reader/ | head -10
else
echo "❌ react-native-passport-reader directory not found"
exit 1
fi
fi
echo "🎉 Build verification completed successfully!"
echo " Emulator testing is temporarily disabled - build testing only"
- name: Install and Test on Android
if: false # Skip emulator/E2E for build-only test - keep logic for future E2E
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ env.ANDROID_API_LEVEL }}
arch: x86_64
target: google_apis
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 8192 -cores 4 -accel on
disable-animations: true
script: |
echo "Installing app on emulator..."
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
[ -f "$APK_PATH" ] || { echo "❌ APK not found at $APK_PATH"; exit 1; }
adb install -r "$APK_PATH" || { echo "❌ App installation failed"; exit 1; }
echo "✅ App installed successfully"
echo "⏰ Giving the emulator a moment to settle..."
sleep 5
echo "🎭 Running Maestro tests..."
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000
maestro test app/tests/e2e/launch.android.flow.yaml --format junit --output app/maestro-results.xml
- name: Upload test results
if: false # Skip for build-only test - keep logic for future E2E
uses: actions/upload-artifact@v4
with:
name: maestro-results-android
path: app/maestro-results.xml
if-no-files-found: warn
e2e-ios:
timeout-minutes: 120
# runs-on: macos-latest-large
runs-on: namespace-profile-apple-silicon-6cpu
concurrency:
group: ${{ github.workflow }}-ios-${{ github.ref }}
cancel-in-progress: true
env:
# iOS project configuration - hardcoded for E2E testing stability
# Note: During migration, project name is "Self" but scheme is still "OpenPassport"
# mobile-deploy.yml uses secrets for production deployment
IOS_PROJECT_NAME: "Self"
IOS_PROJECT_SCHEME: "OpenPassport"
steps:
- uses: actions/checkout@v6
- name: Read and sanitize Node.js version
shell: bash
run: |
if [ ! -f .nvmrc ] || [ -z "$(cat .nvmrc)" ]; then
echo "❌ .nvmrc is missing or empty"; exit 1;
fi
VERSION="$(tr -d '\r\n' < .nvmrc)"
VERSION="${VERSION#v}"
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "Invalid .nvmrc content: '$VERSION'"; exit 1;
fi
echo "NODE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: corepack enable
- run: corepack prepare yarn@4.12.0 --activate
- name: Compute .yarnrc.yml hash
id: yarnrc-hash
uses: ./.github/actions/yarnrc-hash
- name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn
with:
path: |
.yarn/cache
.yarn/install-state.gz
.yarn/unplugged
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
- name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
- name: Check Java installation
run: |
echo "INSTALL_JAVA=false" >> "$GITHUB_ENV"
if command -v java &> /dev/null && java -version &> /dev/null; then
echo "Java already installed: $(java -version 2>&1 | head -n 1)"
else
echo "Java not found or not working, will install..."
echo "INSTALL_JAVA=true" >> "$GITHUB_ENV"
fi
- name: Setup Java environment
if: env.INSTALL_JAVA == 'true'
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
- name: Generate token for self repositories
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: ./.github/actions/generate-github-token
id: github-token
with:
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
configure-netrc: "true"
- name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 5
command: yarn install --immutable --silent
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 5
command: yarn install --immutable --silent
- name: Validate Maestro test file
run: |
[ -f app/tests/e2e/launch.ios.flow.yaml ] || { echo "❌ iOS E2E test file missing"; exit 1; }
- name: Cache Maestro
id: cache-maestro
uses: actions/cache@v4
with:
path: ~/.maestro
key: ${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}
- name: Install Maestro
if: steps.cache-maestro.outputs.cache-hit != 'true'
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
retry_wait_seconds: 10
command: curl -Ls "https://get.maestro.mobile.dev" | bash
- name: Add Maestro to path
run: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- name: Set up Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Configure Xcode path
run: |
echo "🔧 Configuring Xcode path to fix iOS SDK issues..."
# Fix for macOS 15 runner iOS SDK issues
# See: https://github.com/actions/runner-images/issues/12758
sudo xcode-select --switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app
echo "✅ Xcode path configured"
# Verify Xcode setup
echo "Xcode version:"
xcodebuild -version
echo "Xcode path:"
xcode-select -p
# Temporarily disabled ccache to debug CI issues
# - name: Setup ccache
# uses: hendrikmuhs/ccache-action@v1.2
# with:
# key: ${{ github.job }}-${{ runner.os }}
# - name: Add ccache to PATH
# run: echo "/usr/local/opt/ccache/libexec" >> $GITHUB_PATH
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
working-directory: app
- name: Cache Pods
uses: ./.github/actions/cache-pods
with:
path: |
app/ios/Pods
~/Library/Caches/CocoaPods
lockfile: app/ios/Podfile.lock
# DerivedData caching disabled - caused intermittent build failures due to stale cache
# Pod caching still speeds up pod install significantly
- name: Verify iOS Runtime
run: |
echo "📱 Verifying iOS Runtime availability..."
# Check simctl availability (simctl without args returns non-zero, so check if tool exists)
SIMCTL_PATH=$(xcrun -f simctl 2>/dev/null || echo "")
if [ -z "$SIMCTL_PATH" ] || [ ! -f "$SIMCTL_PATH" ]; then
echo "❌ simctl binary not found"
exit 1
fi
# Ensure simulator directories exist (required for Namespace runners)
mkdir -p "$HOME/Library/Developer/CoreSimulator/Devices"
mkdir -p "$HOME/Library/Developer/CoreSimulator/Caches"
echo "📱 Available iOS runtimes:"
xcrun simctl list runtimes | grep iOS
- name: Build dependencies (outside main flow)
run: |
echo "Building dependencies..."
yarn workspace @selfxyz/mobile-app run build:deps || { echo "❌ Dependency build failed"; exit 1; }
echo "✅ Dependencies built successfully"
- name: Install iOS dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 20
max_attempts: 3
retry_wait_seconds: 10
command: |
cd app/ios
echo "📦 Installing pods via centralized script…"
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Setup iOS Simulator
run: |
echo "Setting up iOS Simulator..."
# Ensure simulator directories exist
mkdir -p "$HOME/Library/Developer/CoreSimulator/Devices"
# First, check what simulators are actually available
echo "Available simulators:"
xcrun simctl list devices available || {
echo "❌ Failed to list available devices"
echo "Trying to list all devices:"
xcrun simctl list devices || {
echo "❌ Failed to list any devices"
exit 1
}
}
# Find iPhone SE (3rd generation) simulator
echo "Finding iPhone SE (3rd generation) simulator..."
AVAILABLE_SIMULATOR=$(xcrun simctl list devices available | grep "iPhone SE (3rd generation)" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
if [ -z "$AVAILABLE_SIMULATOR" ]; then
echo "iPhone SE (3rd generation) not found, trying any iPhone SE..."
AVAILABLE_SIMULATOR=$(xcrun simctl list devices available | grep "iPhone SE" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
fi
if [ -z "$AVAILABLE_SIMULATOR" ]; then
echo "No iPhone SE found, trying any iPhone..."
AVAILABLE_SIMULATOR=$(xcrun simctl list devices available | grep "iPhone" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
fi
if [ -z "$AVAILABLE_SIMULATOR" ]; then
echo "❌ No available iPhone simulator found"
echo "Creating a new iPhone SE (3rd generation) simulator..."
# Create a new iPhone SE (3rd generation) simulator
xcrun simctl create "iPhone SE (3rd generation)" "iPhone SE (3rd generation)" || {
echo "❌ Failed to create iPhone SE (3rd generation) simulator"
echo "Trying to create any iPhone SE simulator..."
xcrun simctl create "iPhone SE" "iPhone SE" || {
echo "❌ Failed to create simulator"
exit 1
}
}
AVAILABLE_SIMULATOR=$(xcrun simctl list devices | grep "iPhone SE" | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')
fi
echo "Using simulator: $AVAILABLE_SIMULATOR"
# Get simulator name for display
SIMULATOR_NAME=$(xcrun simctl list devices | grep "$AVAILABLE_SIMULATOR" | sed -E 's/^[[:space:]]*([^(]+).*/\1/' | xargs)
echo "Simulator name: $SIMULATOR_NAME"
# Boot simulator and wait for it to be ready
echo "Booting simulator..."
xcrun simctl boot "$AVAILABLE_SIMULATOR" || {
echo "❌ Failed to boot simulator"
exit 1
}
echo "Waiting for simulator to be ready..."
xcrun simctl bootstatus "$AVAILABLE_SIMULATOR" -b
# Wait for simulator to be fully ready
echo "Waiting for simulator to be fully ready..."
sleep 15
echo "Simulator status:"
xcrun simctl list devices | grep "$AVAILABLE_SIMULATOR"
# Store simulator ID for later use
echo "IOS_SIMULATOR_ID=$AVAILABLE_SIMULATOR" >> $GITHUB_ENV
echo "IOS_SIMULATOR_NAME=$SIMULATOR_NAME" >> $GITHUB_ENV
- name: Resolve iOS workspace
run: |
WORKSPACE_OPEN="app/ios/OpenPassport.xcworkspace"
WORKSPACE_SELF="app/ios/Self.xcworkspace"
if xcodebuild -list -workspace "$WORKSPACE_OPEN" 2>/dev/null | grep -q "OpenPassport"; then
WORKSPACE_PATH="$WORKSPACE_OPEN"
else
WORKSPACE_PATH="$WORKSPACE_SELF"
fi
echo "WORKSPACE_PATH=$WORKSPACE_PATH" >> "$GITHUB_ENV"
echo "Resolved workspace: $WORKSPACE_PATH"
- name: Build iOS App
run: |
echo "Building iOS app..."
echo "Project: ${{ env.IOS_PROJECT_NAME }}, Scheme: ${{ env.IOS_PROJECT_SCHEME }}"
# Verify workspace exists before building
if [ -z "$WORKSPACE_PATH" ]; then
echo "❌ WORKSPACE_PATH is not set"
exit 1
fi
if [ ! -d "$WORKSPACE_PATH" ]; then
echo "❌ Workspace not found at: $WORKSPACE_PATH"
echo "Available workspaces:"
find app/ios -name "*.xcworkspace" -type d
exit 1
fi
# Verify scheme exists by listing available schemes
echo "Verifying scheme availability..."
AVAILABLE_SCHEMES=$(xcodebuild -list -workspace "$WORKSPACE_PATH" 2>/dev/null | grep -A 200 "Schemes:" | grep -v "Schemes:" | xargs)
echo "Available schemes (first 20): $(echo $AVAILABLE_SCHEMES | cut -d' ' -f1-20)..."
if [[ ! "$AVAILABLE_SCHEMES" =~ ${{ env.IOS_PROJECT_SCHEME }} ]]; then
echo "❌ Scheme '${{ env.IOS_PROJECT_SCHEME }}' not found"
echo "Full scheme list:"
xcodebuild -list -workspace "$WORKSPACE_PATH" 2>/dev/null | grep -A 200 "Schemes:" | grep -v "Schemes:" | head -50
exit 1
fi
echo "✅ Using workspace: $WORKSPACE_PATH"
echo "✅ Using scheme: ${{ env.IOS_PROJECT_SCHEME }}"
# Use cached derived data and enable parallel builds for faster compilation
# Additional flags disable indexing, restrict architecture, and use whole-module Swift compilation
# Use the simulator that was set up earlier in the workflow
FORCE_BUNDLING=1 RCT_NO_LAUNCH_PACKAGER=1 \
xcodebuild -workspace "$WORKSPACE_PATH" -scheme ${{ env.IOS_PROJECT_SCHEME }} -configuration Debug -destination "id=${{ env.IOS_SIMULATOR_ID }}" -derivedDataPath app/ios/build -jobs "$(sysctl -n hw.ncpu)" -parallelizeTargets -quiet COMPILER_INDEX_STORE_ENABLE=NO ONLY_ACTIVE_ARCH=YES SWIFT_COMPILATION_MODE=wholemodule || { echo "❌ iOS build failed"; exit 1; }
echo "✅ iOS build succeeded"
- name: Install and Test on iOS
run: |
echo "Installing app on simulator..."
APP_PATH=$(find app/ios/build/Build/Products/Debug-iphonesimulator -name "*.app" | head -1)
[ -z "$APP_PATH" ] && { echo "❌ Could not find built iOS app"; exit 1; }
echo "Found app at: $APP_PATH"
echo "🔍 Determining app bundle ID from built app..."
IOS_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP_PATH/Info.plist")
[ -z "$IOS_BUNDLE_ID" ] && { echo "❌ Could not determine bundle ID from $APP_PATH/Info.plist"; exit 1; }
echo "✅ App Bundle ID: $IOS_BUNDLE_ID"
# Use the dynamic simulator ID
SIMULATOR_ID="${IOS_SIMULATOR_ID:-iPhone SE (3rd generation)}"
echo "Installing on simulator: $SIMULATOR_ID"
echo "Removing any existing app installation..."
xcrun simctl uninstall "$SIMULATOR_ID" "$IOS_BUNDLE_ID" 2>/dev/null || true
echo "Installing app..."
xcrun simctl install "$SIMULATOR_ID" "$APP_PATH"
if [ $? -ne 0 ]; then
echo "❌ iOS app installation failed"
exit 1
fi
echo "Verifying app installation..."
# get_app_container may fail with NSPOSIXErrorDomain if app isn't ready yet - handle gracefully
APP_CONTAINER_OUTPUT=$(xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app 2>&1) || APP_CONTAINER_EXIT=$?
if [ -z "${APP_CONTAINER_EXIT:-}" ] && [ -n "$APP_CONTAINER_OUTPUT" ]; then
echo "✅ App successfully installed at: $APP_CONTAINER_OUTPUT"
else
echo "⚠️ App installation verification returned exit code ${APP_CONTAINER_EXIT:-unknown} (may be expected)"
# Check if app appears in installed apps list as fallback
xcrun simctl listapps "$SIMULATOR_ID" 2>/dev/null | grep -i "$IOS_BUNDLE_ID" || echo "App not found in installed apps list"
fi
unset APP_CONTAINER_OUTPUT APP_CONTAINER_EXIT
echo "🚀 Testing app launch capability..."
xcrun simctl launch "$SIMULATOR_ID" "$IOS_BUNDLE_ID" || {
echo "⚠️ Direct app launch test failed - this might be expected."
}
echo "⏰ Checking simulator readiness..."
sleep 10
# Final readiness check (suppress errors to avoid annotations)
xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1 || sleep 5
echo "🎭 Running Maestro tests..."
echo "Starting test execution..."
# Verify Maestro test file exists
if [ ! -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
echo "❌ Maestro test file not found: app/tests/e2e/launch.ios.flow.yaml"
exit 1
fi
# Run Maestro with error handling for cleanup issues
# Note: Maestro may show NSPOSIXErrorDomain code=3 errors during cleanup when
# terminating the test runner app that's already terminated. This is harmless.
MAESTRO_OUTPUT=$(maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
MAESTRO_EXIT_CODE=$?
# Check if tests actually passed (ignore cleanup errors)
if echo "$MAESTRO_OUTPUT" | grep -q "Flow Passed"; then
echo "✅ Maestro tests passed"
# Suppress harmless cleanup errors (NSPOSIXErrorDomain code=3)
if [ $MAESTRO_EXIT_CODE -ne 0 ] && echo "$MAESTRO_OUTPUT" | grep -q "NSPOSIXErrorDomain.*code=3.*terminate"; then
echo "⚠️ Maestro cleanup warning (harmless): Test runner termination error"
elif [ $MAESTRO_EXIT_CODE -ne 0 ]; then
echo "❌ Maestro test failed with exit code: $MAESTRO_EXIT_CODE"
exit 1
fi
elif echo "$MAESTRO_OUTPUT" | grep -q "Flow Failed"; then
echo "❌ Maestro tests failed"
exit 1
elif [ $MAESTRO_EXIT_CODE -ne 0 ]; then
# Check results file if exit code is non-zero
if [ -f "app/maestro-results.xml" ] && ! grep -q "<failure" app/maestro-results.xml; then
echo "✅ Tests passed (cleanup error caused non-zero exit)"
else
echo "❌ Maestro test failed"
exit 1
fi
fi
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-results-ios
path: app/maestro-results.xml
if-no-files-found: warn