Skip to content

Commit 733fe41

Browse files
authored
feat: unified Github Action for Harness (#76)
GitHub Actions now expose a single public entrypoint, callstackincubator/react-native-harness, instead of requiring users to choose a platform-specific action and then repeat the platform again in the runner input. This removes a confusing setup where users could accidentally mix actions/android with an iOS runner, or actions/ios with an Android runner. The action now reads the selected runner from the Harness config, detects its platform automatically, and runs the correct iOS, Android, or web setup internally. The docs and our own E2E workflow were updated to use this single action path.
1 parent 271d0a7 commit 733fe41

File tree

15 files changed

+605
-129
lines changed

15 files changed

+605
-129
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 5 additions & 45 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
@@ -100,7 +92,7 @@ jobs:
10092
key: apk-playground
10193

10294
- name: Run React Native Harness
103-
uses: ./actions/android
95+
uses: ./
10496
with:
10597
app: android/app/build/outputs/apk/debug/app-debug.apk
10698
runner: android
@@ -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

@@ -189,7 +173,7 @@ jobs:
189173
key: ios-app-playground
190174

191175
- name: Run React Native Harness
192-
uses: ./actions/ios
176+
uses: ./
193177
with:
194178
app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app
195179
runner: ios
@@ -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
@@ -238,7 +214,7 @@ jobs:
238214
pnpm nx run-many -t build --projects="packages/*"
239215
240216
- name: Run React Native Harness
241-
uses: ./actions/web
217+
uses: ./
242218
with:
243219
runner: chromium
244220
projectRoot: apps/playground
@@ -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
@@ -323,7 +291,7 @@ jobs:
323291
- name: Run React Native Harness (expect crash)
324292
id: crash-test
325293
continue-on-error: true
326-
uses: ./actions/android
294+
uses: ./
327295
with:
328296
app: android/app/build/outputs/apk/debug/app-debug.apk
329297
runner: android-crash-pre-rn
@@ -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

@@ -436,7 +396,7 @@ jobs:
436396
- name: Run React Native Harness (expect crash)
437397
id: crash-test
438398
continue-on-error: true
439-
uses: ./actions/ios
399+
uses: ./
440400
with:
441401
app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app
442402
runner: ios-crash-pre-rn

action.yml

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
name: React Native Harness
2+
description: Run React Native Harness tests on iOS, Android or Web
3+
inputs:
4+
runner:
5+
description: The runner to use (must match a runner name defined in your harness config)
6+
required: true
7+
type: string
8+
app:
9+
description: The path to the app (.app for iOS, .apk for Android). Not required for web.
10+
required: false
11+
type: string
12+
projectRoot:
13+
description: The project root directory
14+
required: false
15+
type: string
16+
uploadVisualTestArtifacts:
17+
description: Whether to upload visual test diff and actual images as artifacts
18+
required: false
19+
type: boolean
20+
default: 'true'
21+
harnessArgs:
22+
description: Additional arguments to pass to the Harness CLI
23+
required: false
24+
type: string
25+
default: ''
26+
packageManager:
27+
description: Package manager to use instead of auto-detection (npm, yarn, pnpm, bun, or deno)
28+
required: false
29+
type: string
30+
default: ''
31+
cacheAvd:
32+
description: Whether to cache the AVD
33+
required: false
34+
type: boolean
35+
default: 'true'
36+
runs:
37+
using: 'composite'
38+
steps:
39+
- name: Load React Native Harness configuration
40+
id: load-config
41+
shell: bash
42+
env:
43+
INPUT_RUNNER: ${{ inputs.runner }}
44+
INPUT_PROJECTROOT: ${{ inputs.projectRoot }}
45+
run: |
46+
node ${{ github.action_path }}/actions/shared/index.cjs
47+
- name: Verify native app input
48+
if: fromJson(steps.load-config.outputs.config).platformId != 'web'
49+
shell: bash
50+
run: |
51+
if [ -z "${{ inputs.app }}" ]; then
52+
echo "Error: app input is required for native runners"
53+
echo "Please provide the path to the built app (.apk for Android, .app for iOS)"
54+
exit 1
55+
fi
56+
- name: Metro cache
57+
uses: actions/cache@v4
58+
with:
59+
path: ${{ steps.load-config.outputs.projectRoot }}/.harness/metro-cache
60+
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') }}
61+
restore-keys: |
62+
${{ runner.os }}-metro-cache-
63+
64+
# ── iOS ──────────────────────────────────────────────────────────────────
65+
- uses: futureware-tech/simulator-action@v4
66+
if: fromJson(steps.load-config.outputs.config).platformId == 'ios'
67+
with:
68+
model: ${{ fromJson(steps.load-config.outputs.config).config.device.name }}
69+
os: iOS
70+
os_version: ${{ fromJson(steps.load-config.outputs.config).config.device.systemVersion }}
71+
wait_for_boot: true
72+
erase_before_boot: false
73+
- name: Install app
74+
if: fromJson(steps.load-config.outputs.config).platformId == 'ios'
75+
shell: bash
76+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
77+
run: |
78+
xcrun simctl install booted ${{ inputs.app }}
79+
80+
# ── Android ──────────────────────────────────────────────────────────────
81+
- name: Verify Android config
82+
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
83+
shell: bash
84+
run: |
85+
CONFIG='${{ steps.load-config.outputs.config }}'
86+
if [ -z "$CONFIG.config.device.avd" ] || [ "$CONFIG.config.device.avd" = "null" ]; then
87+
echo "Error: AVD config is required for Android emulators"
88+
echo "Please define the 'avd' property in the runner config"
89+
exit 1
90+
fi
91+
- name: Get architecture of the runner
92+
id: arch
93+
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
94+
shell: bash
95+
run: |
96+
case "${{ runner.arch }}" in
97+
X64)
98+
echo "arch=x86_64" >> $GITHUB_OUTPUT
99+
;;
100+
ARM64)
101+
echo "arch=arm64-v8a" >> $GITHUB_OUTPUT
102+
;;
103+
ARM32)
104+
echo "arch=armeabi-v7a" >> $GITHUB_OUTPUT
105+
;;
106+
*)
107+
echo "arch=x86_64" >> $GITHUB_OUTPUT
108+
;;
109+
esac
110+
- name: Enable KVM group perms
111+
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
112+
shell: bash
113+
run: |
114+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
115+
sudo udevadm control --reload-rules
116+
sudo udevadm trigger --name-match=kvm
117+
ls /dev/kvm
118+
- name: Compute AVD cache key
119+
id: avd-key
120+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) }}
121+
shell: bash
122+
run: |
123+
CONFIG='${{ steps.load-config.outputs.config }}'
124+
AVD_CONFIG=$(echo "$CONFIG" | jq -c '.config.device.avd')
125+
AVD_CONFIG_HASH=$(echo "$AVD_CONFIG" | sha256sum | cut -d' ' -f1)
126+
ARCH="${{ steps.arch.outputs.arch }}"
127+
CACHE_KEY="avd-$ARCH-$AVD_CONFIG_HASH"
128+
echo "key=$CACHE_KEY" >> $GITHUB_OUTPUT
129+
- name: Restore AVD cache
130+
uses: actions/cache/restore@v4
131+
id: avd-cache
132+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) }}
133+
with:
134+
path: |
135+
~/.android/avd
136+
~/.android/adb*
137+
key: ${{ steps.avd-key.outputs.key }}
138+
- name: Create AVD and generate snapshot for caching
139+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) && steps.avd-cache.outputs.cache-hit != 'true' }}
140+
uses: reactivecircus/android-emulator-runner@v2
141+
with:
142+
api-level: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.apiLevel }}
143+
arch: ${{ steps.arch.outputs.arch }}
144+
profile: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.profile }}
145+
disk-size: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.diskSize }}
146+
heap-size: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.heapSize }}
147+
force-avd-creation: false
148+
avd-name: ${{ fromJson(steps.load-config.outputs.config).config.device.name }}
149+
disable-animations: true
150+
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
151+
script: echo "Generated AVD snapshot for caching."
152+
- name: Save AVD cache
153+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) && steps.avd-cache.outputs.cache-hit != 'true' }}
154+
uses: actions/cache/save@v4
155+
with:
156+
path: |
157+
~/.android/avd
158+
~/.android/adb*
159+
key: ${{ steps.avd-key.outputs.key }}
160+
161+
# ── Web ──────────────────────────────────────────────────────────────────
162+
- name: Install Playwright Browsers
163+
if: fromJson(steps.load-config.outputs.config).platformId == 'web'
164+
shell: bash
165+
run: npx playwright install --with-deps chromium
166+
167+
# ── Shared ───────────────────────────────────────────────────────────────
168+
- name: Detect Package Manager
169+
id: detect-pm
170+
shell: bash
171+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
172+
run: |
173+
if [ -n "${{ inputs.packageManager }}" ]; then
174+
case "${{ inputs.packageManager }}" in
175+
pnpm)
176+
echo "manager=pnpm" >> $GITHUB_OUTPUT
177+
echo "runner=pnpm exec " >> $GITHUB_OUTPUT
178+
;;
179+
yarn)
180+
echo "manager=yarn" >> $GITHUB_OUTPUT
181+
echo "runner=yarn " >> $GITHUB_OUTPUT
182+
;;
183+
bun)
184+
echo "manager=bun" >> $GITHUB_OUTPUT
185+
echo "runner=bunx " >> $GITHUB_OUTPUT
186+
;;
187+
deno)
188+
echo "manager=deno" >> $GITHUB_OUTPUT
189+
echo "runner=deno run -A npm:" >> $GITHUB_OUTPUT
190+
;;
191+
npm)
192+
echo "manager=npm" >> $GITHUB_OUTPUT
193+
echo "runner=npx " >> $GITHUB_OUTPUT
194+
;;
195+
*)
196+
echo "Error: Unsupported packageManager '${{ inputs.packageManager }}'"
197+
echo "Supported values: npm, yarn, pnpm, bun, deno"
198+
exit 1
199+
;;
200+
esac
201+
elif [ -f "pnpm-lock.yaml" ]; then
202+
echo "manager=pnpm" >> $GITHUB_OUTPUT
203+
echo "runner=pnpm exec " >> $GITHUB_OUTPUT
204+
elif [ -f "yarn.lock" ]; then
205+
echo "manager=yarn" >> $GITHUB_OUTPUT
206+
echo "runner=yarn " >> $GITHUB_OUTPUT
207+
elif [ -f "bun.lock" ] || [ -f "bun.lockb" ]; then
208+
echo "manager=bun" >> $GITHUB_OUTPUT
209+
echo "runner=bunx " >> $GITHUB_OUTPUT
210+
elif [ -f "deno.lock" ]; then
211+
echo "manager=deno" >> $GITHUB_OUTPUT
212+
echo "runner=deno run -A npm:" >> $GITHUB_OUTPUT
213+
else
214+
echo "manager=npm" >> $GITHUB_OUTPUT
215+
echo "runner=npx " >> $GITHUB_OUTPUT
216+
fi
217+
- name: Run E2E tests
218+
if: fromJson(steps.load-config.outputs.config).platformId != 'android'
219+
shell: bash
220+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
221+
run: ${{ steps.detect-pm.outputs.runner }}react-native-harness --harnessRunner ${{ inputs.runner }} ${{ inputs.harnessArgs }}
222+
- name: Run E2E tests
223+
id: run-tests
224+
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
225+
uses: reactivecircus/android-emulator-runner@v2
226+
with:
227+
working-directory: ${{ steps.load-config.outputs.projectRoot }}
228+
api-level: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.apiLevel }}
229+
arch: ${{ steps.arch.outputs.arch }}
230+
force-avd-creation: false
231+
avd-name: ${{ fromJson(steps.load-config.outputs.config).config.device.name }}
232+
disable-animations: true
233+
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
234+
script: |
235+
echo $(pwd)
236+
adb install -r ${{ inputs.app }}
237+
${{ steps.detect-pm.outputs.runner }}react-native-harness --harnessRunner ${{ inputs.runner }} ${{ inputs.harnessArgs }}
238+
- name: Upload visual test artifacts
239+
if: always() && inputs.uploadVisualTestArtifacts == 'true'
240+
uses: actions/upload-artifact@v4
241+
with:
242+
name: visual-test-diffs-${{ fromJson(steps.load-config.outputs.config).platformId }}
243+
path: |
244+
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
245+
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
246+
if-no-files-found: ignore
247+
- name: Upload crash report artifacts
248+
if: always()
249+
uses: actions/upload-artifact@v4
250+
with:
251+
name: harness-crash-reports-${{ fromJson(steps.load-config.outputs.config).platformId }}
252+
path: ${{ steps.load-config.outputs.projectRoot }}/.harness/crash-reports/**/*
253+
if-no-files-found: ignore

actions/android/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: React Native Harness for Android
2-
description: Run React Native Harness tests on Android
2+
description: '[Deprecated] Use callstackincubator/react-native-harness instead. Run React Native Harness tests on Android'
33
inputs:
44
app:
55
description: The path to the Android app (.apk)

actions/ios/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: React Native Harness for iOS
2-
description: Run React Native Harness tests on iOS
2+
description: '[Deprecated] Use callstackincubator/react-native-harness instead. Run React Native Harness tests on iOS'
33
inputs:
44
app:
55
description: The path to the iOS app (.app)

0 commit comments

Comments
 (0)