Merge pull request #838 from samwel141/fix/wallet-cover-831 #103
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR CI BDD Android e2e tests | |
| on: | |
| pull_request: | |
| types: | |
| - opened | |
| - synchronize | |
| - reopened | |
| - edited | |
| - ready_for_review | |
| paths: | |
| - "apps/bdd/**" | |
| - "apps/native/**" | |
| - "packages/core/**" | |
| - "packages/wallet/**" | |
| - "package.json" | |
| - "yarn.lock" | |
| - ".github/workflows/ci-cd-pr-bdd-android.yml" | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| android-bdd-e2e-tests: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| # Fork guard: skip on fork PRs where secrets are unavailable. | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| env: | |
| NODE_TLS_REJECT_UNAUTHORIZED: "0" | |
| PROJECT_ROOT_ANDROID: ${{ github.workspace }}/apps/native | |
| APPIUM_HOME: ${{ github.workspace }}/.appium | |
| NEXT_PUBLIC_TREETRACKER_WALLET_API: ${{ vars.NEXT_PUBLIC_TREETRACKER_WALLET_API }} | |
| NEXT_PUBLIC_TREETRACKER_USER_API: ${{ vars.NEXT_PUBLIC_TREETRACKER_USER_API }} | |
| NEXT_PUBLIC_ENV: ${{ vars.NEXT_PUBLIC_ENV }} | |
| NEXT_PUBLIC_API_TIMEOUT: ${{ vars.NEXT_PUBLIC_API_TIMEOUT }} | |
| NEXT_PUBLIC_API_RETRIES: ${{ vars.NEXT_PUBLIC_API_RETRIES }} | |
| NEXT_PUBLIC_BASE_URL: ${{ vars.NEXT_PUBLIC_BASE_URL }} | |
| CORS_ORIGINS_DEV: ${{ vars.CORS_ORIGINS_DEV }} | |
| PRIVATE_KEYCLOAK_REALM: ${{ secrets.PRIVATE_KEYCLOAK_REALM }} | |
| PRIVATE_KEYCLOAK_BASE_URL: ${{ secrets.PRIVATE_KEYCLOAK_BASE_URL }} | |
| PRIVATE_KEYCLOAK_CLIENT_SECRET: ${{ secrets.PRIVATE_KEYCLOAK_CLIENT_SECRET }} | |
| DATABASE_URL: ${{ secrets.DATABASE_URL }} | |
| PRIVATE_KEYCLOAK_CLIENT_ID: ${{ secrets.PRIVATE_KEYCLOAK_CLIENT_ID }} | |
| PRIVATE_WALLET_API_KEY: ${{ secrets.PRIVATE_WALLET_API_KEY }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Enable KVM permissions | |
| 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: Set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: "17" | |
| - name: Use Node.js 24 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| - name: Enable Corepack | |
| run: | | |
| corepack enable | |
| corepack prepare yarn@4.9.4 --activate | |
| - name: Cache Yarn dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: .yarn/cache | |
| key: yarn-${{ hashFiles('yarn.lock') }} | |
| restore-keys: yarn- | |
| - name: Cache Gradle | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: gradle-${{ hashFiles('apps/native/package.json', 'yarn.lock') }} | |
| restore-keys: gradle- | |
| - name: Cache Android emulator (AVD) | |
| id: avd-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.android/avd/* | |
| ~/.android/adb* | |
| key: avd-34-google_apis-x86_64 | |
| - name: Cache Expo prebuild output | |
| id: prebuild-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| apps/native/android | |
| !apps/native/android/app/build | |
| key: prebuild-${{ hashFiles('apps/native/app.json', 'apps/native/package.json') }} | |
| restore-keys: prebuild- | |
| - name: Cache debug APK | |
| id: apk-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: apps/native/android/app/build/outputs/apk/debug/app-debug.apk | |
| key: apk-${{ hashFiles('apps/native/app.json', 'apps/native/package.json') }} | |
| restore-keys: apk- | |
| - name: Cache Appium drivers | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ github.workspace }}/.appium | |
| key: appium-v3-uiautomator2-v3 | |
| - name: Install dependencies | |
| run: yarn install --immutable | |
| - name: Start user service | |
| run: | | |
| mkdir -p .logs | |
| nohup yarn user:dev </dev/null >.logs/user.log 2>&1 & | |
| echo $! > .logs/user.pid && disown | |
| - name: Expo prebuild | |
| if: steps.prebuild-cache.outputs.cache-hit != 'true' | |
| run: yarn prebuild | |
| - name: Bundle JS into debug APK | |
| if: steps.apk-cache.outputs.cache-hit != 'true' | |
| working-directory: apps/native | |
| run: | | |
| mkdir -p android/app/src/main/assets android/app/src/main/res | |
| npx expo export:embed \ | |
| --platform android \ | |
| --dev false \ | |
| --bundle-output android/app/src/main/assets/index.android.bundle \ | |
| --assets-dest android/app/src/main/res | |
| - name: Build Android debug APK | |
| if: steps.apk-cache.outputs.cache-hit != 'true' | |
| run: yarn build-apk-android | |
| env: | |
| GRADLE_OPTS: "-Dorg.gradle.parallel=true -Dorg.gradle.caching=true -Dorg.gradle.jvmargs=-Xmx3g" | |
| - name: Verify APK has embedded JS bundle | |
| run: | | |
| APK=apps/native/android/app/build/outputs/apk/debug/app-debug.apk | |
| if unzip -l "$APK" | grep -q 'assets/index.android.bundle'; then | |
| echo "Embedded bundle found in APK" | |
| else | |
| echo "::error::No embedded JS bundle in APK" | |
| exit 1 | |
| fi | |
| - name: Install Appium 3 + UiAutomator2 driver | |
| run: | | |
| set -euo pipefail | |
| npm install -g appium@3 | |
| mkdir -p "$APPIUM_HOME" | |
| if appium driver list --installed 2>&1 | grep -q uiautomator2; then | |
| echo "uiautomator2 already installed (cache hit)" | |
| else | |
| n=0 | |
| until [ $n -ge 3 ]; do | |
| if appium driver install uiautomator2; then break; fi | |
| n=$((n+1)) | |
| echo "driver install failed (attempt $n), retrying in 5s..." | |
| sleep 5 | |
| done | |
| fi | |
| if ! appium driver list --installed 2>&1 | grep -q uiautomator2; then | |
| echo "::error::uiautomator2 driver not installed after retries" | |
| exit 1 | |
| fi | |
| - name: Wait for user service | |
| run: | | |
| if ! npx wait-on tcp:8080 --timeout 60000; then | |
| echo "User service did not become ready in time" | |
| cat .logs/user.log | |
| exit 1 | |
| fi | |
| - name: Create AVD snapshot for caching | |
| if: steps.avd-cache.outputs.cache-hit != 'true' | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: 34 | |
| target: google_apis | |
| arch: x86_64 | |
| profile: pixel_6 | |
| force-avd-creation: false | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none | |
| disable-animations: false | |
| script: echo "Generated AVD snapshot for caching." | |
| # Run tests (always uses cached AVD) | |
| - name: Run Android BDD tests | |
| id: test | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: 34 | |
| target: google_apis | |
| arch: x86_64 | |
| profile: pixel_6 | |
| force-avd-creation: false | |
| emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none | |
| disable-animations: true | |
| script: | | |
| adb wait-for-device | |
| until [ "$(adb shell getprop sys.boot_completed 2>/dev/null || echo '')" = "1" ]; do sleep 2; done | |
| adb reverse tcp:8080 tcp:8080 | |
| adb install -r apps/native/android/app/build/outputs/apk/debug/app-debug.apk | |
| yarn bdd:test:android:report | |
| - name: Capture debug artifacts on failure | |
| if: failure() && steps.test.outcome == 'failure' | |
| run: | | |
| adb exec-out screencap -p > .logs/failure.png || true | |
| adb logcat -d > .logs/logcat.txt || true | |
| echo "----- User service log -----" | |
| tail -200 .logs/user.log 2>/dev/null || true | |
| - name: Upload artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: android-test-output | |
| path: | | |
| apps/bdd/test-artifacts/reports/android/ | |
| .logs/*.png | |
| .logs/*.txt | |
| .logs/*.log | |
| retention-days: 7 | |
| - name: Stop background services | |
| if: always() | |
| run: | | |
| if [ -f .logs/user.pid ]; then kill "$(cat .logs/user.pid)" 2>/dev/null || true; fi | |
| pkill -f 'yarn user:dev' 2>/dev/null || true | |
| deploy-report: | |
| runs-on: ubuntu-latest | |
| needs: android-bdd-e2e-tests | |
| if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main')) | |
| steps: | |
| - name: Download test artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: android-test-output | |
| path: android-test-output | |
| - name: Prepare report | |
| run: | | |
| set -euo pipefail | |
| mkdir -p merged-report | |
| HTML_DIR="android-test-output/apps/bdd/test-artifacts/reports/android/cucumber-html" | |
| if [ -d "$HTML_DIR" ] && [ -f "$HTML_DIR/index.html" ]; then | |
| cp -r "$HTML_DIR"/* merged-report/ | |
| echo "Copied generated cucumber-html report." | |
| else | |
| echo "<h1>No Android BDD report was produced</h1>" > merged-report/index.html | |
| fi | |
| - name: Deploy to GitHub Pages | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_branch: gh-pages | |
| publish_dir: merged-report | |
| destination_dir: ${{ github.event_name == 'pull_request' && format('webdriver/android/pr-{0}', github.event.pull_request.number) || 'webdriver/android/prod' }} | |
| keep_files: true | |
| enable_jekyll: false | |
| - name: Post PR comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| env: | |
| JOB_STATUS: ${{ needs.android-bdd-e2e-tests.result }} | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const prNumber = context.payload.pull_request.number; | |
| const baseUrl = `https://${owner}.github.io/${repo}/webdriver/android`; | |
| const prUrl = `${baseUrl}/pr-${prNumber}/index.html`; | |
| const prodUrl = `${baseUrl}/prod/index.html`; | |
| const status = process.env.JOB_STATUS === 'success' ? '✅ Passed' : '❌ Failed'; | |
| const marker = "<!-- android-e2e-report -->"; | |
| const body = [ | |
| `## Android WebDriver Test Report`, | |
| ``, | |
| `- **PR Report:** ${prUrl}`, | |
| `- **Production Reference:** ${prodUrl}`, | |
| `- **Overall Status:** ${status}`, | |
| ``, | |
| marker, | |
| ].join("\n"); | |
| const comments = await github.rest.issues.listComments({ | |
| owner, repo, issue_number: prNumber, per_page: 100 | |
| }); | |
| const existing = comments.data.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner, repo, comment_id: existing.id, body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number: prNumber, body | |
| }); | |
| } |