Skip to content

Commit 3bcb2ce

Browse files
sunnylqmclaude
andcommitted
ci(e2e-ios): metro/brew caches; skip xcodebuild via app cache + JS inject
Three independent cuts to the remaining wall-clock: - Metro transform cache persisted to .metro-cache and actions/cache. Content-addressed, so staleness only costs hit rate; shared by the 4 ppk bundles in prepare and the base-app bundling inside xcodebuild. Measured locally: prepare 120s cold -> 52s warm. - Homebrew download cache for applesimutils + ccache bottles. - The built .app (plus hermesc, copied out of Pods after the build) is cached keyed on every native input: both ios projects, cpp/, the android/jni sources the podspec compiles, podspec/configs, and the dependency lockfile. On an exact hit pod install and detox build are skipped entirely and scripts/refresh-ios-app-bundle.sh re-injects the current JS (metro bundle + hermesc, replicating react-native-xcode.sh release flags) into the cached app — simulator apps don't enforce code signatures. No restore-keys on purpose: a partial hit would be the wrong binary. Verified on-device: injected app passes the full suite 2/2, including a JS-only API-semantics change picked up with no xcodebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5a571d1 commit 3bcb2ce

4 files changed

Lines changed: 87 additions & 2 deletions

File tree

.github/workflows/e2e_ios.yml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ jobs:
9595
echo "no matching simulator found, skipping pre-boot"
9696
fi
9797
98+
- name: Cache Homebrew downloads
99+
# bottle 按 URL 内容寻址,restore-keys 前缀命中即省下载(装/链接照常)
100+
uses: actions/cache@v5.0.4
101+
with:
102+
path: ~/Library/Caches/Homebrew
103+
key: ${{ runner.os }}-brew-e2e-${{ github.sha }}
104+
restore-keys: |
105+
${{ runner.os }}-brew-e2e-
106+
98107
- name: Install applesimutils and ccache
99108
run: brew trust wix/brew && HOMEBREW_NO_AUTO_UPDATE=1 brew install wix/brew/applesimutils ccache
100109

@@ -127,6 +136,15 @@ jobs:
127136
- name: Install react-native-update-cli dependencies
128137
run: cd react-native-update-cli && bun install --frozen-lockfile && bun run build
129138

139+
- name: Cache Metro transform cache
140+
# 内容寻址(等价 ccache 之于 clang):陈旧条目只影响命中率不影响正确性
141+
uses: actions/cache@v5.0.4
142+
with:
143+
path: Example/e2etest/.metro-cache
144+
key: ${{ runner.os }}-metro-e2e-${{ github.sha }}
145+
restore-keys: |
146+
${{ runner.os }}-metro-e2e-
147+
130148
- name: Prepare local update artifacts
131149
env:
132150
E2E_PLATFORM: ios
@@ -141,7 +159,20 @@ jobs:
141159
restore-keys: |
142160
${{ runner.os }}-cocoapods-e2e-
143161
162+
- name: Cache built e2e app (keyed on native inputs)
163+
# 命中 = native 输入(两个 ios 工程、cpp/jni、podspec、依赖锁)一字未改,
164+
# 跳过 pod install + xcodebuild,只重新注入 JS bundle(见下)。
165+
# 故意不设 restore-keys:部分命中就是错的二进制,宁可全量重建。
166+
id: cache-ios-app
167+
uses: actions/cache@v5.0.4
168+
with:
169+
path: |
170+
Example/e2etest/ios/build/Build/Products/Release-iphonesimulator/AwesomeProject.app
171+
Example/e2etest/ios/build/hermesc
172+
key: ${{ runner.os }}-e2e-app-xcode-26.4-${{ hashFiles('Example/e2etest/ios/**', 'Example/e2etest/package.json', 'Example/e2etest/bun.lock', 'react-native-update.podspec', 'react-native.config.js', 'expo-module.config.json', 'ios/**', 'cpp/**', 'android/jni/**') }}
173+
144174
- name: Install CocoaPods dependencies
175+
if: steps.cache-ios-app.outputs.cache-hit != 'true'
145176
# USE_CCACHE=1 让 react_native_post_install 给所有 pod target 接上
146177
# RN 自带的 ccache 编译器包装脚本
147178
run: cd Example/e2etest/ios && USE_CCACHE=1 pod install
@@ -160,9 +191,17 @@ jobs:
160191
run: cd Example/e2etest && bunx detox build-framework-cache
161192

