Fix Android VPN startup state and duplicate connect handling (#9085) #111
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: Firebase Test Lab (Android) | |
| on: | |
| workflow_dispatch: | |
| # The nightly schedule lives in app-smoke-tests.yml, which calls this | |
| # workflow alongside the desktop platforms. | |
| push: | |
| branches: | |
| - main | |
| # Callable from umbrella workflows (app-smoke-tests.yml). Empty inputs fall | |
| # through to the defaults in scripts/android/ftl-run.sh and the Makefile. | |
| workflow_call: | |
| inputs: | |
| devices: | |
| description: "Semicolon-separated FTL device specs" | |
| required: false | |
| type: string | |
| default: "" | |
| flaky_attempts: | |
| description: "Reruns allowed per failing device" | |
| required: false | |
| type: string | |
| default: "" | |
| test_target: | |
| description: "Flutter integration-test entrypoint to build" | |
| required: false | |
| type: string | |
| default: "" | |
| secrets: | |
| FIREBASE_SERVICE_ACCOUNT: | |
| required: true | |
| APP_ENV: | |
| required: true | |
| # Base64 of a consts version of | |
| # integration_test/auth/auth_smoke_credentials.dart (see | |
| # auth_smoke_credentials.example.dart). Optional: without it the | |
| # committed fromEnvironment shim still compiles, and the auth smoke | |
| # suite fails at runtime with a clear message. | |
| AUTH_SMOKE_CREDENTIALS: | |
| required: false | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ftl-android-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| ftl-android: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 90 | |
| env: | |
| AUTH_SMOKE_CREDENTIALS_AVAILABLE: ${{ secrets.AUTH_SMOKE_CREDENTIALS != '' }} | |
| FTL_RESULTS_BUCKET: lantern-android-ftl-results | |
| FTL_RESULTS_DIR: ftl-${{ github.run_id }}-${{ github.run_attempt }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| # No git operations after checkout need the token. | |
| persist-credentials: false | |
| - name: Authenticate to Google Cloud | |
| id: gcp-auth | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} | |
| - name: Set up gcloud | |
| uses: google-github-actions/setup-gcloud@v2 | |
| # Fail fast on auth/permission problems before spending ~30 minutes | |
| # building the APKs. | |
| - name: Verify Firebase Test Lab access | |
| shell: bash | |
| env: | |
| GCP_PROJECT_ID: ${{ steps.gcp-auth.outputs.project_id }} | |
| run: | | |
| gcloud firebase test android models list \ | |
| --project "$GCP_PROJECT_ID" >/dev/null | |
| - name: Set up Go | |
| uses: ./.github/actions/setup-go | |
| with: | |
| cache-scope: android-ftl | |
| gomobile-cache: "true" | |
| - name: Set up Android | |
| uses: ./.github/actions/setup-android | |
| with: | |
| # This job invokes Gradle directly; keep in sync with the wrapper. | |
| gradle-version: "8.13" | |
| - name: Install Flutter | |
| uses: ./.github/actions/setup-flutter | |
| - name: Decode APP_ENV | |
| uses: timheuer/base64-to-file@v1.2 | |
| with: | |
| fileName: "app.env" | |
| fileDir: ${{ github.workspace }} | |
| encodedString: ${{ secrets.APP_ENV }} | |
| # Overwrites the committed fromEnvironment shim with real credentials. | |
| # Skipped when the secret is absent so an empty file never clobbers the | |
| # shim; the auth smoke suite then fails at runtime with a clear message. | |
| - name: Write auth smoke credentials | |
| if: ${{ env.AUTH_SMOKE_CREDENTIALS_AVAILABLE == 'true' }} | |
| uses: timheuer/base64-to-file@v1.2 | |
| with: | |
| fileName: "auth_smoke_credentials.dart" | |
| fileDir: ${{ github.workspace }}/integration_test/auth | |
| encodedString: ${{ secrets.AUTH_SMOKE_CREDENTIALS }} | |
| - name: Install dependencies (gomobile) | |
| run: make install-android-deps | |
| - name: Fetch packages and generate code | |
| run: make pubget gen | |
| - name: Build liblantern.aar | |
| run: make android | |
| - name: Build integration test APKs (app + androidTest) | |
| shell: bash | |
| env: | |
| ANDROID_INTEGRATION_TARGET: ${{ inputs.test_target || 'integration_test/android_all_e2e_test.dart' }} | |
| run: | | |
| set -euo pipefail | |
| if [[ ! "$ANDROID_INTEGRATION_TARGET" =~ ^integration_test/[A-Za-z0-9_./-]+_test\.dart$ ]] || | |
| [[ "$ANDROID_INTEGRATION_TARGET" == *".."* ]] || | |
| [[ ! -f "$ANDROID_INTEGRATION_TARGET" ]]; then | |
| echo "::error::Invalid Android integration-test target: $ANDROID_INTEGRATION_TARGET" | |
| exit 1 | |
| fi | |
| make android-integration-apks ANDROID_GRADLE=gradle \ | |
| "ANDROID_INTEGRATION_TARGET=$ANDROID_INTEGRATION_TARGET" | |
| - name: Run tests on Firebase Test Lab | |
| id: ftl | |
| shell: bash | |
| env: | |
| FTL_PROJECT: ${{ steps.gcp-auth.outputs.project_id }} | |
| # Empty on non-workflow_call triggers; the script's :- defaults | |
| # treat empty and unset alike. | |
| FTL_DEVICES: ${{ inputs.devices }} | |
| FTL_FLAKY_ATTEMPTS: ${{ inputs.flaky_attempts }} | |
| run: scripts/android/ftl-run.sh | |
| # Runs on success and on test failure, but not when an earlier step | |
| # (build, auth) died before any tests ran. | |
| - name: Write test summary | |
| if: always() && steps.ftl.outcome != 'skipped' | |
| shell: bash | |
| run: | | |
| set -uo pipefail | |
| { | |
| echo "## Firebase Test Lab results" | |
| echo | |
| # Link to the run in the Firebase console, as printed by gcloud. | |
| url=$(grep -o 'https://console.firebase.google.com[^ ]*' ftl-output.log | head -1 || true) | |
| [ -n "$url" ] && echo "[View full results in the Firebase console]($url)" && echo | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| mkdir -p ftl-results | |
| gcloud storage cp --recursive \ | |
| "gs://$FTL_RESULTS_BUCKET/$FTL_RESULTS_DIR/*" ftl-results/ 2>/dev/null || true | |
| python3 - >> "$GITHUB_STEP_SUMMARY" <<'EOF' | |
| import glob, os, xml.etree.ElementTree as ET | |
| rows = [] | |
| skipped_files = [] | |
| for path in sorted(glob.glob("ftl-results/**/test_result_*.xml", recursive=True)): | |
| device = os.path.relpath(path, "ftl-results").split(os.sep)[0] | |
| # A malformed XML from Test Lab must not fail the summary (and | |
| # with it the whole job) when the tests themselves passed. | |
| try: | |
| suite = ET.parse(path).getroot() | |
| if suite.tag == "testsuites": | |
| suite = suite.find("testsuite") | |
| assert suite is not None | |
| except Exception: | |
| skipped_files.append(path) | |
| continue | |
| total = int(suite.get("tests", 0)) | |
| failures = int(suite.get("failures", 0)) + int(suite.get("errors", 0)) | |
| skipped = int(suite.get("skipped", 0)) | |
| passed = total - failures - skipped | |
| status = "✅" if failures == 0 else "❌" | |
| rows.append((device, status, total, passed, failures, skipped, | |
| suite.get("time", "?"))) | |
| if rows: | |
| print("| Device | Status | Tests | Passed | Failed | Skipped | Time (s) |") | |
| print("|---|---|---|---|---|---|---|") | |
| for r in rows: | |
| print("| {} | {} | {} | {} | {} | {} | {} |".format(*r)) | |
| else: | |
| print("_No JUnit test results found in the results bucket._") | |
| for path in skipped_files: | |
| print(f"\n_Could not parse `{path}`; see the Firebase console for that device._") | |
| EOF | |
| # Test Lab writes logcat, screen video, and JUnit XML per device; attach | |
| # them to the run so failures can be debugged without GCS access. The | |
| # summary step above is what downloads ftl-results/. | |
| - name: Upload Test Lab artifacts | |
| if: always() && steps.ftl.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ftl-results-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| ftl-results/ | |
| ftl-output.log | |
| if-no-files-found: warn | |
| retention-days: 14 |