feat: multi-provider pull requests page with in-app reviews (#4849) #72
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 EAS Production | |
| # Production builds and OTA updates run from CI (Linux) — never from a laptop. | |
| # Under the fingerprint runtime-version policy the fingerprint must be computed | |
| # in the same OS/pnpm as the EAS build; a macOS `eas build` computes a different | |
| # fingerprint (platform-specific deps + pnpm version) and errors. On this Linux | |
| # runner, with corepack pinning pnpm 10.24 in eas.json, local == build. | |
| # | |
| # Every merge to main that touches the mobile app reconciles, per platform: | |
| # 1. Store builds: if the latest production build's version differs from | |
| # app.config.ts, cut a new build with --auto-submit (TestFlight + | |
| # Play internal track). Bumping `version` is therefore all it takes to | |
| # start the next release train — the first build of a version enters | |
| # external-TestFlight beta review immediately, and later builds of the | |
| # same version auto-approve. Releasing to the App Store stays a manual | |
| # App Store Connect step. | |
| # 2. OTA: publish a production-channel update for each platform where at | |
| # least one finished production build matches the current native | |
| # fingerprint. Old-version binaries with a matching fingerprint receive | |
| # it too. When native drift means no binary could install the update, | |
| # it is skipped and flagged in the job summary instead of published | |
| # into the void. | |
| # workflow_dispatch remains as a manual override for both modes (e.g. to | |
| # retry an errored build or force an OTA). | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "build (+ auto-submit to TestFlight) or update (OTA)" | |
| required: true | |
| type: choice | |
| default: build | |
| options: | |
| - build | |
| - update | |
| platform: | |
| description: "Target platform" | |
| required: true | |
| type: choice | |
| default: ios | |
| options: | |
| - ios | |
| - android | |
| - all | |
| message: | |
| description: "OTA update message (mode=update only)" | |
| required: false | |
| type: string | |
| push: | |
| branches: [main] | |
| paths: | |
| - apps/mobile/** | |
| - packages/client-runtime/** | |
| - packages/contracts/** | |
| - packages/shared/** | |
| - assets/** | |
| - scripts/** | |
| - patches/** | |
| - pnpm-lock.yaml | |
| - pnpm-workspace.yaml | |
| - .github/workflows/mobile-eas-production.yml | |
| # Serialize runs so OTAs publish in merge order. GitHub keeps at most one | |
| # queued run per group, so a burst of merges collapses into one run of the | |
| # newest commit — intermediate commits don't need their own OTA. | |
| concurrency: | |
| group: mobile-eas-production | |
| cancel-in-progress: false | |
| jobs: | |
| production: | |
| name: EAS Production ${{ github.event_name == 'push' && 'auto' || inputs.mode }} | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| permissions: | |
| contents: read | |
| env: | |
| APP_VARIANT: production | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| steps: | |
| - id: expo-token | |
| name: Check for EXPO_TOKEN | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| run: | | |
| if [ -n "$EXPO_TOKEN" ]; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| echo "EXPO_TOKEN is not available; skipping EAS production job." | |
| fi | |
| - name: Checkout | |
| if: steps.expo-token.outputs.present == 'true' | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| # No sparse-checkout here: it makes actions/checkout fetch with | |
| # --filter=blob:none, and eas-cli archives the project via | |
| # `git clone --depth 1 file://<workspace>`, which fails (exit 128) | |
| # when the partial clone can't serve the unfetched blobs. | |
| - name: Setup Vite+ | |
| if: steps.expo-token.outputs.present == 'true' | |
| uses: voidzero-dev/setup-vp@v1 | |
| with: | |
| node-version-file: package.json | |
| cache: true | |
| run-install: | | |
| args: | |
| - --filter=@t3tools/mobile... | |
| - name: Expose pnpm | |
| if: steps.expo-token.outputs.present == 'true' | |
| run: | | |
| pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")" | |
| vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin" | |
| echo "$vp_pnpm_bin" >> "$GITHUB_PATH" | |
| "$vp_pnpm_bin/pnpm" --version | |
| - name: Setup EAS | |
| if: steps.expo-token.outputs.present == 'true' | |
| uses: expo/expo-github-action@v8 | |
| with: | |
| eas-version: latest | |
| token: ${{ secrets.EXPO_TOKEN }} | |
| # npm, not pnpm: this only installs eas-cli into the action's own | |
| # tool dir, and pnpm 11 hard-fails that install on dtrace-provider's | |
| # ignored build script (no allowBuilds config outside the repo). | |
| packager: npm | |
| - name: Pull production environment variables | |
| if: steps.expo-token.outputs.present == 'true' | |
| working-directory: apps/mobile | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| run: eas env:pull production --non-interactive | |
| - name: Build and submit (manual) | |
| if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'build' | |
| working-directory: apps/mobile | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| run: eas build --platform ${{ inputs.platform }} --profile production --auto-submit --non-interactive --no-wait | |
| - name: Publish OTA update (manual) | |
| if: steps.expo-token.outputs.present == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'update' | |
| working-directory: apps/mobile | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| run: | | |
| eas update \ | |
| --channel production \ | |
| --environment production \ | |
| --platform ${{ inputs.platform }} \ | |
| --message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \ | |
| --non-interactive | |
| # No --status filter on build:list: an in-queue/in-progress build must | |
| # count as existing, or every merge during the build window would cut a | |
| # duplicate. Builds started here stay attached to this serialized run so | |
| # the queued run for a later merge cannot overtake them and lose its OTA. | |
| # After an errored build, retry via workflow_dispatch mode=build — pushes | |
| # won't re-trigger it until the app version changes. | |
| - id: store_builds | |
| name: Ensure store builds exist for the current app version | |
| if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push' | |
| continue-on-error: true | |
| working-directory: apps/mobile | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| run: | | |
| failed=0 | |
| version="$(npx expo config --json --type public | jq -r '.version')" | |
| for platform in ios android; do | |
| latest="$(eas build:list --platform "$platform" --build-profile production --limit 1 --json --non-interactive | jq -r '.[0].appVersion // "none"')" | |
| if [ "$latest" = "$version" ]; then | |
| echo "$platform: production build for $version already exists (or is in progress)" | |
| continue | |
| fi | |
| echo "$platform: latest production build is $latest, app.config.ts says $version — building" | |
| if eas build --platform "$platform" --profile production --auto-submit --non-interactive; then | |
| echo ":building_construction: $platform: cut production build for $version (auto-submitted)" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| failed=1 | |
| echo ":x: $platform: production build or submission failed for $version" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| done | |
| exit "$failed" | |
| - name: Publish fingerprint-gated OTA | |
| if: steps.expo-token.outputs.present == 'true' && github.event_name == 'push' | |
| working-directory: apps/mobile | |
| env: | |
| EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} | |
| run: | | |
| message="$(git log -1 --pretty=%s | head -c 120) ($(git rev-parse --short=9 HEAD))" | |
| for platform in ios android; do | |
| # eas-cli prints an environment-loaded notice to stdout before the | |
| # JSON even with --json, so discard everything before the document. | |
| hash="$(eas fingerprint:generate --platform "$platform" --environment production --json --non-interactive | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')" | |
| matching="$(eas build:list --platform "$platform" --build-profile production --status finished --fingerprint-hash "$hash" --limit 1 --json --non-interactive | jq 'length')" | |
| if [ "$matching" -gt 0 ]; then | |
| eas update \ | |
| --channel production \ | |
| --environment production \ | |
| --platform "$platform" \ | |
| --message "$message" \ | |
| --non-interactive | |
| echo ":white_check_mark: $platform: OTA published to production (fingerprint \`$hash\`)" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo ":warning: $platform: no finished production build matches fingerprint \`$hash\` — OTA skipped; JS changes reach $platform only once a matching build ships" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| done | |
| - name: Propagate store build failure | |
| if: steps.store_builds.outcome == 'failure' | |
| run: exit 1 |