diff --git a/.github/workflows/fork-ci.yml b/.github/workflows/fork-ci.yml index 3588b71953a..e4d0a849f70 100644 --- a/.github/workflows/fork-ci.yml +++ b/.github/workflows/fork-ci.yml @@ -185,8 +185,8 @@ jobs: fi echo "deploy=${deploy}" >>"${GITHUB_OUTPUT}" - dispatch_mobile_ota: - name: Dispatch Mobile OTA + dispatch_mobile_releases: + name: Dispatch Mobile Releases needs: [check, test, mobile_native_static_analysis, release_smoke, deployment_scope] if: | github.event_name == 'workflow_dispatch' && @@ -209,3 +209,8 @@ jobs: -f platform=all \ -f sha="$GITHUB_SHA" \ -f message="Integration ${GITHUB_SHA}" + gh workflow run mobile-eas-development.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref fork/integration \ + -f platform=ios \ + -f sha="$GITHUB_SHA" diff --git a/.github/workflows/mobile-eas-development.yml b/.github/workflows/mobile-eas-development.yml new file mode 100644 index 00000000000..cc5e9c4a4ad --- /dev/null +++ b/.github/workflows/mobile-eas-development.yml @@ -0,0 +1,119 @@ +name: Mobile EAS Development + +# Keep the installable development client current without rebuilding it for +# JavaScript-only changes. Expo Fingerprint reuses a compatible native build +# and publishes an OTA update; native changes produce a new internal build. +on: + workflow_dispatch: + inputs: + platform: + description: "Target platform" + required: true + type: choice + default: ios + options: + - ios + - android + - all + sha: + description: "Exact fork/integration SHA (blank uses its current tip)" + required: false + type: string + +concurrency: + group: mobile-eas-development + cancel-in-progress: false + +jobs: + development: + name: EAS Development + runs-on: ubuntu-24.04 + permissions: + contents: read + env: + APP_VARIANT: development + NODE_OPTIONS: --max-old-space-size=8192 + MOBILE_VERSION_POLICY: fingerprint + T3CODE_MOBILE_EAS_PROJECT_ID: ${{ vars.T3CODE_MOBILE_EAS_PROJECT_ID }} + T3CODE_MOBILE_EXPO_OWNER: ${{ vars.T3CODE_MOBILE_EXPO_OWNER }} + T3CODE_MOBILE_IOS_BUNDLE_IDENTIFIER: ${{ vars.T3CODE_MOBILE_IOS_BUNDLE_IDENTIFIER }} + T3CODE_MOBILE_IOS_TEAM_ID: ${{ vars.T3CODE_MOBILE_IOS_TEAM_ID }} + 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 development job." + fi + + - name: Checkout + if: steps.expo-token.outputs.present == 'true' + uses: actions/checkout@v6 + with: + ref: fork/integration + fetch-depth: 0 + + - name: Resolve approved integration source + if: steps.expo-token.outputs.present == 'true' + env: + REQUESTED_SHA: ${{ inputs.sha }} + run: | + integration_sha="$(git rev-parse HEAD)" + target_sha="${REQUESTED_SHA:-$integration_sha}" + if [[ ! "$target_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "sha must be an exact 40-character commit SHA" >&2 + exit 1 + fi + git fetch origin "$target_sha" + git merge-base --is-ancestor "$target_sha" "$integration_sha" + git checkout --detach "$target_sha" + test "$(git rev-parse HEAD)" = "$target_sha" + + - 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: true + + - 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 }} + packager: npm + + - name: Pull development environment variables + if: steps.expo-token.outputs.present == 'true' + working-directory: apps/mobile + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + run: eas env:pull development --non-interactive + + - name: Publish compatible update or development build + if: steps.expo-token.outputs.present == 'true' + uses: expo/expo-github-action/continuous-deploy-fingerprint@main + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + with: + profile: development + branch: development + platform: ${{ inputs.platform }} + environment: development + working-directory: apps/mobile + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json index 5b243d128ce..ecd4a09eac6 100644 --- a/apps/mobile/eas.json +++ b/apps/mobile/eas.json @@ -12,6 +12,7 @@ "NODE_OPTIONS": "--max-old-space-size=4096" }, "channel": "development", + "environment": "development", "developmentClient": true, "distribution": "internal" }, diff --git a/docs/fork-stack.md b/docs/fork-stack.md index 0b0841b71d2..87176646795 100644 --- a/docs/fork-stack.md +++ b/docs/fork-stack.md @@ -75,6 +75,13 @@ commit. Unknown paths are runtime-affecting by default. This preserves safe depl contains mixed changes or a new source directory appears, while avoiding fleet rebuilds and mobile OTA updates for tests, snapshots, documentation, agent instructions, and GitHub-only metadata. +Runtime-affecting integrations also publish both mobile release tracks from the exact tested SHA. +The production track receives an OTA update. The development track uses Expo Fingerprint: it +publishes an OTA update when a compatible development client already exists, or creates a new +internal iOS development build when native inputs changed. Manual runs of +`Mobile EAS Development` may target iOS, Android, or both; automatic integration publishing targets +iOS. + The manifest contains the permanent `fork/tim`, `fork/candidates`, and `fork/changes` PRs. The synchronizer rebases that provenance chain onto the latest upstream `main` and rebuilds `fork/integration`. Other open repository PRs are ignored. Temporary state is retained after a