Skip to content

Commit bd2cdb8

Browse files
committed
feat: add Metro caching
1 parent 05efce8 commit bd2cdb8

7 files changed

Lines changed: 41 additions & 71 deletions

File tree

.github/workflows/e2e-tests.yml

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,6 @@ jobs:
5757
node-version: '24.10.0'
5858
cache: 'pnpm'
5959

60-
- name: Metro cache
61-
uses: actions/cache@v4
62-
with:
63-
path: apps/playground/.harness/metro-cache
64-
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
65-
restore-keys: |
66-
${{ runner.os }}-metro-cache-
67-
6860
- name: Install dependencies
6961
run: |
7062
pnpm install
@@ -131,14 +123,6 @@ jobs:
131123
node-version: '24.10.0'
132124
cache: 'pnpm'
133125

134-
- name: Metro cache
135-
uses: actions/cache@v4
136-
with:
137-
path: apps/playground/.harness/metro-cache
138-
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
139-
restore-keys: |
140-
${{ runner.os }}-metro-cache-
141-
142126
- name: Install Watchman
143127
run: brew install watchman
144128

@@ -221,14 +205,6 @@ jobs:
221205
node-version: '24.10.0'
222206
cache: 'pnpm'
223207

224-
- name: Metro cache
225-
uses: actions/cache@v4
226-
with:
227-
path: apps/playground/.harness/metro-cache
228-
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
229-
restore-keys: |
230-
${{ runner.os }}-metro-cache-
231-
232208
- name: Install dependencies
233209
run: |
234210
pnpm install
@@ -278,14 +254,6 @@ jobs:
278254
node-version: '24.10.0'
279255
cache: 'pnpm'
280256

281-
- name: Metro cache
282-
uses: actions/cache@v4
283-
with:
284-
path: apps/playground/.harness/metro-cache
285-
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
286-
restore-keys: |
287-
${{ runner.os }}-metro-cache-
288-
289257
- name: Install dependencies
290258
run: |
291259
pnpm install
@@ -376,14 +344,6 @@ jobs:
376344
node-version: '24.10.0'
377345
cache: 'pnpm'
378346

379-
- name: Metro cache
380-
uses: actions/cache@v4
381-
with:
382-
path: apps/playground/.harness/metro-cache
383-
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
384-
restore-keys: |
385-
${{ runner.os }}-metro-cache-
386-
387347
- name: Install Watchman
388348
run: brew install watchman
389349

