Skip to content
Source
Sign inSign up

mobile-eas-preview.yml

455 lines20.1 KB

1name: Mobile EAS Preview
2​
3on:
4 pull_request:
5 types: [opened, reopened, synchronize, labeled]
6​
7jobs:
8 preview:
9 name: EAS Preview (iOS)
10 # Fork PRs deploy one reviewed commit per label through mobile-eas-preview-fork.yml.
11 if: |
12 github.event.pull_request.head.repo.full_name == github.repository &&
13 contains(github.event.pull_request.labels.*.name, 'πŸš€ Mobile Continuous Deployment') &&
14 (github.event.action != 'labeled' || github.event.label.name == 'πŸš€ Mobile Continuous Deployment')
15 runs-on: blacksmith-8vcpu-ubuntu-2404
16 concurrency:
17 group: mobile-eas-preview-${{ github.event.pull_request.number }}
18 cancel-in-progress: true
19 permissions:
20 contents: read
21 pull-requests: write
22 env:
23 APP_VARIANT: preview
24 NODE_OPTIONS: --max-old-space-size=8192
25 MOBILE_VERSION_POLICY: fingerprint
26 steps:
27 - id: expo-token
28 name: Check for EXPO_PREVIEW_TOKEN
29 env:
30 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
31 run: |
32 if [ -n "$EXPO_TOKEN" ]; then
33 echo "present=true" >> "$GITHUB_OUTPUT"
34 else
35 echo "present=false" >> "$GITHUB_OUTPUT"
36 echo "EXPO_PREVIEW_TOKEN is not available; skipping EAS preview."
37 fi
38​
39 - name: Checkout
40 if: steps.expo-token.outputs.present == 'true'
41 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
42 with:
43 fetch-depth: 0
44 # No sparse-checkout here: it makes actions/checkout fetch with
45 # --filter=blob:none, and eas-cli archives the project via
46 # `git clone --depth 1 file://<workspace>`, which fails (exit 128)
47 # when the partial clone can't serve the unfetched blobs.
48​
49 - name: Setup Vite+
50 if: steps.expo-token.outputs.present == 'true'
51 uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0
52 with:
53 node-version-file: package.json
54 cache: true
55 run-install: |
56 args:
57 - --filter=@t3tools/mobile...
58​
59 - name: Expose pnpm
60 if: steps.expo-token.outputs.present == 'true'
61 run: |
62 pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
63 vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
64 echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
65 "$vp_pnpm_bin/pnpm" --version
66​
67 - name: Setup EAS
68 if: steps.expo-token.outputs.present == 'true'
69 uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1
70 with:
71 # eas-cli 22 renamed `project` to `app` in build JSON, and
72 # continuous-deploy-fingerprint still reads `build.project.ownerAccount`.
73 # Unpin once https://github.com/expo/expo-github-action/issues/358 is fixed.
74 eas-version: 21.8.0
75 token: ${{ secrets.EXPO_PREVIEW_TOKEN }}
76 # npm, not pnpm: this only installs eas-cli into the action's own
77 # tool dir, and pnpm 11 hard-fails that install on dtrace-provider's
78 # ignored build script (no allowBuilds config outside the repo).
79 packager: npm
80​
81 - name: Pull preview environment variables
82 if: steps.expo-token.outputs.present == 'true'
83 working-directory: apps/mobile
84 env:
85 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
86 run: |
87 eas env:pull preview --non-interactive
88 # EAS Update disables dotenv loading. Keep the downloaded native config
89 # in its process environment so Android build and OTA fingerprints match.
90 node --input-type=module <<'NODE'
91 import { appendFileSync, readFileSync } from "node:fs";
92 import { parseEnv } from "node:util";
93 const env = parseEnv(readFileSync(".env.local", "utf8"));
94 const googleServicesFile = env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE;
95 if (googleServicesFile) {
96 appendFileSync(process.env.GITHUB_ENV, `T3CODE_ANDROID_GOOGLE_SERVICES_FILE=${googleServicesFile}\n`);
97 }
98 NODE
99​
100 - name: Deploy with fingerprint check
101 if: steps.expo-token.outputs.present == 'true'
102 uses: expo/expo-github-action/continuous-deploy-fingerprint@5273c2136bd09c2a341fe3340d3e767ce5a13015 # v9.0.2
103 env:
104 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
105 # The Expo action otherwise suppresses the EAS command's failure output.
106 RUNNER_DEBUG: "1"
107 with:
108 profile: preview:dev
109 branch: pr-${{ github.event.pull_request.number }}
110 # Android is the android job below: its client builds on Blacksmith.
111 platform: ios
112 environment: preview
113 working-directory: apps/mobile
114 github-token: ${{ secrets.GITHUB_TOKEN }}
115​
116 # Android publishes its own update and comment, because the Expo action can only
117 # find a client that EAS built. A fingerprint with no client is built by
118 # android-build with `eas build --local` on Blacksmith (about 4 minutes warm,
119 # against about 40 on EAS) and recorded as a mobile-android-preview-build/<fingerprint>
120 # tag whose message is the install link. Clients EAS built earlier still count.
121 android:
122 name: EAS Preview (Android)
123 if: |
124 github.event.pull_request.head.repo.full_name == github.repository &&
125 contains(github.event.pull_request.labels.*.name, 'πŸš€ Mobile Continuous Deployment') &&
126 (github.event.action != 'labeled' || github.event.label.name == 'πŸš€ Mobile Continuous Deployment')
127 runs-on: blacksmith-8vcpu-ubuntu-2404
128 concurrency:
129 group: mobile-eas-preview-android-${{ github.event.pull_request.number }}
130 cancel-in-progress: true
131 permissions:
132 contents: read
133 pull-requests: write
134 outputs:
135 build: ${{ steps.client.outputs.build }}
136 fingerprint: ${{ steps.client.outputs.fingerprint }}
137 env:
138 APP_VARIANT: preview
139 NODE_OPTIONS: --max-old-space-size=8192
140 MOBILE_VERSION_POLICY: fingerprint
141 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
142 GH_TOKEN: ${{ github.token }}
143 PR: ${{ github.event.pull_request.number }}
144 steps:
145 - id: expo-token
146 name: Check for EXPO_PREVIEW_TOKEN
147 run: |
148 if [ -n "$EXPO_TOKEN" ]; then
149 echo "present=true" >> "$GITHUB_OUTPUT"
150 else
151 echo "present=false" >> "$GITHUB_OUTPUT"
152 echo "EXPO_PREVIEW_TOKEN is not available; skipping the Android preview."
153 fi
154​
155 - name: Checkout
156 if: steps.expo-token.outputs.present == 'true'
157 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
158 with:
159 fetch-depth: 0
160​
161 # The steps below run the PR's dependency and build scripts; they get no token.
162 # checkout keeps it in a file under RUNNER_TEMP that .git/config includes, so both
163 # go. Asking checkout not to persist it fails here: that cleanup runs `git submodule
164 # foreach`, which stops at the vendored gitlinks under .repos (no .gitmodules entry).
165 - name: Drop checkout credentials
166 if: steps.expo-token.outputs.present == 'true'
167 run: |
168 for dir in . .trusted; do
169 [ -e "$dir/.git" ] || continue
170 git -C "$dir" config --local --get-regexp '^includeif\..*\.path$' | while read -r key file; do
171 rm -f "$file"
172 git -C "$dir" config --local --unset-all "$key"
173 done
174 git -C "$dir" config --local --name-only --get-regexp '^http\..*\.extraheader$' | while read -r key; do
175 git -C "$dir" config --local --unset-all "$key"
176 done
177 if git -C "$dir" config --local --get-regexp 'extraheader|includeif'; then
178 echo "::error::checkout credentials are still configured in $dir"
179 exit 1
180 fi
181 done
182 rm -f "$RUNNER_TEMP"/git-credentials-*.config
183 if ls "$RUNNER_TEMP"/git-credentials-*.config 2>/dev/null; then
184 echo "::error::a checkout credential file is still present"
185 exit 1
186 fi
187​
188 - name: Setup Vite+
189 if: steps.expo-token.outputs.present == 'true'
190 uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0
191 with:
192 node-version-file: package.json
193 cache: true
194 run-install: |
195 args:
196 - --filter=@t3tools/mobile...
197​
198 - name: Expose pnpm
199 if: steps.expo-token.outputs.present == 'true'
200 run: |
201 pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
202 vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
203 echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
204 "$vp_pnpm_bin/pnpm" --version
205​
206 - name: Setup EAS
207 if: steps.expo-token.outputs.present == 'true'
208 uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1
209 with:
210 eas-version: latest
211 token: ${{ secrets.EXPO_PREVIEW_TOKEN }}
212 packager: npm
213​
214 - name: Pull preview environment variables
215 if: steps.expo-token.outputs.present == 'true'
216 working-directory: apps/mobile
217 run: |
218 eas env:pull preview --non-interactive
219 node --input-type=module <<'NODE'
220 import { appendFileSync, readFileSync } from "node:fs";
221 import { parseEnv } from "node:util";
222 const env = parseEnv(readFileSync(".env.local", "utf8"));
223 const googleServicesFile = env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE;
224 if (googleServicesFile) {
225 appendFileSync(process.env.GITHUB_ENV, `T3CODE_ANDROID_GOOGLE_SERVICES_FILE=${googleServicesFile}\n`);
226 }
227 NODE
228​
229 # The same command the Expo action runs, so this is the runtime version the update gets.
230 - id: client
231 name: Find the client for this fingerprint
232 if: steps.expo-token.outputs.present == 'true'
233 working-directory: apps/mobile
234 shell: bash
235 run: |
236 hash="$(eas env:exec --non-interactive preview "npx expo-updates fingerprint:generate --platform android" | sed -n '/^{/,$p' | jq -er '.hash | select(type == "string" and length > 0)')"
237 echo "fingerprint=$hash" >> "$GITHUB_OUTPUT"
238 tag="mobile-android-preview-build/$hash"
239 link=""
240 if git fetch --quiet origin "refs/tags/$tag:refs/tags/$tag" 2>/dev/null; then
241 link="$(git tag -l --format='%(contents:subject)' "$tag")"
242 else
243 # A client EAS built before this job existed; expired ones are gone.
244 link="$(eas build:list --platform android --build-profile preview:dev --runtime-version "$hash" --status finished --limit 5 --json --non-interactive \
245 | sed -n '/^\[/,$p' \
246 | jq -r --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '[.[] | select((.expirationDate // "9999") > $now)][0] | select(. != null) | "https://expo.dev/accounts/pingdotgg/projects/t3-code/builds/\(.id)"')"
247 fi
248 echo "link=$link" >> "$GITHUB_OUTPUT"
249 if [ -z "$link" ]; then echo "build=true" >> "$GITHUB_OUTPUT"; fi
250 echo "android fingerprint \`$hash\`: ${link:-no client yet, building on Blacksmith}" >> "$GITHUB_STEP_SUMMARY"
251​
252 - id: update
253 name: Publish the update
254 if: steps.expo-token.outputs.present == 'true'
255 working-directory: apps/mobile
256 shell: bash
257 run: |
258 group="$(eas update --auto --branch "pr-$PR" --platform android --environment preview --non-interactive --json | sed -n '/^\[/,$p' | jq -er '.[0].group')"
259 echo "group=$group" >> "$GITHUB_OUTPUT"
260​
261 - name: Comment
262 if: steps.expo-token.outputs.present == 'true'
263 env:
264 FINGERPRINT: ${{ steps.client.outputs.fingerprint }}
265 CLIENT: ${{ steps.client.outputs.link }}
266 GROUP: ${{ steps.update.outputs.group }}
267 run: node .github/scripts/mobile-preview-comment.cjs
268​
269 android-build:
270 name: Android preview client
271 needs: android
272 if: needs.android.outputs.build == 'true'
273 runs-on: blacksmith-32vcpu-ubuntu-2404
274 timeout-minutes: 60
275 concurrency:
276 # One build per fingerprint; a second PR with the same one finds the tag.
277 group: mobile-android-preview-build-${{ needs.android.outputs.fingerprint }}
278 cancel-in-progress: false
279 permissions:
280 contents: write # pushes the build record tag
281 pull-requests: write
282 env:
283 APP_VARIANT: preview
284 NODE_OPTIONS: --max-old-space-size=8192
285 MOBILE_VERSION_POLICY: fingerprint
286 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
287 FINGERPRINT: ${{ needs.android.outputs.fingerprint }}
288 BUILD_TAG: mobile-android-preview-build/${{ needs.android.outputs.fingerprint }}
289 # Phones only: emulators use the development client from the native-client cache.
290 ORG_GRADLE_PROJECT_reactNativeArchitectures: armeabi-v7a,arm64-v8a
291 steps:
292 - name: Checkout
293 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
294 with:
295 fetch-depth: 0
296​
297 - id: record
298 name: Check build record
299 run: |
300 if git fetch --quiet origin "refs/tags/$BUILD_TAG:refs/tags/$BUILD_TAG" 2>/dev/null; then
301 echo "built=true" >> "$GITHUB_OUTPUT"
302 echo "link=$(git tag -l --format='%(contents:subject)' "$BUILD_TAG")" >> "$GITHUB_OUTPUT"
303 fi
304​
305 # The steps below run the PR's dependency and build scripts; they get no token.
306 # checkout keeps it in a file under RUNNER_TEMP that .git/config includes, so both
307 # go. Asking checkout not to persist it fails here: that cleanup runs `git submodule
308 # foreach`, which stops at the vendored gitlinks under .repos (no .gitmodules entry).
309 - name: Drop checkout credentials
310 run: |
311 for dir in . .trusted; do
312 [ -e "$dir/.git" ] || continue
313 git -C "$dir" config --local --get-regexp '^includeif\..*\.path$' | while read -r key file; do
314 rm -f "$file"
315 git -C "$dir" config --local --unset-all "$key"
316 done
317 git -C "$dir" config --local --name-only --get-regexp '^http\..*\.extraheader$' | while read -r key; do
318 git -C "$dir" config --local --unset-all "$key"
319 done
320 if git -C "$dir" config --local --get-regexp 'extraheader|includeif'; then
321 echo "::error::checkout credentials are still configured in $dir"
322 exit 1
323 fi
324 done
325 rm -f "$RUNNER_TEMP"/git-credentials-*.config
326 if ls "$RUNNER_TEMP"/git-credentials-*.config 2>/dev/null; then
327 echo "::error::a checkout credential file is still present"
328 exit 1
329 fi
330​
331 - name: Prepare build directories
332 if: steps.record.outputs.built != 'true'
333 run: |
334 cache="$HOME/.cache/t3-android-preview"
335 {
336 echo "GRADLE_USER_HOME=$cache/gradle"
337 echo "CCACHE_DIR=$cache/ccache"
338 echo "CCACHE_MAXSIZE=5G"
339 # Gradle installs the NDK fresh each run, so key on the compiler's version, not its mtime.
340 echo "CCACHE_COMPILERCHECK=%compiler% -v"
341 echo "pnpm_config_store_dir=$cache/pnpm-store"
342 # eas build --local copies the project here. A fixed path keeps
343 # ccache keys stable, and the same disk as the store lets pnpm hardlink.
344 echo "EAS_LOCAL_BUILD_WORKINGDIR=$cache/eas-build"
345 echo "CCACHE_BASEDIR=$cache/eas-build"
346 echo "CMAKE_C_COMPILER_LAUNCHER=ccache"
347 echo "CMAKE_CXX_COMPILER_LAUNCHER=ccache"
348 } >> "$GITHUB_ENV"
349​
350 - name: Mount build cache
351 if: steps.record.outputs.built != 'true'
352 uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
353 with:
354 key: ${{ github.repository }}-android-preview-build
355 path: ~/.cache/t3-android-preview
356​
357 - name: Configure Gradle and ccache
358 if: steps.record.outputs.built != 'true'
359 run: |
360 mkdir -p "$GRADLE_USER_HOME" "$CCACHE_DIR"
361 rm -rf "$EAS_LOCAL_BUILD_WORKINGDIR"
362 # No daemon: nothing should hold the sticky disk open when it is committed.
363 printf 'org.gradle.caching=true\norg.gradle.daemon=false\n' > "$GRADLE_USER_HOME/gradle.properties"
364 sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
365 sudo apt-get update -qq && sudo apt-get install -y -qq ccache
366 ccache --zero-stats
367​
368 - name: Setup Node
369 if: steps.record.outputs.built != 'true'
370 uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
371 with:
372 node-version-file: package.json
373​
374 - name: Enable corepack
375 if: steps.record.outputs.built != 'true'
376 run: |
377 npm install --global corepack@latest
378 corepack enable
379 pnpm --version
380​
381 - name: Install mobile dependencies
382 if: steps.record.outputs.built != 'true'
383 run: pnpm install --frozen-lockfile --prefer-offline --filter=@t3tools/mobile...
384​
385 - name: Setup Java
386 if: steps.record.outputs.built != 'true'
387 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
388 with:
389 distribution: temurin
390 java-version: 17
391​
392 - name: Setup EAS
393 if: steps.record.outputs.built != 'true'
394 uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1
395 with:
396 eas-version: latest
397 token: ${{ secrets.EXPO_PREVIEW_TOKEN }}
398 packager: npm
399​
400 - name: Pull preview environment variables
401 if: steps.record.outputs.built != 'true'
402 working-directory: apps/mobile
403 run: |
404 eas env:pull preview --non-interactive
405 node --input-type=module <<'NODE'
406 import { appendFileSync, readFileSync } from "node:fs";
407 import { parseEnv } from "node:util";
408 const env = parseEnv(readFileSync(".env.local", "utf8"));
409 const googleServicesFile = env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE;
410 if (googleServicesFile) {
411 appendFileSync(process.env.GITHUB_ENV, `T3CODE_ANDROID_GOOGLE_SERVICES_FILE=${googleServicesFile}\n`);
412 }
413 NODE
414​
415 # `eas upload` hosts the APK on EAS and returns the page testers install from.
416 - id: build
417 name: Build and upload
418 if: steps.record.outputs.built != 'true'
419 working-directory: apps/mobile
420 shell: bash
421 run: |
422 eas build --local --platform android --profile preview:dev --non-interactive --output "$RUNNER_TEMP/t3code-preview.apk"
423 eas upload --platform android --build-path "$RUNNER_TEMP/t3code-preview.apk" --fingerprint "$FINGERPRINT" --non-interactive --json | tee "$RUNNER_TEMP/upload.json"
424 link="$(sed -n '/^{/,$p' "$RUNNER_TEMP/upload.json" | jq -r '.url // empty')"
425 if [ -z "$link" ]; then
426 echo "::error::eas upload returned no install link"
427 exit 1
428 fi
429 echo "link=$link" >> "$GITHUB_OUTPUT"
430 {
431 echo "### Android preview client for \`$FINGERPRINT\`: $link"
432 echo '```'
433 ccache --show-stats
434 echo '```'
435 } >> "$GITHUB_STEP_SUMMARY"
436​
437 # The tag is the record later runs look up; its message is the install link.
438 - name: Record build
439 if: steps.record.outputs.built != 'true'
440 env:
441 LINK: ${{ steps.build.outputs.link }}
442 GH_TOKEN: ${{ github.token }}
443 run: |
444 git config user.name "github-actions[bot]"
445 git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
446 git tag -a "$BUILD_TAG" -m "$LINK"
447 # Only this step has the token; the checkout's copy was removed before the build.
448 git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "refs/tags/$BUILD_TAG"
449​
450 - name: Comment
451 env:
452 GH_TOKEN: ${{ github.token }}
453 PR: ${{ github.event.pull_request.number }}
454 CLIENT: ${{ steps.build.outputs.link || steps.record.outputs.link }}
455 run: node .github/scripts/mobile-preview-comment.cjs