@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.
- package/dist/steps/easFunctions.js +2 -0
- package/dist/steps/functions/installMaestro.d.ts +6 -1
- package/dist/steps/functions/installMaestro.js +107 -33
- package/dist/steps/functions/maestroResultParser.d.ts +30 -0
- package/dist/steps/functions/maestroResultParser.js +78 -5
- package/dist/steps/functions/maestroScreenshots.d.ts +6 -0
- package/dist/steps/functions/maestroScreenshots.js +123 -0
- package/dist/steps/functions/maestroTests.js +187 -69
- package/dist/steps/functions/restoreBuildCache.js +2 -1
- package/dist/steps/functions/saveBuildCache.js +4 -2
- package/dist/steps/functions/saveCache.js +3 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +7 -0
- package/dist/steps/functions/startAppiumRemoteSession.d.ts +17 -0
- package/dist/steps/functions/startAppiumRemoteSession.js +244 -0
- package/dist/steps/functions/startArgentRemoteSession.js +7 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +10 -1
- package/dist/steps/utils/appiumEvents.d.ts +10 -0
- package/dist/steps/utils/appiumEvents.js +175 -0
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +2 -1
- package/dist/steps/utils/remoteDeviceRunSession.js +72 -46
- package/dist/utils/AndroidEmulatorUtils.d.ts +1 -0
- package/dist/utils/AndroidEmulatorUtils.js +46 -4
- package/package.json +3 -3
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
//
|
|
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
|
|
182
|
-
//
|
|
183
|
-
//
|
|
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 (
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
267
|
+
includeTags,
|
|
268
|
+
excludeTags,
|
|
228
269
|
});
|
|
229
|
-
logger.info(`Running
|
|
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)(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 &&
|
|
350
|
+
if (retryFailedOnly &&
|
|
351
|
+
(backend === 'maestro-runner' || (outputFormat === 'junit' && outputPath))) {
|
|
270
352
|
let failed;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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({
|
|
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,
|
|
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
|
-
|
|
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 {};
|