Skip to content

Merge pull request #838 from samwel141/fix/wallet-cover-831 #103

Merge pull request #838 from samwel141/fix/wallet-cover-831

Merge pull request #838 from samwel141/fix/wallet-cover-831 #103

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
});
}