Skip to content

Commit 669c9e5

Browse files
authored
fix: parallelize Android E2E, cache Ruby gems, and CI improvements (#916)
1 parent 82cf5be commit 669c9e5

3 files changed

Lines changed: 185 additions & 48 deletions

File tree

.claude/rules/ci-caching.xml

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,24 @@
3737
</antipattern>
3838
</rule>
3939

40-
<rule severity="HIGH" enforcement="STRICT">
41-
<name>Cache Key Design</name>
42-
<description>Cache keys should reflect dependencies</description>
40+
<rule severity="CRITICAL" enforcement="BLOCKING">
41+
<name>Cache Key Design — Immutable Keys</name>
42+
<description>GitHub Actions caches are IMMUTABLE — cache/save fails if the key already exists</description>
43+
<problem>
44+
actions/cache/save cannot overwrite an existing cache entry. Using static keys
45+
(e.g. runner.os-gradle) means the first save succeeds, but every subsequent save
46+
fails with "Unable to reserve cache with key X, another job may be creating this cache."
47+
Caches become permanently stale.
48+
</problem>
4349
<guidelines>
44-
<guideline>Don't use version suffixes (v2, v3) - just delete caches with gh cache delete --all</guideline>
45-
<guideline>Include all files that affect cache contents in hashFiles()</guideline>
46-
<guideline>DerivedData key should include everything Pods key includes, plus more</guideline>
50+
<guideline>Save key MUST be unique per run: prefix-${{ github.run_id }}</guideline>
51+
<guideline>Restore uses exact key + restore-keys prefix fallback to get the latest</guideline>
52+
<guideline>Don't use version suffixes (v2, v3) — purge with gh cache delete --all</guideline>
53+
<guideline>Old cache entries are evicted automatically by GitHub's LRU policy</guideline>
4754
</guidelines>
4855
<example>
49-
Pods: runner.os-pods-hashFiles('Podfile.lock')
50-
DD: runner.os-dd-hashFiles('Podfile.lock', 'package.json', 'bun.lock')-xcode16.4
56+
Restore: key=runner.os-gradle-${{ github.run_id }}, restore-keys=runner.os-gradle-
57+
Save: key=runner.os-gradle-${{ github.run_id }}
5158
</example>
5259
</rule>
5360

.github/workflows/e2e-android-test.yml

Lines changed: 155 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,17 @@ on:
2323
- 'src/**'
2424
- 'packages/react-native-quick-crypto/android/**'
2525

26+
env:
27+
EMULATOR_API_LEVEL: 34
28+
2629
jobs:
27-
e2e-tests-android:
30+
# ============================================================================
31+
# Build Job - Gradle build + lint (runs in parallel with AVD setup)
32+
# ============================================================================
33+
build:
34+
name: Build
2835
runs-on: ubuntu-latest
2936
env:
30-
EMULATOR_API_LEVEL: 34
31-
OUTPUT_DIR: ~/output
3237
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.caching=true'
3338

3439
steps:
@@ -51,11 +56,6 @@ jobs:
5156
- name: Install Bun
5257
uses: ./.github/actions/setup-bun
5358

54-
- name: Create Directories
55-
run: |
56-
mkdir -p $HOME/output
57-
mkdir -p $HOME/.maestro/tests/
58-
5959
- name: Setup JDK
6060
uses: actions/setup-java@v4
6161
with:
@@ -87,13 +87,15 @@ jobs:
8787
packages/react-native-quick-crypto/android/build
8888
node_modules/.bun/react-native-nitro-modules*/node_modules/react-native-nitro-modules/android/.cxx
8989
node_modules/.bun/react-native-nitro-modules*/node_modules/react-native-nitro-modules/android/build
90-
key: ${{ runner.os }}-gradle
90+
key: ${{ runner.os }}-gradle-${{ github.run_id }}
91+
restore-keys: |
92+
${{ runner.os }}-gradle-
9193
9294
- name: Build Android App
9395
working-directory: ./example/android
9496
run: |
9597
echo "Building Android app (x86_64 only)..."
96-
./gradlew :app:assembleDebug -PreactNativeArchitectures=x86_64 --build-cache > $HOME/output/android-build.log 2>&1
98+
./gradlew :app:assembleDebug -PreactNativeArchitectures=x86_64 --build-cache 2>&1 | tee $HOME/android-build.log
9799
98100
- name: Run Gradle Lint
99101
working-directory: ./example/android
@@ -104,27 +106,166 @@ jobs:
104106
with:
105107
report-path: packages/react-native-quick-crypto/android/build/reports/lint-results-debug.xml
106108

109+
- name: Stop Gradle Daemon
110+
if: always()
111+
working-directory: ./example/android
112+
run: ./gradlew --stop
113+
114+
- name: Upload APK
115+
uses: actions/upload-artifact@v4
116+
with:
117+
name: android-apk
118+
path: example/android/app/build/outputs/apk/debug/app-debug.apk
119+
retention-days: 1
120+
121+
- name: Upload Build Log
122+
if: always()
123+
uses: actions/upload-artifact@v4
124+
with:
125+
name: android-build-log
126+
path: ~/android-build.log
127+
retention-days: 5
128+
129+
- name: Save Gradle cache
130+
if: always()
131+
uses: actions/cache/save@v5
132+
with:
133+
path: |
134+
~/.gradle/caches
135+
~/.gradle/wrapper
136+
example/android/.gradle
137+
example/android/build
138+
example/android/app/.cxx
139+
example/android/app/build
140+
packages/react-native-quick-crypto/android/.cxx
141+
packages/react-native-quick-crypto/android/build
142+
node_modules/.bun/react-native-nitro-modules*/node_modules/react-native-nitro-modules/android/.cxx
143+
node_modules/.bun/react-native-nitro-modules*/node_modules/react-native-nitro-modules/android/build
144+
key: ${{ runner.os }}-gradle-${{ github.run_id }}
145+
146+
# ============================================================================
147+
# AVD Job - Create and cache emulator snapshot (runs in parallel with build)
148+
# ============================================================================
149+
avd:
150+
name: Emulator
151+
runs-on: ubuntu-latest
152+
153+
steps:
154+
- name: Setup Android SDK
155+
uses: android-actions/setup-android@v3
156+
157+
- name: Enable KVM
158+
run: |
159+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
160+
sudo udevadm control --reload-rules
161+
sudo udevadm trigger --name-match=kvm
162+
163+
- name: Restore AVD cache
164+
uses: actions/cache/restore@v5
165+
id: avd-cache
166+
with:
167+
path: |
168+
~/.android/avd/*
169+
~/.android/adb*
170+
key: avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}-${{ github.run_id }}
171+
restore-keys: |
172+
avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}-
173+
174+
- name: Create AVD and Generate Snapshot for Caching
175+
if: steps.avd-cache.outputs.cache-hit != 'true'
176+
uses: reactivecircus/android-emulator-runner@v2
177+
with:
178+
api-level: ${{ env.EMULATOR_API_LEVEL }}
179+
arch: x86_64
180+
profile: pixel_7_pro
181+
force-avd-creation: false
182+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
183+
disable-animations: false
184+
script: echo "Generated AVD snapshot for caching."
185+
186+
- name: Save AVD cache
187+
if: steps.avd-cache.outputs.cache-hit != 'true'
188+
uses: actions/cache/save@v5
189+
with:
190+
path: |
191+
~/.android/avd/*
192+
~/.android/adb*
193+
key: avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}-${{ github.run_id }}
194+
195+
# ============================================================================
196+
# Test Job - Run E2E tests (needs both build and AVD)
197+
# ============================================================================
198+
test:
199+
name: Test
200+
needs: [build, avd]
201+
runs-on: ubuntu-latest
202+
env:
203+
OUTPUT_DIR: ~/output
204+
205+
steps:
206+
- name: Checkout
207+
uses: actions/checkout@v5
208+
209+
- name: Setup Node.js
210+
uses: actions/setup-node@v5
211+
with:
212+
node-version: '20'
213+
214+
- name: Install Bun
215+
uses: ./.github/actions/setup-bun
216+
217+
- name: Create Directories
218+
run: |
219+
mkdir -p $HOME/output
220+
mkdir -p $HOME/.maestro/tests/
221+
222+
- name: Setup Android SDK
223+
uses: android-actions/setup-android@v3
224+
225+
- name: Restore node_modules cache
226+
uses: actions/cache/restore@v5
227+
with:
228+
path: node_modules
229+
key: ${{ runner.os }}-node-modules-${{ github.run_id }}
230+
restore-keys: |
231+
${{ runner.os }}-node-modules-
232+
233+
- name: Install Dependencies
234+
run: bun install
235+
236+
- name: Save node_modules cache
237+
uses: actions/cache/save@v5
238+
with:
239+
path: node_modules
240+
key: ${{ runner.os }}-node-modules-${{ github.run_id }}
241+
242+
- name: Download APK
243+
uses: actions/download-artifact@v4
244+
with:
245+
name: android-apk
246+
path: example/android/app/build/outputs/apk/debug/
247+
107248
- name: Install Maestro CLI
108249
run: |
109250
export MAESTRO_VERSION=2.0.10
110251
curl -Ls "https://get.maestro.mobile.dev" | bash
111252
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
112253
113254
- name: Enable KVM
114-
if: env.SKIP_KVM != 'true'
115255
run: |
116256
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
117257
sudo udevadm control --reload-rules
118258
sudo udevadm trigger --name-match=kvm
119259
120260
- name: Restore AVD cache
121-
uses: actions/cache@v5
122-
id: avd-cache
261+
uses: actions/cache/restore@v5
123262
with:
124263
path: |
125264
~/.android/avd/*
126265
~/.android/adb*
127-
key: avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}
266+
key: avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}-${{ github.run_id }}
267+
restore-keys: |
268+
avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}-
128269
129270
- name: Run E2E Tests
130271
uses: reactivecircus/android-emulator-runner@v2
@@ -140,11 +281,6 @@ jobs:
140281
working-directory: ./example
141282
script: ./test/e2e/scripts/test-android.sh
142283

143-
- name: Stop Gradle Daemon
144-
if: always()
145-
working-directory: ./example/android
146-
run: ./gradlew --stop
147-
148284
- name: Collect Screenshots
149285
if: always()
150286
run: |
@@ -176,23 +312,6 @@ jobs:
176312
${{ env.OUTPUT_DIR }}/screenshots/*.png
177313
retention-days: 5
178314

179-
- name: Save Gradle cache
180-
if: always()
181-
uses: actions/cache/save@v5
182-
with:
183-
path: |
184-
~/.gradle/caches
185-
~/.gradle/wrapper
186-
example/android/.gradle
187-
example/android/build
188-
example/android/app/.cxx
189-
example/android/app/build
190-
packages/react-native-quick-crypto/android/.cxx
191-
packages/react-native-quick-crypto/android/build
192-
node_modules/.bun/react-native-nitro-modules*/node_modules/react-native-nitro-modules/android/.cxx
193-
node_modules/.bun/react-native-nitro-modules*/node_modules/react-native-nitro-modules/android/build
194-
key: ${{ runner.os }}-gradle
195-
196315
- name: Exit with Test Result
197316
if: always()
198317
run: |

.github/workflows/e2e-ios-test.yml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ jobs:
5858
- name: Install Bun
5959
uses: ./.github/actions/setup-bun
6060

61+
- name: Setup Ruby (bundle)
62+
uses: ruby/setup-ruby@v1
63+
with:
64+
ruby-version: 3.3.4
65+
bundler-cache: true
66+
working-directory: example
67+
6168
- name: Create Directories
6269
run: |
6370
mkdir -p $HOME/output
@@ -76,14 +83,18 @@ jobs:
7683
~/Library/Caches/CocoaPods
7784
packages/react-native-quick-crypto/ios/libsodium-stable
7885
packages/react-native-quick-crypto/deps
79-
key: ${{ runner.os }}-pods
86+
key: ${{ runner.os }}-pods-${{ github.run_id }}
87+
restore-keys: |
88+
${{ runner.os }}-pods-
8089
8190
- name: Restore DerivedData cache
8291
id: dd-cache
8392
uses: actions/cache/restore@v5
8493
with:
8594
path: ${{ env.DERIVED_DATA_PATH }}
86-
key: ${{ runner.os }}-dd
95+
key: ${{ runner.os }}-dd-${{ github.run_id }}
96+
restore-keys: |
97+
${{ runner.os }}-dd-
8798
8899
- name: Boot iOS Simulator (background)
89100
run: |
@@ -228,14 +239,14 @@ jobs:
228239
~/Library/Caches/CocoaPods
229240
packages/react-native-quick-crypto/ios/libsodium-stable
230241
packages/react-native-quick-crypto/deps
231-
key: ${{ runner.os }}-pods
242+
key: ${{ runner.os }}-pods-${{ github.run_id }}
232243

233244
- name: Save DerivedData cache
234245
if: always()
235246
uses: actions/cache/save@v5
236247
with:
237248
path: ${{ env.DERIVED_DATA_PATH }}
238-
key: ${{ runner.os }}-dd
249+
key: ${{ runner.os }}-dd-${{ github.run_id }}
239250

240251
- name: Exit with Test Result
241252
if: always()

0 commit comments

Comments
 (0)