--- name: Android - Build and test on: pull_request: paths: - '**' - '!.github/workflows/**' - '.github/workflows/android-app.yml' - '!.github/CODEOWNERS' - '!audits/**' - '!ci/**' - '!dist-assets/**' - '!docs/**' - '!graphics/**' - '!desktop/**' - '!ios/**' - '!test/**' - '!scripts/**' - '!windows/**' - '!**/**.md' - '!**/osv-scanner.toml' schedule: # At 00:00 UTC every day. # Notifications for scheduled workflows are sent to the user who last modified the cron # syntax in the workflow file. If you update this you must have notifications for # Github Actions enabled, so these don't go unnoticed. # https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/notifications-for-workflow-runs - cron: '0 0 * * *' workflow_dispatch: inputs: override_container_image: description: Override container image type: string required: false run_firebase_tests: description: Run firebase tests type: boolean required: false mockapi_test_repeat: description: Mockapi test repeat (self hosted) default: '1' required: true type: string e2e_test_repeat: description: e2e test repeat (self hosted) default: '0' required: true type: string e2e_tests_infra_flavor: description: > Infra environment to run e2e tests on (prod/stagemole). If set to 'stagemole' test-related artefacts will be uploaded. default: 'stagemole' required: true type: string # Build if main is updated to ensure up-to-date caches are available push: branches: [main] permissions: {} env: DEFAULT_E2E_REPEAT: 0 SCHEDULE_E2E_REPEAT: 10 jobs: prepare: name: Prepare runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Use custom container image if specified if: ${{ github.event.inputs.override_container_image != '' }} run: echo "inner_container_image=${{ github.event.inputs.override_container_image }}" >> $GITHUB_ENV - name: Use default container image and resolve digest if: ${{ github.event.inputs.override_container_image == '' }} run: | echo "inner_container_image=$(cat ./building/android-container-image.txt)" >> $GITHUB_ENV # Preparing variables this way instead of using `env.*` due to: # https://github.com/orgs/community/discussions/26388 - name: Prepare environment variables run: | echo "INNER_E2E_TEST_INFRA_FLAVOR=${{ github.event.inputs.e2e_tests_infra_flavor || 'stagemole' }}" \ >> $GITHUB_ENV echo "INNER_E2E_TEST_REPEAT=${{ github.event.inputs.e2e_test_repeat || (github.event_name == 'schedule' && env.SCHEDULE_E2E_REPEAT) || env.DEFAULT_E2E_REPEAT }}" \ >> $GITHUB_ENV outputs: container_image: ${{ env.inner_container_image }} E2E_TEST_INFRA_FLAVOR: ${{ env.INNER_E2E_TEST_INFRA_FLAVOR }} E2E_TEST_REPEAT: ${{ env.INNER_E2E_TEST_REPEAT }} build-native: name: Build native # Used by wait for jobs. needs: prepare runs-on: ubuntu-latest container: image: "${{ needs.prepare.outputs.container_image }}" strategy: matrix: include: - abi: "x86_64" task-variant: "X86_64" - abi: "x86" task-variant: "X86" - abi: "arm64-v8a" task-variant: "Arm64" - abi: "armeabi-v7a" task-variant: "Arm" steps: # Fix for HOME path overridden by GH runners when building in containers, see: # https://github.com/actions/runner/issues/863 - name: Fix HOME path run: echo "HOME=/root" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Checkout wireguard-go-rs recursively run: | git config --global --add safe.directory '*' git submodule update --init wireguard-go-rs/libwg/wireguard-go - name: Calculate native lib cache hash id: native-lib-cache-hash shell: bash run: | git config --global --add safe.directory $(pwd) non_android_hash="$(git grep --cached -l '' -- ':!android/' \ | xargs -d '\n' sha1sum \ | sha1sum \ | awk '{print $1}')" echo "native_lib_hash=$non_android_hash" >> $GITHUB_OUTPUT - name: Cache native libraries uses: actions/cache@v4 id: cache-native-libs env: cache_hash: ${{ steps.native-lib-cache-hash.outputs.native_lib_hash }} with: path: ./android/app/build/rustJniLibs/android key: android-native-libs-${{ runner.os }}-${{ matrix.abi }}-${{ env.cache_hash }} - name: Build native libraries if: steps.cache-native-libs.outputs.cache-hit != 'true' uses: burrunan/gradle-cache-action@v3 with: job-id: jdk17 arguments: cargoBuild${{ matrix.task-variant }} gradle-version: wrapper build-root-directory: android execution-only-caches: false # Disable if logs are hard to follow. concurrent: true read-only: ${{ github.ref != 'refs/heads/main' }} - name: Upload native libs uses: actions/upload-artifact@v4 with: name: native-libs-${{ matrix.abi }} path: android/app/build/rustJniLibs/android if-no-files-found: error retention-days: 7 run-lint-and-tests: name: Run lint and test tasks needs: [prepare] runs-on: ubuntu-latest container: image: ${{ needs.prepare.outputs.container_image }} strategy: fail-fast: false matrix: include: - gradle-task: | testDebugUnitTest -x :test:arch:testDebugUnitTest :app:testOssProdDebugUnitTest :service:testOssProdDebugUnitTest :lib:billing:testDebugUnitTest :lib:daemon-grpc:testDebugUnitTest :lib:shared:testDebugUnitTest - gradle-task: :test:arch:test --rerun-tasks - gradle-task: detekt - gradle-task: lint steps: # Fix for HOME path overridden by GH runners when building in containers, see: # https://github.com/actions/runner/issues/863 - name: Fix HOME path run: echo "HOME=/root" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Run gradle task uses: burrunan/gradle-cache-action@v3 with: job-id: jdk17 arguments: ${{ matrix.gradle-task }} gradle-version: wrapper build-root-directory: android execution-only-caches: false # Disable if logs are hard to follow. concurrent: true read-only: ${{ github.ref != 'refs/heads/main' }} build-app: name: Build app needs: [prepare] runs-on: ubuntu-latest container: image: ${{ needs.prepare.outputs.container_image }} steps: # Fix for HOME path overridden by GH runners when building in containers, see: # https://github.com/actions/runner/issues/863 - name: Fix HOME path run: echo "HOME=/root" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Prepare dummy debug keystore env: KEYSTORE: ${{ vars.ANDROID_DUMMY_DEBUG_KEYSTORE }} run: | echo "$KEYSTORE" | tr -d '\n\r' | base64 -d > /root/.android/debug.keystore - name: Compile app uses: burrunan/gradle-cache-action@v3 with: job-id: jdk17 arguments: | compileOssProdDebugKotlin -x cargoBuild gradle-version: wrapper build-root-directory: android execution-only-caches: false # Disable if logs are hard to follow. concurrent: true read-only: ${{ github.ref != 'refs/heads/main' }} - name: Wait for other jobs (native, relay list) uses: kachick/wait-other-jobs@v3.6.0 with: wait-seconds-before-first-polling: '0' wait-list: | [ { "workflowFile": "android-app.yml", "jobMatchMode": "prefix", "jobName": "Build native" } ] - uses: actions/download-artifact@v4 with: pattern: native-libs-* path: android/app/build/rustJniLibs/android merge-multiple: true - name: Build app uses: burrunan/gradle-cache-action@v3 with: job-id: jdk17 arguments: | assembleOssProdDebug -x cargoBuild gradle-version: wrapper build-root-directory: android execution-only-caches: true # Disable if logs are hard to follow. concurrent: true read-only: ${{ github.ref != 'refs/heads/main' }} - name: Build stagemole app uses: burrunan/gradle-cache-action@v3 if: > (needs.prepare.outputs.E2E_TEST_REPEAT != '0' && needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'stagemole') || github.event.inputs.run_firebase_tests == 'true' with: job-id: jdk17 arguments: | assemblePlayStagemoleDebug -x cargoBuild gradle-version: wrapper build-root-directory: android execution-only-caches: true # Disable if logs are hard to follow. concurrent: true read-only: ${{ github.ref != 'refs/heads/main' }} - name: Upload apks uses: actions/upload-artifact@v4 with: name: apks path: android/app/build/outputs/apk if-no-files-found: error retention-days: 7 build-instrumented-tests: name: Build instrumented test packages needs: [prepare] runs-on: ubuntu-latest container: image: ${{ needs.prepare.outputs.container_image }} strategy: matrix: include: - test-type: app assemble-command: assembleOssProdAndroidTest artifact-path: android/app/build/outputs/apk - test-type: mockapi assemble-command: :test:mockapi:assemble artifact-path: android/test/mockapi/build/outputs/apk - test-type: e2e assemble-command: :test:e2e:assemble artifact-path: android/test/e2e/build/outputs/apk steps: # Fix for HOME path overridden by GH runners when building in containers, see: # https://github.com/actions/runner/issues/863 - name: Fix HOME path run: echo "HOME=/root" >> $GITHUB_ENV - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - name: Prepare dummy debug keystore env: KEYSTORE: ${{ vars.ANDROID_DUMMY_DEBUG_KEYSTORE }} run: | echo "$KEYSTORE" | tr -d '\n\r' | base64 -d > /root/.android/debug.keystore - name: Assemble instrumented test apk uses: burrunan/gradle-cache-action@v3 with: job-id: jdk17 arguments: | ${{ matrix.assemble-command }} -x cargoBuild -x mergeOssProdDebugJniLibFolders -x mergePlayStagemoleDebugJniLibFolders gradle-version: wrapper build-root-directory: android execution-only-caches: false # Disable if logs are hard to follow. concurrent: true read-only: ${{ github.ref != 'refs/heads/main' }} - name: Upload apks uses: actions/upload-artifact@v4 with: name: ${{ matrix.test-type }}-instrumentation-apks path: ${{ matrix.artifact-path }} if-no-files-found: error retention-days: 7 instrumented-tests: name: Run instrumented tests runs-on: [self-hosted, android-device] needs: [build-app, build-instrumented-tests] strategy: fail-fast: false matrix: include: - test-type: app path: android/app/build/outputs/apk test-repeat: 1 - test-type: mockapi path: android/test/mockapi/build/outputs/apk test-repeat: ${{ github.event_name == 'schedule' && 80 || github.event.inputs.mockapi_test_repeat || 1 }} steps: - name: Prepare report dir if: ${{ matrix.test-repeat != 0 }} id: prepare-report-dir env: INNER_REPORT_DIR: /tmp/${{ matrix.test-type }}-${{ github.run_id }}-${{ github.run_attempt }} run: | mkdir -p $INNER_REPORT_DIR echo "report_dir=$INNER_REPORT_DIR" >> $GITHUB_OUTPUT - name: Checkout repository if: ${{ matrix.test-repeat != 0 }} uses: actions/checkout@v4 with: submodules: true - uses: actions/download-artifact@v4 if: ${{ matrix.test-repeat != 0 }} with: name: apks path: android/app/build/outputs/apk - uses: actions/download-artifact@v4 if: ${{ matrix.test-repeat != 0 }} with: name: ${{ matrix.test-type }}-instrumentation-apks path: ${{ matrix.path }} - name: Calculate timeout id: calculate-timeout run: echo "timeout=$(( ${{ matrix.test-repeat }} * 10 ))" >> $GITHUB_OUTPUT shell: bash - name: Run instrumented test script if: ${{ matrix.test-repeat != 0 }} timeout-minutes: ${{ fromJSON(steps.calculate-timeout.outputs.timeout) }} shell: bash -ieo pipefail {0} env: AUTO_FETCH_TEST_HELPER_APKS: true TEST_TYPE: ${{ matrix.test-type }} BILLING_FLAVOR: oss INFRA_FLAVOR: prod REPORT_DIR: ${{ steps.prepare-report-dir.outputs.report_dir }} run: ./android/scripts/run-instrumented-tests-repeat.sh ${{ matrix.test-repeat }} - name: Upload instrumentation report (${{ matrix.test-type }}) uses: actions/upload-artifact@v4 if: always() && matrix.test-repeat != 0 with: name: ${{ matrix.test-type }}-instrumentation-report path: ${{ steps.prepare-report-dir.outputs.report_dir }} if-no-files-found: ignore retention-days: 7 instrumented-e2e-tests: name: Run instrumented e2e tests runs-on: [self-hosted, android-device] needs: [prepare, build-app, build-instrumented-tests] if: needs.prepare.outputs.E2E_TEST_REPEAT != '0' steps: - name: Resolve unique runner test account secret name if: needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'prod' run: | echo "RUNNER_SECRET_NAME=ANDROID_PROD_TEST_ACCOUNT_$(echo $RUNNER_NAME | tr '[:lower:]-' '[:upper:]_')" \ >> $GITHUB_ENV - name: Resolve runner test account if: needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'prod' run: echo "RESOLVED_TEST_ACCOUNT=${{ secrets[env.RUNNER_SECRET_NAME] }}" >> $GITHUB_ENV - name: Prepare report dir id: prepare-report-dir env: INNER_REPORT_DIR: /tmp/${{ github.run_id }}-${{ github.run_attempt }} run: | mkdir -p $INNER_REPORT_DIR echo "report_dir=$INNER_REPORT_DIR" >> $GITHUB_OUTPUT - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - uses: actions/download-artifact@v4 with: name: apks path: android/app/build/outputs/apk - uses: actions/download-artifact@v4 with: name: e2e-instrumentation-apks path: android/test/e2e/build/outputs/apk - name: Calculate timeout id: calculate-timeout run: echo "timeout=$(( ${{ needs.prepare.outputs.E2E_TEST_REPEAT }} * 15 ))" >> $GITHUB_OUTPUT shell: bash - name: Run instrumented test script timeout-minutes: ${{ fromJSON(steps.calculate-timeout.outputs.timeout) }} shell: bash -ieo pipefail {0} env: AUTO_FETCH_TEST_HELPER_APKS: true TEST_TYPE: e2e BILLING_FLAVOR: ${{ needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'prod' && 'oss' || 'play' }} INFRA_FLAVOR: "${{ needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR }}" PARTNER_AUTH: |- ${{ needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'stagemole' && secrets.STAGEMOLE_PARTNER_AUTH || '' }} VALID_TEST_ACCOUNT_NUMBER: ${{ env.RESOLVED_TEST_ACCOUNT }} INVALID_TEST_ACCOUNT_NUMBER: '0000000000000000' ENABLE_BILLING_TESTS: true ENABLE_HIGHLY_RATE_LIMITED_TESTS: ${{ github.event_name == 'schedule' && 'true' || 'false' }} ENABLE_RAAS_TESTS: true RAAS_HOST: '192.168.105.1' RAAS_TRAFFIC_GENERATOR_TARGET_HOST: '45.83.223.209' RAAS_TRAFFIC_GENERATOR_TARGET_PORT: '80' REPORT_DIR: ${{ steps.prepare-report-dir.outputs.report_dir }} run: ./android/scripts/run-instrumented-tests-repeat.sh ${{ needs.prepare.outputs.E2E_TEST_REPEAT }} - name: Upload e2e instrumentation report uses: actions/upload-artifact@v4 if: > always() && needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'stagemole' with: name: e2e-instrumentation-report path: ${{ steps.prepare-report-dir.outputs.report_dir }} firebase-tests: name: Run firebase tests if: github.event.inputs.run_firebase_tests == 'true' runs-on: ubuntu-latest timeout-minutes: 30 needs: [build-app, build-instrumented-tests] env: FIREBASE_ENVIRONMENT_VARIABLES: "\ clearPackageData=true,\ runnerBuilder=de.mannodermaus.junit5.AndroidJUnit5Builder,\ invalid_test_account_number=0000000000000000,\ ENABLE_HIGHLY_RATE_LIMITED_TESTS=${{ github.event_name == 'schedule' && 'true' || 'false' }},\ partner_auth=${{ secrets.STAGEMOLE_PARTNER_AUTH }},\ ENABLE_RAAS_TESTS=false" strategy: fail-fast: false matrix: include: - test-type: mockapi arg-spec-file: mockapi-oss.yml path: android/test/mockapi/build/outputs/apk - test-type: e2e arg-spec-file: e2e-play-stagemole.yml path: android/test/e2e/build/outputs/apk steps: - name: Checkout repository uses: actions/checkout@v4 with: submodules: true - uses: actions/download-artifact@v4 with: name: apks path: android/app/build/outputs/apk - uses: actions/download-artifact@v4 with: name: ${{ matrix.test-type }}-instrumentation-apks path: ${{ matrix.path }} - name: Run tests on Firebase Test Lab uses: asadmansr/Firebase-Test-Lab-Action@v1.0 env: SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} with: arg-spec: | android/test/firebase/${{ matrix.arg-spec-file }}:default --environment-variables ${{ env.FIREBASE_ENVIRONMENT_VARIABLES }}