Files
self/.github/workflows/mobile-e2e.yml
2026-02-05 07:50:21 -08:00

934 lines
43 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: 26
# 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
MAESTRO_CACHE_VERSION: v1 # Bump this to clear Maestro cache
# Disable Maestro recording/artifacts (keep for debugging - set to "true" to re-enable)
ENABLE_MAESTRO_RECORDING: false
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 }}-${{ env.MAESTRO_CACHE_VERSION }}
restore-keys: |
${{ runner.os }}-maestro-${{ env.MAESTRO_VERSION }}-
- name: Validate Maestro cache
if: steps.cache-maestro.outputs.cache-hit == 'true'
run: |
echo "✅ Maestro restored from cache"
echo "Validating cached Maestro installation..."
if [ ! -f "$HOME/.maestro/bin/maestro" ]; then
echo "❌ Maestro binary not found in cache - cache is corrupted"
echo "Clearing corrupted cache..."
rm -rf ~/.maestro
echo "MAESTRO_CACHE_VALID=false" >> $GITHUB_ENV
else
echo "✅ Maestro binary found in cache"
# Test if Maestro is executable
if "$HOME/.maestro/bin/maestro" --version &>/dev/null; then
echo "✅ Maestro cache is valid"
echo "MAESTRO_CACHE_VALID=true" >> $GITHUB_ENV
else
echo "❌ Maestro binary is not executable - cache is corrupted"
echo "Clearing corrupted cache..."
rm -rf ~/.maestro
echo "MAESTRO_CACHE_VALID=false" >> $GITHUB_ENV
fi
fi
- name: Install Maestro
if: steps.cache-maestro.outputs.cache-hit != 'true' || env.MAESTRO_CACHE_VALID == 'false'
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: Verify Maestro installation
run: |
echo "🔍 Verifying Maestro installation..."
echo "Maestro path: $(which maestro)"
echo "Maestro version:"
maestro --version || {
echo "❌ Maestro installation verification failed"
echo "Maestro binary location:"
find ~/.maestro -name maestro -type f 2>/dev/null || echo "No maestro binary found"
echo ""
echo "To fix: Bump MAESTRO_CACHE_VERSION in workflow file"
exit 1
}
echo "✅ Maestro installation verified"
- 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 }}
E2E_TESTING: 1
- 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
env:
E2E_TESTING: 1
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
# E2E_TESTING compilation condition excludes NFCPassportReader which isn't available on simulator
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 'SWIFT_ACTIVE_COMPILATION_CONDITIONS=$(inherited) E2E_TESTING' || { echo "❌ iOS build failed"; exit 1; }
echo "✅ iOS build succeeded"
- name: Install and Test on iOS
continue-on-error: true
id: maestro-test
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 ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ 🔍 PRE-TEST DIAGNOSTICS ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Create directory for test artifacts
mkdir -p app/test-artifacts
# Check Maestro version and health
echo "📦 Maestro installation details:"
echo "Binary location: $(which maestro)"
echo "Maestro version:"
maestro --version || {
echo "❌ Failed to get Maestro version"
echo "This is a critical error - Maestro is not working"
exit 1
}
echo ""
# Check Java (required by Maestro)
echo "☕ Java status (required by Maestro):"
if command -v java &>/dev/null; then
java -version 2>&1 | head -3
echo "✅ Java is available"
else
echo "⚠️ Java not found - this may cause Maestro to fail"
fi
echo ""
# Check simulator state
echo "📱 Simulator state:"
xcrun simctl list devices | grep -A 2 "$SIMULATOR_ID" || echo "⚠️ Simulator not found"
echo ""
# Check if app is installed
echo "📲 Checking app installation:"
xcrun simctl listapps "$SIMULATOR_ID" | grep -i "$IOS_BUNDLE_ID" && echo "✅ App is installed" || echo "⚠️ App not found in installed apps"
echo ""
# Check if app is running
echo "🏃 Checking if app is running:"
APP_PID=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list | grep "$IOS_BUNDLE_ID" | awk '{print $1}' || echo "")
if [ -n "$APP_PID" ]; then
echo "✅ App process found (PID: $APP_PID)"
else
echo " App not currently running (will be launched by Maestro)"
fi
echo ""
# Verify test file
echo "📄 Maestro test file:"
if [ -f "app/tests/e2e/launch.ios.flow.yaml" ]; then
echo "✅ Found: app/tests/e2e/launch.ios.flow.yaml"
echo "Contents:"
cat app/tests/e2e/launch.ios.flow.yaml
else
echo "❌ Test file not found"
fi
echo ""
# Note: Maestro 1.41.0 doesn't have daemon or devices commands
# The test command handles daemon management internally
echo "🔧 Maestro will manage its daemon automatically during test execution"
echo ""
# Take a screenshot before running tests (if recording enabled)
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
echo "📸 Taking pre-test screenshot..."
xcrun simctl io "$SIMULATOR_ID" screenshot app/test-artifacts/pre-test-screenshot.png || echo "⚠️ Screenshot failed"
echo ""
fi
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ 🎭 STARTING MAESTRO TEST ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Start native simulator recording BEFORE running tests (if recording enabled)
# This ensures we capture everything even if Maestro fails immediately
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
echo "📹 Starting native simulator recording..."
xcrun simctl io "$SIMULATOR_ID" recordVideo --codec=h264 app/test-artifacts/simulator-recording.mp4 &
RECORDING_PID=$!
echo "✅ Recording started (PID: $RECORDING_PID)"
# Give recording a moment to initialize
sleep 2
else
echo " Maestro recording disabled (ENABLE_MAESTRO_RECORDING=false)"
fi
# Run Maestro with verbose output and capture all output (including errors)
echo "🎭 Starting Maestro test with verbose logging..."
echo "Command: maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml"
echo "Environment variables:"
echo " MAESTRO_DEVICE_ID: $SIMULATOR_ID"
echo " MAESTRO_DRIVER_STARTUP_TIMEOUT: 180000"
echo " MAESTRO_CLI_NO_ANALYTICS: $MAESTRO_CLI_NO_ANALYTICS"
echo " Working directory: $(pwd)"
echo ""
set +e # Don't exit on error
# Run with explicit device ID, increased timeout, and verbose output
# Note: exit code 2 typically means Maestro couldn't connect to device or daemon
MAESTRO_OUTPUT=$(MAESTRO_DEVICE_ID="$SIMULATOR_ID" MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 maestro test app/tests/e2e/launch.ios.flow.yaml --format junit --output app/maestro-results.xml 2>&1)
MAESTRO_EXIT_CODE=$?
set -e
echo "Maestro command completed with exit code: $MAESTRO_EXIT_CODE"
echo ""
# Stop the simulator recording (if recording was started)
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ] && [ -n "${RECORDING_PID:-}" ]; then
echo ""
echo "🛑 Stopping recording..."
kill -SIGINT $RECORDING_PID 2>/dev/null || true
wait $RECORDING_PID 2>/dev/null || true
sleep 2 # Give time for video to finalize
fi
# Take a screenshot after test (if recording enabled)
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
echo "📸 Taking post-test screenshot..."
xcrun simctl io "$SIMULATOR_ID" screenshot app/test-artifacts/post-test-screenshot.png || echo "⚠️ Screenshot failed"
echo ""
fi
# Save Maestro output to file for debugging
echo "$MAESTRO_OUTPUT" > app/test-artifacts/maestro-output.log
echo "📝 Maestro output saved to maestro-output.log"
# Show Maestro output
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ 📋 MAESTRO OUTPUT ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo "$MAESTRO_OUTPUT"
echo ""
# Post-test diagnostics
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ 🔍 POST-TEST DIAGNOSTICS ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Check if app is still running
echo "🏃 App status after test:"
APP_PID_AFTER=$(xcrun simctl spawn "$SIMULATOR_ID" launchctl list | grep "$IOS_BUNDLE_ID" | awk '{print $1}' || echo "")
if [ -n "$APP_PID_AFTER" ]; then
echo "✅ App still running (PID: $APP_PID_AFTER)"
else
echo "⚠️ App not running"
fi
echo ""
# Check simulator logs for any crashes
echo "🔍 Checking for crash logs..."
CRASH_LOGS=$(find ~/Library/Logs/DiagnosticReports -name "Self*.crash" -o -name "OpenPassport*.crash" -mmin -5 2>/dev/null | head -5)
if [ -n "$CRASH_LOGS" ]; then
echo "⚠️ Recent crash logs found:"
echo "$CRASH_LOGS"
# Copy crash logs to artifacts
for log in $CRASH_LOGS; do
cp "$log" app/test-artifacts/ 2>/dev/null || true
done
else
echo "✅ No recent crash logs"
fi
echo ""
# Check system log for relevant messages
echo "📋 Recent simulator system logs (last 50 lines):"
xcrun simctl spawn "$SIMULATOR_ID" log show --predicate 'process == "Self" OR process == "OpenPassport"' --last 5m --style compact 2>/dev/null | tail -50 > app/test-artifacts/simulator-system.log || echo "⚠️ Could not retrieve system logs"
if [ -f app/test-artifacts/simulator-system.log ]; then
echo "✅ System logs saved to simulator-system.log"
echo "Last 20 lines:"
tail -20 app/test-artifacts/simulator-system.log
fi
echo ""
# Check if video was created (if recording was enabled)
if [ "${ENABLE_MAESTRO_RECORDING}" = "true" ]; then
if [ -f "app/test-artifacts/simulator-recording.mp4" ]; then
VIDEO_SIZE=$(ls -lh app/test-artifacts/simulator-recording.mp4 | awk '{print $5}')
echo "✅ Video recording saved: simulator-recording.mp4 ($VIDEO_SIZE)"
echo ""
echo "📹 ===================================================="
echo "📹 VIDEO RECORDING AVAILABLE"
echo "📹 ===================================================="
echo "📹 To view the test recording:"
echo "📹 1. Go to: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "📹 2. Scroll to the 'Artifacts' section at the bottom"
echo "📹 3. Download 'maestro-artifacts-ios'"
echo "📹 4. Extract and open 'simulator-recording.mp4'"
echo "📹 ===================================================="
echo ""
else
echo "⚠️ Video recording not created"
fi
else
echo " Video recording disabled (ENABLE_MAESTRO_RECORDING=false)"
fi
# Analyze test results
echo "🔍 Analyzing test results..."
echo "Maestro exit code: $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"
echo "Check the video recording and maestro-output.log for details"
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 with exit code: $MAESTRO_EXIT_CODE"
echo "Check the video recording and maestro-output.log for details"
exit 1
fi
fi
- name: Upload test results
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
uses: actions/upload-artifact@v4
with:
name: maestro-results-ios
path: app/maestro-results.xml
if-no-files-found: warn
- name: Upload test artifacts (video and screenshots)
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
uses: actions/upload-artifact@v4
with:
name: maestro-artifacts-ios
path: app/test-artifacts/
if-no-files-found: warn
retention-days: 7
- name: Artifact download instructions
if: always() && env.ENABLE_MAESTRO_RECORDING == 'true'
run: |
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ 📹 TEST ARTIFACTS UPLOADED ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "📦 Artifact name: maestro-artifacts-ios"
echo "🎬 Contains:"
echo " 📹 simulator-recording.mp4 - Full video of simulator during test"
echo " 📝 maestro-output.log - Complete Maestro command output"
echo " 📋 simulator-system.log - iOS simulator system logs"
echo " 📸 pre-test-screenshot.png - Simulator state before test"
echo " 📸 post-test-screenshot.png - Simulator state after test"
echo " 📊 maestro-results.xml - Test results (if generated)"
echo " 💥 *.crash - Crash logs (if any crashes occurred)"
echo "⏰ Retention: 7 days"
echo ""
echo "🔗 Direct link to this run:"
echo " https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo ""
echo "💡 How to download:"
echo " 1. Click the link above (or find this run in the Actions tab)"
echo " 2. Scroll down to the 'Artifacts' section"
echo " 3. Click 'maestro-artifacts-ios' to download the ZIP"
echo " 4. Extract and review:"
echo " 🎬 Play simulator-recording.mp4 to see what happened"
echo " 📝 Read maestro-output.log for Maestro errors"
echo " 📋 Check simulator-system.log for iOS system errors"
echo " 📸 Compare pre/post screenshots to see UI state"
echo " 💥 Review crash logs if app crashed"
echo ""
echo "📝 Alternative - Using GitHub CLI:"
echo " gh run download ${{ github.run_id }} -n maestro-artifacts-ios"
echo ""
# List what files were actually created
echo "📂 Files in test-artifacts directory:"
ls -lh app/test-artifacts/ 2>/dev/null || echo " (No artifacts directory found)"
echo ""
- name: Fail job if tests failed
if: steps.maestro-test.outcome == 'failure'
run: |
echo "❌ Maestro tests failed - failing job after artifact upload"
exit 1