@expo/build-tools 21.5.1 → 21.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,6 @@ const projectSources_1 = require("../common/projectSources");
15
15
  const customBuildContext_1 = require("../customBuildContext");
16
16
  const datadog_1 = require("../datadog");
17
17
  const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
18
- const compositeFunctions_1 = require("../steps/compositeFunctions");
19
18
  const easFunctionGroups_1 = require("../steps/easFunctionGroups");
20
19
  const easFunctions_1 = require("../steps/easFunctions");
21
20
  const retry_1 = require("../utils/retry");
@@ -55,11 +54,13 @@ async function runCustomBuildAsync(ctx) {
55
54
  steps: ctx.job.steps,
56
55
  hooks: ctx.job.hooks,
57
56
  // Eager for job steps (always run), lazy loader for hooks (running anchors only).
58
- compositeFunctionCatalog: await (0, compositeFunctions_1.buildCompositeFunctionCatalogAsync)(projectRoot, {
59
- steps: ctx.job.steps,
57
+ compositeFunctionCatalog: await (0, steps_1.buildLocalCompositeFunctionCatalogAsync)(projectRoot, {
58
+ rootSteps: ctx.job.steps,
59
+ logger: ctx.logger,
60
+ }),
61
+ loadCompositeFunction: (0, steps_1.createLocalCompositeFunctionLoader)(projectRoot, {
60
62
  logger: ctx.logger,
61
63
  }),
62
- loadCompositeFunction: (0, compositeFunctions_1.createCompositeFunctionLoader)(projectRoot, ctx.logger),
63
64
  })
64
65
  : new steps_1.BuildConfigParser(globalContext, {
65
66
  externalFunctions: easFunctions,
@@ -4,7 +4,6 @@ exports.parseJobHooksAsync = parseJobHooksAsync;
4
4
  const eas_build_job_1 = require("@expo/eas-build-job");
5
5
  const steps_1 = require("@expo/steps");
6
6
  const customBuildContext_1 = require("../customBuildContext");
7
- const compositeFunctions_1 = require("../steps/compositeFunctions");
8
7
  const easFunctionGroups_1 = require("../steps/easFunctionGroups");
9
8
  const easFunctions_1 = require("../steps/easFunctions");
10
9
  /**
@@ -60,7 +59,7 @@ async function parseJobHooksAsync(ctx, wrappedAnchors) {
60
59
  const hookEntriesByKey = {};
61
60
  const orderedSteps = [];
62
61
  const compositeFunctionCatalog = {};
63
- const loadCompositeFunction = (0, compositeFunctions_1.createCompositeFunctionLoader)(ctx.getReactNativeProjectDirectory(), ctx.logger);
62
+ const loadCompositeFunction = (0, steps_1.createLocalCompositeFunctionLoader)(ctx.getReactNativeProjectDirectory(), { logger: ctx.logger });
64
63
  for (const anchor of wrappedAnchors) {
65
64
  for (const side of ['before', 'after']) {
66
65
  const key = `${side}_${anchor}`;
package/dist/generic.js CHANGED
@@ -11,7 +11,6 @@ const promises_1 = __importDefault(require("fs/promises"));
11
11
  const nullthrows_1 = __importDefault(require("nullthrows"));
12
12
  const projectSources_1 = require("./common/projectSources");
13
13
  const customBuildContext_1 = require("./customBuildContext");
14
- const compositeFunctions_1 = require("./steps/compositeFunctions");
15
14
  const easFunctionGroups_1 = require("./steps/easFunctionGroups");
16
15
  const easFunctions_1 = require("./steps/easFunctions");
17
16
  const outputs_1 = require("./utils/outputs");
@@ -38,8 +37,8 @@ async function runGenericJobAsync(ctx) {
38
37
  try {
39
38
  const projectRoot = ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory);
40
39
  // Eager for job steps (always run), lazy loader for hooks (running anchors only).
41
- const compositeFunctionCatalog = await (0, compositeFunctions_1.buildCompositeFunctionCatalogAsync)(projectRoot, {
42
- steps: ctx.job.steps,
40
+ const compositeFunctionCatalog = await (0, steps_1.buildLocalCompositeFunctionCatalogAsync)(projectRoot, {
41
+ rootSteps: ctx.job.steps,
43
42
  logger: ctx.logger,
44
43
  });
45
44
  const parser = new steps_1.StepsConfigParser(globalContext, {
@@ -48,7 +47,9 @@ async function runGenericJobAsync(ctx) {
48
47
  steps: ctx.job.steps,
49
48
  hooks: ctx.job.hooks,
50
49
  compositeFunctionCatalog,
51
- loadCompositeFunction: (0, compositeFunctions_1.createCompositeFunctionLoader)(projectRoot, ctx.logger),
50
+ loadCompositeFunction: (0, steps_1.createLocalCompositeFunctionLoader)(projectRoot, {
51
+ logger: ctx.logger,
52
+ }),
52
53
  });
53
54
  return await parser.parseAsync();
54
55
  }
@@ -1,7 +1,6 @@
1
1
  import { type bunyan } from '@expo/logger';
2
2
  import { BuildFunction } from '@expo/steps';
3
3
  import { type CustomBuildContext } from '../../customBuildContext';
4
- export declare const DEFAULT_AGENT_DEVICE_VERSION = "0.20.3";
5
4
  export declare function createStartAgentDeviceRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
6
5
  export declare function stopAgentDeviceEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }: {
7
6
  eventCollection: {
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_AGENT_DEVICE_VERSION = void 0;
7
6
  exports.createStartAgentDeviceRemoteSessionBuildFunction = createStartAgentDeviceRemoteSessionBuildFunction;
8
7
  exports.stopAgentDeviceEventCollectionSafelyAsync = stopAgentDeviceEventCollectionSafelyAsync;
9
8
  const eas_build_job_1 = require("@expo/eas-build-job");
@@ -17,7 +16,6 @@ const agentDeviceArtifacts_1 = require("../utils/agentDeviceArtifacts");
17
16
  const agentDeviceEvents_1 = require("../utils/agentDeviceEvents");
18
17
  const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
19
18
  const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
20
- exports.DEFAULT_AGENT_DEVICE_VERSION = '0.20.3';
21
19
  const AGENT_DEVICE_REPO_URL = 'https://github.com/callstack/agent-device.git';
22
20
  const SRC_DIR = '/tmp/agent-device-src';
23
21
  const AGENT_DEVICE_STATE_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device');
@@ -39,6 +37,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
39
37
  required: false,
40
38
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
41
39
  }),
40
+ steps_1.BuildStepInput.createProvider({
41
+ id: 'max_idle_time_minutes',
42
+ required: false,
43
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
44
+ }),
42
45
  ],
43
46
  fn: async ({ logger, global }, { inputs, env, signal }) => {
44
47
  // Fail fast before any expensive setup if the injected env
@@ -48,9 +51,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
48
51
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
49
52
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
50
53
  const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
51
- const packageVersion = inputs.package_version.value ?? exports.DEFAULT_AGENT_DEVICE_VERSION;
54
+ const packageVersion = inputs.package_version.value;
55
+ // A missing or non-positive value disables the idle timeout (opt-in feature).
56
+ const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
52
57
  const { runtimePlatform } = global;
53
- logger.info(`Starting agent-device remote session (version: ${packageVersion}, runtime: ${runtimePlatform}).`);
58
+ logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
54
59
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
55
60
  await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
56
61
  }
@@ -111,6 +116,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
111
116
  deviceRunSessionId,
112
117
  logger,
113
118
  signal,
119
+ idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
120
+ ? {
121
+ maxIdleTimeMinutes,
122
+ getLastEventObservedAt: eventCollection.getLastEventObservedAt,
123
+ }
124
+ : undefined,
114
125
  });
115
126
  }
116
127
  finally {
@@ -174,7 +185,7 @@ async function startAgentDeviceDaemonAsync({ packageVersion, env, logger, }) {
174
185
  },
175
186
  extras: {
176
187
  packageSpec,
177
- packageVersion,
188
+ packageVersion: packageVersion ?? 'latest',
178
189
  bunVersion,
179
190
  bunInstallConfigured: Boolean(env.BUN_INSTALL?.trim()),
180
191
  },
@@ -184,7 +195,9 @@ async function startAgentDeviceDaemonAsync({ packageVersion, env, logger, }) {
184
195
  }
185
196
  }
186
197
  async function startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger, }) {
187
- logger.info(`Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`);
198
+ logger.info(packageVersion
199
+ ? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
200
+ : `Cloning agent-device (latest) into ${SRC_DIR}.`);
188
201
  await cloneAgentDeviceAsync({ packageVersion, env, logger });
189
202
  logger.info('Installing agent-device dependencies.');
190
203
  await (0, turtle_spawn_1.default)('bun', ['install', '--production'], {
@@ -201,7 +214,7 @@ async function startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger,
201
214
  });
202
215
  }
203
216
  async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
204
- const branchArgs = ['--branch', `v${packageVersion}`];
217
+ const branchArgs = packageVersion ? ['--branch', `v${packageVersion}`] : [];
205
218
  await (0, turtle_spawn_1.default)('git', ['clone', '--depth', '1', ...branchArgs, AGENT_DEVICE_REPO_URL, SRC_DIR], {
206
219
  env,
207
220
  logger,
@@ -233,7 +246,7 @@ async function getBunVersionForDiagnosticsAsync(env) {
233
246
  }
234
247
  }
235
248
  function createAgentDevicePackageSpec(packageVersion) {
236
- const versionSpec = packageVersion.replace(/^v(?=\d)/, '');
249
+ const versionSpec = packageVersion ? packageVersion.replace(/^v(?=\d)/, '') : 'latest';
237
250
  return `${AGENT_DEVICE_PACKAGE_NAME}@${versionSpec}`;
238
251
  }
239
252
  function getGlobalAgentDeviceDaemonPath(env) {
@@ -52,6 +52,11 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
52
52
  required: false,
53
53
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
54
54
  }),
55
+ steps_1.BuildStepInput.createProvider({
56
+ id: 'max_idle_time_minutes',
57
+ required: false,
58
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
59
+ }),
55
60
  ],
56
61
  fn: async ({ logger, global }, { inputs, env, signal }) => {
57
62
  // Fail fast before any expensive setup if the injected env
@@ -62,6 +67,8 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
62
67
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
63
68
  const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
64
69
  const packageVersion = inputs.package_version.value;
70
+ // A missing or non-positive value disables the idle timeout (opt-in feature).
71
+ const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
65
72
  warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger });
66
73
  const versionSpec = packageVersion ?? 'latest';
67
74
  const { runtimePlatform } = global;
@@ -172,6 +179,12 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
172
179
  deviceRunSessionId,
173
180
  logger,
174
181
  signal,
182
+ idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
183
+ ? {
184
+ maxIdleTimeMinutes,
185
+ getLastEventObservedAt: eventCollection.getLastEventObservedAt,
186
+ }
187
+ : undefined,
175
188
  });
176
189
  }
177
190
  finally {
@@ -1,11 +1,10 @@
1
1
  import { type bunyan } from '@expo/logger';
2
2
  import { type CustomBuildContext } from '../../customBuildContext';
3
+ import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
3
4
  export declare function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }: {
4
5
  ctx: CustomBuildContext;
5
6
  deviceRunSessionId: string;
6
7
  stateDir: string;
7
8
  logger: bunyan;
8
9
  pollIntervalMs?: number;
9
- }): Promise<{
10
- stopAsync: () => Promise<void>;
11
- }>;
10
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -21,6 +21,17 @@ const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
21
21
  const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
22
22
  const ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS = 30_000;
23
23
  const ARGENT_ARTIFACT_FETCH_TIMEOUT_MS = 10_000;
24
+ // Kinds the EAS dashboard groups device run session artifacts by. Only media types the dashboard
25
+ // renders specially are mapped; anything else stays unclassified and lands in its "Other" group.
26
+ // Note that these kinds only affect grouping and labelling. The dashboard gates its inline video
27
+ // player on the `__eas_screen_recording` metadata flag, which Argent artifacts deliberately do not
28
+ // set, so a `screen-recording` kind here does not add a player to the session page.
29
+ const ARGENT_ARTIFACT_KIND_BY_MIME_TYPE = new Map([
30
+ ['image/png', 'screenshot'],
31
+ ['image/jpeg', 'screenshot'],
32
+ ['video/mp4', 'screen-recording'],
33
+ ['video/quicktime', 'screen-recording'],
34
+ ]);
24
35
  const ArgentArtifactSchema = zod_1.z.object({
25
36
  id: zod_1.z.string(),
26
37
  filename: zod_1.z.string(),
@@ -30,6 +41,16 @@ const ArgentArtifactSchema = zod_1.z.object({
30
41
  const ArgentArtifactsListResponseSchema = zod_1.z.object({
31
42
  artifacts: zod_1.z.array(ArgentArtifactSchema),
32
43
  });
44
+ function getArgentArtifactKind(artifact) {
45
+ // Directories are repackaged as a tarball before upload, so the reported media type describes the
46
+ // contents rather than the file we actually store.
47
+ if (artifact.isDirectory) {
48
+ return undefined;
49
+ }
50
+ // Media types are case-insensitive and may carry parameters, e.g. `image/png; charset=binary`.
51
+ const mediaType = artifact.mimeType.split(';')[0].trim().toLowerCase();
52
+ return ARGENT_ARTIFACT_KIND_BY_MIME_TYPE.get(mediaType);
53
+ }
33
54
  async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, signal, }) {
34
55
  logger.info('Started polling Argent tool-server for artifacts.');
35
56
  const seenArtifactIds = new Set();
@@ -126,7 +147,7 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
126
147
  artifactId: artifact.id,
127
148
  name: `${filename} (${artifact.id})`,
128
149
  filename,
129
- kind: undefined,
150
+ kind: getArgentArtifactKind(artifact),
130
151
  size,
131
152
  stream: (0, node_fs_1.createReadStream)(temporaryArtifactPath),
132
153
  });
@@ -1,5 +1,6 @@
1
1
  import { type bunyan } from '@expo/logger';
2
2
  import { type CustomBuildContext } from '../../customBuildContext';
3
+ import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
3
4
  export declare const ARGENT_EVENT_LOG_FILENAME = "tool-server-events.jsonl";
4
5
  export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionId, eventLogPath, logger, pollIntervalMs, }: {
5
6
  ctx: CustomBuildContext;
@@ -7,6 +8,4 @@ export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionI
7
8
  eventLogPath: string;
8
9
  logger: bunyan;
9
10
  pollIntervalMs?: number;
10
- }): Promise<{
11
- stopAsync: () => Promise<void>;
12
- }>;
11
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -53,12 +53,20 @@ export type DeviceRunSessionEventSource = {
53
53
  deviceRunSessionId: string;
54
54
  }) => DeviceRunSessionEventParseResult;
55
55
  };
56
+ export type DeviceRunSessionEventCollection = {
57
+ stopAsync: () => Promise<void>;
58
+ /**
59
+ * Local arrival time of the most recently collected event, or `undefined`
60
+ * when no event has been observed yet. Uses the collector's clock rather
61
+ * than the event's embedded timestamp so producer clock skew cannot affect
62
+ * idle detection.
63
+ */
64
+ getLastEventObservedAt: () => Date | undefined;
65
+ };
56
66
  export declare function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSessionId, source, logger, pollIntervalMs, }: {
57
67
  ctx: CustomBuildContext;
58
68
  deviceRunSessionId: string;
59
69
  source: DeviceRunSessionEventSource;
60
70
  logger: bunyan;
61
71
  pollIntervalMs?: number;
62
- }): Promise<{
63
- stopAsync: () => Promise<void>;
64
- }>;
72
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -58,7 +58,13 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
58
58
  const error = err instanceof Error ? err : new Error(String(err));