162193
- name: Detox build (ios.sim.release)
194+
if: steps.cache-ios-app.outputs.cache-hit != 'true'
163195
env:
164196
RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli
165-
run: cd Example/e2etest && E2E_PLATFORM=ios bunx detox build --configuration ios.sim.release
197+
run: |
198+
cd Example/e2etest && E2E_PLATFORM=ios bunx detox build --configuration ios.sim.release
199+
# hermesc 进缓存区:命中 run 没有 Pods,注入脚本需要它编 HBC
200+
cp ios/Pods/hermes-engine/destroot/bin/hermesc ios/build/hermesc
201+
202+
- name: Refresh JS bundle in cached app
203+
if: steps.cache-ios-app.outputs.cache-hit == 'true'
204+
run: cd Example/e2etest && ./scripts/refresh-ios-app-bundle.sh
166205

167206
- name: Detox test (ios.sim.release)
168207
env:

Example/e2etest/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,4 @@ coverage/
7676
android/app/.cxx/
7777
*.xcresult
7878
result*.xml
79+
.metro-cache/

Example/e2etest/metro.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
const path = require('path');
12
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
3+
const {FileStore} = require('metro-cache');
24

35
/**
46
* Metro configuration
57
* https://reactnative.dev/docs/metro
68
*
79
* @type {import('@react-native/metro-config').MetroConfig}
810
*/
9-
const config = {};
11+
const config = {
12+
// transform cache 是内容寻址的,落到项目内固定目录让 CI 用 actions/cache
13+
// 跨 run 持久化(prep 的 4 次 ppk 打包 + xcodebuild 里的基座打包共享)。
14+
cacheStores: [new FileStore({root: path.join(__dirname, '.metro-cache')})],
15+
};
1016

1117
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
#
3+
# 在缓存命中的 .app 里重新注入当前源码的 JS bundle + assets,跳过整个
4+
# pod install + xcodebuild(native 输入没变时它们只会产出同样的二进制)。
5+
# 复刻 react-native-xcode.sh 的 release 流程:metro bundle → hermesc
6+
# (-emit-binary -max-diagnostic-width=80 -O)。模拟器 app 不做签名校验,
7+
# 替换资源后可直接安装运行;注入结果由 e2e 套件本身验证(BINARY_BASE
8+
# 断言 + 完整更新流)。
9+
set -euo pipefail
10+
11+
cd "$(dirname "$0")/.."
12+
13+
APP=ios/build/Build/Products/Release-iphonesimulator/AwesomeProject.app
14+
# 构建后由 workflow 从 Pods/hermes-engine 拷贝进缓存区(缓存命中时无 Pods)
15+
HERMESC=ios/build/hermesc
16+
17+
if [ ! -d "$APP" ]; then
18+
echo "error: cached app not found: $APP" >&2
19+
exit 1
20+
fi
21+
if [ ! -x "$HERMESC" ]; then
22+
echo "error: cached hermesc not found: $HERMESC" >&2
23+
exit 1
24+
fi
25+
26+
TMP=$(mktemp -d)
27+
trap 'rm -rf "$TMP"' EXIT
28+
29+
bunx react-native bundle \
30+
--platform ios \
31+
--dev false \
32+
--entry-file index.js \
33+
--bundle-output "$TMP/main.jsbundle" \
34+
--assets-dest "$APP"
35+
36+
"$HERMESC" -emit-binary -max-diagnostic-width=80 -O \
37+
-out "$APP/main.jsbundle" "$TMP/main.jsbundle"
38+
39+
echo "refreshed JS bundle in $APP"

0 commit comments

Comments
 (0)