name: Firebase Test Lab on: push: branches: [main] paths: - 'android/**' - '.github/workflows/firebase-test-lab.yml' pull_request: branches: [main] paths: - 'android/**' - '.github/workflows/firebase-test-lab.yml' # Allow manual trigger for release verification workflow_dispatch: inputs: build_type: description: 'Build type to test' required: true default: 'release' type: choice options: - release - debug skip_robo: description: 'Skip Robo tests' required: false default: false type: boolean skip_instrumentation: description: 'Skip instrumentation tests' required: false default: false type: boolean concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # ============================================================================ # Build Job: Compile the Android app and test APK # ============================================================================ build: name: Build APKs runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 with: gradle-version: wrapper - name: Cache Gradle dependencies uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper android/.gradle key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle.kts', 'android/gradle/libs.versions.toml') }} restore-keys: | ${{ runner.os }}-gradle- - name: Build release APK run: | cd android ./gradlew :app:assembleProdRelease :app:assembleProdDebugAndroidTest --no-daemon - name: Upload app APK uses: actions/upload-artifact@v4 with: name: app-release-apk path: android/app/build/outputs/apk/prod/release/*.apk retention-days: 7 - name: Upload test APK uses: actions/upload-artifact@v4 with: name: app-test-apk path: android/app/build/outputs/apk/androidTest/prod/debug/*.apk retention-days: 7 - name: Upload AAB (for Robo tests) uses: actions/upload-artifact@v4 with: name: app-release-aab path: android/app/build/outputs/bundle/prodRelease/*.aab retention-days: 7 # ============================================================================ # Robo Tests Job: Crash/ANR detection via autonomous crawl # ============================================================================ robo-tests: name: Robo Tests needs: build runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY_TEST_LAB }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 with: project_id: ${{ secrets.FIREBASE_PROJECT_ID || 'kordant-android' }} - name: Download AAB uses: actions/download-artifact@v4 with: name: app-release-aab path: build/ - name: Download APK (fallback) uses: actions/download-artifact@v4 with: name: app-release-apk path: build/ - name: Run Robo tests id: robo run: | # Check which file type is available (prefer AAB) AAB_FILE=$(find build -name "*.aab" -type f 2>/dev/null | head -1) APK_FILE=$(find build -name "*prod-release.apk" -type f 2>/dev/null | head -1) SCRIPT_DIR="android/firebase-test-lab" if [ -n "$AAB_FILE" ]; then echo "Using AAB: $AAB_FILE" gcloud firebase test android run \ --type robo \ --project "${{ secrets.FIREBASE_PROJECT_ID || 'kordant-android' }}" \ --app-package "com.kordant.android" \ --aab "$AAB_FILE" \ --robo-script "$SCRIPT_DIR/robo_script.json" \ --device model=Pixel6,version=33,locale=en_US,orientation=portrait \ --device model=Pixel6,version=33,locale=en_US,orientation=landscape \ --device model=Pixel6,version=33,locale=es_ES,orientation=portrait \ --device model=Pixel6,version=33,locale=es_ES,orientation=landscape \ --device model=Pixel4,version=30,locale=en_US,orientation=portrait \ --device model=Pixel4,version=30,locale=en_US,orientation=landscape \ --device model=Pixel4,version=30,locale=es_ES,orientation=portrait \ --device model=Pixel4,version=30,locale=es_ES,orientation=landscape \ --device model=GalaxyS21,version=31,locale=en_US,orientation=portrait \ --device model=GalaxyS21,version=31,locale=en_US,orientation=landscape \ --device model=GalaxyS21,version=31,locale=es_ES,orientation=portrait \ --device model=GalaxyS21,version=31,locale=es_ES,orientation=landscape \ --device model=RedmiNote8,version=29,locale=en_US,orientation=portrait \ --device model=RedmiNote8,version=29,locale=en_US,orientation=landscape \ --device model=RedmiNote8,version=29,locale=es_ES,orientation=portrait \ --device model=RedmiNote8,version=29,locale=es_ES,orientation=landscape \ --device model=AquestM2,version=28,locale=en_US,orientation=portrait \ --device model=AquestM2,version=28,locale=en_US,orientation=landscape \ --device model=AquestM2,version=28,locale=es_ES,orientation=portrait \ --device model=AquestM2,version=28,locale=es_ES,orientation=landscape \ --timeout 60m \ --max-crawl-time 600 \ --record-video \ --performance-metrics \ --results-history-name "Kordant Android Robo CI" \ --fail-fast \ || ROBO_EXIT=$? elif [ -n "$APK_FILE" ]; then echo "Using APK: $APK_FILE" gcloud firebase test android run \ --type robo \ --project "${{ secrets.FIREBASE_PROJECT_ID || 'kordant-android' }}" \ --app "$APK_FILE" \ --robo-script "$SCRIPT_DIR/robo_script.json" \ --device model=Pixel6,version=33,locale=en_US,orientation=portrait \ --device model=Pixel6,version=33,locale=en_US,orientation=landscape \ --device model=Pixel6,version=33,locale=es_ES,orientation=portrait \ --device model=Pixel6,version=33,locale=es_ES,orientation=landscape \ --device model=Pixel4,version=30,locale=en_US,orientation=portrait \ --device model=Pixel4,version=30,locale=en_US,orientation=landscape \ --device model=Pixel4,version=30,locale=es_ES,orientation=portrait \ --device model=Pixel4,version=30,locale=es_ES,orientation=landscape \ --device model=GalaxyS21,version=31,locale=en_US,orientation=portrait \ --device model=GalaxyS21,version=31,locale=en_US,orientation=landscape \ --device model=GalaxyS21,version=31,locale=es_ES,orientation=portrait \ --device model=GalaxyS21,version=31,locale=es_ES,orientation=landscape \ --device model=RedmiNote8,version=29,locale=en_US,orientation=portrait \ --device model=RedmiNote8,version=29,locale=en_US,orientation=landscape \ --device model=RedmiNote8,version=29,locale=es_ES,orientation=portrait \ --device model=RedmiNote8,version=29,locale=es_ES,orientation=landscape \ --device model=AquestM2,version=28,locale=en_US,orientation=portrait \ --device model=AquestM2,version=28,locale=en_US,orientation=landscape \ --device model=AquestM2,version=28,locale=es_ES,orientation=portrait \ --device model=AquestM2,version=28,locale=es_ES,orientation=landscape \ --timeout 60m \ --max-crawl-time 600 \ --record-video \ --performance-metrics \ --results-history-name "Kordant Android Robo CI" \ --fail-fast \ || ROBO_EXIT=$? else echo "No APK or AAB found!" exit 1 fi echo "ROBO_EXIT_CODE=${ROBO_EXIT:-0}" >> $GITHUB_OUTPUT - name: Upload Robo test results if: always() uses: actions/upload-artifact@v4 with: name: robo-test-results path: | android/firebase-test-lab/robo_script.json build/*.aab build/*prod-release*.apk retention-days: 14 - name: Mark build as failed if Robo tests failed if: steps.robo.outputs.ROBO_EXIT_CODE != '0' run: | echo "❌ Robo tests failed with exit code ${{ steps.robo.outputs.ROBO_EXIT_CODE }}" echo "Review results in Firebase Console" exit 1 # ============================================================================ # Instrumentation Tests Job: UI tests with assertions # ============================================================================ instrumentation-tests: name: Instrumentation Tests needs: build runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Checkout repository uses: actions/checkout@v4 - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY_TEST_LAB }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 with: project_id: ${{ secrets.FIREBASE_PROJECT_ID || 'kordant-android' }} - name: Download APKs uses: actions/download-artifact@v4 with: name: app-release-apk path: build/apk/ - uses: actions/download-artifact@v4 with: name: app-test-apk path: build/apk/ - name: Run Instrumentation tests id: instrumentation run: | APP_APK=$(find build/apk -name "*prod-release.apk" -type f 2>/dev/null | head -1) TEST_APK=$(find build/apk -name "*androidTest*.apk" -type f 2>/dev/null | head -1) if [ -z "$APP_APK" ] || [ -z "$TEST_APK" ]; then echo "Error: Could not find APK files." echo "App APK: ${APP_APK:-not found}" echo "Test APK: ${TEST_APK:-not found}" exit 1 fi echo "App APK: $APP_APK" echo "Test APK: $TEST_APK" gcloud firebase test android run \ --type instrumentation \ --project "${{ secrets.FIREBASE_PROJECT_ID || 'kordant-android' }}" \ --app "$APP_APK" \ --test "$TEST_APK" \ --device model=Pixel6,version=33,locale=en_US,orientation=portrait \ --device model=Pixel6,version=33,locale=en_US,orientation=landscape \ --device model=Pixel6,version=33,locale=es_ES,orientation=portrait \ --device model=Pixel6,version=33,locale=es_ES,orientation=landscape \ --device model=Pixel4,version=30,locale=en_US,orientation=portrait \ --device model=Pixel4,version=30,locale=en_US,orientation=landscape \ --device model=Pixel4,version=30,locale=es_ES,orientation=portrait \ --device model=Pixel4,version=30,locale=es_ES,orientation=landscape \ --device model=GalaxyS21,version=31,locale=en_US,orientation=portrait \ --device model=GalaxyS21,version=31,locale=en_US,orientation=landscape \ --device model=GalaxyS21,version=31,locale=es_ES,orientation=portrait \ --device model=GalaxyS21,version=31,locale=es_ES,orientation=landscape \ --device model=RedmiNote8,version=29,locale=en_US,orientation=portrait \ --device model=RedmiNote8,version=29,locale=en_US,orientation=landscape \ --device model=RedmiNote8,version=29,locale=es_ES,orientation=portrait \ --device model=RedmiNote8,version=29,locale=es_ES,orientation=landscape \ --device model=AquestM2,version=28,locale=en_US,orientation=portrait \ --device model=AquestM2,version=28,locale=en_US,orientation=landscape \ --device model=AquestM2,version=28,locale=es_ES,orientation=portrait \ --device model=AquestM2,version=28,locale=es_ES,orientation=landscape \ --timeout 60m \ --num-flaky-test-attempts 2 \ --record-video \ --performance-metrics \ --results-history-name "Kordant Android Instrumentation CI" \ --fail-fast \ || INSTR_EXIT=$? echo "INSTR_EXIT_CODE=${INSTR_EXIT:-0}" >> $GITHUB_OUTPUT - name: Upload instrumentation test results if: always() uses: actions/upload-artifact@v4 with: name: instrumentation-test-results path: build/apk/ retention-days: 14 - name: Mark build as failed if instrumentation tests failed if: steps.instrumentation.outputs.INSTR_EXIT_CODE != '0' run: | echo "❌ Instrumentation tests failed with exit code ${{ steps.instrumentation.outputs.INSTR_EXIT_CODE }}" echo "Review results in Firebase Console" exit 1 # ============================================================================ # Summary Job: Collect all test results # ============================================================================ test-summary: name: Test Summary needs: [robo-tests, instrumentation-tests] if: always() runs-on: ubuntu-latest steps: - name: Check test results run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📊 Firebase Test Lab - CI Results Summary" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "View detailed results in Firebase Console:" echo " https://console.firebase.google.com/project/${{ secrets.FIREBASE_PROJECT_ID || 'kordant-android' }}/testlab" echo "" echo "Devices tested:" echo " ✅ Pixel 6 (API 33) - primary target" echo " ✅ Pixel 4 (API 30) - older device" echo " ✅ Galaxy S21 (API 31) - Samsung" echo " ✅ Redmi Note 8 (API 29) - Xiaomi" echo " ✅ Aquest M2 (API 28) - low-end device" echo "" echo "Orientations: portrait, landscape" echo "Locales: en_US, es_ES" echo "" - name: Send notification on failure if: failure() run: | echo "::warning::Firebase Test Lab tests failed. Check the Firebase Console for details." - name: Determine overall status run: | if [ "${{ needs.robo-tests.result }}" = "failure" ] || [ "${{ needs.instrumentation-tests.result }}" = "failure" ]; then echo "❌ Firebase Test Lab: FAILED" exit 1 else echo "✅ Firebase Test Lab: PASSED" fi