[eas-cli] Clear the simulator session file when start fails before ready

When the session errors or times out before it is ready and the stop is
confirmed, reset .env.eas-simulator so simulator:exec does not report the
session as still starting. Log the saved path only after the interrupt
check, and shorten the changeset.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
szdziedzicandClaude Opus 5.5 committed 2026-11-13 17:25:03 +01:00
1 parent 5e3a735952
commit 94ab208fd1
4 files changed
+61 -7

No files matched your search

+1 -1
View File
@@ -2,4 +2,4 @@
'eas-cli': patch
---
Make `eas simulator:exec` fail, instead of running the command against a local device, when `.env.eas-simulator` has no session or the session is still starting; sessions started with `--out-config-type env` write no file, so run their commands without `eas simulator:exec`. Print the full path of `.env.eas-simulator` in `eas simulator:start`.
Make `eas simulator:exec` fail when `.env.eas-simulator` has no ready session, instead of running the command against a local device.
@@ -2834,6 +2834,34 @@ describe(Simulator, () => {
await expect(command.runAsync()).rejects.toThrow(/errored before the .* session was ready/);
expect(mockEnsureDeviceRunSessionStoppedAsync).toHaveBeenCalled();
// The file must not keep saying that the session is starting.
expect(mockResetSimulatorEnvAsync).toHaveBeenCalledWith(projectDir, 'session-123');
});
it('keeps the dotenv file when it cannot confirm that a session which errored before it was ready stopped', async () => {
mockByIdAsync.mockResolvedValue(
makeDeviceRunSession({ status: DeviceRunSessionStatus.Errored, remoteConfig: null })
);
mockEnsureDeviceRunSessionStoppedAsync.mockRejectedValue(new Error('network failed'));
const { command } = createCommand(['--platform', 'ios', '--non-interactive']);
await expect(command.runAsync()).rejects.toThrow(/errored before the .* session was ready/);
expect(mockResetSimulatorEnvAsync).not.toHaveBeenCalled();
});
it('keeps the original error when it cannot clean up the dotenv file of a session that errored before it was ready', async () => {
mockByIdAsync.mockResolvedValue(
makeDeviceRunSession({ status: DeviceRunSessionStatus.Errored, remoteConfig: null })
);
mockResetSimulatorEnvAsync.mockRejectedValue(new Error('read-only file system'));
const { command } = createCommand(['--platform', 'ios', '--non-interactive']);
await expect(command.runAsync()).rejects.toThrow(/errored before the .* session was ready/);
expect(Log.warn).toHaveBeenCalledWith(
`The session stopped, but ${simulatorDotenvPath} could not be cleaned up: read-only file system`
);
});
it('throws when the turtle job run finishes before the session is ready', async () => {
@@ -3172,6 +3200,7 @@ describe(Simulator, () => {
await expect(command.runAsync()).rejects.toThrow(/Timed out after \d+s waiting for/);
expect(mockEnsureDeviceRunSessionStoppedAsync).toHaveBeenCalled();
expect(mockResetSimulatorEnvAsync).toHaveBeenCalledWith(projectDir, 'session-123');
});
it('prints JSON only and skips the human-readable output with --json', async () => {
@@ -55,7 +55,8 @@ export default class SimulatorExec extends EasCommand {
if (simulatorEnv[EAS_SIMULATOR_SESSION_STATUS] === EAS_SIMULATOR_SESSION_STATUS_STARTING) {
throw new Error(
`Simulator session ${sessionId} in ${simulatorEnvFilePath} is still starting. ` +
'Wait until `eas simulator:start` reports that the session is ready, then run this command again.'
'Wait until `eas simulator:start` reports that the session is ready, then run this command again. ' +
'If `eas simulator:start` is no longer running, start a new session with `eas simulator:start`.'
);
}
@@ -582,7 +582,9 @@ With local egress, actions wait for the connection and require interactive mode.
}
} catch (err) {
pollSpinner.fail(`Failed while polling for ${flags.type} session to be ready`);
await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
if (await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId)) {
await resetUnreadySimulatorEnvAsync(projectDir, deviceRunSessionId);
}
sessionInterrupt.dispose();
throw err;
}
@@ -600,7 +602,9 @@ With local egress, actions wait for the connection and require interactive mode.
if (!remoteConfig) {
pollSpinner.fail(`Timed out waiting for ${flags.type} session to start`);
await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId);
if (await ensureDeviceRunSessionStoppedSafelyAsync(graphqlClient, deviceRunSessionId)) {
await resetUnreadySimulatorEnvAsync(projectDir, deviceRunSessionId);
}
sessionInterrupt.dispose();
throw new Error(
`Timed out after ${Math.round(STARTUP_TIMEOUT_MS / 1000)}s waiting for ${flags.type} session to start (excluding time in the queue). ${link(deviceRunSessionUrl)}`
@@ -835,9 +839,6 @@ With local egress, actions wait for the connection and require interactive mode.
if (jsonFlag || imported.config?.actions?.length) {
// Keep the previous session's saved credentials until setup succeeds.
simulatorEnvWritten = await writeReadySimulatorEnvAsync();
if (simulatorEnvWritten && !jsonFlag) {
Log.log(`Saved the session configuration to ${getSimulatorEnvFilePath(projectDir)}`);
}
if (sessionInterrupt.signal.aborted) {
await stopDeviceRunSessionAfterInterruptAsync({
graphqlClient,
@@ -848,6 +849,9 @@ With local egress, actions wait for the connection and require interactive mode.
});
return;
}
if (simulatorEnvWritten && !jsonFlag) {
Log.log(`Saved the session configuration to ${getSimulatorEnvFilePath(projectDir)}`);
}
}
if (jsonFlag) {
sessionInterrupt.dispose();
@@ -1118,6 +1122,26 @@ async function stopDeviceRunSessionCreatedAfterCancelAsync(
}
}
/**
* Clears the dotenv file of a session that stopped before it was ready, so that
* `eas simulator:exec` does not report the session as still starting. A failure only warns, so
* that the caller can still throw the error that stopped the session.
*/
async function resetUnreadySimulatorEnvAsync(
projectDir: string,
deviceRunSessionId: string
): Promise<void> {
try {
await resetSimulatorEnvAsync(projectDir, deviceRunSessionId);
} catch (error) {
Log.warn(
`The session stopped, but ${getSimulatorEnvFilePath(projectDir)} could not be cleaned up: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
async function resetSimulatorEnvVerboseAsync(
projectDir: string,
deviceRunSessionId: string