Skip to content

Commit a15bbea

Browse files
authored
ci(mobile): cache deps + parallelize OTA + decouple lint to speed up OTA ~3x (#14326)
1 parent e4a04e2 commit a15bbea

1 file changed

Lines changed: 170 additions & 41 deletions

File tree

.github/workflows/mobile.yml

Lines changed: 170 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,74 @@ env:
2828
NODE_VERSION: '22.21.1'
2929

3030
jobs:
31-
# Initialize: Install dependencies, lint & typecheck
32-
mobile-init:
33-
name: Mobile Init (Install, Lint & Typecheck)
31+
# Install dependencies and warm the node_modules cache shared by every downstream job.
32+
# Kept separate from lint/typecheck so OTA + build jobs don't wait on verify (~3.5 min).
33+
mobile-install:
34+
name: Mobile Install
3435
runs-on: ubuntu-latest
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v4
39+
40+
- name: Setup Node.js
41+
uses: actions/setup-node@v4
42+
with:
43+
node-version: ${{ env.NODE_VERSION }}
44+
cache: 'npm'
45+
cache-dependency-path: package-lock.json
46+
47+
- name: Create concatenated patch file
48+
id: patch-file
49+
run: |
50+
ls -d -- packages/*/patches/*.patch 2>/dev/null | xargs cat > combined-patch-file.txt || touch combined-patch-file.txt
51+
echo "patch_checksum=$(sha256sum combined-patch-file.txt | cut -d' ' -f1)" >> $GITHUB_OUTPUT
52+
53+
- name: Cache node modules
54+
id: cache-node-modules
55+
uses: actions/cache@v4
56+
with:
57+
path: |
58+
node_modules
59+
packages/mobile/node_modules
60+
packages/common/node_modules
61+
packages/libs/node_modules
62+
packages/libs/dist
63+
packages/sdk/node_modules
64+
packages/sdk/dist
65+
packages/harmony/node_modules
66+
packages/dotenv-linter/bin
67+
key: npm-cache-mobile-${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ steps.patch-file.outputs.patch_checksum }}
68+
restore-keys: |
69+
npm-cache-mobile-${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-
70+
npm-cache-mobile-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
71+
72+
- name: Install dependencies
73+
env:
74+
CI: true
75+
NODE_OPTIONS: --max-old-space-size=8192
76+
run: |
77+
if [[ -d node_modules ]]; then
78+
echo "Using cached node_modules, running postinstall..."
79+
npm run postinstall
80+
# Ensure dotenv-linter binary is installed (install script downloads it)
81+
if [[ ! -f packages/dotenv-linter/bin/dotenv-linter ]]; then
82+
echo "dotenv-linter binary missing, running install script..."
83+
(cd packages/dotenv-linter && npm run install)
84+
fi
85+
else
86+
echo "No cache found, running fresh install..."
87+
# Clear npm cache to avoid EEXIST conflicts
88+
npm cache clean --force || true
89+
# Try npm ci first, fallback to npm install if lock file is out of sync
90+
npm ci --prefer-offline || npm install --prefer-offline
91+
fi
92+
93+
# Verify: lint & typecheck. Runs in parallel with OTA/build jobs — failure still fails the workflow,
94+
# but does not block the OTA publish on the critical path.
95+
mobile-verify:
96+
name: Mobile Verify (Lint & Typecheck)
97+
runs-on: ubuntu-latest
98+
needs: mobile-install
3599
steps:
36100
- name: Checkout code
37101
uses: actions/checkout@v4
@@ -81,16 +145,13 @@ jobs:
81145
if [[ -d node_modules ]]; then
82146
echo "Using cached node_modules, running postinstall..."
83147
npm run postinstall
84-
# Ensure dotenv-linter binary is installed (install script downloads it)
85148
if [[ ! -f packages/dotenv-linter/bin/dotenv-linter ]]; then
86149
echo "dotenv-linter binary missing, running install script..."
87150
(cd packages/dotenv-linter && npm run install)
88151
fi
89152
else
90153
echo "No cache found, running fresh install..."
91-
# Clear npm cache to avoid EEXIST conflicts
92154
npm cache clean --force || true
93-
# Try npm ci first, fallback to npm install if lock file is out of sync
94155
npm ci --prefer-offline || npm install --prefer-offline
95156
fi
96157
@@ -100,7 +161,7 @@ jobs:
100161
mobile-version-check:
101162
name: Mobile Version Change Check
102163
runs-on: ubuntu-latest
103-
needs: mobile-init
164+
needs: mobile-install
104165
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch'
105166
outputs:
106167
current_version: ${{ steps.version-check.outputs.current_version }}
@@ -129,12 +190,17 @@ jobs:
129190
echo "version_unchanged=false" >> "$GITHUB_OUTPUT"
130191
fi
131192
132-
# OTA Release: publish JS bundle updates when native app version is unchanged
193+
# OTA Release: publish JS bundle updates when native app version is unchanged.
194+
# Matrixed over platforms so iOS and Android publish in parallel.
133195
mobile-ota-release:
134-
name: Mobile OTA Release (CodePush)
196+
name: Mobile OTA Release (CodePush, ${{ matrix.platform }})
135197
runs-on: ubuntu-latest
136-
needs: [mobile-init, mobile-version-check]
198+
needs: [mobile-install, mobile-version-check]
137199
if: (github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.mobile-version-check.outputs.version_unchanged == 'true') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.ota_channel == 'rc' || github.event.inputs.ota_channel == ''))
200+
strategy:
201+
fail-fast: false
202+
matrix:
203+
platform: [ios, android]
138204
steps:
139205
- name: Checkout code
140206
uses: actions/checkout@v4
@@ -148,11 +214,48 @@ jobs:
148214
cache: 'npm'
149215
cache-dependency-path: package-lock.json
150216

217+
- name: Create concatenated patch file
218+
id: patch-file
219+
run: |
220+
ls -d -- packages/*/patches/*.patch 2>/dev/null | xargs cat > combined-patch-file.txt || touch combined-patch-file.txt
221+
echo "patch_checksum=$(sha256sum combined-patch-file.txt | cut -d' ' -f1)" >> $GITHUB_OUTPUT
222+
223+
- name: Cache node modules
224+
id: cache-node-modules
225+
uses: actions/cache@v4
226+
with:
227+
path: |
228+
node_modules
229+
packages/mobile/node_modules
230+
packages/common/node_modules
231+
packages/libs/node_modules
232+
packages/libs/dist
233+
packages/sdk/node_modules
234+
packages/sdk/dist
235+
packages/harmony/node_modules
236+
packages/dotenv-linter/bin
237+
key: npm-cache-mobile-${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ steps.patch-file.outputs.patch_checksum }}
238+
restore-keys: |
239+
npm-cache-mobile-${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-
240+
npm-cache-mobile-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
241+
151242
- name: Install dependencies
152243
env:
153244
CI: true
154245
NODE_OPTIONS: --max-old-space-size=8192
155-
run: npm ci --prefer-offline || npm install --prefer-offline
246+
run: |
247+
if [[ -d node_modules ]]; then
248+
echo "Using cached node_modules, running postinstall..."
249+
npm run postinstall
250+
if [[ ! -f packages/dotenv-linter/bin/dotenv-linter ]]; then
251+
echo "dotenv-linter binary missing, running install script..."
252+
(cd packages/dotenv-linter && npm run install)
253+
fi
254+
else
255+
echo "No cache found, running fresh install..."
256+
npm cache clean --force || true
257+
npm ci --prefer-offline || npm install --prefer-offline
258+
fi
156259
157260
- name: Configure AWS credentials
158261
uses: aws-actions/configure-aws-credentials@v4
@@ -170,7 +273,7 @@ jobs:
170273
echo "channel=rc" >> "$GITHUB_OUTPUT"
171274
fi
172275
173-
- name: Publish OTA updates (iOS + Android)
276+
- name: Publish OTA update (${{ matrix.platform }})
174277
env:
175278
OTA_S3_BUCKET: ${{ secrets.OTA_S3_BUCKET }}
176279
OTA_S3_PREFIX: mobile-ota
@@ -185,32 +288,29 @@ jobs:
185288
OTA_APP_VERSION="${MAJOR}.${MINOR}.${OTA_PATCH}"
186289
187290
cd packages/mobile
188-
npx code-push create-history --binary-version "$BINARY_VERSION" --platform ios --identifier "$OTA_CHANNEL" || true
189-
npx code-push create-history --binary-version "$BINARY_VERSION" --platform android --identifier "$OTA_CHANNEL" || true
190-
191-
npx code-push release \
192-
--binary-version "$BINARY_VERSION" \
193-
--app-version "$OTA_APP_VERSION" \
194-
--platform ios \
195-
--identifier "$OTA_CHANNEL" \
196-
--entry-file index.js
291+
npx code-push create-history --binary-version "$BINARY_VERSION" --platform ${{ matrix.platform }} --identifier "$OTA_CHANNEL" || true
197292
198293
npx code-push release \
199294
--binary-version "$BINARY_VERSION" \
200295
--app-version "$OTA_APP_VERSION" \
201-
--platform android \
296+
--platform ${{ matrix.platform }} \
202297
--identifier "$OTA_CHANNEL" \
203298
--entry-file index.js
204299
205-
# OTA Release (Production): manual + environment approval gate
300+
# OTA Release (Production): manual + environment approval gate.
301+
# Matrixed over platforms so iOS and Android publish in parallel.
206302
mobile-ota-release-production:
207-
name: Mobile OTA Release (Production, Approved)
303+
name: Mobile OTA Release (Production, ${{ matrix.platform }})
208304
runs-on: ubuntu-latest
209-
needs: [mobile-init, mobile-version-check]
305+
needs: [mobile-install, mobile-version-check]
210306
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ota_channel == 'production'
211307
environment:
212308
name: mobile-production-ota
213309
url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
310+
strategy:
311+
fail-fast: false
312+
matrix:
313+
platform: [ios, android]
214314
steps:
215315
- name: Checkout code
216316
uses: actions/checkout@v4
@@ -224,11 +324,48 @@ jobs:
224324
cache: 'npm'
225325
cache-dependency-path: package-lock.json
226326

327+
- name: Create concatenated patch file
328+
id: patch-file
329+
run: |
330+
ls -d -- packages/*/patches/*.patch 2>/dev/null | xargs cat > combined-patch-file.txt || touch combined-patch-file.txt
331+
echo "patch_checksum=$(sha256sum combined-patch-file.txt | cut -d' ' -f1)" >> $GITHUB_OUTPUT
332+
333+
- name: Cache node modules
334+
id: cache-node-modules
335+
uses: actions/cache@v4
336+
with:
337+
path: |
338+
node_modules
339+
packages/mobile/node_modules
340+
packages/common/node_modules
341+
packages/libs/node_modules
342+
packages/libs/dist
343+
packages/sdk/node_modules
344+
packages/sdk/dist
345+
packages/harmony/node_modules
346+
packages/dotenv-linter/bin
347+
key: npm-cache-mobile-${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ steps.patch-file.outputs.patch_checksum }}
348+
restore-keys: |
349+
npm-cache-mobile-${{ runner.os }}-node-${{ env.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-
350+
npm-cache-mobile-${{ runner.os }}-${{ hashFiles('package-lock.json') }}-
351+
227352
- name: Install dependencies
228353
env:
229354
CI: true
230355
NODE_OPTIONS: --max-old-space-size=8192
231-
run: npm ci --prefer-offline || npm install --prefer-offline
356+
run: |
357+
if [[ -d node_modules ]]; then
358+
echo "Using cached node_modules, running postinstall..."
359+
npm run postinstall
360+
if [[ ! -f packages/dotenv-linter/bin/dotenv-linter ]]; then
361+
echo "dotenv-linter binary missing, running install script..."
362+
(cd packages/dotenv-linter && npm run install)
363+
fi
364+
else
365+
echo "No cache found, running fresh install..."
366+
npm cache clean --force || true
367+
npm ci --prefer-offline || npm install --prefer-offline
368+
fi
232369
233370
- name: Configure AWS credentials
234371
uses: aws-actions/configure-aws-credentials@v4
@@ -237,7 +374,7 @@ jobs:
237374
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
238375
aws-region: us-east-1
239376

240-
- name: Publish OTA updates to production (iOS + Android)
377+
- name: Publish OTA update to production (${{ matrix.platform }})
241378
env:
242379
OTA_S3_BUCKET: ${{ secrets.OTA_S3_BUCKET }}
243380
OTA_S3_PREFIX: mobile-ota
@@ -251,28 +388,20 @@ jobs:
251388
OTA_APP_VERSION="${MAJOR}.${MINOR}.${OTA_PATCH}"
252389
253390
cd packages/mobile
254-
npx code-push create-history --binary-version "$BINARY_VERSION" --platform ios --identifier "$OTA_CHANNEL" || true
255-
npx code-push create-history --binary-version "$BINARY_VERSION" --platform android --identifier "$OTA_CHANNEL" || true
256-
257-
npx code-push release \
258-
--binary-version "$BINARY_VERSION" \
259-
--app-version "$OTA_APP_VERSION" \
260-
--platform ios \
261-
--identifier "$OTA_CHANNEL" \
262-
--entry-file index.js
391+
npx code-push create-history --binary-version "$BINARY_VERSION" --platform ${{ matrix.platform }} --identifier "$OTA_CHANNEL" || true
263392
264393
npx code-push release \
265394
--binary-version "$BINARY_VERSION" \
266395
--app-version "$OTA_APP_VERSION" \
267-
--platform android \
396+
--platform ${{ matrix.platform }} \
268397
--identifier "$OTA_CHANNEL" \
269398
--entry-file index.js
270399
271400
# iOS Release Candidate: Build and upload
272401
mobile-build-upload-releasecandidate-ios:
273402
name: iOS Release Candidate Build & Upload
274403
runs-on: macos-15
275-
needs: [mobile-init, mobile-version-check]
404+
needs: [mobile-install, mobile-version-check]
276405
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && needs.mobile-version-check.outputs.version_changed == 'true'
277406
steps:
278407
- name: Checkout code
@@ -446,7 +575,7 @@ jobs:
446575
mobile-build-upload-releasecandidate-android:
447576
name: Android Release Candidate Build & Upload
448577
runs-on: ubuntu-latest
449-
needs: [mobile-init, mobile-version-check]
578+
needs: [mobile-install, mobile-version-check]
450579
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && needs.mobile-version-check.outputs.version_changed == 'true'
451580
steps:
452581
- name: Checkout code
@@ -662,7 +791,7 @@ jobs:
662791
mobile-build-upload-production-ios-main:
663792
name: iOS Production Build & Upload
664793
runs-on: macos-15
665-
needs: [mobile-init, mobile-version-check]
794+
needs: [mobile-install, mobile-version-check]
666795
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && needs.mobile-version-check.outputs.version_changed == 'true'
667796
environment:
668797
name: mobile-production-ios
@@ -823,7 +952,7 @@ jobs:
823952
mobile-build-upload-production-android-main:
824953
name: Android Production Build & Upload
825954
runs-on: ubuntu-latest
826-
needs: [mobile-init, mobile-version-check]
955+
needs: [mobile-install, mobile-version-check]
827956
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && needs.mobile-version-check.outputs.version_changed == 'true'
828957
environment:
829958
name: mobile-production-android

0 commit comments

Comments
 (0)