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 "