name: Mobile SDK Demo 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: v1 GH_GEMS_CACHE_VERSION: v1 # 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 # E2E Testing flag for conditional compilation E2E_TESTING: 1 on: pull_request: branches: - dev - staging - main paths: - "packages/mobile-sdk-demo/**" - "packages/mobile-sdk-alpha/**" - ".github/workflows/mobile-sdk-demo-e2e.yml" jobs: android-e2e: name: Android E2E Tests Demo App # Currently build-only for Android. E2E steps are preserved but skipped (if: false). # To re-enable full E2E: change `if: false` to `if: true` on emulator steps. concurrency: group: ${{ github.workflow }}-android-${{ github.ref }} cancel-in-progress: true timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - 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.6.0 --activate - 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 }}-${{ hashFiles('.yarnrc.yml') }} - 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: 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_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} - 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 packages/mobile-sdk-demo/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: Enable KVM group perms run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Build dependencies run: | echo "Building dependencies..." echo "πŸ“¦ Building @selfxyz/common to refresh shared types..." yarn workspace @selfxyz/common build || { echo "❌ @selfxyz/common build failed"; exit 1; } echo "βœ… @selfxyz/common build succeeded" yarn workspace mobile-sdk-demo run prebuild || { echo "❌ Dependency build failed"; exit 1; } echo "βœ… Dependencies built successfully" - name: Build Android APK run: | echo "Building Android APK..." chmod +x packages/mobile-sdk-demo/android/gradlew (cd packages/mobile-sdk-demo/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 build run: | echo "πŸ” Verifying build artifacts..." APK_PATH="packages/mobile-sdk-demo/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 -c%s "$APK_PATH" 2>/dev/null || echo "unknown") echo "πŸ“± APK size: $APK_SIZE bytes" echo "πŸŽ‰ Build verification completed successfully!" echo "ℹ️ Emulator testing is temporarily disabled - build testing only" - name: Run Maestro tests 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="packages/mobile-sdk-demo/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 packages/mobile-sdk-demo/tests/e2e/launch.android.flow.yaml --format junit --output packages/mobile-sdk-demo/maestro-results-android.xml - name: Upload Android 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: packages/mobile-sdk-demo/maestro-results-android.xml if-no-files-found: warn ios-e2e: timeout-minutes: 60 runs-on: macos-latest-large name: iOS E2E Tests Demo App concurrency: group: ${{ github.workflow }}-ios-${{ github.ref }} cancel-in-progress: true env: IOS_WORKSPACE_PATH: packages/mobile-sdk-demo/ios/SelfDemoApp.xcworkspace IOS_PROJECT_SCHEME: SelfDemoApp steps: - uses: actions/checkout@v4 - 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.6.0 --activate - 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 }}-${{ hashFiles('.yarnrc.yml') }} - 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: 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_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} - 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 packages/mobile-sdk-demo/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..." sudo xcode-select --switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app echo "βœ… Xcode path configured" echo "Xcode version:" xcodebuild -version echo "Xcode path:" xcode-select -p - 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: Cache Pods uses: ./.github/actions/cache-pods with: path: | packages/mobile-sdk-demo/ios/Pods ~/Library/Caches/CocoaPods lockfile: packages/mobile-sdk-demo/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..." echo "Available iOS runtimes:" xcrun simctl list runtimes | grep iOS - name: Build dependencies run: | echo "Building dependencies..." echo "πŸ“¦ Building @selfxyz/common to refresh shared types..." yarn workspace @selfxyz/common build || { echo "❌ @selfxyz/common build failed"; exit 1; } echo "βœ… @selfxyz/common build succeeded" yarn workspace mobile-sdk-demo run prebuild || { 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: | if [ -n "${SELFXYZ_INTERNAL_REPO_PAT}" ]; then echo "πŸ”‘ Using SELFXYZ_INTERNAL_REPO_PAT for private pod access" echo "::add-mask::${SELFXYZ_INTERNAL_REPO_PAT}" fi cd packages/mobile-sdk-demo/ios echo "πŸ“¦ Installing pods via cache-fix script…" bash scripts/pod-install-with-cache-fix.sh env: SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} GIT_TERMINAL_PROMPT: 0 - name: Setup iOS Simulator run: | echo "Setting up iOS Simulator..." 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 } } 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..." xcrun simctl create "iPhone SE (3rd generation)" "iPhone SE (3rd generation)" || { echo "❌ Failed to create simulator" exit 1 } AVAILABLE_SIMULATOR=$(xcrun simctl list devices | grep "iPhone SE (3rd generation)" | head -1 | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/') fi if [ -z "$AVAILABLE_SIMULATOR" ]; then echo "❌ Unable to determine simulator ID" exit 1 fi SIMULATOR_NAME=$(xcrun simctl list devices | grep "$AVAILABLE_SIMULATOR" | sed -E 's/^\s*([^()]+) \(.*/\1/' | head -1) echo "Using simulator: $SIMULATOR_NAME ($AVAILABLE_SIMULATOR)" echo "Booting simulator..." xcrun simctl boot "$AVAILABLE_SIMULATOR" || true echo "Waiting for simulator to be ready..." xcrun simctl bootstatus "$AVAILABLE_SIMULATOR" -b echo "Waiting for simulator to be fully ready..." sleep 15 echo "Simulator status:" xcrun simctl list devices | grep "$AVAILABLE_SIMULATOR" echo "IOS_SIMULATOR_ID=$AVAILABLE_SIMULATOR" >> $GITHUB_ENV echo "IOS_SIMULATOR_NAME=$SIMULATOR_NAME" >> $GITHUB_ENV - name: Build iOS App run: | echo "Building iOS app..." WORKSPACE_PATH="${{ env.IOS_WORKSPACE_PATH }}" echo "Workspace: $WORKSPACE_PATH, Scheme: ${{ env.IOS_PROJECT_SCHEME }}" if [ -z "$WORKSPACE_PATH" ]; then echo "❌ IOS_WORKSPACE_PATH is not set" exit 1 fi if [ ! -d "$WORKSPACE_PATH" ]; then echo "❌ Workspace not found at: $WORKSPACE_PATH" find packages/mobile-sdk-demo/ios -name "*.xcworkspace" -type d exit 1 fi 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" xcodebuild -list -workspace "$WORKSPACE_PATH" 2>/dev/null | grep -A 200 "Schemes:" | grep -v "Schemes:" | head -50 exit 1 fi 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 packages/mobile-sdk-demo/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 packages/mobile-sdk-demo/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" IOS_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print CFBundleIdentifier" "$APP_PATH/Info.plist") [ -z "$IOS_BUNDLE_ID" ] && { echo "❌ Could not determine bundle ID"; exit 1; } echo "βœ… App Bundle ID: $IOS_BUNDLE_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..." if xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1; then echo "βœ… App successfully installed" else echo "❌ App installation verification failed" exit 1 fi echo "πŸš€ Testing app launch capability..." xcrun simctl launch "$SIMULATOR_ID" "$IOS_BUNDLE_ID" || { echo "⚠️ Direct app launch test failed - continuing regardless." } echo "⏰ Checking simulator readiness..." sleep 10 xcrun simctl get_app_container "$SIMULATOR_ID" "$IOS_BUNDLE_ID" app >/dev/null 2>&1 || sleep 5 echo "Running Maestro tests..." maestro test packages/mobile-sdk-demo/tests/e2e/launch.ios.flow.yaml --format junit --output packages/mobile-sdk-demo/maestro-results-ios.xml - name: Upload iOS test results if: always() uses: actions/upload-artifact@v4 with: name: maestro-results-ios path: packages/mobile-sdk-demo/maestro-results-ios.xml if-no-files-found: warn