@expo/build-tools 22.0.0 → 22.2.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.
@@ -11,6 +11,7 @@ const promises_1 = __importDefault(require("fs/promises"));
11
11
  const os_1 = __importDefault(require("os"));
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const zod_1 = require("zod");
14
+ const maestroBackend_1 = require("./maestroBackend");
14
15
  const maestroFlowDiscovery_1 = require("./maestroFlowDiscovery");
15
16
  const maestroResultParser_1 = require("./maestroResultParser");
16
17
  const maestroScreenshots_1 = require("./maestroScreenshots");
@@ -36,28 +37,40 @@ function isFilesystemError(err) {
36
37
  const code = err.code;
37
38
  return (code === 'ENOSPC' || code === 'EACCES' || code === 'EROFS' || code === 'EIO' || code === 'EPERM');
38
39
  }
39
- // `outputPath: null` means "let maestro pick" (no --output flag). Junit and
40
- // other declared formats pass an explicit path so downstream upload steps
41
- // know where to find the result.
42
- function buildMaestroArgs(args) {
43
- const out = ['test'];
44
- if (args.output_format) {
45
- out.push(`--format=${args.output_format.toUpperCase()}`);
46
- }
47
- if (args.outputPath) {
48
- out.push(`--output=${args.outputPath}`);
49
- }
50
- if (args.shards !== undefined) {
51
- out.push(`--shard-split=${args.shards}`);
52
- }
53
- if (args.include_tags) {
54
- out.push(`--include-tags=${args.include_tags}`);
55
- }
56
- if (args.exclude_tags) {
57
- out.push(`--exclude-tags=${args.exclude_tags}`);
40
+ function buildMaestroArgs({ backend, platform, flowPaths, output, outputFormat, shards, includeTags, excludeTags, }) {
41
+ switch (backend) {
42
+ case 'maestro': {
43
+ const args = ['test'];
44
+ if (outputFormat) {
45
+ args.push(`--format=${outputFormat.toUpperCase()}`);
46
+ }
47
+ if (output) {
48
+ args.push(`--output=${output}`);
49
+ }
50
+ if (shards !== undefined) {
51
+ args.push(`--shard-split=${shards}`);
52
+ }
53
+ if (includeTags) {
54
+ args.push(`--include-tags=${includeTags}`);
55
+ }
56
+ if (excludeTags) {
57
+ args.push(`--exclude-tags=${excludeTags}`);
58
+ }
59
+ args.push(...flowPaths);
60
+ return { executable: 'maestro', args };
61
+ }
62
+ case 'maestro-runner': {
63
+ const args = [`--platform=${platform}`, 'test', `--output=${output}`, '--flatten'];
64
+ if (includeTags) {
65
+ args.push(`--include-tags=${includeTags}`);
66
+ }
67
+ if (excludeTags) {
68
+ args.push(`--exclude-tags=${excludeTags}`);
69
+ }
70
+ args.push(...flowPaths);
71
+ return { executable: 'maestro-runner', args };
72
+ }
58
73
  }
59
- out.push(...args.flow_path);
60
- return out;
61
74
  }
62
75
  function createMaestroTestsBuildFunction(ctx) {
63
76
  return new steps_1.BuildFunction({
@@ -115,6 +128,11 @@ function createMaestroTestsBuildFunction(ctx) {
115
128
  required: false,
116
129
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
117
130
  }),
131
+ steps_1.BuildStepInput.createProvider({
132
+ id: 'backend',
133
+ required: false,
134
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
135
+ }),
118
136
  ],
119
137
  outputProviders: [
120
138
  steps_1.BuildStepOutput.createProvider({ id: 'junit_report_directory', required: true }),
@@ -156,12 +174,25 @@ function createMaestroTestsBuildFunction(ctx) {
156
174
  if (finalReportPath !== undefined) {
157
175
  outputs.final_report_path.set(finalReportPath);
158
176
  }
177
+ // Resolved after the output assignments above: an invalid backend input or
178
+ // EAS_MAESTRO_BACKEND throws, and downstream `if: always()` upload steps still
179
+ // need the outputs interpolated.
180
+ const backend = (0, maestroBackend_1.resolveMaestroBackend)({
181
+ input: inputs.backend.value,
182
+ env,
183
+ });
159
184
  const flowPaths = parseInput(FlowPathSchema, inputs.flow_path.value, 'flow_path must be a non-empty array of non-empty strings.');
160
185
  const retries = parseInput(RetriesSchema, inputs.retries.value, 'retries must be a non-negative integer.');
161
186
  const shards = parseInput(ShardsSchema, inputs.shards.value, 'shards must be a positive integer.');
162
187
  const androidConnectionMode = parseInput(AndroidConnectionModeSchema, inputs.android_connection_mode.value ||
163
188
  env.EAS_MAESTRO_ANDROID_CONNECTION_MODE ||
164
189
  undefined, 'android_connection_mode and EAS_MAESTRO_ANDROID_CONNECTION_MODE must be either "adb" or "dadb".');
190
+ if (backend === 'maestro-runner' && shards !== undefined && shards > 1) {
191
+ throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', 'maestro-runner does not support EAS Maestro test sharding. Remove shards or set it to 1.');
192
+ }
193
+ if (backend === 'maestro-runner' && outputFormat !== undefined && outputFormat !== 'junit') {
194
+ throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', `maestro-runner only supports the "junit" output_format, but received "${outputFormat}".`);
195
+ }
165
196
  const retryFailedOnly = inputs.retry_failed_only.value;
166
197
  try {
167
198
  await promises_1.default.mkdir(junitReportDirectory, { recursive: true });
@@ -169,7 +200,7 @@ function createMaestroTestsBuildFunction(ctx) {
169
200
  catch (err) {
170
201
  throw new eas_build_job_1.SystemError('Failed to create JUnit report directory', { cause: err });
171
202
  }
172
- // Legacy-only (Maestro < 2.6.0 reports carry no `file=` attribute): the
203
+ // Official Maestro legacy-only (Maestro < 2.6.0 reports carry no `file=` attribute): the
173
204
  // flow scan is built lazily in the retry branch below and memoized so
174
205
  // retries share one scan. Never runs when the report has `file=`.
175
206
  let nameToPathPromise;
@@ -178,14 +209,20 @@ function createMaestroTestsBuildFunction(ctx) {
178
209
  // numeric err.status → maestro exited non-zero → retry.
179
210
  // else (signal-only, OOM kill, unknown) → infra → SystemError, never
180
211
  // downgraded to "tests failed".
181
- // Retry-failed-only (junit mode): after a failed attempt, subset to the failing
182
- // flows. The failed-flow parsers return null when the JUnit cannot be
183
- // trusted; we then fall through to dumb retry (re-run everything).
212
+ // Retry-failed-only: after a failed attempt, subset to the failing flows. The
213
+ // failed-flow parsers return null when a report cannot be trusted; we then fall
214
+ // through to dumb retry (re-run everything).
184
215
  let flowsToRun = flowPaths;
185
216
  let lastAttemptExitCode = null;
186
217
  const harvested = [];
218
+ const reportDirectories = backend === 'maestro' ? [junitReportDirectory] : [];
187
219
  const totalAttempts = retries + 1;
188
- if (platform === 'android' && androidConnectionMode === 'dadb') {
220
+ if (backend === 'maestro-runner' &&
221
+ platform === 'android' &&
222
+ androidConnectionMode === 'dadb') {
223
+ logger.info('maestro-runner does not support DADB. Using the default ADB connection.');
224
+ }
225
+ if (backend === 'maestro' && platform === 'android' && androidConnectionMode === 'dadb') {
189
226
  try {
190
227
  const adbOverrideDirectoryPath = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'maestro-tests-adb-override-'));
191
228
  await promises_1.default.writeFile(path_1.default.join(adbOverrideDirectoryPath, 'adb'), '#!/bin/sh\nexit 1\n', {
@@ -213,23 +250,37 @@ function createMaestroTestsBuildFunction(ctx) {
213
250
  // Do not restart ADB or remove the override. This keeps Maestro in direct DADB mode.
214
251
  }
215
252
  for (let attempt = 0; attempt <= retries; attempt++) {
253
+ // maestro-runner writes its JUnit report and screenshot metadata to this directory.
254
+ const runnerOutputDirectory = path_1.default.join(testsDirectory, `${platform}-maestro-runner-attempt-${attempt}`);
216
255
  const outputPath = outputFormat === 'junit'
217
256
  ? path_1.default.join(junitReportDirectory, `${platform}-maestro-junit-attempt-${attempt}.xml`)
218
- : outputFormat
257
+ : backend === 'maestro' && outputFormat
219
258
  ? path_1.default.join(testsDirectory, `${platform}-maestro-${outputFormat}.${outputFormat}`)
220
259
  : null;
221
- const maestroArgs = buildMaestroArgs({
222
- flow_path: flowsToRun,
223
- outputPath,
224
- output_format: outputFormat,
260
+ const { executable, args: maestroArgs } = buildMaestroArgs({
261
+ backend,
262
+ platform,
263
+ flowPaths: flowsToRun,
264
+ output: backend === 'maestro-runner' ? runnerOutputDirectory : outputPath,
265
+ outputFormat,
225
266
  shards,
226
- include_tags: includeTags,
227
- exclude_tags: excludeTags,
267
+ includeTags,
268
+ excludeTags,
228
269
  });
229
- logger.info(`Running maestro (attempt ${attempt + 1}/${totalAttempts}): maestro ${maestroArgs.join(' ')}`);
270
+ logger.info(`Running ${executable} (attempt ${attempt + 1}/${totalAttempts}): ${executable} ${maestroArgs.join(' ')}`);
271
+ // The runner output directory is deterministic and can survive a prior run; clear it
272
+ // best-effort so a crash before it writes fresh output can't resurrect stale results.
273
+ if (backend === 'maestro-runner') {
274
+ try {
275
+ await promises_1.default.rm(runnerOutputDirectory, { recursive: true, force: true });
276
+ }
277
+ catch (err) {
278
+ logger.warn({ err }, `Failed to clear ${runnerOutputDirectory} before the attempt.`);
279
+ }
280
+ }
230
281
  const attemptStartedAtMs = Date.now();
231
282
  try {
232
- await (0, turtle_spawn_1.default)('maestro', maestroArgs, {
283
+ await (0, turtle_spawn_1.default)(executable, maestroArgs, {
233
284
  cwd: stepCtx.workingDirectory,
234
285
  env: spawnEnv,
235
286
  logger,
@@ -239,53 +290,100 @@ function createMaestroTestsBuildFunction(ctx) {
239
290
  }
240
291
  catch (err) {
241
292
  if (err && (err.code === 'ENOENT' || err.code === 'EACCES')) {
242
- throw new eas_build_job_1.SystemError('Failed to invoke maestro', { cause: err });
293
+ throw new eas_build_job_1.SystemError(`Failed to invoke ${executable}`, { cause: err });
243
294
  }
244
295
  if (err && typeof err.status === 'number') {
245
296
  lastAttemptExitCode = err.status;
246
297
  }
247
298
  else {
248
- throw new eas_build_job_1.SystemError('Unexpected spawn failure invoking maestro', { cause: err });
299
+ throw new eas_build_job_1.SystemError(`Unexpected spawn failure invoking ${executable}`, {
300
+ cause: err,
301
+ });
302
+ }
303
+ }
304
+ // maestro-runner writes a report directory. Copy its JUnit file into the existing
305
+ // per-attempt directory so retry parsing and final report merging remain shared.
306
+ if (backend === 'maestro-runner' && outputPath) {
307
+ try {
308
+ await promises_1.default.copyFile(path_1.default.join(runnerOutputDirectory, 'junit-report.xml'), outputPath);
309
+ }
310
+ catch (err) {
311
+ logger.warn({ err }, `Failed to collect the maestro-runner JUnit report for attempt ${attempt + 1}.`);
249
312
  }
250
313
  }
314
+ if (backend === 'maestro-runner') {
315
+ reportDirectories.push(runnerOutputDirectory);
316
+ }
251
317
  // Harvest this attempt's failure screenshots before any retry subsetting. Gated on
252
318
  // junit: test-case-result rows (and therefore the summary icons) only exist for junit
253
319
  // runs, so harvesting other formats would just create orphan artifacts the website hides.
254
320
  if (outputFormat === 'junit') {
255
- const failedFlowNames = outputPath
256
- ? await (0, maestroResultParser_1.parseFailedFlowNamesFromJUnitFile)(outputPath)
257
- : new Set();
258
- harvested.push(...(await (0, maestroScreenshots_1.harvestFailureScreenshotsAsync)({
259
- testsDirectory,
260
- capturedSinceMs: attemptStartedAtMs,
261
- attemptIndex: attempt,
262
- failedFlowNames,
263
- logger,
264
- })));
321
+ let screenshots;
322
+ switch (backend) {
323
+ case 'maestro': {
324
+ const failedFlowNames = outputPath
325
+ ? await (0, maestroResultParser_1.parseFailedFlowNamesFromJUnitFile)(outputPath)
326
+ : new Set();
327
+ screenshots = await (0, maestroScreenshots_1.harvestFailureScreenshotsAsync)({
328
+ testsDirectory,
329
+ capturedSinceMs: attemptStartedAtMs,
330
+ attemptIndex: attempt,
331
+ failedFlowNames,
332
+ logger,
333
+ });
334
+ break;
335
+ }
336
+ case 'maestro-runner':
337
+ screenshots = await (0, maestroScreenshots_1.harvestMaestroRunnerFailureScreenshotsAsync)({
338
+ reportDirectory: runnerOutputDirectory,
339
+ capturedSinceMs: attemptStartedAtMs,
340
+ attemptIndex: attempt,
341
+ logger,
342
+ });
343
+ break;
344
+ }
345
+ harvested.push(...screenshots);
265
346
  }
266
347
  if (lastAttemptExitCode === 0 || attempt === retries) {
267
348
  break;
268
349
  }
269
- if (retryFailedOnly && outputFormat === 'junit' && outputPath) {
350
+ if (retryFailedOnly &&
351
+ (backend === 'maestro-runner' || (outputFormat === 'junit' && outputPath))) {
270
352
  let failed;
271
- if (await (0, maestroResultParser_1.junitFileHasFileAttrs)(outputPath)) {
272
- failed = await (0, maestroResultParser_1.parseFailedFlowsFromFileAttrs)({
273
- junitFile: outputPath,
274
- workingDirectory: stepCtx.workingDirectory,
275
- });
276
- }
277
- else {
278
- // Legacy (Maestro < 2.6.0): map failed testcase names back to flow
279
- // paths via the flow-file scan. DELETE this arm once the fleet is
280
- // on >= 2.6.0.
281
- const nameToPath = await (nameToPathPromise ??= (0, maestroFlowDiscovery_1.buildFlowNameToPathMap)({
282
- inputFlowPaths: flowPaths,
283
- projectRoot: stepCtx.workingDirectory,
284
- logger,
285
- }));
286
- failed = nameToPath
287
- ? await (0, maestroResultParser_1.parseFailedFlowsFromJUnit)({ junitFile: outputPath, nameToPath })
288
- : null;
353
+ switch (backend) {
354
+ case 'maestro-runner':
355
+ failed = await (0, maestroResultParser_1.parseFailedFlowsFromMaestroRunnerReport)({
356
+ reportDirectory: runnerOutputDirectory,
357
+ workingDirectory: stepCtx.workingDirectory,
358
+ });
359
+ break;
360
+ case 'maestro':
361
+ if (!outputPath) {
362
+ failed = null;
363
+ break;
364
+ }
365
+ if (await (0, maestroResultParser_1.junitFileHasFileAttrs)(outputPath)) {
366
+ failed = await (0, maestroResultParser_1.parseFailedFlowsFromFileAttrs)({
367
+ junitFile: outputPath,
368
+ workingDirectory: stepCtx.workingDirectory,
369
+ });
370
+ break;
371
+ }
372
+ // Legacy (Maestro < 2.6.0): map failed testcase names back to flow
373
+ // paths via the flow-file scan. DELETE this arm once the fleet is
374
+ // on >= 2.6.0.
375
+ const nameToPath = await (nameToPathPromise ??= (0, maestroFlowDiscovery_1.buildFlowNameToPathMap)({
376
+ inputFlowPaths: flowPaths,
377
+ projectRoot: stepCtx.workingDirectory,
378
+ logger,
379
+ }));
380
+ failed = nameToPath
381
+ ? await (0, maestroResultParser_1.parseFailedFlowsFromJUnit)({
382
+ junitFile: outputPath,
383
+ nameToPath,
384
+ })
385
+ : null;
386
+ break;
289
387
  }
290
388
  if (failed !== null && failed.length > 0) {
291
389
  flowsToRun = failed;
@@ -335,7 +433,13 @@ function createMaestroTestsBuildFunction(ctx) {
335
433
  // Upload before the ERR_MAESTRO_TESTS_FAILED throw below so fully-failed runs (which need
336
434
  // screenshots most) still upload. Harvest only ran for junit, so guard the same way.
337
435
  if (outputFormat === 'junit') {
338
- await uploadFailureScreenshotsAsync({ harvested, junitReportDirectory, ctx, logger });
436
+ await uploadFailureScreenshotsAsync({
437
+ harvested,
438
+ backend,
439
+ reportDirectories,
440
+ ctx,
441
+ logger,
442
+ });
339
443
  }
340
444
  // The retry loop exits via success (0), numeric status (retryable),
341
445
  // or throw (infra). A non-null non-zero status means the user's tests
@@ -349,14 +453,28 @@ function createMaestroTestsBuildFunction(ctx) {
349
453
  // Reduce harvested failure screenshots to what's worth uploading, then upload them as workflow
350
454
  // artifacts. Best-effort and verdict-neutral: never throws, so a screenshot problem can't mask
351
455
  // the maestro test result. Caller guards on junit (harvest only runs for junit).
352
- async function uploadFailureScreenshotsAsync({ harvested, junitReportDirectory, ctx, logger, }) {
456
+ async function uploadFailureScreenshotsAsync({ harvested, backend, reportDirectories, ctx, logger, }) {
353
457
  // Reduce to the attempts worth uploading — every failed attempt for flaky flows, only the final
354
458
  // attempt for all-failed flows. See computePureFailureFlowNames / selectFailureScreenshots.
355
459
  // Guard the JUnit re-parse so a malformed/missing report can't throw past here and mask the
356
460
  // test verdict (the whole step is verdict-neutral for screenshots).
357
461
  let selected;
358
462
  try {
359
- const pureFailureFlowNames = (0, maestroScreenshots_1.computePureFailureFlowNames)(await (0, maestroResultParser_1.parseJUnitTestCases)(junitReportDirectory));
463
+ let flowResults;
464
+ switch (backend) {
465
+ case 'maestro':
466
+ flowResults = (await Promise.all(reportDirectories.map(directory => (0, maestroResultParser_1.parseJUnitTestCases)(directory)))).flat();
467
+ break;
468
+ case 'maestro-runner': {
469
+ const results = await Promise.all(reportDirectories.map(directory => (0, maestroResultParser_1.parseMaestroRunnerReport)(directory)));
470
+ // Use whichever reports parsed. An unreadable report (e.g. a final retry that crashed
471
+ // before writing report.json) contributes no flows rather than discarding screenshots
472
+ // harvested from the attempts that did report — mirroring the maestro path above.
473
+ flowResults = results.flatMap(result => result?.flows ?? []);
474
+ break;
475
+ }
476
+ }
477
+ const pureFailureFlowNames = (0, maestroScreenshots_1.computePureFailureFlowNames)(flowResults);
360
478
  selected = (0, maestroScreenshots_1.selectFailureScreenshots)(harvested, pureFailureFlowNames);
361
479
  }
362
480
  catch (err) {
@@ -201,12 +201,13 @@ async function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets,
201
201
  logger.info(`Restoring Gradle cache key: ${cacheKey}`);
202
202
  const gradleCachesPath = path_1.default.join(os_1.default.homedir(), '.gradle', 'caches');
203
203
  const buildCachePath = path_1.default.join(gradleCachesPath, 'build-cache-1');
204
+ const journalPath = path_1.default.join(gradleCachesPath, 'journal-1');
204
205
  const { archivePath, matchedKey } = await (0, restoreCache_1.downloadCacheAsync)({
205
206
  logger,
206
207
  jobId,
207
208
  expoApiServerURL,
208
209
  robotAccessToken,
209
- paths: [buildCachePath],
210
+ paths: [buildCachePath, journalPath],
210
211
  key: cacheKey,
211
212
  keyPrefixes: [gradleCacheKey_1.GRADLE_CACHE_KEY_PREFIX],
212
213
  platform: eas_build_job_1.Platform.ANDROID,
@@ -117,6 +117,7 @@ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, })
117
117
  }
118
118
  const gradleCachesPath = path_1.default.join(os_1.default.homedir(), '.gradle', 'caches');
119
119
  const buildCachePath = path_1.default.join(gradleCachesPath, 'build-cache-1');
120
+ const journalPath = path_1.default.join(gradleCachesPath, 'journal-1');
120
121
  try {
121
122
  await fs_1.default.promises.access(buildCachePath);
122
123
  }
@@ -130,9 +131,10 @@ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, })
130
131
  const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
131
132
  const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
132
133
  const expoApiServerURL = (0, nullthrows_1.default)(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
134
+ await fs_1.default.promises.mkdir(journalPath, { recursive: true });
133
135
  logger.info('Compressing Gradle build cache...');
134
136
  const { archivePath } = await (0, saveCache_1.compressCacheAsync)({
135
- paths: [buildCachePath],
137
+ paths: [buildCachePath, journalPath],
136
138
  workingDirectory: gradleCachesPath,
137
139
  verbose: env.EXPO_DEBUG === '1',
138
140
  logger,
@@ -146,7 +148,7 @@ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, })
146
148
  robotAccessToken,
147
149
  archivePath,
148
150
  key: cacheKey,
149
- paths: [buildCachePath],
151
+ paths: [buildCachePath, journalPath],
150
152
  size,
151
153
  platform: eas_build_job_1.Platform.ANDROID,
152
154
  });
@@ -303,7 +303,10 @@ async function compressCacheAsync({ paths, workingDirectory, verbose, logger, })
303
303
  for (const { absolutePath, archivePath: targetRelativePath } of allFiles) {
304
304
  const targetPath = path_1.default.join(tempDir, targetRelativePath);
305
305
  await fs_1.default.promises.mkdir(path_1.default.dirname(targetPath), { recursive: true });
306
+ // We want to keep source timestamps since Gradle may check them when pruning cache.
307
+ const { atime, mtime } = await fs_1.default.promises.stat(absolutePath);
306
308
  await fs_1.default.promises.copyFile(absolutePath, targetPath);
309
+ await fs_1.default.promises.utimes(targetPath, atime, mtime);
307
310
  if (verbose) {
308
311
  logger.info(`- ${targetRelativePath}`);
309
312
  }
@@ -42,6 +42,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
42
42
  required: false,
43
43
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
44
44
  }),
45
+ steps_1.BuildStepInput.createProvider({
46
+ id: 'max_duration_seconds',
47
+ required: false,
48
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
49
+ }),
45
50
  ],
46
51
  fn: async ({ logger, global }, { inputs, env, signal }) => {
47
52
  // Fail fast before any expensive setup if the injected env
@@ -54,6 +59,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
54
59
  const packageVersion = inputs.package_version.value;
55
60
  // A missing or non-positive value disables the idle timeout (opt-in feature).
56
61
  const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
62
+ const maxDurationSeconds = inputs.max_duration_seconds?.value;
57
63
  const { runtimePlatform } = global;
58
64
  logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
59
65
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
@@ -115,6 +121,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
115
121
  ctx,
116
122
  deviceRunSessionId,
117
123
  logger,
124
+ maxDurationSeconds,
118
125
  signal,
119
126
  idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
120
127
  ? {
@@ -0,0 +1,17 @@
1
+ import { type bunyan } from '@expo/logger';
2
+ import { BuildFunction, BuildRuntimePlatform, type BuildStepEnv } from '@expo/steps';
3
+ import { type CustomBuildContext } from '../../customBuildContext';
4
+ export declare function createStartAppiumRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
5
+ export declare function resolveAppium3VersionSpec(packageVersion: string | undefined): string;
6
+ type AppiumDevice = {
7
+ platformName: 'iOS' | 'Android';
8
+ automationName: 'XCUITest' | 'UiAutomator2';
9
+ driverName: 'xcuitest' | 'uiautomator2';
10
+ udid: string;
11
+ };
12
+ export declare function resolveAppiumDeviceAsync({ runtimePlatform, env, logger, }: {
13
+ runtimePlatform: BuildRuntimePlatform;
14
+ env: BuildStepEnv;
15
+ logger: bunyan;
16
+ }): Promise<AppiumDevice>;
17
+ export {};