mirror of
https://github.com/selfxyz/self.git
synced 2026-02-19 02:24:25 -05:00
934 lines
43 KiB
YAML
934 lines
43 KiB
YAML
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
|