Skip to content

Commit b5b03b7

Browse files
committed
perf(ci): cache the fixture app build for the layer-3 differential
The differential job took ~30 minutes, of which 1331s (22 min, 79%) was building the Expo fixture app and only 347s was the differential itself — rebuilt from scratch on every run for an app that changes almost never. Cache the built .app, keyed on everything that can change the binary: the app's sources, native config, dependency graph, the build step itself, the iOS runtime, and the Xcode version. Mirrors the existing setup-apple-replay prebuilt-runner cache (same action pin, same Xcode-key + source-hash shape). On a hit the build is skipped entirely and the bundle is installed straight onto the booted simulator (~seconds), taking the job to roughly 8 minutes. On a miss it falls back to exactly the previous behaviour and repopulates, so the worst case is unchanged. The existing simctl verification still gates both paths, so a bad cache cannot produce a vacuous green: if the app is not installed, the job fails loudly rather than running scenarios against nothing. Note the first run after this lands is necessarily a miss.
1 parent df1fb6b commit b5b03b7

2 files changed

Lines changed: 51 additions & 1 deletion

File tree

.github/workflows/conformance-differential.yml

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,60 @@ jobs:
7878
runtime-version: ${{ env.IOS_RUNTIME_VERSION }}
7979
preferred-device-name: iPhone 17 Pro
8080

81+
# Building the fixture app costs ~22 minutes — 79% of this job — for an app
82+
# that changes almost never, so cache the built .app and rebuild only when
83+
# its sources or the toolchain change. Mirrors the setup-apple-replay
84+
# prebuilt-runner cache. A miss simply falls back to the full build.
85+
- name: Resolve Xcode cache key
86+
id: xcode
87+
run: |
88+
set -euo pipefail
89+
XCODE_KEY="$(xcodebuild -version | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]$//' | tr ' ' '-' | tr -cd '[:alnum:]._-')"
90+
echo "key=$XCODE_KEY" >> "$GITHUB_OUTPUT"
91+
92+
- name: Cache fixture app build
93+
id: fixture-app-cache
94+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
95+
with:
96+
path: ${{ github.workspace }}/.tmp/fixture-app
97+
# Every input that can change the binary: app sources, native config,
98+
# dependency graph, the build step itself, and the toolchain.
99+
key: fixture-app-ios-${{ env.IOS_RUNTIME_VERSION }}-${{ steps.xcode.outputs.key }}-${{ hashFiles('examples/test-app/src/**', 'examples/test-app/app/**', 'examples/test-app/modules/**', 'examples/test-app/app.config.js', 'examples/test-app/package.json', 'examples/test-app/pnpm-lock.yaml', 'examples/test-app/pnpm-workspace.yaml', '.github/workflows/conformance-differential.yml') }}
100+
81101
# The scenarios drive the real fixture app (com.callstack.agentdevicelab).
82102
# Release avoids needing a Metro server for the JS bundle; `expo run:ios`
83103
# prebuilds, builds, installs onto the booted simulator, and launches.
84-
- name: Build and install the fixture app
104+
- name: Build the fixture app
105+
if: steps.fixture-app-cache.outputs.cache-hit != 'true'
85106
run: |
86107
set -euo pipefail
87108
pnpm test-app:install
88109
pnpm --dir examples/test-app exec expo run:ios \
89110
--configuration Release \
90111
--device "iPhone 17 Pro" \
91112
--no-bundler
113+
# Stash the built bundle at a stable path so the cache has something
114+
# deterministic to keep (DerivedData paths are hashed per project).
115+
APP_PATH="$(find "$HOME/Library/Developer/Xcode/DerivedData" -type d -name '*.app' -path '*Release-iphonesimulator*' | head -1)"
116+
if [ -z "$APP_PATH" ]; then
117+
echo "::error::Built the fixture app but could not locate its .app bundle to cache."
118+
exit 1
119+
fi
120+
mkdir -p "${{ github.workspace }}/.tmp/fixture-app"
121+
cp -R "$APP_PATH" "${{ github.workspace }}/.tmp/fixture-app/"
122+
echo "cached $(basename "$APP_PATH")"
123+
124+
# On a hit the app was never built this run, so install it explicitly. (On a
125+
# miss `expo run:ios` already installed it; installing again is harmless.)
126+
- name: Install the fixture app
127+
run: |
128+
set -euo pipefail
129+
APP="$(find "${{ github.workspace }}/.tmp/fixture-app" -maxdepth 1 -type d -name '*.app' | head -1)"
130+
if [ -z "$APP" ]; then
131+
echo "::error::No fixture app bundle at .tmp/fixture-app (cache restored empty?)."
132+
exit 1
133+
fi
134+
xcrun simctl install booted "$APP"
92135
93136
- name: Verify the fixture app is installed
94137
run: |

scripts/maestro-conformance/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ The workflow builds and installs the fixture app, verifies its bundle id, pins
148148
the Maestro CLI to the same version as layers 1-2, and passes `--maestro` so the
149149
flow routes through the compat engine.
150150

151+
The built `.app` is **cached** (keyed on the app's sources, its dependency graph,
152+
the iOS runtime, and the Xcode version), because building it costs ~22 minutes
153+
versus ~6 minutes for the differential itself — 79% of the job, for an app that
154+
changes almost never. A cache hit installs the bundle directly; a miss falls back
155+
to the full build and repopulates. If you change anything under
156+
`examples/test-app`, expect the next run to rebuild.
157+
151158
**Investigate locally, not through CI.** A device iteration in CI is ~40 minutes;
152159
`--only` plus a local simulator is minutes:
153160

0 commit comments

Comments
 (0)