59
59
  logger.warn({ err: error }, 'Could not persist device run session events to the artifact.');
60
60
  reportEventLogFailure(error, 'setup');
61
- return { stopAsync: async () => { } };
61
+ return {
62
+ stopAsync: async () => { },
63
+ // Collection is disabled, so session activity is invisible here. Report
64
+ // fresh activity so an enabled idle timeout never stops a session it
65
+ // cannot observe.
66
+ getLastEventObservedAt: () => new Date(),
67
+ };
62
68
  }
63
69
  let didReportRealtimeLogFailure = false;
64
70
  const reportRealtimeLogFailure = (error, operation) => {
@@ -83,6 +89,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
83
89
  }
84
90
  const states = new Map();
85
91
  const controller = new AbortController();
92
+ let lastEventObservedAt;
86
93
  let parseFailureCount = 0;
87
94
  const parseFailureCounts = {
88
95
  'invalid-json': 0,
@@ -106,6 +113,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
106
113
  source,
107
114
  deviceRunSessionId,
108
115
  writeEvent: event => {
116
+ lastEventObservedAt = new Date();
109
117
  eventLogStream.write(event);
110
118
  realtimeLogStream?.write({ ...event, logId: event.eventId });
111
119
  },
@@ -170,6 +178,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
170
178
  // A diagnostics failure must not recreate an unhandled rejection in this best-effort poller.
171
179
  });
