2828 NODE_VERSION : ' 22.21.1'
2929
3030jobs :
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