action.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ runs:
3434
INPUT_PROJECTROOT: ${{ inputs.projectRoot }}
3535
run: |
3636
node ${{ github.action_path }}/actions/shared/index.cjs
37+
- name: Metro cache
38+
uses: actions/cache@v4
39+
with:
40+
path: ${{ steps.load-config.outputs.projectRoot }}/.harness/metro-cache
41+
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
42+
restore-keys: |
43+
${{ runner.os }}-metro-cache-
3744
3845
# ── iOS ──────────────────────────────────────────────────────────────────
3946
- uses: futureware-tech/simulator-action@v4
@@ -47,7 +54,7 @@ runs:
4754
- name: Install app
4855
if: fromJson(steps.load-config.outputs.config).platformId == 'ios'
4956
shell: bash
50-
working-directory: ${{ inputs.projectRoot }}
57+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
5158
run: |
5259
xcrun simctl install booted ${{ inputs.app }}
5360
@@ -162,14 +169,14 @@ runs:
162169
- name: Run E2E tests
163170
if: fromJson(steps.load-config.outputs.config).platformId != 'android'
164171
shell: bash
165-
working-directory: ${{ inputs.projectRoot }}
172+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
166173
run: ${{ steps.detect-pm.outputs.runner }}react-native-harness --harnessRunner ${{ inputs.runner }} ${{ inputs.harnessArgs }}
167174
- name: Run E2E tests
168175
id: run-tests
169176
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
170177
uses: reactivecircus/android-emulator-runner@v2
171178
with:
172-
working-directory: ${{ inputs.projectRoot }}
179+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
173180
api-level: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.apiLevel }}
174181
arch: ${{ steps.arch.outputs.arch }}
175182
force-avd-creation: false
@@ -186,13 +193,13 @@ runs:
186193
with:
187194
name: visual-test-diffs-${{ fromJson(steps.load-config.outputs.config).platformId }}
188195
path: |
189-
${{ inputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
190-
${{ inputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
196+
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
197+
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
191198
if-no-files-found: ignore
192199
- name: Upload crash report artifacts
193200
if: always()
194201
uses: actions/upload-artifact@v4
195202
with:
196203
name: harness-crash-reports-${{ fromJson(steps.load-config.outputs.config).platformId }}
197-
path: ${{ inputs.projectRoot }}/.harness/crash-reports/**/*
204+
path: ${{ steps.load-config.outputs.projectRoot }}/.harness/crash-reports/**/*
198205
if-no-files-found: ignore

actions/shared/index.cjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4486,7 +4486,7 @@ var run = async () => {
44864486
}
44874487
const projectRoot = projectRootInput ? import_node_path6.default.resolve(projectRootInput) : process.cwd();
44884488
console.info(`Loading React Native Harness config from: ${projectRoot}`);
4489-
const { config } = await getConfig(projectRoot);
4489+
const { config, projectRoot: resolvedProjectRoot } = await getConfig(projectRoot);
44904490
const runner = config.runners.find((runner2) => runner2.name === runnerInput);
44914491
if (!runner) {
44924492
throw new Error(`Runner ${runnerInput} not found in config`);
@@ -4495,7 +4495,9 @@ var run = async () => {
44954495
if (!githubOutput) {
44964496
throw new Error("GITHUB_OUTPUT environment variable is not set");
44974497
}
4498+
const relativeProjectRoot = import_node_path6.default.relative(process.cwd(), resolvedProjectRoot) || ".";
44984499
const output = `config=${JSON.stringify(runner)}
4500+
projectRoot=${relativeProjectRoot}
44994501
`;
45004502
import_node_fs6.default.appendFileSync(githubOutput, output);
45014503
} catch (error) {

packages/github-action/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ The action reads your `rn-harness.config.mjs` file, resolves the `runner` you pa
2626
- `uploadVisualTestArtifacts` (optional): Whether to upload visual test diff and actual images as artifacts
2727
- `harnessArgs` (optional): Additional arguments to pass to the Harness CLI
2828
- Crash artifacts persisted to `.harness/crash-reports/` are uploaded automatically when present
29+
- Metro cache persisted to `.harness/metro-cache/` is restored and saved automatically when present
2930

3031
## Behavior
3132

3233
Depending on the selected runner, the action:
3334

34-
- For Android runners, loads and validates your Harness configuration, sets up the Android emulator with architecture detection, caches AVD snapshots, installs your app on the emulator, and runs the Harness tests
35-
- For iOS runners, loads and validates your Harness configuration, sets up the iOS simulator, installs your app on the simulator, and runs the Harness tests
36-
- For web runners, loads and validates your Harness configuration, installs Playwright Chromium, and runs the Harness tests
35+
- For Android runners, loads and validates your Harness configuration, restores Metro cache, sets up the Android emulator with architecture detection, caches AVD snapshots, installs your app on the emulator, and runs the Harness tests
36+
- For iOS runners, loads and validates your Harness configuration, restores Metro cache, sets up the iOS simulator, installs your app on the simulator, and runs the Harness tests
37+
- For web runners, loads and validates your Harness configuration, restores Metro cache, installs Playwright Chromium, and runs the Harness tests
3738

3839
Runner configuration requirements:
3940

packages/github-action/src/action.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ runs:
3434
INPUT_PROJECTROOT: ${{ inputs.projectRoot }}
3535
run: |
3636
node ${{ github.action_path }}/actions/shared/index.cjs
37+
- name: Metro cache
38+
uses: actions/cache@v4
39+
with:
40+
path: ${{ steps.load-config.outputs.projectRoot }}/.harness/metro-cache
41+
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }}
42+
restore-keys: |
43+
${{ runner.os }}-metro-cache-
3744
3845
# ── iOS ──────────────────────────────────────────────────────────────────
3946
- uses: futureware-tech/simulator-action@v4
@@ -47,7 +54,7 @@ runs:
4754
- name: Install app
4855
if: fromJson(steps.load-config.outputs.config).platformId == 'ios'
4956
shell: bash
50-
working-directory: ${{ inputs.projectRoot }}
57+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
5158
run: |
5259
xcrun simctl install booted ${{ inputs.app }}
5360
@@ -162,14 +169,14 @@ runs:
162169
- name: Run E2E tests
163170
if: fromJson(steps.load-config.outputs.config).platformId != 'android'
164171
shell: bash
165-
working-directory: ${{ inputs.projectRoot }}
172+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
166173
run: ${{ steps.detect-pm.outputs.runner }}react-native-harness --harnessRunner ${{ inputs.runner }} ${{ inputs.harnessArgs }}
167174
- name: Run E2E tests
168175
id: run-tests
169176
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
170177
uses: reactivecircus/android-emulator-runner@v2
171178
with:
172-
working-directory: ${{ inputs.projectRoot }}
179+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
173180
api-level: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.apiLevel }}
174181
arch: ${{ steps.arch.outputs.arch }}
175182
force-avd-creation: false
@@ -186,13 +193,13 @@ runs:
186193
with:
187194
name: visual-test-diffs-${{ fromJson(steps.load-config.outputs.config).platformId }}
188195
path: |
189-
${{ inputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
190-
${{ inputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
196+
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
197+
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
191198
if-no-files-found: ignore
192199
- name: Upload crash report artifacts
193200
if: always()
194201
uses: actions/upload-artifact@v4
195202
with:
196203
name: harness-crash-reports-${{ fromJson(steps.load-config.outputs.config).platformId }}
197-
path: ${{ inputs.projectRoot }}/.harness/crash-reports/**/*
204+
path: ${{ steps.load-config.outputs.projectRoot }}/.harness/crash-reports/**/*
198205
if-no-files-found: ignore

packages/github-action/src/shared/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const run = async (): Promise<void> => {
1717

1818
console.info(`Loading React Native Harness config from: ${projectRoot}`);
1919

20-
const { config } = await getConfig(projectRoot);
20+
const { config, projectRoot: resolvedProjectRoot } =
21+
await getConfig(projectRoot);
2122

2223
const runner = config.runners.find((runner) => runner.name === runnerInput);
2324

@@ -30,7 +31,9 @@ const run = async (): Promise<void> => {
3031
throw new Error('GITHUB_OUTPUT environment variable is not set');
3132
}
3233

33-
const output = `config=${JSON.stringify(runner)}\n`;
34+
const relativeProjectRoot =
35+
path.relative(process.cwd(), resolvedProjectRoot) || '.';
36+
const output = `config=${JSON.stringify(runner)}\nprojectRoot=${relativeProjectRoot}\n`;
3437
fs.appendFileSync(githubOutput, output);
3538
} catch (error) {
3639
if (error instanceof Error) {

website/src/docs/guides/ci-cd.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -210,21 +210,11 @@ jobs:
210210
runner: ios
211211
```
212212
213-
## Metro cache (optional)
213+
## Metro cache
214214
215-
React Native Harness can persist Metro's transformation cache under `.harness/metro-cache` in your project root. Enabling it in config (`unstable__enableMetroCache: true`) speeds up repeated Metro runs. In CI, you can cache this directory to avoid re-transforming unchanged files between workflow runs:
215+
React Native Harness can persist Metro's transformation cache under `.harness/metro-cache` in your project root. Enabling it in config (`unstable__enableMetroCache: true`) speeds up repeated Metro runs.
216216

217-
```yaml
218-
- name: Metro cache
219-
uses: actions/cache@v4
220-
with:
221-
path: .harness/metro-cache
222-
key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/pnpm-lock.yaml', '**/yarn.lock', '**/package-lock.json', '**/metro.config.js', '**/metro.config.mjs', '**/babel.config.js', '**/babel.config.mjs') }}
223-
restore-keys: |
224-
${{ runner.os }}-metro-cache-
225-
```
226-
227-
Use a key that includes your lockfile and Metro/Babel config paths so the cache invalidates when dependencies or bundler config change.
217+
When you use the `callstackincubator/react-native-harness` GitHub Action, Metro cache restoration and saving is handled automatically for the resolved `projectRoot`. You do not need to add a separate `actions/cache` step for `.harness/metro-cache`.
228218

229219
## Build Artifact Caching
230220

0 commit comments

Comments
 (0)