Repository navigation
fix(payment): pin promo-code checkout presentment to USD so the $269 minimum passes (#23299) #27962
Workflow file for this run
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: Mobile App Checks | |
| on: | |
| # Nightly calls run these same jobs against one immutable main snapshot. | |
| workflow_call: | |
| inputs: | |
| nightly_main_sha: | |
| description: Main SHA pinned by the unfiltered nightly caller | |
| type: string | |
| required: false | |
| default: '' | |
| push: | |
| branches: main | |
| # Stacked PRs (V6 on V5, #14358) must run this workflow. `branches: main` | |
| # made GitHub skip the entire workflow — the iOS job never existed, which | |
| # reads as "green" and proves nothing. | |
| pull_request: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '41 4 * * *' | |
| permissions: | |
| contents: read | |
| issues: read | |
| concurrency: | |
| # PRs cancel their own older runs. Main pushes key on the exact SHA with | |
| # cancel-in-progress false, matching desktop-swift-ci: mobile release | |
| # admission (verify_mobile_release_admission.py) requires a first-attempt | |
| # push-event Mobile Release Eligibility success on the exact release SHA, | |
| # and a pending push run replaced by a newer event leaves a cancelled check | |
| # that no rerun can re-mint (attempt 2 does not count). The daily cron and | |
| # manual dispatches keep per-run groups so they can never evict a push run. | |
| group: mobile-app-checks-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || (github.event_name == 'push' && github.sha) || format('run-{0}', github.run_id) }}-${{ inputs.nightly_main_sha != '' && format('nightly-{0}', github.run_id) || 'normal' }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| # Abort a stalled git HTTP transfer instead of hanging. Hosted-runner fetches | |
| # from github.com intermittently stop receiving data for 7-10 minutes before | |
| # the connection resets (Backend Hermetic E2E run 39435458398 spent 498 s in | |
| # "Checkout code" and hit its 10-minute cap). With these set, git aborts any | |
| # transfer that stays under 1 KB/s for 60 s, and actions/checkout retries the | |
| # fetch (its built-in 3 attempts); a healthy depth-1 fetch takes ~20 s. | |
| # Applies to every git network call in the workflow, including script steps. | |
| GIT_HTTP_LOW_SPEED_LIMIT: "1000" | |
| GIT_HTTP_LOW_SPEED_TIME: "60" | |
| jobs: | |
| changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| outputs: | |
| diff_base: ${{ steps.changes.outputs.diff_base }} | |
| full_ci: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.tier.outputs.full_ci) }} | |
| has_app_codegen: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_codegen) }} | |
| has_app_compile_smoke: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_compile_smoke) }} | |
| has_app_android_pr: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_android_pr) }} | |
| has_app_ios_compile: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_ios_compile) }} | |
| has_app_journeys: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_journeys) }} | |
| has_app_journeys_pr: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_journeys_pr) }} | |
| has_app_dart: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_dart) }} | |
| has_app_l10n: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_app_l10n) }} | |
| has_flutter_generated: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changes.outputs.has_flutter_generated) }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| # Full history is required: detect-changes diffs against the merge | |
| # base. blob:none keeps the commit graph but skips file contents, | |
| # which is what made the unfiltered clone of this ~1.25GB repo take | |
| # 7-13 minutes. | |
| fetch-depth: 0 | |
| filter: blob:none | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Resolve current CI tier | |
| id: tier | |
| uses: ./.github/actions/ci-tier | |
| - name: Detect changed paths | |
| id: changes | |
| uses: ./.github/actions/detect-changes | |
| generated-files: | |
| name: Generated Files | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| needs: changes | |
| if: needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_flutter_generated == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| fetch-depth: 1 | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| # Match repo-checks.yml so dependency resolution cannot drift when | |
| # the stable channel advances beyond the repository's lockfile. | |
| flutter-version: 3.44.5 | |
| cache: true | |
| # flutter-action already caches the SDK and ~/.pub-cache. build_runner's | |
| # incremental state is not covered by it and is what makes codegen slow. | |
| - name: Cache build_runner state | |
| uses: actions/cache@v6 | |
| with: | |
| path: app/.dart_tool/build | |
| key: ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }}- | |
| ${{ runner.os }}-flutter-buildrunner- | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| - name: Check Flutter generated code | |
| if: needs.changes.outputs.has_app_codegen == 'true' | |
| working-directory: app | |
| run: | | |
| dart run build_runner build --delete-conflicting-outputs | |
| git diff --exit-code -- lib || { | |
| echo "Generated Dart files are stale. Run: cd app && dart run build_runner build --delete-conflicting-outputs" | |
| exit 1 | |
| } | |
| if [ -n "$(git ls-files --others --exclude-standard -- lib)" ]; then | |
| echo "Generated Dart files are stale; untracked generated files were produced:" | |
| git ls-files --others --exclude-standard -- lib | |
| exit 1 | |
| fi | |
| - name: Check Flutter l10n | |
| if: needs.changes.outputs.has_app_l10n == 'true' | |
| working-directory: app | |
| run: | | |
| flutter gen-l10n | |
| git diff --exit-code -- lib/l10n || { | |
| echo "Generated localization files are stale. Run: cd app && flutter gen-l10n" | |
| exit 1 | |
| } | |
| if [ -n "$(git ls-files --others --exclude-standard -- lib/l10n)" ]; then | |
| echo "Generated localization files are stale; untracked generated files were produced:" | |
| git ls-files --others --exclude-standard -- lib/l10n | |
| exit 1 | |
| fi | |
| analyze-and-test: | |
| name: Dart Analyze & Tests | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: github.event_name == 'schedule' || needs.changes.outputs.has_app_dart == 'true' | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| - name: Cache build_runner state | |
| uses: actions/cache@v6 | |
| with: | |
| path: app/.dart_tool/build | |
| key: ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }}- | |
| ${{ runner.os }}-flutter-buildrunner- | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| # Also satisfies test.sh's generated-file prerequisites, so it skips its | |
| # own pub get and build_runner run. | |
| - name: Prepare Flutter dev compile inputs | |
| working-directory: app | |
| run: | | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| { | |
| echo "API_BASE_URL=" | |
| echo "USE_WEB_AUTH=true" | |
| echo "USE_AUTH_CUSTOM_TOKEN=true" | |
| echo "STAGING_API_URL=" | |
| } > .dev.env | |
| : > .env | |
| - name: Generate Flutter code | |
| working-directory: app | |
| run: dart run build_runner build --delete-conflicting-outputs | |
| - name: Run analyzer ratchet | |
| working-directory: app | |
| run: bash scripts/analyze_ratchet.sh | |
| - name: Run Flutter tests | |
| id: flutter_tests | |
| working-directory: app | |
| run: | | |
| set -o pipefail | |
| bash test.sh \ | |
| --reporter expanded \ | |
| --file-reporter "json:$GITHUB_WORKSPACE/flutter-test-results.json" \ | |
| 2>&1 | tee "$GITHUB_WORKSPACE/flutter-test-output.log" | |
| - name: Summarize failed Flutter tests | |
| if: ${{ failure() && steps.flutter_tests.outcome == 'failure' }} | |
| env: | |
| FLUTTER_TEST_RESULTS: ${{ github.workspace }}/flutter-test-results.json | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| from collections import defaultdict | |
| from pathlib import Path | |
| report = Path(os.environ['FLUTTER_TEST_RESULTS']) | |
| summary = Path(os.environ['GITHUB_STEP_SUMMARY']) | |
| tests = {} | |
| errors = defaultdict(list) | |
| failures = [] | |
| if report.exists(): | |
| for raw_line in report.read_text().splitlines(): | |
| try: | |
| event = json.loads(raw_line) | |
| except json.JSONDecodeError: | |
| continue | |
| event_type = event.get('type') | |
| if event_type == 'testStart': | |
| test = event.get('test', {}) | |
| tests[str(test.get('id'))] = test | |
| elif event_type == 'error': | |
| details = str(event.get('error', 'Unknown error')) | |
| if event.get('stackTrace'): | |
| details = f"{details}\n{event['stackTrace']}" | |
| errors[str(event.get('testID'))].append(details) | |
| elif event_type == 'testDone' and event.get('result') in {'failure', 'error'}: | |
| failures.append(event) | |
| with summary.open('a') as output: | |
| output.write('## Flutter test diagnostics\n\n') | |
| if not failures: | |
| output.write( | |
| 'Flutter exited unsuccessfully but emitted no failed-test event. ' | |
| 'Download the `flutter-test-diagnostics` artifact for the complete JSON stream and console log.\n' | |
| ) | |
| for failure in failures: | |
| test_id = str(failure.get('testID')) | |
| test = tests.get(test_id, {}) | |
| name = test.get('name', f'Unknown test (id {test_id})') | |
| output.write(f'### {name}\n\n') | |
| details = '\n'.join(errors[test_id]).strip() | |
| if details: | |
| output.write('```text\n') | |
| output.write(details[-6000:].replace('```', '``\\`')) | |
| output.write('\n```\n\n') | |
| PY | |
| - name: Upload Flutter test diagnostics | |
| if: ${{ failure() && steps.flutter_tests.outcome == 'failure' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: flutter-test-diagnostics | |
| path: | | |
| flutter-test-results.json | |
| flutter-test-output.log | |
| if-no-files-found: warn | |
| journeys-hermetic: | |
| name: Seeded Journeys (hermetic) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: github.event_name == 'schedule' || (github.event_name == 'push' && needs.changes.outputs.has_app_journeys == 'true') || (needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_app_journeys_pr == 'true') | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| fetch-depth: 1 | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| # The canonical verify entrypoint resolves the dev-harness python; its | |
| # import probe needs python-dotenv (same dependency the backend venv | |
| # provides locally). Nothing else: the lane is synthetic-fixture, | |
| # loopback-only, and secret-free — safe for untrusted fork PRs. | |
| - name: Install dev-harness python deps | |
| run: python3 -m pip install --quiet --disable-pip-version-check python-dotenv | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| - name: Prepare Flutter dev compile inputs | |
| working-directory: app | |
| run: | | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| { | |
| echo "API_BASE_URL=" | |
| echo "USE_WEB_AUTH=true" | |
| echo "USE_AUTH_CUSTOM_TOKEN=true" | |
| echo "STAGING_API_URL=" | |
| } > .dev.env | |
| : > .env | |
| - name: Generate Flutter code | |
| working-directory: app | |
| run: dart run build_runner build --delete-conflicting-outputs | |
| # Same command as the local lane (SCA-490): focused fast feedback with | |
| # mechanical discovery, receipt validation, and fail-closed selection. | |
| - name: Run hermetic seeded journeys | |
| id: journeys | |
| run: | | |
| bash scripts/dev-harness/mobile-verify.sh fast --all \ | |
| --evidence-dir "$GITHUB_WORKSPACE/journey-evidence" --json | |
| - name: Upload journey evidence | |
| if: ${{ always() && steps.journeys.conclusion != 'skipped' }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: journey-evidence | |
| path: journey-evidence | |
| if-no-files-found: warn | |
| android-compile-smoke: | |
| name: Android Compile Smoke | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: github.event_name == 'schedule' || (github.event_name == 'push' && needs.changes.outputs.has_app_compile_smoke == 'true') || (needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_app_android_pr == 'true') | |
| timeout-minutes: 25 | |
| env: | |
| # Read by app/android/app/build.gradle (ndkVersion) and the NDK install | |
| # step below; test_mobile_ci_infra_guards.py keeps it equal to | |
| # build.gradle's fallback pin. | |
| NDK_VERSION: "28.2.13676358" | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| fetch-depth: 1 | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Free runner disk space | |
| # ubuntu-latest ships with ~14GB free; the Gradle build runs out of | |
| # space during stripDebugSymbols. Remove unused preinstalled toolchains | |
| # (keep /usr/local/lib/android — the build needs the Android SDK). | |
| run: | | |
| sudo rm -rf /usr/share/dotnet /usr/local/.ghcup /opt/hostedtoolcache/CodeQL \ | |
| /usr/local/share/boost /usr/local/share/chromium /usr/local/lib/node_modules | |
| sudo docker image prune --all --force | |
| df -h / | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| - name: Cache build_runner state | |
| uses: actions/cache@v6 | |
| with: | |
| path: app/.dart_tool/build | |
| key: ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }}- | |
| ${{ runner.os }}-flutter-buildrunner- | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| - name: Prepare Flutter dev compile inputs | |
| working-directory: app | |
| run: | | |
| mkdir -p android/app/src/dev/ android/app/src/prod/ | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| cp setup/prebuilt/google-services-local.json android/app/src/dev/google-services.json | |
| cp setup/prebuilt/google-services-local.json android/app/src/prod/google-services.json | |
| cp setup/prebuilt/key.properties android/key.properties | |
| { | |
| echo "API_BASE_URL=" | |
| echo "USE_WEB_AUTH=true" | |
| echo "USE_AUTH_CUSTOM_TOKEN=true" | |
| echo "STAGING_API_URL=" | |
| } > .dev.env | |
| : > .env | |
| - name: Generate Flutter code for compile smoke | |
| working-directory: app | |
| run: dart run build_runner build --delete-conflicting-outputs | |
| - name: Setup Gradle for Flutter compile smoke | |
| uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| # Cold Gradle homes took 11-12m in runs 40022514904/40025498700; | |
| # with 11m of runner/Flutter setup, run 40030083284 hit the 25m cap. | |
| # Basic caching keys caches + wrapper by OS/arch and Gradle build | |
| # files, not commit SHA, so backend-only edits reuse the same entry. | |
| # Populate on PRs too: the main-only JVM writer left all three | |
| # attempts cold. GitHub scopes PR writes to that PR's merge ref. | |
| # This provider's keys are separate from the JVM job's enhanced | |
| # cache, and an exact hit skips saving another copy on every commit. | |
| cache-provider: basic | |
| cache-read-only: false | |
| - name: Restore pinned Android NDK | |
| # Same corrupt-download flake as the emulator job hit (runs 39506049904, | |
| # 39626150998): AGP auto-installs a missing NDK inside Gradle configure | |
| # with no retry, and one truncated dl.google.com response fails the job | |
| # before any app code runs. Restore-only: this job is the Mobile App | |
| # Checks wall, so the one-time cache save stays on the emulator job, | |
| # which runs in every event this job does; install_android_ndk.py | |
| # covers a miss with one retry. | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: /usr/local/lib/android/sdk/ndk/${{ env.NDK_VERSION }} | |
| key: ${{ runner.os }}-android-ndk-${{ env.NDK_VERSION }} | |
| - name: Install pinned Android NDK | |
| run: python3 .github/scripts/install_android_ndk.py --ndk-version "$NDK_VERSION" | |
| - name: Build Flutter Android dev debug APK | |
| working-directory: app | |
| env: | |
| # GitHub Actions sets CI=true; android/app/build.gradle reads Codemagic | |
| # keystore env vars in that case even for debug APK builds. | |
| # The debug keystore is the in-repo prebuilt file — no secrets. | |
| CM_KEYSTORE_PATH: ${{ github.workspace }}/app/setup/prebuilt/debug.keystore | |
| CM_KEYSTORE_PASSWORD: android | |
| CM_KEY_ALIAS: androiddebugkey | |
| CM_KEY_PASSWORD: android | |
| # arm64-only since c8f35061fc: extra ABIs OOM'd ubuntu-latest during | |
| # stripDebugSymbols. Does not compile the JNI opus shim for armeabi-v7a or x86_64. | |
| run: flutter build apk --debug --flavor dev --target-platform android-arm64 | |
| # --analyze-size is release-mode + single-ABI only and would be a second | |
| # ~10-15 min compile. Debug is not Play download size; this is a coarse | |
| # zip breakdown of the APK this job already built. Never a gate. | |
| - name: Report debug APK size | |
| run: | | |
| python3 .github/scripts/report_debug_apk_size.py \ | |
| --apk app/build/app/outputs/flutter-apk/app-dev-debug.apk \ | |
| --json "$GITHUB_WORKSPACE/apk-size-report.json" \ | |
| --summary "$GITHUB_STEP_SUMMARY" | |
| - name: Upload debug APK size report | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: apk-size-${{ github.event.pull_request.head.sha || github.sha }} | |
| path: apk-size-report.json | |
| if-no-files-found: error | |
| retention-days: 5 | |
| - name: Upload Flutter Android dev debug APK | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: app-dev-debug-${{ github.event.pull_request.head.sha || github.sha }} | |
| path: app/build/app/outputs/flutter-apk/app-dev-debug.apk | |
| if-no-files-found: error | |
| retention-days: 5 | |
| compression-level: 0 | |
| android-unit-tests: | |
| name: Android JVM Unit Tests | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: github.event_name == 'schedule' || (github.event_name == 'push' && needs.changes.outputs.has_app_compile_smoke == 'true') || (needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_app_android_pr == 'true') | |
| timeout-minutes: 25 | |
| env: | |
| # Read by app/android/app/build.gradle (ndkVersion) and the NDK install | |
| # step below; test_mobile_ci_infra_guards.py keeps it equal to | |
| # build.gradle's fallback pin. | |
| NDK_VERSION: "28.2.13676358" | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| fetch-depth: 1 | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| - name: Cache build_runner state | |
| uses: actions/cache@v6 | |
| with: | |
| path: app/.dart_tool/build | |
| key: ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }}- | |
| ${{ runner.os }}-flutter-buildrunner- | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| - name: Prepare Flutter dev compile inputs | |
| working-directory: app | |
| run: | | |
| mkdir -p android/app/src/dev/ android/app/src/prod/ | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| cp setup/prebuilt/google-services-local.json android/app/src/dev/google-services.json | |
| cp setup/prebuilt/google-services-local.json android/app/src/prod/google-services.json | |
| cp setup/prebuilt/key.properties android/key.properties | |
| { | |
| echo "API_BASE_URL=" | |
| echo "USE_WEB_AUTH=true" | |
| echo "USE_AUTH_CUSTOM_TOKEN=true" | |
| echo "STAGING_API_URL=" | |
| } > .dev.env | |
| : > .env | |
| - name: Generate Flutter code for JVM unit tests | |
| working-directory: app | |
| run: dart run build_runner build --delete-conflicting-outputs | |
| # gradlew, gradle-wrapper.jar, and local.properties are gitignored. | |
| # On main they were created by the APK compile in the same job before | |
| # this Gradle task ran. The parallel job never compiles an APK, so | |
| # generate those files only (`--config-only`). | |
| - name: Generate Android Gradle wrapper | |
| working-directory: app | |
| run: flutter build apk --debug --flavor dev --target-platform android-arm64 --config-only | |
| - name: Setup Gradle for Android JVM unit tests | |
| uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| # Sole Gradle User Home writer. Compile-smoke is restore-only so | |
| # this job's ~1m POST save is not on the workflow wall (JVM finishes | |
| # minutes before Android Compile Smoke in the V5 sample). | |
| cache-read-only: ${{ github.ref != 'refs/heads/main' }} | |
| cache-cleanup: on-success | |
| gradle-home-cache-includes: | | |
| caches | |
| notifications | |
| wrapper | |
| - name: Restore pinned Android NDK | |
| # Gradle configure resolves ndkVersion for :app:testDevDebugUnitTest | |
| # too, so without this the job auto-downloads the NDK the same way the | |
| # emulator job did before runs 39506049904/39626150998. Restore-only: | |
| # the emulator job seeds the cache key; install_android_ndk.py covers | |
| # a miss with one retry. | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: /usr/local/lib/android/sdk/ndk/${{ env.NDK_VERSION }} | |
| key: ${{ runner.os }}-android-ndk-${{ env.NDK_VERSION }} | |
| - name: Install pinned Android NDK | |
| run: python3 .github/scripts/install_android_ndk.py --ndk-version "$NDK_VERSION" | |
| - name: Run Android JVM unit tests | |
| working-directory: app/android | |
| env: | |
| CM_KEYSTORE_PATH: ${{ github.workspace }}/app/setup/prebuilt/debug.keystore | |
| CM_KEYSTORE_PASSWORD: android | |
| CM_KEY_ALIAS: androiddebugkey | |
| CM_KEY_PASSWORD: android | |
| # Same target-platform as the APK job so a shared Gradle cache hit | |
| # does not rebuild every ABI. This job does not wait for the APK. | |
| run: ./gradlew :app:testDevDebugUnitTest -Ptarget-platform=android-arm64 | |
| android-emulator-checks: | |
| name: Android Emulator Acceptance | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || (github.event_name == 'push' && needs.changes.outputs.has_app_compile_smoke == 'true') || (needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_app_android_pr == 'true') | |
| timeout-minutes: 35 | |
| env: | |
| # Read by app/android/app/build.gradle (ndkVersion) and the NDK install | |
| # step below; test_mobile_ci_infra_guards.py keeps it equal to | |
| # build.gradle's fallback pin. | |
| NDK_VERSION: "28.2.13676358" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - uses: actions/setup-java@v5 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| - uses: gradle/actions/setup-gradle@v6 | |
| with: | |
| gradle-version: '8.14.2' | |
| cache-read-only: true | |
| - name: Install acceptance build tools | |
| run: | | |
| "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "build-tools;36.0.0" "platforms;android-36" | |
| - name: Cache pinned Android NDK | |
| # AGP auto-installs a missing NDK inside Gradle configure with no | |
| # retry, so one truncated dl.google.com response fails the job before | |
| # any app code runs ("Error on ZipFile unknown archive": runs | |
| # 39506049904, 39469917718, 39626150998 — dead 5 s into a download | |
| # that takes ~22 s when healthy). Cache the extracted NDK so most runs | |
| # make no dl.google.com request at all. Sole writer: the restore-only | |
| # compile-smoke and unit-test jobs keep post-job saves off their wall, | |
| # and this job runs in every event they do. | |
| uses: actions/cache@v6 | |
| with: | |
| path: /usr/local/lib/android/sdk/ndk/${{ env.NDK_VERSION }} | |
| key: ${{ runner.os }}-android-ndk-${{ env.NDK_VERSION }} | |
| - name: Install pinned Android NDK | |
| # Cache-miss path: explicit install, corrupt-archive purge, one retry. | |
| run: python3 .github/scripts/install_android_ndk.py --ndk-version "$NDK_VERSION" | |
| - name: Prepare secret-free local app inputs | |
| working-directory: app | |
| run: | | |
| mkdir -p android/app/src/dev/ android/app/src/prod/ | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| cp setup/prebuilt/google-services-local.json android/app/src/dev/google-services.json | |
| cp setup/prebuilt/google-services-local.json android/app/src/prod/google-services.json | |
| cp setup/prebuilt/key.properties android/key.properties | |
| { | |
| echo 'API_BASE_URL=' | |
| echo 'USE_WEB_AUTH=true' | |
| echo 'USE_AUTH_CUSTOM_TOKEN=true' | |
| echo 'STAGING_API_URL=' | |
| } > .dev.env | |
| : > .env | |
| flutter pub get | |
| dart run build_runner build --delete-conflicting-outputs | |
| bash scripts/validate_mobile_build_config.sh --flavor dev --profile local_dev | |
| - name: Build minified AOT APK for emulator acceptance | |
| working-directory: app | |
| env: | |
| CM_KEYSTORE_PATH: ${{ github.workspace }}/app/setup/prebuilt/debug.keystore | |
| CM_KEYSTORE_PASSWORD: android | |
| CM_KEY_ALIAS: androiddebugkey | |
| CM_KEY_PASSWORD: android | |
| # One ABI limits R8/native-build memory. Existing arm64 compile remains. | |
| run: flutter build apk --release --flavor dev --target-platform android-x64 --dart-define=OMI_APP_PROFILE=local_dev | |
| - name: Enable Linux KVM | |
| 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: Exercise actual native lifecycle and packaged startup | |
| uses: reactivecircus/android-emulator-runner@4c44018e59b437e86cdfc41da381398f93ed8808 # v2 | |
| with: | |
| api-level: 36 | |
| target: google_apis | |
| arch: x86_64 | |
| disable-animations: true | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot -memory 2048 | |
| # One explicit strict Bash invocation: no later smoke can hide a | |
| # native failure, independent of the action's script parser. | |
| script: >- | |
| bash -euo pipefail -c ' | |
| python3 app/scripts/android_fgs_probe.py --serial emulator-5554 --gradle gradle --rounds 5 --output android-acceptance/native && | |
| python3 app/scripts/android_startup_smoke.py --serial emulator-5554 --apk app/build/app/outputs/flutter-apk/app-dev-release.apk --aapt "$ANDROID_HOME/build-tools/36.0.0/aapt" --rounds 3 --output android-acceptance/startup' | |
| - name: Upload emulator acceptance evidence | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: android-acceptance-${{ github.event.pull_request.head.sha || github.sha }} | |
| path: android-acceptance/ | |
| if-no-files-found: error | |
| retention-days: 14 | |
| ios-compile-check: | |
| name: iOS Compile Check | |
| runs-on: macos-26 | |
| needs: changes | |
| if: needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_app_ios_compile == 'true' | |
| # Local cold Xcode time was 334s on an M-series Mac. Hosted macos-26 still | |
| # pays Flutter SDK, pub get, build_runner, and CocoaPods on top of that. | |
| # Cold p50 is ~33 min, but a slow cold runner measured 39:25 for the real | |
| # steps alone (16:12 manifest checks + 16:27 Xcode build, run 39634604539 | |
| # attempt 1); the post-job Post Setup Flutter cache save (59 s on the | |
| # sibling success run 119312386446) then hit the 40-min ceiling and the | |
| # job reported cancelled after every real step passed. 50 keeps the | |
| # wedge-guard above that cold path; budget counts p50 (19), not the cap. | |
| timeout-minutes: 50 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| fetch-depth: 0 | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Select and assert pinned Xcode | |
| run: desktop/macos/scripts/run-swift-ci.sh --select-toolchain | |
| # Manifest flow coverage bootstraps the pinned Swift formatter. Share the | |
| # desktop binary cache so a cold source build cannot consume the iOS | |
| # compiler's job budget (16 minutes on PR #21488). | |
| - name: Restore Swift formatter and linter tools | |
| id: ios-swift-tools-cache | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: | | |
| ~/.cache/omi-swift-format/*/.build/release/swift-format | |
| ~/.cache/omi-swiftlint/*/swiftlint | |
| key: ${{ runner.os }}-desktop-swift-tools-v3-xcode266-${{ hashFiles('desktop/macos/scripts/swift-format-wrapper.sh', 'desktop/macos/scripts/swiftlint-wrapper.sh') }} | |
| restore-keys: | | |
| ${{ runner.os }}-desktop-swift-tools-v3-xcode266- | |
| # The macOS-scoped selection includes desktop-beta-admission-firestore- | |
| # contention, whose run.sh needs backend/.venv or uv (the same fallback | |
| # desktop-swift-ci.yml relies on). Nightly 39586610411 failed the iOS | |
| # job at that check's setup with neither present. | |
| - name: Install uv for manifest checks | |
| uses: astral-sh/setup-uv@ecd24dd710f2fb0dca1693a67af11fc4a5c5ec84 | |
| with: | |
| # Exact version resolves locally; an unset version means "latest", | |
| # which fetches the astral-sh/versions manifest and has hard-failed | |
| # CI on that network call. Matches desktop-swift-ci.yml and | |
| # backend/Dockerfile UV_VERSION. | |
| version: "0.11.13" | |
| - name: Run macOS-scoped iOS manifest checks | |
| run: | | |
| python3 .github/scripts/run_checks.py --lane ci --platform macos \ | |
| ${{ inputs.nightly_main_sha != '' && '--changed-files /tmp/nightly-all-files.txt --skip-pr-body-checks --keep-going' || '' }} \ | |
| --exclusive-platform --base "${{ needs.changes.outputs.diff_base }}" \ | |
| --skip-pr-body-checks | |
| - name: Save Swift tools before iOS compilation | |
| if: steps.ios-swift-tools-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@v6 | |
| with: | |
| path: | | |
| ~/.cache/omi-swift-format/*/.build/release/swift-format | |
| ~/.cache/omi-swiftlint/*/swiftlint | |
| key: ${{ steps.ios-swift-tools-cache.outputs.cache-primary-key }} | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| - name: Cache build_runner state | |
| uses: actions/cache@v6 | |
| with: | |
| path: app/.dart_tool/build | |
| key: ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }}- | |
| ${{ runner.os }}-flutter-buildrunner- | |
| - name: Cache CocoaPods | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| app/ios/Pods | |
| ~/Library/Caches/CocoaPods | |
| key: ${{ runner.os }}-cocoapods-${{ hashFiles('app/ios/Podfile.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cocoapods- | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| - name: Prepare Flutter dev compile inputs | |
| working-directory: app | |
| run: | | |
| mkdir -p android/app/src/dev/ android/app/src/prod/ ios/Config/Dev ios/Config/Prod | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| cp setup/prebuilt/google-services-local.json android/app/src/dev/google-services.json | |
| cp setup/prebuilt/google-services-local.json android/app/src/prod/google-services.json | |
| cp setup/prebuilt/key.properties android/key.properties | |
| cp setup/prebuilt/GoogleService-Info-Local.plist ios/Config/Dev/GoogleService-Info.plist | |
| cp setup/prebuilt/GoogleService-Info-Local.plist ios/Config/Prod/GoogleService-Info.plist | |
| cp setup/prebuilt/GoogleService-Info-Local.plist ios/Runner/GoogleService-Info.plist | |
| { | |
| echo "API_BASE_URL=" | |
| echo "USE_WEB_AUTH=true" | |
| echo "USE_AUTH_CUSTOM_TOKEN=true" | |
| echo "STAGING_API_URL=" | |
| } > .dev.env | |
| : > .env | |
| - name: Generate Flutter code for iOS compile | |
| working-directory: app | |
| run: dart run build_runner build --delete-conflicting-outputs | |
| # omiWatchApp is a watchOS companion. `flutter build ios --simulator` | |
| # without `-d` fails after pod install: "A device ID is required to | |
| # build an app with a watchOS companion app." (run 35256673731). | |
| - name: Pick an iPhone simulator | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| raw = subprocess.check_output( | |
| ["xcrun", "simctl", "list", "devices", "available", "-j"], | |
| text=True, | |
| ) | |
| data = json.loads(raw) | |
| picked = None | |
| for runtime, devices in data.get("devices", {}).items(): | |
| if "iOS" not in runtime: | |
| continue | |
| for device in devices: | |
| if device.get("isAvailable") and "iPhone" in device.get("name", ""): | |
| picked = device | |
| if picked is None: | |
| print(raw, file=sys.stderr) | |
| raise SystemExit("no available iPhone simulator") | |
| udid = picked["udid"] | |
| print(f"Using simulator {picked['name']} ({udid})") | |
| with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as handle: | |
| handle.write(f"IOS_SIMULATOR_UDID={udid}\n") | |
| PY | |
| - name: Build Flutter iOS simulator debug | |
| working-directory: app | |
| # Flavor scheme is `dev` (Debug-dev). omiWatchApp.xcscheme lacks | |
| # customLLDBInitFile; Flutter warns, the compile still proceeds. | |
| run: flutter build ios --simulator --debug --flavor dev --no-codesign -d "$IOS_SIMULATOR_UDID" | |
| - name: Prove stable iOS compiler emits no Siri metadata | |
| working-directory: app | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| from pathlib import Path | |
| bundle = Path('build/ios/iphonesimulator/Runner.app') | |
| if not bundle.is_dir(): | |
| raise SystemExit(f'missing built Runner.app: {bundle}') | |
| actions = bundle / 'Metadata.appintents/extract.actionsdata' | |
| payload = json.loads(actions.read_text()) if actions.is_file() else {} | |
| for kind in ('actions', 'entities'): | |
| if payload.get(kind): | |
| raise SystemExit(f'Xcode 26.6 unexpectedly emitted {kind}: {sorted(payload[kind])}') | |
| print('Xcode 26.6 Runner metadata: no Siri actions or entities') | |
| PY | |
| dart-tests-kiritimati: | |
| # Full far-timezone suite runs on main and nightly; PRs retain the ordinary | |
| # Dart suite. An unrelated fork does not claim timezone coverage. | |
| name: Dart Tests (Pacific/Kiritimati) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| if: inputs.nightly_main_sha != '' || github.event_name == 'schedule' || (github.event_name == 'push' && needs.changes.outputs.has_app_dart == 'true') | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ inputs.nightly_main_sha || github.sha }} | |
| - name: Inventory all nightly paths | |
| if: inputs.nightly_main_sha != '' | |
| shell: bash | |
| run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: stable | |
| flutter-version: 3.44.5 | |
| cache: true | |
| - name: Cache build_runner state | |
| uses: actions/cache@v6 | |
| with: | |
| path: app/.dart_tool/build | |
| key: ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-flutter-buildrunner-${{ hashFiles('app/pubspec.lock') }}- | |
| ${{ runner.os }}-flutter-buildrunner- | |
| - name: Resolve Flutter dependencies | |
| working-directory: app | |
| run: flutter pub get | |
| - name: Prepare Flutter dev compile inputs | |
| working-directory: app | |
| run: | | |
| cp lib/firebase_options_local.dart lib/firebase_options_dev.dart | |
| cp lib/firebase_options_local.dart lib/firebase_options_prod.dart | |
| { | |
| echo "API_BASE_URL=" | |
| echo "USE_WEB_AUTH=true" | |
| echo "USE_AUTH_CUSTOM_TOKEN=true" | |
| echo "STAGING_API_URL=" | |
| } > .dev.env | |
| : > .env | |
| - name: Generate Flutter code | |
| working-directory: app | |
| run: dart run build_runner build --delete-conflicting-outputs | |
| - name: Run Flutter tests under Pacific/Kiritimati | |
| run: TZ=Pacific/Kiritimati bash app/test.sh | |
| - name: Run Flutter tests under Pacific/Pago_Pago | |
| if: always() | |
| run: TZ=Pacific/Pago_Pago bash app/test.sh | |
| # This is the only mobile status that a release-admission caller should | |
| # consume. Lane D must query this exact check name on the exact source SHA, | |
| # then write the normalized evidence expected by | |
| # verify_mobile_release_admission.py: | |
| # | |
| # {"check_name":"Mobile Release Eligibility", | |
| # "workflow_name":"Mobile App Checks", | |
| # "workflow_path":".github/workflows/mobile-app-checks.yml", | |
| # "event":"push", "status":"completed", "conclusion":"success", | |
| # "head_branch":"main", "head_sha":"<source_sha>", | |
| # "repository":"BasedHardware/omi"} | |
| # | |
| # Selected Dart coverage must succeed, including on cheap-tier forks. Other | |
| # conditional jobs may be skipped for inapplicable or deferred heavy coverage. | |
| # Changes itself must succeed; failures and cancellations remain ineligible. | |
| mobile-release-eligibility: | |
| name: Mobile Release Eligibility | |
| runs-on: ubuntu-latest | |
| needs: | |
| - changes | |
| - generated-files | |
| - analyze-and-test | |
| - journeys-hermetic | |
| - android-compile-smoke | |
| - android-unit-tests | |
| - android-emulator-checks | |
| - ios-compile-check | |
| - dart-tests-kiritimati | |
| if: ${{ always() }} | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Aggregate conditional mobile checks | |
| env: | |
| CHANGES_RESULT: ${{ needs.changes.result }} | |
| DART_EXPECTED: ${{ github.event_name == 'schedule' || needs.changes.outputs.has_app_dart == 'true' }} | |
| FULL_CI: ${{ needs.changes.outputs.full_ci }} | |
| HAS_GENERATED: ${{ needs.changes.outputs.has_flutter_generated }} | |
| HAS_DART: ${{ needs.changes.outputs.has_app_dart }} | |
| HAS_JOURNEYS: ${{ needs.changes.outputs.has_app_journeys_pr }} | |
| HAS_ANDROID: ${{ needs.changes.outputs.has_app_android_pr }} | |
| HAS_IOS: ${{ needs.changes.outputs.has_app_ios_compile }} | |
| GENERATED_FILES_RESULT: ${{ needs.generated-files.result }} | |
| ANALYZE_AND_TEST_RESULT: ${{ needs.analyze-and-test.result }} | |
| JOURNEYS_RESULT: ${{ needs.journeys-hermetic.result }} | |
| ANDROID_COMPILE_RESULT: ${{ needs.android-compile-smoke.result }} | |
| ANDROID_UNIT_RESULT: ${{ needs.android-unit-tests.result }} | |
| ANDROID_EMULATOR_RESULT: ${{ needs.android-emulator-checks.result }} | |
| EMULATOR_EXPECTED: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || (github.event_name == 'push' && needs.changes.outputs.has_app_compile_smoke == 'true') || (needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.has_app_android_pr == 'true') }} | |
| IOS_COMPILE_RESULT: ${{ needs.ios-compile-check.result }} | |
| DART_TIMEZONE_RESULT: ${{ needs.dart-tests-kiritimati.result }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$CHANGES_RESULT" != "success" ]]; then | |
| echo "Detect Changes did not complete successfully: $CHANGES_RESULT" >&2 | |
| exit 1 | |
| fi | |
| if [[ ( "${HAS_DART:-false}" == true || "${DART_EXPECTED:-false}" == true ) && "$ANALYZE_AND_TEST_RESULT" != success ]]; then | |
| echo "Required Dart Analyze & Tests did not pass: $ANALYZE_AND_TEST_RESULT" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$FULL_CI" != "true" ]]; then | |
| if [[ "$HAS_GENERATED" == true || "$HAS_JOURNEYS" == true || "$HAS_ANDROID" == true || "$HAS_IOS" == true ]]; then | |
| notice='fork PR — a maintainer adds label ci:full, then Re-run all jobs' | |
| echo "::notice title=Heavy CI deferred::$notice" | |
| echo "Heavy CI deferred: $notice" >> "$GITHUB_STEP_SUMMARY" | |
| for spec in \ | |
| "$HAS_GENERATED:$GENERATED_FILES_RESULT" \ | |
| "$HAS_DART:$DART_TIMEZONE_RESULT" \ | |
| "$HAS_JOURNEYS:$JOURNEYS_RESULT" \ | |
| "$HAS_ANDROID:$ANDROID_COMPILE_RESULT" \ | |
| "$HAS_ANDROID:$ANDROID_UNIT_RESULT" \ | |
| "$HAS_ANDROID:$ANDROID_EMULATOR_RESULT" \ | |
| "$HAS_IOS:$IOS_COMPILE_RESULT"; do | |
| required="${spec%%:*}" | |
| result="${spec#*:}" | |
| if [[ "$required" == true && "$result" != skipped ]]; then | |
| echo "A deferred mobile check must be skipped, got: $result" >&2 | |
| exit 1 | |
| fi | |
| done | |
| exit 0 | |
| fi | |
| if [[ "${HAS_DART:-false}" == true || "${DART_EXPECTED:-false}" == true ]]; then | |
| echo 'Mobile Release Eligibility: Dart Analyze & Tests passed; no deferred heavy checks selected' | tee -a "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo 'Mobile Release Eligibility: no mobile heavy checks selected' | tee -a "$GITHUB_STEP_SUMMARY" | |
| fi | |
| exit 0 | |
| fi | |
| if [[ "$EMULATOR_EXPECTED" == true && "$ANDROID_EMULATOR_RESULT" != success ]]; then | |
| echo "Required Android Emulator Acceptance did not pass: $ANDROID_EMULATOR_RESULT" >&2 | |
| exit 1 | |
| fi | |
| for result in \ | |
| "$GENERATED_FILES_RESULT" \ | |
| "$ANALYZE_AND_TEST_RESULT" \ | |
| "$JOURNEYS_RESULT" \ | |
| "$ANDROID_COMPILE_RESULT" \ | |
| "$ANDROID_UNIT_RESULT" \ | |
| "$ANDROID_EMULATOR_RESULT" \ | |
| "$IOS_COMPILE_RESULT" \ | |
| "$DART_TIMEZONE_RESULT"; do | |
| case "$result" in | |
| success|skipped) ;; | |
| *) | |
| echo "A mobile check failed, was cancelled, or is otherwise ineligible: $result" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| echo "Mobile Release Eligibility: success" | |
| post-merge-failure-issues: | |
| name: Post-merge mobile failure issues | |
| needs: [changes, analyze-and-test, journeys-hermetic, android-compile-smoke, android-unit-tests, android-emulator-checks, dart-tests-kiritimati] | |
| if: ${{ always() && inputs.nightly_main_sha == '' && (github.event_name == 'push' || github.event_name == 'schedule') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Reconcile one issue per mobile suite | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| DART: ${{ needs.analyze-and-test.result }} | |
| JOURNEYS: ${{ needs.journeys-hermetic.result }} | |
| ANDROID_COMPILE: ${{ needs.android-compile-smoke.result }} | |
| ANDROID_JVM: ${{ needs.android-unit-tests.result }} | |
| ANDROID_EMULATOR: ${{ needs.android-emulator-checks.result }} | |
| TIMEZONES: ${{ needs.dart-tests-kiritimati.result }} | |
| EVENT: ${{ github.event_name }} | |
| DART_EXPECTED: ${{ needs.changes.outputs.has_app_dart }} | |
| JOURNEYS_EXPECTED: ${{ needs.changes.outputs.has_app_journeys }} | |
| ANDROID_EXPECTED: ${{ needs.changes.outputs.has_app_compile_smoke }} | |
| TIMEZONES_EXPECTED: ${{ needs.changes.outputs.has_app_dart }} | |
| run: | | |
| set -euo pipefail | |
| failed=0 | |
| for spec in "Dart Analyze & Tests:$DART:$DART_EXPECTED" "Seeded Journeys:$JOURNEYS:$JOURNEYS_EXPECTED" "Android Compile Smoke:$ANDROID_COMPILE:$ANDROID_EXPECTED" "Android JVM Unit Tests:$ANDROID_JVM:$ANDROID_EXPECTED" "Android Emulator Acceptance:$ANDROID_EMULATOR:$ANDROID_EXPECTED" "Dart Far Timezones:$TIMEZONES:$TIMEZONES_EXPECTED"; do | |
| suite="${spec%%:*}" | |
| rest="${spec#*:}" | |
| result="${rest%%:*}" | |
| expected="${rest#*:}" | |
| [[ "$EVENT" == push && "$expected" != true && "$result" == skipped ]] && continue | |
| title="CI post-merge failure: $suite" | |
| issue_number="$(gh api --method GET "repos/$REPO/issues?state=open&per_page=100" --paginate --slurp | jq -r --arg title "$title" '[.[][] | select(.title == $title and .pull_request == null)][0].number // empty')" | |
| if [[ "$result" == success ]]; then | |
| if [[ -n "$issue_number" ]]; then | |
| gh issue close "$issue_number" --repo "$REPO" --reason completed | |
| fi | |
| continue | |
| fi | |
| failed=1 | |
| printf 'The %s suite was %s on a main or nightly run.\n\nEvidence: %s\n' "$suite" "$result" "$RUN_URL" > /tmp/ci-suite-issue.md | |
| if [[ -n "$issue_number" ]]; then | |
| gh issue edit "$issue_number" --repo "$REPO" --body-file /tmp/ci-suite-issue.md | |
| else | |
| gh issue create --repo "$REPO" --title "$title" --body-file /tmp/ci-suite-issue.md | |
| fi | |
| done | |
| exit "$failed" |