Skip to content

Commit 7617f33

Browse files
authored
feat: inline iOS and Android simulator handling (#90)
## Summary - move iOS simulator boot, reuse, shutdown, and conditional app installation into `platform-ios` so Harness owns the full simulator lifecycle - move Android emulator boot, AVD reuse/creation, shutdown, and conditional APK installation into `platform-android` so Harness no longer needs a third-party runtime action - run GitHub Action hooks through Harness lifecycle hooks for both native platforms, while keeping Android AVD cache warm-up external for now
1 parent 4c93648 commit 7617f33

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+4601
-1025
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ jobs:
110110
timeout-minutes: 30
111111
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios')) }}
112112
env:
113+
HARNESS_DEBUG: true
113114
DEBUG: 'Metro:*'
114115
steps:
115116
- name: Checkout code
@@ -349,6 +350,7 @@ jobs:
349350
timeout-minutes: 30
350351
if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios') }}
351352
env:
353+
HARNESS_DEBUG: true
352354
DEBUG: 'Metro:*'
353355
steps:
354356
- name: Checkout code

action.yml

Lines changed: 22 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ runs:
5252
env:
5353
INPUT_RUNNER: ${{ inputs.runner }}
5454
INPUT_PROJECTROOT: ${{ inputs.projectRoot }}
55+
HARNESS_AVD_CACHING: ${{ inputs.cacheAvd }}
5556
run: |
5657
node ${{ github.action_path }}/actions/shared/index.cjs
5758
- name: Verify native app input
@@ -72,34 +73,20 @@ runs:
7273
${{ runner.os }}-metro-cache-
7374
7475
# ── iOS ──────────────────────────────────────────────────────────────────
75-
- uses: futureware-tech/simulator-action@v4
76-
if: fromJson(steps.load-config.outputs.config).platformId == 'ios'
77-
with:
78-
model: ${{ fromJson(steps.load-config.outputs.config).config.device.name }}
79-
os: iOS
80-
os_version: ${{ fromJson(steps.load-config.outputs.config).config.device.systemVersion }}
81-
wait_for_boot: true
82-
erase_before_boot: false
83-
- name: Install app
84-
if: fromJson(steps.load-config.outputs.config).platformId == 'ios'
85-
shell: bash
86-
working-directory: ${{ steps.load-config.outputs.projectRoot }}
87-
run: |
88-
xcrun simctl install booted ${{ inputs.app }}
76+
# iOS simulator boot and app installation are handled by Harness itself.
8977
# ── Android ──────────────────────────────────────────────────────────────
9078
- name: Verify Android config
91-
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
79+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJson(steps.load-config.outputs.config).config.device.type == 'emulator' }}
9280
shell: bash
9381
run: |
94-
CONFIG='${{ steps.load-config.outputs.config }}'
95-
if [ -z "$CONFIG.config.device.avd" ] || [ "$CONFIG.config.device.avd" = "null" ]; then
82+
if [ '${{ fromJson(steps.load-config.outputs.config).config.device.avd }}' = 'null' ]; then
9683
echo "Error: AVD config is required for Android emulators"
9784
echo "Please define the 'avd' property in the runner config"
9885
exit 1
9986
fi
10087
- name: Get architecture of the runner
10188
id: arch
102-
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
89+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJson(steps.load-config.outputs.config).config.device.type == 'emulator' }}
10390
shell: bash
10491
run: |
10592
case "${{ runner.arch }}" in
@@ -117,7 +104,7 @@ runs:
117104
;;
118105
esac
119106
- name: Enable KVM group perms
120-
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
107+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJson(steps.load-config.outputs.config).config.device.type == 'emulator' }}
121108
shell: bash
122109
run: |
123110
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
@@ -126,47 +113,24 @@ runs:
126113
ls /dev/kvm
127114
- name: Compute AVD cache key
128115
id: avd-key
129-
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) }}
116+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJson(steps.load-config.outputs.config).config.device.type == 'emulator' && fromJson(steps.load-config.outputs.config).action.avdCachingEnabled }}
130117
shell: bash
131118
run: |
132-
CONFIG='${{ steps.load-config.outputs.config }}'
133-
AVD_CONFIG=$(echo "$CONFIG" | jq -c '.config.device.avd')
134-
AVD_CONFIG_HASH=$(echo "$AVD_CONFIG" | sha256sum | cut -d' ' -f1)
119+
CACHE_CONFIG='${{ toJson(fromJson(steps.load-config.outputs.config).action.avdCacheConfig) }}'
120+
AVD_CONFIG_HASH=$(printf '%s' "$CACHE_CONFIG" | sha256sum | cut -d' ' -f1)
121+
AVD_NAME='${{ fromJson(steps.load-config.outputs.config).config.device.name }}'
135122
ARCH="${{ steps.arch.outputs.arch }}"
136-
CACHE_KEY="avd-$ARCH-$AVD_CONFIG_HASH"
123+
CACHE_KEY="avd-$AVD_NAME-$ARCH-$AVD_CONFIG_HASH"
137124
echo "key=$CACHE_KEY" >> $GITHUB_OUTPUT
138125
- name: Restore AVD cache
139126
uses: actions/cache/restore@v4
140127
id: avd-cache
141-
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) }}
128+
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJson(steps.load-config.outputs.config).config.device.type == 'emulator' && fromJson(steps.load-config.outputs.config).action.avdCachingEnabled }}
142129
with:
143130
path: |
144131
~/.android/avd
145132
~/.android/adb*
146133
key: ${{ steps.avd-key.outputs.key }}
147-
- name: Create AVD and generate snapshot for caching
148-
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) && steps.avd-cache.outputs.cache-hit != 'true' }}
149-
uses: reactivecircus/android-emulator-runner@v2
150-
with:
151-
api-level: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.apiLevel }}
152-
arch: ${{ steps.arch.outputs.arch }}
153-
profile: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.profile }}
154-
disk-size: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.diskSize }}
155-
heap-size: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.heapSize }}
156-
force-avd-creation: false
157-
avd-name: ${{ fromJson(steps.load-config.outputs.config).config.device.name }}
158-
disable-animations: true
159-
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
160-
script: echo "Generated AVD snapshot for caching."
161-
- name: Save AVD cache
162-
if: ${{ fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJSON(inputs.cacheAvd) && steps.avd-cache.outputs.cache-hit != 'true' }}
163-
uses: actions/cache/save@v4
164-
with:
165-
path: |
166-
~/.android/avd
167-
~/.android/adb*
168-
key: ${{ steps.avd-key.outputs.key }}
169-
170134
# ── Web ──────────────────────────────────────────────────────────────────
171135
- name: Install Playwright Browsers
172136
if: fromJson(steps.load-config.outputs.config).platformId == 'web'
@@ -225,116 +189,27 @@ runs:
225189
fi
226190
- name: Run E2E tests
227191
id: run-tests
228-
if: fromJson(steps.load-config.outputs.config).platformId != 'android'
229192
shell: bash
230193
working-directory: ${{ steps.load-config.outputs.projectRoot }}
231194
env:
232195
PRE_RUN_HOOK: ${{ inputs.preRunHook }}
233196
AFTER_RUN_HOOK: ${{ inputs.afterRunHook }}
234197
HARNESS_RUNNER: ${{ inputs.runner }}
198+
HARNESS_APP_PATH: ${{ inputs.app }}
199+
HARNESS_AVD_CACHING: ${{ inputs.cacheAvd }}
235200
run: |
236201
export HARNESS_PROJECT_ROOT="$PWD"
237202
238-
if [ -n "$PRE_RUN_HOOK" ]; then
239-
pre_hook_file="$(mktemp "${RUNNER_TEMP:-/tmp}/harness-pre-run.XXXXXX.sh")"
240-
trap 'rm -f "$pre_hook_file"' EXIT
241-
printf '%s\n' "$PRE_RUN_HOOK" > "$pre_hook_file"
242-
chmod +x "$pre_hook_file"
243-
bash "$pre_hook_file"
244-
rm -f "$pre_hook_file"
245-
trap - EXIT
246-
fi
247-
248203
set +e
249204
${{ steps.detect-pm.outputs.runner }}react-native-harness --harnessRunner ${{ inputs.runner }} ${{ inputs.harnessArgs }}
250205
harness_exit_code=$?
251206
set -e
252207
253-
export HARNESS_EXIT_CODE="$harness_exit_code"
254-
after_run_exit_code=0
255-
if [ -n "$AFTER_RUN_HOOK" ]; then
256-
after_hook_file="$(mktemp "${RUNNER_TEMP:-/tmp}/harness-after-run.XXXXXX.sh")"
257-
trap 'rm -f "$after_hook_file"' EXIT
258-
printf '%s\n' "$AFTER_RUN_HOOK" > "$after_hook_file"
259-
chmod +x "$after_hook_file"
260-
set +e
261-
bash "$after_hook_file"
262-
after_run_exit_code=$?
263-
set -e
264-
rm -f "$after_hook_file"
265-
trap - EXIT
266-
fi
267-
268208
echo "harness_exit_code=$harness_exit_code" >> "$GITHUB_OUTPUT"
269209
270210
if [ "$harness_exit_code" -ne 0 ]; then
271211
exit "$harness_exit_code"
272212
fi
273-
274-
if [ "$after_run_exit_code" -ne 0 ]; then
275-
exit "$after_run_exit_code"
276-
fi
277-
- name: Run E2E tests
278-
id: run-tests-android
279-
if: fromJson(steps.load-config.outputs.config).platformId == 'android'
280-
uses: reactivecircus/android-emulator-runner@v2
281-
env:
282-
PRE_RUN_HOOK: ${{ inputs.preRunHook }}
283-
AFTER_RUN_HOOK: ${{ inputs.afterRunHook }}
284-
HARNESS_RUNNER: ${{ inputs.runner }}
285-
# android-emulator-runner executes each script line via `sh -c`, so multi-line
286-
# shell control flow must live in a separate bash script instead of `with.script`.
287-
HARNESS_ANDROID_SESSION_SCRIPT: |-
288-
export HARNESS_PROJECT_ROOT="$PWD"
289-
290-
adb install -r "${{ inputs.app }}"
291-
292-
if [ -n "$PRE_RUN_HOOK" ]; then
293-
pre_hook_file="$(mktemp "${RUNNER_TEMP:-/tmp}/harness-pre-run.XXXXXX.sh")"
294-
printf "%s\n" "$PRE_RUN_HOOK" > "$pre_hook_file"
295-
chmod +x "$pre_hook_file"
296-
bash "$pre_hook_file"
297-
rm -f "$pre_hook_file"
298-
fi
299-
300-
set +e
301-
${{ steps.detect-pm.outputs.runner }}react-native-harness --harnessRunner "${{ inputs.runner }}" ${{ inputs.harnessArgs }}
302-
harness_exit_code=$?
303-
echo "harness_exit_code=$harness_exit_code" >> "$GITHUB_OUTPUT"
304-
set -e
305-
306-
export HARNESS_EXIT_CODE="$harness_exit_code"
307-
after_run_exit_code=0
308-
if [ -n "$AFTER_RUN_HOOK" ]; then
309-
after_hook_file="$(mktemp "${RUNNER_TEMP:-/tmp}/harness-after-run.XXXXXX.sh")"
310-
printf "%s\n" "$AFTER_RUN_HOOK" > "$after_hook_file"
311-
chmod +x "$after_hook_file"
312-
set +e
313-
bash "$after_hook_file"
314-
after_run_exit_code=$?
315-
set -e
316-
rm -f "$after_hook_file"
317-
fi
318-
319-
if [ "$harness_exit_code" -ne 0 ]; then
320-
exit "$harness_exit_code"
321-
fi
322-
323-
if [ "$after_run_exit_code" -ne 0 ]; then
324-
exit "$after_run_exit_code"
325-
fi
326-
with:
327-
working-directory: ${{ steps.load-config.outputs.projectRoot }}
328-
api-level: ${{ fromJson(steps.load-config.outputs.config).config.device.avd.apiLevel }}
329-
arch: ${{ steps.arch.outputs.arch }}
330-
force-avd-creation: false
331-
avd-name: ${{ fromJson(steps.load-config.outputs.config).config.device.name }}
332-
disable-animations: true
333-
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
334-
# Keep `script` to a single line so the emulator action does not split our bash
335-
# session apart before the hooks and Harness command run.
336-
script: >-
337-
harness_script_file="$(mktemp "${RUNNER_TEMP:-/tmp}/harness-android-run.XXXXXX.sh")"; printf '%s\n' "$HARNESS_ANDROID_SESSION_SCRIPT" > "$harness_script_file"; chmod +x "$harness_script_file"; bash "$harness_script_file"; status=$?; rm -f "$harness_script_file"; exit "$status"
338213
- name: Upload visual test artifacts
339214
if: always() && inputs.uploadVisualTestArtifacts == 'true'
340215
uses: actions/upload-artifact@v4
@@ -344,6 +219,14 @@ runs:
344219
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-diff.png
345220
${{ steps.load-config.outputs.projectRoot }}/**/__image_snapshots__/**/*-actual.png
346221
if-no-files-found: ignore
222+
- name: Save AVD cache
223+
if: ${{ always() && fromJson(steps.load-config.outputs.config).platformId == 'android' && fromJson(steps.load-config.outputs.config).config.device.type == 'emulator' && fromJson(steps.load-config.outputs.config).action.avdCachingEnabled && steps.avd-cache.outputs.cache-hit != 'true' }}
224+
uses: actions/cache/save@v4
225+
with:
226+
path: |
227+
~/.android/avd
228+
~/.android/adb*
229+
key: ${{ steps.avd-key.outputs.key }}
347230
- name: Upload crash report artifacts
348231
if: always()
349232
uses: actions/upload-artifact@v4

0 commit comments

Comments
 (0)