Skip to content

Commit 60720e1

Browse files
Lykhoydaclaude
andauthored
fix(story-06): reuse warm sim + env-configurable runner ready timeout (#387) (#494)
iOS acceptance data: WITHOUT shutdown-all, iOS was green 2/3 (one flaky 'multiple booted'); WITH the #490 shutdown-all it went RN_FAST_RUNNER_DOWN 2/2. shutdown-all cold-boots the TARGET sim, and the XCUITest runner's warm launch then exceeds the 30s ready gate. Three fixes: - Pre-boot now PREFERS an already-booted (warm) iPhone and shuts down only EXTRAS — keeps the runner launch fast AND leaves exactly one booted (the reason shutdown-all existed). - READY_TIMEOUT_MS is overridable via RN_FAST_RUNNER_READY_TIMEOUT_MS; the iOS lane sets 120s so a slow CI warm launch is not a false DOWN. - The smoke driver dumps the worker stderr on open failure (the open envelope swallows the real runner/xcodebuild error). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent bc65c77 commit 60720e1

6 files changed

Lines changed: 80 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'rn-dev-agent-plugin': patch
3+
---
4+
5+
Make the rn-fast-runner warm-launch ready gate overridable via `RN_FAST_RUNNER_READY_TIMEOUT_MS` (default 30s) so a slow CI simulator that needs longer to install+launch+attach the XCUITest runner is not a false `RN_FAST_RUNNER_DOWN`. The nightly iOS device-smoke lane also now reuses the image's already-booted (warm) simulator and shuts down only extras, instead of a blanket `shutdown all` that cold-boots the target and makes the runner launch time out.

.github/workflows/nightly-device-smoke.yml

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,26 @@ jobs:
4646
- name: Pre-boot simulator
4747
run: |
4848
set -euo pipefail
49-
UDID="$(xcrun simctl list devices available --json | jq -r '[.devices[] | .[] | select(.name == "iPhone 16")][0].udid // empty')"
49+
# Prefer an ALREADY-BOOTED iPhone as the target. device_snapshot open
50+
# refuses on >1 booted, but a blanket `shutdown all` + fresh boot
51+
# cold-boots the target and the XCUITest runner launch then times out
52+
# (device-proven RN_FAST_RUNNER_DOWN). Reusing the image's warm sim and
53+
# only shutting down EXTRAS keeps the runner launch fast AND leaves
54+
# exactly one booted.
55+
UDID="$(xcrun simctl list devices booted --json | jq -r '[.devices[] | .[] | select(.name | startswith("iPhone"))][0].udid // empty')"
5056
if [ -z "$UDID" ]; then
51-
echo "No 'iPhone 16' on this image — falling back to the first available iPhone" >&2
52-
UDID="$(xcrun simctl list devices available --json | jq -r '[.devices[] | .[] | select(.name | startswith("iPhone"))][0].udid // empty')"
57+
UDID="$(xcrun simctl list devices available --json | jq -r '[.devices[] | .[] | select(.name == "iPhone 16")][0].udid // empty')"
58+
if [ -z "$UDID" ]; then
59+
echo "No 'iPhone 16' on this image — falling back to the first available iPhone" >&2
60+
UDID="$(xcrun simctl list devices available --json | jq -r '[.devices[] | .[] | select(.name | startswith("iPhone"))][0].udid // empty')"
61+
fi
62+
[ -n "$UDID" ]
63+
xcrun simctl boot "$UDID"
5364
fi
54-
[ -n "$UDID" ]
55-
# Shut down any pre-booted simulators first: device_snapshot open
56-
# resolves "the booted iOS device" and refuses on >1 booted. The
57-
# macos image can auto-boot a default sim, so boot EXACTLY one.
58-
xcrun simctl shutdown all || true
59-
xcrun simctl boot "$UDID"
65+
# Shut down every OTHER booted sim so open sees exactly one.
66+
for d in $(xcrun simctl list devices booted --json | jq -r '[.devices[] | .[] | .udid] | .[]'); do
67+
[ "$d" = "$UDID" ] || xcrun simctl shutdown "$d" || true
68+
done
6069
xcrun simctl bootstatus "$UDID" -b
6170
echo "SMOKE_UDID=$UDID" >> "$GITHUB_ENV"
6271
- name: Build runner (fresh — no DerivedData cache)
@@ -79,6 +88,10 @@ jobs:
7988
- name: Run golden set
8089
env:
8190
SMOKE_DEBUG_DIR: ${{ runner.temp }}/smoke-debug
91+
# CI simulators install+launch+attach the XCUITest runner slower than
92+
# a dev machine — widen the 30s ready gate so a slow warm launch is
93+
# not a false RN_FAST_RUNNER_DOWN.
94+
RN_FAST_RUNNER_READY_TIMEOUT_MS: '120000'
8295
run: npm run smoke:ios
8396
- name: Collect simulator diagnostics (on failure)
8497
if: failure()

scripts/cdp-bridge/dist/runners/rn-fast-runner-client.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ import { RUNNER_PROTOCOL_VERSION, REQUIRED_IOS_COMMANDS, getPluginVersion, class
1010
import { buildRunnerQuiescenceEnv } from './quiescence.js';
1111
import { artifactProvenanceToState, resolveIosRunnerArtifacts } from './runner-artifacts.js';
1212
const DEFAULT_PORT = 22088;
13-
const READY_TIMEOUT_MS = 30_000;
13+
// Warm-launch ready gate. Overridable via RN_FAST_RUNNER_READY_TIMEOUT_MS
14+
// because a cold/slow CI simulator can need well over 30s to install + launch
15+
// + attach the XCUITest runner (device-proven on GitHub macos runners).
16+
export function resolveReadyTimeoutMs() {
17+
const raw = Number(process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS);
18+
return Number.isFinite(raw) && raw > 0 ? raw : 30_000;
19+
}
20+
const READY_TIMEOUT_MS = resolveReadyTimeoutMs();
1421
// A cold `xcodebuild test` compiles the runner project before launching it; on a
1522
// fresh machine (no prebuilt .xctestrun) that can take several minutes, so the
1623
// ready-signal timeout is widened for the build path.

scripts/cdp-bridge/src/runners/rn-fast-runner-client.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@ import { buildRunnerQuiescenceEnv } from './quiescence.js';
3636
import { artifactProvenanceToState, resolveIosRunnerArtifacts } from './runner-artifacts.js';
3737

3838
const DEFAULT_PORT = 22088;
39-
const READY_TIMEOUT_MS = 30_000;
39+
// Warm-launch ready gate. Overridable via RN_FAST_RUNNER_READY_TIMEOUT_MS
40+
// because a cold/slow CI simulator can need well over 30s to install + launch
41+
// + attach the XCUITest runner (device-proven on GitHub macos runners).
42+
export function resolveReadyTimeoutMs(): number {
43+
const raw = Number(process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS);
44+
return Number.isFinite(raw) && raw > 0 ? raw : 30_000;
45+
}
46+
const READY_TIMEOUT_MS = resolveReadyTimeoutMs();
4047
// A cold `xcodebuild test` compiles the runner project before launching it; on a
4148
// fresh machine (no prebuilt .xctestrun) that can take several minutes, so the
4249
// ready-signal timeout is widened for the build path.

scripts/cdp-bridge/test/smoke/device-smoke.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ test(`Phase B golden set (${PLATFORM})`, { timeout: 900_000 }, async () => {
175175
attachOnly: true,
176176
}),
177177
);
178+
if (open.envelope?.ok !== true) {
179+
// Surface the worker's stderr: the open envelope swallows the underlying
180+
// runner spawn / xcodebuild error (RN_FAST_RUNNER_DOWN just says "retry").
181+
console.error('--- supervisor stderr (open failure) ---\n' + s.stderrText().slice(-4000));
182+
}
178183
assert.equal(open.envelope?.ok, true, `open failed: ${open.text.slice(0, 500)}`);
179184

180185
let snap = record('snapshot', await callTool(s, 'device_snapshot', { action: 'snapshot' }));
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Story 06 Phase B (#387): the rn-fast-runner warm-launch ready gate is
2+
// overridable via RN_FAST_RUNNER_READY_TIMEOUT_MS so a slow CI simulator (where
3+
// install+launch+attach of the XCUITest runner can exceed 30s) can widen it.
4+
import { test } from 'node:test';
5+
import assert from 'node:assert/strict';
6+
import { resolveReadyTimeoutMs } from '../../dist/runners/rn-fast-runner-client.js';
7+
8+
function withEnv(value: string | undefined, fn: () => void) {
9+
const prev = process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS;
10+
if (value === undefined) delete process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS;
11+
else process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS = value;
12+
try {
13+
fn();
14+
} finally {
15+
if (prev === undefined) delete process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS;
16+
else process.env.RN_FAST_RUNNER_READY_TIMEOUT_MS = prev;
17+
}
18+
}
19+
20+
test('ready timeout defaults to 30s when the env var is unset', () => {
21+
withEnv(undefined, () => assert.equal(resolveReadyTimeoutMs(), 30_000));
22+
});
23+
24+
test('ready timeout honors a positive override', () => {
25+
withEnv('120000', () => assert.equal(resolveReadyTimeoutMs(), 120_000));
26+
});
27+
28+
test('ready timeout ignores non-numeric / non-positive values (falls back to 30s)', () => {
29+
withEnv('nonsense', () => assert.equal(resolveReadyTimeoutMs(), 30_000));
30+
withEnv('0', () => assert.equal(resolveReadyTimeoutMs(), 30_000));
31+
withEnv('-5', () => assert.equal(resolveReadyTimeoutMs(), 30_000));
32+
});

0 commit comments

Comments
 (0)