Skip to content

Commit 21ffee2

Browse files
authored
fix: E2E CI caching and reliability improvements (#898)
1 parent 1d64bd7 commit 21ffee2

11 files changed

Lines changed: 489 additions & 70 deletions

File tree

.claude/rules/architecture.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,20 @@
9393
<description>ncrypto sharable library used by Node.js</description>
9494
<focus>Abstracts OpenSSL calls and utilities from Node.js code</focus>
9595
</reference>
96+
<reference>
97+
<path>$REPOS/nitro</path>
98+
<description>Nitro Modules source</description>
99+
<focus>iOS CI caching patterns, Nitro bridging examples</focus>
100+
</reference>
101+
<reference>
102+
<path>$REPOS/spicy</path>
103+
<description>Spicy Golf app</description>
104+
<focus>Android E2E patterns with Maestro</focus>
105+
</reference>
96106
</references>
97107
<instructions>
98108
Update this library's submodule of ncrypto occasionally when implementing new features.
109+
For CI work, reference Nitro (iOS) and Spicy (Android) workflows.
99110
</instructions>
100111
</rule>
101112

.claude/rules/ci-caching.xml

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<rules category="ci-caching">
2+
<metadata>
3+
<trigger>when working on GitHub Actions workflows or CI caching</trigger>
4+
</metadata>
5+
6+
<rule severity="HIGH" enforcement="STRICT">
7+
<name>Reference Implementations</name>
8+
<description>Use these repos as reference for CI patterns</description>
9+
<references>
10+
<reference>
11+
<path>$REPOS/nitro</path>
12+
<description>Super-fast iOS builds with proper caching</description>
13+
<files>.github/workflows/build-ios.yml</files>
14+
</reference>
15+
<reference>
16+
<path>$REPOS/spicy</path>
17+
<description>Working Android E2E with Maestro</description>
18+
<files>.github/workflows/e2e.yml, tests/e2e/scripts/</files>
19+
</reference>
20+
</references>
21+
</rule>
22+
23+
<rule severity="CRITICAL" enforcement="BLOCKING">
24+
<name>iOS Pods and DerivedData Cache Consistency</name>
25+
<description>Pods project files contain hardcoded paths to DerivedData</description>
26+
<problem>
27+
Pods cache contains xcodeproj files that reference paths like ios/build/generated/ios/*.cpp.
28+
If Pods restores but DerivedData doesn't, build fails with "Build input file cannot be found".
29+
</problem>
30+
<solutions>
31+
<solution>Use exact-match only for Pods cache (no restore-keys fallback)</solution>
32+
<solution>Ensure DerivedData cache key is superset of Pods cache key</solution>
33+
<solution>Restore DerivedData AFTER pod install runs (like Nitro does)</solution>
34+
</solutions>
35+
<antipattern>
36+
Using restore-keys for Pods cache can restore stale Pods that reference non-existent codegen files.
37+
</antipattern>
38+
</rule>
39+
40+
<rule severity="HIGH" enforcement="STRICT">
41+
<name>Cache Key Design</name>
42+
<description>Cache keys should reflect dependencies</description>
43+
<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>
47+
</guidelines>
48+
<example>
49+
Pods: runner.os-pods-hashFiles('Podfile.lock')
50+
DD: runner.os-dd-hashFiles('Podfile.lock', 'package.json', 'bun.lock')-xcode16.4
51+
</example>
52+
</rule>
53+
54+
<rule severity="HIGH" enforcement="STRICT">
55+
<name>Android Maestro App Launch</name>
56+
<description>Don't launch app before Maestro</description>
57+
<problem>
58+
If you launch the app via adb before Maestro runs, Maestro's launchApp command
59+
will restart it, potentially breaking Metro connection.
60+
</problem>
61+
<solution>
62+
Let Maestro handle app launch. Only install the APK before Maestro runs.
63+
</solution>
64+
</rule>
65+
66+
<rule severity="MEDIUM" enforcement="GUIDANCE">
67+
<name>Debugging CI Failures</name>
68+
<description>Commands for investigating CI issues</description>
69+
<commands>
70+
<command>gh run list --branch BRANCH --limit 5</command>
71+
<command>gh run view RUN_ID --log-failed</command>
72+
<command>gh run download RUN_ID --name ARTIFACT -D /tmp/output</command>
73+
<command>gh cache list</command>
74+
<command>gh cache delete --all</command>
75+
</commands>
76+
</rule>
77+
78+
<rule severity="MEDIUM" enforcement="GUIDANCE">
79+
<name>Build Speed Targets</name>
80+
<description>Expected build times with proper caching</description>
81+
<targets>
82+
<target>iOS incremental build: under 2 minutes</target>
83+
<target>Android incremental build: under 3 minutes</target>
84+
<target>Full iOS build (no cache): ~15-20 minutes</target>
85+
<target>Full Android build (no cache): ~10 minutes</target>
86+
</targets>
87+
<notes>
88+
ccache handles C++ compilation caching.
89+
DerivedData/Gradle caches handle build artifacts.
90+
</notes>
91+
</rule>
92+
</rules>

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

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,17 @@ jobs:
2727
env:
2828
EMULATOR_API_LEVEL: 34
2929
OUTPUT_DIR: ~/output
30+
GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.caching=true'
3031

3132
steps:
33+
- name: Maximize build space
34+
uses: AdityaGarg8/remove-unwanted-software@v5
35+
with:
36+
remove-dotnet: 'true'
37+
remove-haskell: 'true'
38+
remove-codeql: 'true'
39+
remove-docker-images: 'true'
40+
3241
- name: Checkout
3342
uses: actions/checkout@v5
3443

@@ -50,17 +59,20 @@ jobs:
5059
with:
5160
distribution: 'corretto'
5261
java-version: '17'
53-
cache: gradle
5462

55-
- name: Cache Android SDK
56-
uses: actions/cache@v4
63+
- name: Restore Gradle cache
64+
uses: actions/cache/restore@v5
5765
with:
5866
path: |
59-
"${{ env.ANDROID_SDK_ROOT }}"
60-
~/.android
61-
key: "${{ runner.os }}-android-sdk-${{ hashFiles('.github/workflows/e2e-android-test.yml') }}"
67+
~/.gradle/caches
68+
~/.gradle/wrapper
69+
example/android/.gradle
70+
example/android/build
71+
example/android/app/.cxx
72+
example/android/app/build
73+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
6274
restore-keys: |
63-
"${{ runner.os }}-android-sdk-"
75+
${{ runner.os }}-gradle-
6476
6577
- name: Install System Dependencies
6678
run: |
@@ -73,6 +85,12 @@ jobs:
7385
- name: Install Dependencies
7486
run: bun install
7587

88+
- name: Build Android App
89+
working-directory: ./example/android
90+
run: |
91+
echo "Building Android app (x86_64 only)..."
92+
./gradlew :app:assembleDebug -PreactNativeArchitectures=x86_64 --build-cache > $HOME/output/android-build.log 2>&1
93+
7694
- name: Install Maestro CLI
7795
run: |
7896
export MAESTRO_VERSION=2.0.10
@@ -86,29 +104,14 @@ jobs:
86104
sudo udevadm control --reload-rules
87105
sudo udevadm trigger --name-match=kvm
88106
89-
- name: AVD cache
90-
uses: actions/cache@v4
107+
- name: Restore AVD cache
108+
uses: actions/cache@v5
91109
id: avd-cache
92110
with:
93111
path: |
94112
~/.android/avd/*
95113
~/.android/adb*
96-
key: avd-${{ env.EMULATOR_API_LEVEL }}
97-
98-
- name: Create AVD and Generate Snapshot for Caching
99-
if: steps.avd-cache.outputs.cache-hit != 'true'
100-
uses: reactivecircus/android-emulator-runner@v2
101-
with:
102-
api-level: ${{ env.EMULATOR_API_LEVEL }}
103-
arch: x86_64
104-
force-avd-creation: false
105-
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
106-
disable-animations: false
107-
script: echo "Generated AVD snapshot for caching."
108-
109-
- name: Make scripts executable
110-
run: chmod +x test/e2e/*.sh
111-
working-directory: ./example/
114+
key: avd-pixel7pro-${{ env.EMULATOR_API_LEVEL }}
112115

113116
- name: Run E2E Tests
114117
uses: reactivecircus/android-emulator-runner@v2
@@ -117,12 +120,29 @@ jobs:
117120
with:
118121
api-level: ${{ env.EMULATOR_API_LEVEL }}
119122
arch: x86_64
120-
profile: pixel_6
123+
profile: pixel_7_pro
121124
force-avd-creation: false
122125
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
123126
disable-animations: true
124-
working-directory: ./example/
125-
script: ./test/e2e/run-android.sh
127+
working-directory: ./example
128+
script: ./test/e2e/scripts/test-android.sh
129+
130+
- name: Stop Gradle Daemon
131+
if: always()
132+
working-directory: ./example/android
133+
run: ./gradlew --stop
134+
135+
- name: Collect Screenshots
136+
if: always()
137+
run: |
138+
mkdir -p $HOME/output/screenshots
139+
LATEST_SCREENSHOT=$(find $HOME/output -name "screenshot-*.png" -type f 2>/dev/null | sort -r | head -1)
140+
if [ -n "$LATEST_SCREENSHOT" ]; then
141+
echo "Copying screenshot from $LATEST_SCREENSHOT to screenshots/android-test-result.png"
142+
cp "$LATEST_SCREENSHOT" $HOME/output/screenshots/android-test-result.png
143+
else
144+
echo "No screenshot found to copy"
145+
fi
126146
127147
- name: Post Maestro Screenshot to PR
128148
if: always()
@@ -143,6 +163,19 @@ jobs:
143163
${{ env.OUTPUT_DIR }}/screenshots/*.png
144164
retention-days: 5
145165

166+
- name: Save Gradle cache
167+
if: always()
168+
uses: actions/cache/save@v5
169+
with:
170+
path: |
171+
~/.gradle/caches
172+
~/.gradle/wrapper
173+
example/android/.gradle
174+
example/android/build
175+
example/android/app/.cxx
176+
example/android/app/build
177+
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
178+
146179
- name: Exit with Test Result
147180
if: always()
148181
run: |

0 commit comments

Comments
 (0)