Skip to content
Source
Sign inSign up

mobile-eas-preview-fork.yml

355 lines15.2 KB

1name: Mobile EAS Preview (Fork)
2​
3# EAS previews for fork PRs. This runs the PR's code with an Expo token: the
4# app config and plugins run on this runner, the iOS client builds on EAS and
5# the Android one on Blacksmith (android-build), with preview signing credentials. The label is the maintainer's sign-off
6# that they reviewed that code, so it covers exactly the commit it was applied
7# to: this pins that commit and consumes the label before running anything. A
8# later push deploys nothing until the label is applied again.
9#
10# Same-repo PRs deploy on every push while labeled, via mobile-eas-preview.yml.
11​
12on:
13 pull_request_target:
14 types: [labeled]
15​
16permissions:
17 contents: read
18​
19jobs:
20 preview:
21 name: EAS Preview
22 if: >-
23 github.event.pull_request.head.repo.full_name != github.repository &&
24 github.event.label.name == 'πŸš€ Mobile Continuous Deployment'
25 runs-on: blacksmith-8vcpu-ubuntu-2404
26 concurrency:
27 group: mobile-eas-preview-fork-${{ github.event.pull_request.number }}
28 cancel-in-progress: true
29 permissions:
30 contents: read
31 pull-requests: write
32 outputs:
33 build: ${{ steps.client.outputs.build }}
34 fingerprint: ${{ steps.client.outputs.fingerprint }}
35 env:
36 APP_VARIANT: preview
37 NODE_OPTIONS: --max-old-space-size=8192
38 MOBILE_VERSION_POLICY: fingerprint
39 steps:
40 # If a maintainer removed the label first, that removal wins.
41 - id: consume
42 name: Consume the deployment label
43 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
44 with:
45 script: |
46 try {
47 await github.rest.issues.removeLabel({
48 owner: context.repo.owner,
49 repo: context.repo.repo,
50 issue_number: context.payload.pull_request.number,
51 name: context.payload.label.name,
52 });
53 core.setOutput("consumed", "true");
54 } catch (error) {
55 if (error.status !== 404) throw error;
56 core.info("The label was removed before this run could consume it. Skipping.");
57 core.setOutput("consumed", "false");
58 }
59​
60 - id: expo-token
61 name: Check for EXPO_PREVIEW_TOKEN
62 if: steps.consume.outputs.consumed == 'true'
63 env:
64 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
65 run: |
66 if [ -n "$EXPO_TOKEN" ]; then
67 echo "present=true" >> "$GITHUB_OUTPUT"
68 else
69 echo "present=false" >> "$GITHUB_OUTPUT"
70 echo "EXPO_PREVIEW_TOKEN is not set; skipping EAS preview."
71 fi
72​
73 # The head SHA from the label event, not the branch, so a push after the
74 # label cannot swap in unreviewed code. No credentials stay in .git for
75 # the PR's scripts to read.
76 - name: Checkout reviewed commit
77 if: steps.expo-token.outputs.present == 'true'
78 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
79 with:
80 ref: ${{ github.event.pull_request.head.sha }}
81 fetch-depth: 0
82 # No sparse-checkout: see mobile-eas-preview.yml.
83​
84 # The comment script comes from main, not from the PR.
85 - name: Checkout trusted scripts
86 if: steps.expo-token.outputs.present == 'true'
87 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
88 with:
89 path: .trusted
90 sparse-checkout: .github/scripts
91​
92 # The steps below run the PR's dependency and build scripts; they get no token.
93 # checkout keeps it in a file under RUNNER_TEMP that .git/config includes, so both
94 # go. Asking checkout not to persist it fails here: that cleanup runs `git submodule
95 # foreach`, which stops at the vendored gitlinks under .repos (no .gitmodules entry).
96 - name: Drop checkout credentials
97 if: steps.expo-token.outputs.present == 'true'
98 run: |
99 for dir in . .trusted; do
100 [ -e "$dir/.git" ] || continue
101 git -C "$dir" config --local --get-regexp '^includeif\..*\.path$' | while read -r key file; do
102 rm -f "$file"
103 git -C "$dir" config --local --unset-all "$key"
104 done
105 git -C "$dir" config --local --name-only --get-regexp '^http\..*\.extraheader$' | while read -r key; do
106 git -C "$dir" config --local --unset-all "$key"
107 done
108 if git -C "$dir" config --local --get-regexp 'extraheader|includeif'; then
109 echo "::error::checkout credentials are still configured in $dir"
110 exit 1
111 fi
112 done
113 rm -f "$RUNNER_TEMP"/git-credentials-*.config
114 if ls "$RUNNER_TEMP"/git-credentials-*.config 2>/dev/null; then
115 echo "::error::a checkout credential file is still present"
116 exit 1
117 fi
118​
119 # No caches: pull_request_target saves them under main's scope, where
120 # trusted workflows with other secrets would restore what PR code wrote.
121 - name: Setup Vite+
122 if: steps.expo-token.outputs.present == 'true'
123 uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0
124 with:
125 node-version-file: package.json
126 cache: false
127 run-install: |
128 args:
129 - --filter=@t3tools/mobile...
130​
131 - name: Expose pnpm
132 if: steps.expo-token.outputs.present == 'true'
133 run: |
134 pnpm_version="$(node --print "require('./package.json').packageManager.split('@').pop()")"
135 vp_pnpm_bin="$HOME/.vite-plus/package_manager/pnpm/$pnpm_version/pnpm/bin"
136 echo "$vp_pnpm_bin" >> "$GITHUB_PATH"
137 "$vp_pnpm_bin/pnpm" --version
138​
139 - name: Setup EAS
140 if: steps.expo-token.outputs.present == 'true'
141 uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1
142 with:
143 # Pinned and installed with npm for the same reasons as mobile-eas-preview.yml.
144 eas-version: 21.8.0
145 eas-cache: false
146 token: ${{ secrets.EXPO_PREVIEW_TOKEN }}
147 packager: npm
148​
149 - name: Pull preview environment variables
150 if: steps.expo-token.outputs.present == 'true'
151 working-directory: apps/mobile
152 env:
153 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
154 run: |
155 eas env:pull preview --non-interactive
156 # See mobile-eas-preview.yml: keeps Android build and OTA fingerprints matching.
157 node --input-type=module <<'NODE'
158 import { appendFileSync, readFileSync } from "node:fs";
159 import { parseEnv } from "node:util";
160 const env = parseEnv(readFileSync(".env.local", "utf8"));
161 const googleServicesFile = env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE;
162 if (googleServicesFile) {
163 appendFileSync(process.env.GITHUB_ENV, `T3CODE_ANDROID_GOOGLE_SERVICES_FILE=${googleServicesFile}\n`);
164 }
165 NODE
166​
167 - name: Deploy with fingerprint check
168 if: steps.expo-token.outputs.present == 'true'
169 uses: expo/expo-github-action/continuous-deploy-fingerprint@5273c2136bd09c2a341fe3340d3e767ce5a13015 # v9.0.2
170 env:
171 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
172 RUNNER_DEBUG: "1"
173 # The action only comments for pull_request events; this hook gives
174 # it the PR under pull_request_target. Without it, it skips the comment.
175 EXPO_TEST_GITHUB_PULL: ${{ github.event.pull_request.number }}
176 with:
177 profile: preview:dev
178 branch: pr-${{ github.event.pull_request.number }}
179 # Android is below: its client builds on Blacksmith (android-build).
180 platform: ios
181 environment: preview
182 working-directory: apps/mobile
183 github-token: ${{ secrets.GITHUB_TOKEN }}
184​
185 # Android, as in mobile-eas-preview.yml: the same fingerprint command the Expo
186 # action runs. A client built for a same-repo PR (its tag) or by EAS is reused.
187 - id: client
188 name: Find the Android client for this fingerprint
189 if: steps.expo-token.outputs.present == 'true'
190 working-directory: apps/mobile
191 shell: bash
192 env:
193 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
194 run: |
195 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)')"
196 echo "fingerprint=$hash" >> "$GITHUB_OUTPUT"
197 tag="mobile-android-preview-build/$hash"
198 link=""
199 if git fetch --quiet origin "refs/tags/$tag:refs/tags/$tag" 2>/dev/null; then
200 link="$(git tag -l --format='%(contents:subject)' "$tag")"
201 else
202 link="$(eas build:list --platform android --build-profile preview:dev --runtime-version "$hash" --status finished --limit 5 --json --non-interactive \
203 | sed -n '/^\[/,$p' \
204 | 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)"')"
205 fi
206 echo "link=$link" >> "$GITHUB_OUTPUT"
207 if [ -z "$link" ]; then echo "build=true" >> "$GITHUB_OUTPUT"; fi
208​
209 - id: update
210 name: Publish the Android update
211 if: steps.expo-token.outputs.present == 'true'
212 working-directory: apps/mobile
213 shell: bash
214 env:
215 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
216 PR: ${{ github.event.pull_request.number }}
217 run: |
218 group="$(eas update --auto --branch "pr-$PR" --platform android --environment preview --non-interactive --json | sed -n '/^\[/,$p' | jq -er '.[0].group')"
219 echo "group=$group" >> "$GITHUB_OUTPUT"
220​
221 - name: Comment
222 if: steps.expo-token.outputs.present == 'true'
223 env:
224 GH_TOKEN: ${{ github.token }}
225 PR: ${{ github.event.pull_request.number }}
226 FINGERPRINT: ${{ steps.client.outputs.fingerprint }}
227 CLIENT: ${{ steps.client.outputs.link }}
228 GROUP: ${{ steps.update.outputs.group }}
229 run: node .trusted/.github/scripts/mobile-preview-comment.cjs
230​
231 # The reviewed commit's Android client, built cold: no sticky disk, because PR
232 # code could leave Gradle or ccache state that trusted builds would restore, and
233 # no build record tag, so nobody else is served an APK a fork's code built.
234 android-build:
235 name: Android preview client
236 needs: preview
237 if: needs.preview.outputs.build == 'true'
238 runs-on: blacksmith-32vcpu-ubuntu-2404
239 timeout-minutes: 60
240 permissions:
241 contents: read
242 pull-requests: write
243 env:
244 APP_VARIANT: preview
245 NODE_OPTIONS: --max-old-space-size=8192
246 MOBILE_VERSION_POLICY: fingerprint
247 FINGERPRINT: ${{ needs.preview.outputs.fingerprint }}
248 # Phones only: emulators use the development client from the native-client cache.
249 ORG_GRADLE_PROJECT_reactNativeArchitectures: armeabi-v7a,arm64-v8a
250 steps:
251 - name: Checkout reviewed commit
252 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
253 with:
254 ref: ${{ github.event.pull_request.head.sha }}
255 fetch-depth: 0
256​
257 # The comment script comes from main, not from the PR.
258 - name: Checkout trusted scripts
259 uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
260 with:
261 path: .trusted
262 sparse-checkout: .github/scripts
263​
264 # The steps below run the PR's dependency and build scripts; they get no token.
265 # checkout keeps it in a file under RUNNER_TEMP that .git/config includes, so both
266 # go. Asking checkout not to persist it fails here: that cleanup runs `git submodule
267 # foreach`, which stops at the vendored gitlinks under .repos (no .gitmodules entry).
268 - name: Drop checkout credentials
269 run: |
270 for dir in . .trusted; do
271 [ -e "$dir/.git" ] || continue
272 git -C "$dir" config --local --get-regexp '^includeif\..*\.path$' | while read -r key file; do
273 rm -f "$file"
274 git -C "$dir" config --local --unset-all "$key"
275 done
276 git -C "$dir" config --local --name-only --get-regexp '^http\..*\.extraheader$' | while read -r key; do
277 git -C "$dir" config --local --unset-all "$key"
278 done
279 if git -C "$dir" config --local --get-regexp 'extraheader|includeif'; then
280 echo "::error::checkout credentials are still configured in $dir"
281 exit 1
282 fi
283 done
284 rm -f "$RUNNER_TEMP"/git-credentials-*.config
285 if ls "$RUNNER_TEMP"/git-credentials-*.config 2>/dev/null; then
286 echo "::error::a checkout credential file is still present"
287 exit 1
288 fi
289​
290 - name: Setup Node
291 uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
292 with:
293 node-version-file: package.json
294​
295 - name: Enable corepack
296 run: |
297 npm install --global corepack@latest
298 corepack enable
299 pnpm --version
300​
301 - name: Install mobile dependencies
302 run: pnpm install --frozen-lockfile --filter=@t3tools/mobile...
303​
304 - name: Setup Java
305 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
306 with:
307 distribution: temurin
308 java-version: 17
309​
310 - name: Setup EAS
311 uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # v8.2.1
312 with:
313 eas-version: latest
314 eas-cache: false
315 token: ${{ secrets.EXPO_PREVIEW_TOKEN }}
316 packager: npm
317​
318 - name: Pull preview environment variables
319 working-directory: apps/mobile
320 env:
321 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
322 run: |
323 eas env:pull preview --non-interactive
324 node --input-type=module <<'NODE'
325 import { appendFileSync, readFileSync } from "node:fs";
326 import { parseEnv } from "node:util";
327 const env = parseEnv(readFileSync(".env.local", "utf8"));
328 const googleServicesFile = env.T3CODE_ANDROID_GOOGLE_SERVICES_FILE;
329 if (googleServicesFile) {
330 appendFileSync(process.env.GITHUB_ENV, `T3CODE_ANDROID_GOOGLE_SERVICES_FILE=${googleServicesFile}\n`);
331 }
332 NODE
333​
334 - id: build
335 name: Build and upload
336 working-directory: apps/mobile
337 shell: bash
338 env:
339 EXPO_TOKEN: ${{ secrets.EXPO_PREVIEW_TOKEN }}
340 run: |
341 eas build --local --platform android --profile preview:dev --non-interactive --output "$RUNNER_TEMP/t3code-preview.apk"
342 eas upload --platform android --build-path "$RUNNER_TEMP/t3code-preview.apk" --fingerprint "$FINGERPRINT" --non-interactive --json | tee "$RUNNER_TEMP/upload.json"
343 link="$(sed -n '/^{/,$p' "$RUNNER_TEMP/upload.json" | jq -r '.url // empty')"
344 if [ -z "$link" ]; then
345 echo "::error::eas upload returned no install link"
346 exit 1
347 fi
348 echo "link=$link" >> "$GITHUB_OUTPUT"
349​
350 - name: Comment
351 env:
352 GH_TOKEN: ${{ github.token }}
353 PR: ${{ github.event.pull_request.number }}
354 CLIENT: ${{ steps.build.outputs.link }}
355 run: node .trusted/.github/scripts/mobile-preview-comment.cjs