172
180
  return {
181
+ getLastEventObservedAt: () => lastEventObservedAt,
173
182
  stopAsync: async () => {
174
183
  controller.abort();
175
184
  await pollingPromise;
@@ -15,11 +15,22 @@ export declare function selectXcodeDeveloperDirectoryAsync({ env, logger, }: {
15
15
  env: BuildStepEnv;
16
16
  logger: bunyan;
17
17
  }): Promise<void>;
18
- export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }: {
18
+ export type DeviceRunSessionIdleTimeout = {
19
+ /** Stop the session after this many minutes without observed activity. */
20
+ maxIdleTimeMinutes: number;
21
+ /**
22
+ * Local arrival time of the most recent session event, or `undefined` when
23
+ * no event has been observed yet. The idle clock starts when the wait
24
+ * begins, so a session nobody ever connects to still times out.
25
+ */
26
+ getLastEventObservedAt: () => Date | undefined;
27
+ };
28
+ export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }: {
19
29
  ctx: CustomBuildContext;
20
30
  deviceRunSessionId: string;
21
31
  logger: bunyan;
22
32
  signal?: AbortSignal;
33
+ idleTimeout?: DeviceRunSessionIdleTimeout;
23
34
  }): Promise<void>;
24
35
  /**
25
36
  * Install ffmpeg when the runtime does not already provide it, so argent's
@@ -97,6 +97,16 @@ const DEVICE_RUN_SESSION_STATUS_QUERY = (0, gql_tada_1.graphql)(`
97
97
  }
98
98
  }
99
99
  `);
100
+ const ENSURE_DEVICE_RUN_SESSION_STOPPED_MUTATION = (0, gql_tada_1.graphql)(`
101
+ mutation EnsureDeviceRunSessionStopped($deviceRunSessionId: ID!) {
102
+ deviceRunSession {
103
+ ensureDeviceRunSessionStopped(deviceRunSessionId: $deviceRunSessionId) {
104
+ id
105
+ status
106
+ }
107
+ }
108
+ }
109
+ `);
100
110
  const DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS = 5_000;
101
111
  function getDeviceRunSessionIdOrThrow(env) {
102
112
  const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
@@ -142,10 +152,26 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
142
152
  stdio: ['ignore', 'pipe', 'pipe'],
143
153
  });
144
154
  }
145
- async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }) {
155
+ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }) {
146
156
  logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
157
+ if (idleTimeout) {
158
+ logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
159
+ }
147
160
  let pollErrorCount = 0;
161
+ let lastActivityAt = new Date();
148
162
  while (!signal?.aborted) {
163
+ if (idleTimeout) {
164
+ const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
165
+ if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
166
+ lastActivityAt = lastEventObservedAt;
167
+ }
168
+ if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
169
+ logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
170
+ `${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
171
+ await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
172
+ return;
173
+ }
174
+ }
149
175
  try {
150
176
  const result = await ctx.graphqlClient
151
177
  .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
@@ -180,6 +206,26 @@ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, lo
180
206
  await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
181
207
  }
182
208
  }
209
+ // Best effort: when this fails, the caller still tears the session down and the
210
+ // job run finishes, which clients also treat as the session ending.
211
+ async function ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger, }) {
212
+ try {
213
+ const result = await ctx.graphqlClient
214
+ .mutation(ENSURE_DEVICE_RUN_SESSION_STOPPED_MUTATION, { deviceRunSessionId })
215
+ .toPromise();
216
+ if (result.error) {
217
+ throw result.error;
218
+ }
219
+ }
220
+ catch (err) {
221
+ const error = err instanceof Error ? err : new Error(String(err));
222
+ sentry_1.Sentry.capture('Could not mark idle device run session as stopped', error, {
223
+ level: 'warning',
224
+ extras: { deviceRunSessionId },
225
+ });
226
+ logger.warn({ err: error }, `Could not mark device run session ${deviceRunSessionId} as stopped. The session job ends anyway.`);
227
+ }
228
+ }
183
229
  async function sleepUntilAbortedAsync(timeoutMs, signal) {
184
230
  try {
185
231
  await (0, promises_1.setTimeout)(timeoutMs, undefined, signal ? { signal } : undefined);
@@ -20,17 +20,19 @@ async function uploadServeSimMetricsFileAsync(ctx, { deviceRunSessionId, udid, f
20
20
  logger.info(`serve-sim metrics file for ${udid} is empty; skipping upload.`);
21
21
  return;
22
22
  }
23
+ const deviceName = typeof metadata?.deviceName === 'string' ? metadata.deviceName : undefined;
23
24
  try {
24
- logger.info(`Uploading serve-sim metrics for ${udid} (${(0, artifacts_1.formatBytes)(size)}).`);
25
+ logger.info(`Uploading serve-sim metrics for ${deviceName ?? udid} (${(0, artifacts_1.formatBytes)(size)}).`);
25
26
  await (0, deviceRunSessionArtifacts_1.uploadDeviceRunSessionArtifactAsync)(ctx, {
26
27
  deviceRunSessionId,
27
28
  artifactId: `metrics-${udid}`,
28
- name: `Performance metrics (${udid.slice(0, 8)})`,
29
+ name: `Performance metrics (${deviceName ?? udid.slice(0, 8)})`,
29
30
  filename: METRICS_ARTIFACT_FILENAME,
30
31
  kind: 'performance-metrics',
31
32
  metadata: {
32
33
  __eas_type: 'performance-metrics',
33
34
  udid,
35
+ ...(deviceName && { deviceName }),
34
36
  ...(metadata && {
35
37
  hostCores: metadata.hostCores,
36
38
  sampleIntervalMs: metadata.sampleIntervalMs,
@@ -1,5 +1,7 @@
1
1
  import { SpawnOptions, SpawnResult } from '@expo/turtle-spawn';
2
2
  import { Env } from '@expo/eas-build-job';
3
+ /** Test-only: clears the memoized versions promise so each test fetches fresh. */
4
+ export declare function resetCachedEasCliVersionsForTest(): void;
3
5
  export declare function resolveEasCommandPrefixAndEnvAsync(): Promise<{
4
6
  cmd: string;
5
7
  args: string[];
@@ -3,11 +3,48 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resetCachedEasCliVersionsForTest = resetCachedEasCliVersionsForTest;
6
7
  exports.resolveEasCommandPrefixAndEnvAsync = resolveEasCommandPrefixAndEnvAsync;
7
8
  exports.runEasCliCommand = runEasCliCommand;
8
9
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
10
  const eas_build_job_1 = require("@expo/eas-build-job");
10
11
  const packageManager_1 = require("./packageManager");
12
+ const sentry_1 = require("../sentry");
13
+ let cachedEasCliVersionsPromise;
14
+ /**
15
+ * Resolves the `eas-cli` versions to install for staging/production. Prefers the
16
+ * versions committed to `cli-versions.json` (fetched from expo/eas-cli); on any
17
+ * failure (e.g. GitHub is unreachable or slow) reports to Sentry and falls back
18
+ * to the `latest-eas-build*` npm dist-tags so the build can still proceed.
19
+ *
20
+ * Memoized: `resolveEasCommandPrefixAndEnvAsync` runs more than once per build,
21
+ * so the (never-rejecting) result promise is cached to fetch GitHub at most once
22
+ * per process.
23
+ */
24
+ async function resolveEasCliVersionsAsync() {
25
+ if (!cachedEasCliVersionsPromise) {
26
+ cachedEasCliVersionsPromise = fetchEasCliVersionsWithFallbackAsync();
27
+ }
28
+ return await cachedEasCliVersionsPromise;
29
+ }
30
+ async function fetchEasCliVersionsWithFallbackAsync() {
31
+ try {
32
+ return await (0, eas_build_job_1.fetchEasCliVersionsAsync)();
33
+ }
34
+ catch (error) {
35
+ const message = error instanceof eas_build_job_1.EasCliVersionsFetchTimeoutError
36
+ ? 'Timed out fetching cli-versions.json; falling back to npm dist-tags'
37
+ : 'Failed to fetch cli-versions.json; falling back to npm dist-tags';
38
+ sentry_1.Sentry.capture(message, error instanceof Error ? error : new Error(String(error)), {
39
+ level: 'warning',
40
+ });
41
+ return { STAGING: eas_build_job_1.EasCliNpmTags.STAGING, PRODUCTION: eas_build_job_1.EasCliNpmTags.PRODUCTION };
42
+ }
43
+ }
44
+ /** Test-only: clears the memoized versions promise so each test fetches fresh. */
45
+ function resetCachedEasCliVersionsForTest() {
46
+ cachedEasCliVersionsPromise = undefined;
47
+ }
11
48
  async function probeEasdAsync() {
12
49
  try {
13
50
  const result = await (0, turtle_spawn_1.default)('easd', ['--help'], {
@@ -25,23 +62,26 @@ async function resolveEasCommandPrefixAndEnvAsync() {
25
62
  if (await probeEasdAsync()) {
26
63
  return { cmd: 'easd', args: [], extraEnv: {} };
27
64
  }
65
+ const versions = await resolveEasCliVersionsAsync();
28
66
  return {
29
67
  cmd: 'npx',
30
- args: [...npxArgsPrefix, `eas-cli@${eas_build_job_1.EasCliNpmTags.STAGING}`],
68
+ args: [...npxArgsPrefix, `eas-cli@${versions.STAGING}`],
31
69
  extraEnv: {},
32
70
  };
33
71
  }
34
72
  else if (process.env.ENVIRONMENT === 'staging') {
73
+ const versions = await resolveEasCliVersionsAsync();
35
74
  return {
36
75
  cmd: 'npx',
37
- args: [...npxArgsPrefix, `eas-cli@${eas_build_job_1.EasCliNpmTags.STAGING}`],
76
+ args: [...npxArgsPrefix, `eas-cli@${versions.STAGING}`],
38
77
  extraEnv: { EXPO_STAGING: '1' },
39
78
  };
40
79
  }
41
80
  else {
81
+ const versions = await resolveEasCliVersionsAsync();
42
82
  return {
43
83
  cmd: 'npx',
44
- args: [...npxArgsPrefix, `eas-cli@${eas_build_job_1.EasCliNpmTags.PRODUCTION}`],
84
+ args: [...npxArgsPrefix, `eas-cli@${versions.PRODUCTION}`],
45
85
  extraEnv: {},
46
86
  };
47
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "21.5.1",
3
+ "version": "21.7.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -39,14 +39,14 @@
39
39
  "@expo/config": "55.0.10",
40
40
  "@expo/config-plugins": "55.0.7",
41
41
  "@expo/downloader": "21.0.0",
42
- "@expo/eas-build-job": "21.5.0",
42
+ "@expo/eas-build-job": "21.6.0",
43
43
  "@expo/env": "^0.4.0",
44
44
  "@expo/logger": "21.0.0",
45
45
  "@expo/package-manager": "1.9.10",
46
46
  "@expo/plist": "^0.3.5",
47
47
  "@expo/results": "^1.0.0",
48
48
  "@expo/spawn-async": "1.7.2",
49
- "@expo/steps": "21.5.0",
49
+ "@expo/steps": "21.6.0",
50
50
  "@expo/template-file": "21.0.2",
51
51
  "@expo/turtle-spawn": "21.0.0",
52
52
  "@expo/xcpretty": "^4.3.1",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "7d7ebb27f6a9c9fccef53e6203cdeb59c7aa4ba0"
103
+ "gitHead": "f923181857de80919ec7ee9861c9b71c2ee5635d"
104
104
  }
@@ -1,11 +0,0 @@
1
- /**
2
- * Loads local function.yml files from the project and builds the catalog consumed by
3
- * {@link StepsConfigParser}. Keeps filesystem I/O in build-tools; expansion logic lives in @expo/steps.
4
- */
5
- import { bunyan } from '@expo/logger';
6
- import { CompositeFunctionCatalog, CompositeFunctionConfig, Step } from '@expo/eas-build-job';
7
- export declare function createCompositeFunctionLoader(projectRoot: string, logger?: bunyan): (compositeFunctionPath: string) => Promise<CompositeFunctionConfig>;
8
- export declare function buildCompositeFunctionCatalogAsync(projectRoot: string, { steps, logger }: {
9
- steps: readonly Step[];
10
- logger?: bunyan;
11
- }): Promise<CompositeFunctionCatalog>;
@@ -1,62 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createCompositeFunctionLoader = createCompositeFunctionLoader;
7
- exports.buildCompositeFunctionCatalogAsync = buildCompositeFunctionCatalogAsync;
8
- const eas_build_job_1 = require("@expo/eas-build-job");
9
- const steps_1 = require("@expo/steps");
10
- const promises_1 = __importDefault(require("fs/promises"));
11
- const path_1 = __importDefault(require("path"));
12
- const yaml_1 = __importDefault(require("yaml"));
13
- const zod_1 = require("zod");
14
- async function loadLocalCompositeFunctionConfigAsync(projectRoot, compositeFunctionPath, { logger } = {}) {
15
- const resolvedPath = (0, steps_1.resolveLocalCompositeFunctionPath)(projectRoot, compositeFunctionPath);
16
- for (const ext of ['yml', 'yaml']) {
17
- const absolutePath = path_1.default.join(resolvedPath, `function.${ext}`);
18
- let rawContents;
19
- try {
20
- rawContents = await promises_1.default.readFile(absolutePath, 'utf-8');
21
- }
22
- catch (err) {
23
- if (err?.code === 'ENOENT') {
24
- continue;
25
- }
26
- throw new Error(`Failed to read local composite function "${compositeFunctionPath}" from ${absolutePath}`, {
27
- cause: err,
28
- });
29
- }
30
- let parsed;
31
- try {
32
- parsed = yaml_1.default.parse(rawContents);
33
- }
34
- catch (err) {
35
- throw new Error(`Failed to parse local composite function "${compositeFunctionPath}" YAML at ${absolutePath}`, {
36
- cause: err,
37
- });
38
- }
39
- let config;
40
- try {
41
- config = eas_build_job_1.CompositeFunctionConfigZ.parse(parsed);
42
- }
43
- catch (err) {
44
- if (err instanceof zod_1.ZodError) {
45
- throw new Error(`Invalid composite function "${compositeFunctionPath}": ${zod_1.z.prettifyError(err)}`);
46
- }
47
- throw err;
48
- }
49
- logger?.debug(`Loaded local composite function "${compositeFunctionPath}" from ${path_1.default.relative(projectRoot, absolutePath)}`);
50
- return config;
51
- }
52
- throw new Error(`Local composite function "${compositeFunctionPath}" was referenced by a step but no such composite function exists. A local composite function is resolved from a "function.yml" (or "function.yaml") file at the referenced path relative to the EAS project root (e.g. "uses: ${compositeFunctionPath}" resolves "${compositeFunctionPath}/function.yml"). The recommended convention is to keep composite functions under ".eas/functions/<name>".`);
53
- }
54
- function createCompositeFunctionLoader(projectRoot, logger) {
55
- return compositeFunctionPath => loadLocalCompositeFunctionConfigAsync(projectRoot, compositeFunctionPath, { logger });
56
- }
57
- async function buildCompositeFunctionCatalogAsync(projectRoot, { steps, logger }) {
58
- return (0, steps_1.buildCompositeFunctionCatalogFromStepsAsync)({
59
- rootSteps: steps,
60
- loadCompositeFunction: createCompositeFunctionLoader(projectRoot, logger),
61
- });
62
- }