@expo/build-tools 22.0.0 → 22.3.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/builders/android.js +2 -2
- package/dist/builders/ios.js +2 -2
- package/dist/steps/easFunctions.js +6 -0
- package/dist/steps/functions/downloadBuild.d.ts +9 -2
- package/dist/steps/functions/downloadBuild.js +83 -14
- package/dist/steps/functions/installBuild.d.ts +12 -0
- package/dist/steps/functions/installBuild.js +85 -0
- package/dist/steps/functions/installMaestro.d.ts +6 -1
- package/dist/steps/functions/installMaestro.js +107 -33
- package/dist/steps/functions/launchApplication.d.ts +10 -0
- package/dist/steps/functions/launchApplication.js +67 -0
- 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/readIpaInfo.d.ts +1 -0
- package/dist/steps/functions/readIpaInfo.js +2 -0
- package/dist/steps/functions/repack.d.ts +5 -2
- package/dist/steps/functions/repack.js +50 -2
- package/dist/steps/functions/restoreBuildCache.d.ts +3 -3
- package/dist/steps/functions/restoreBuildCache.js +21 -7
- package/dist/steps/functions/saveBuildCache.d.ts +3 -3
- package/dist/steps/functions/saveBuildCache.js +21 -6
- 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 +17 -1
- package/dist/steps/functions/uploadToAsc.js +11 -1
- package/dist/steps/utils/appiumEvents.d.ts +10 -0
- package/dist/steps/utils/appiumEvents.js +175 -0
- package/dist/steps/utils/ios/AscApiClient.d.ts +1 -0
- package/dist/steps/utils/ios/AscApiUtils.d.ts +11 -2
- package/dist/steps/utils/ios/AscApiUtils.js +36 -2
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +6 -3
- package/dist/steps/utils/remoteDeviceRunSession.js +81 -52
- package/dist/utils/AndroidEmulatorUtils.d.ts +1 -0
- package/dist/utils/AndroidEmulatorUtils.js +46 -4
- package/dist/utils/cacheKey.d.ts +8 -2
- package/dist/utils/cacheKey.js +12 -8
- package/package.json +6 -4
|
@@ -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) {
|
|
@@ -3,6 +3,7 @@ export type IpaInfo = {
|
|
|
3
3
|
bundleIdentifier: string;
|
|
4
4
|
bundleShortVersion: string;
|
|
5
5
|
bundleVersion: string;
|
|
6
|
+
dtPlatformName: string | null;
|
|
6
7
|
};
|
|
7
8
|
export declare function createReadIpaInfoBuildFunction(): BuildFunction;
|
|
8
9
|
export declare function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo>;
|
|
@@ -70,10 +70,12 @@ async function readIpaInfoAsync(ipaPath) {
|
|
|
70
70
|
if (typeof bundleVersion !== 'string') {
|
|
71
71
|
throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleVersion in Info.plist');
|
|
72
72
|
}
|
|
73
|
+
const dtPlatformName = typeof infoPlist.DTPlatformName === 'string' ? infoPlist.DTPlatformName : null;
|
|
73
74
|
return {
|
|
74
75
|
bundleIdentifier,
|
|
75
76
|
bundleShortVersion,
|
|
76
77
|
bundleVersion,
|
|
78
|
+
dtPlatformName,
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
catch (error) {
|
|
@@ -11,11 +11,14 @@ export declare function resolveAndroidSigningOptionsAsync({ job, tmpDir, }: {
|
|
|
11
11
|
tmpDir: string;
|
|
12
12
|
}): Promise<AndroidSigningOptions | undefined>;
|
|
13
13
|
/**
|
|
14
|
-
* Resolves iOS signing options from the job secrets
|
|
14
|
+
* Resolves iOS signing options from the job secrets, dispatching on the
|
|
15
|
+
* requested signing backend.
|
|
15
16
|
*/
|
|
16
|
-
export declare function resolveIosSigningOptionsAsync({ job, logger, useAppEntitlements, entitlementsPath, }: {
|
|
17
|
+
export declare function resolveIosSigningOptionsAsync({ job, logger, backend, useAppEntitlements, entitlementsPath, tmpDir, }: {
|
|
17
18
|
job: Job;
|
|
18
19
|
logger: bunyan;
|
|
20
|
+
backend?: 'fastlane' | 'zsign';
|
|
19
21
|
useAppEntitlements?: boolean;
|
|
20
22
|
entitlementsPath?: string;
|
|
23
|
+
tmpDir: string;
|
|
21
24
|
}): Promise<IosSigningOptions | undefined>;
|
|
@@ -53,6 +53,12 @@ function createRepackBuildFunction() {
|
|
|
53
53
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
54
54
|
required: false,
|
|
55
55
|
}),
|
|
56
|
+
steps_1.BuildStepInput.createProvider({
|
|
57
|
+
id: 'ios_signing_backend',
|
|
58
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
59
|
+
required: false,
|
|
60
|
+
allowedValues: ['fastlane', 'zsign'],
|
|
61
|
+
}),
|
|
56
62
|
steps_1.BuildStepInput.createProvider({
|
|
57
63
|
id: 'repack_version',
|
|
58
64
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
@@ -123,8 +129,10 @@ function createRepackBuildFunction() {
|
|
|
123
129
|
iosSigningOptions: await resolveIosSigningOptionsAsync({
|
|
124
130
|
job: stepsCtx.global.staticContext.job,
|
|
125
131
|
logger: stepsCtx.logger,
|
|
132
|
+
backend: inputs.ios_signing_backend.value,
|
|
126
133
|
useAppEntitlements: inputs.ios_signing_use_source_app_entitlements.value,
|
|
127
134
|
entitlementsPath: inputs.ios_signing_app_entitlements_path.value,
|
|
135
|
+
tmpDir,
|
|
128
136
|
}),
|
|
129
137
|
logger: stepsCtx.logger,
|
|
130
138
|
spawnAsync: repackSpawnAsync,
|
|
@@ -234,14 +242,26 @@ async function resolveAndroidSigningOptionsAsync({ job, tmpDir, }) {
|
|
|
234
242
|
};
|
|
235
243
|
}
|
|
236
244
|
/**
|
|
237
|
-
* Resolves iOS signing options from the job secrets
|
|
245
|
+
* Resolves iOS signing options from the job secrets, dispatching on the
|
|
246
|
+
* requested signing backend.
|
|
238
247
|
*/
|
|
239
|
-
async function resolveIosSigningOptionsAsync({ job, logger, useAppEntitlements, entitlementsPath, }) {
|
|
248
|
+
async function resolveIosSigningOptionsAsync({ job, logger, backend, useAppEntitlements, entitlementsPath, tmpDir, }) {
|
|
240
249
|
const iosJob = job;
|
|
241
250
|
const buildCredentials = iosJob.secrets?.buildCredentials;
|
|
242
251
|
if (iosJob.simulator || buildCredentials == null) {
|
|
243
252
|
return undefined;
|
|
244
253
|
}
|
|
254
|
+
const commonOptions = { buildCredentials, logger, useAppEntitlements, entitlementsPath };
|
|
255
|
+
return backend === 'zsign'
|
|
256
|
+
? await createIosZsignOptionsAsync({ ...commonOptions, tmpDir })
|
|
257
|
+
: await createIosFastlaneOptionsAsync(commonOptions);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Creates signing options for the fastlane backend: certificates are imported
|
|
261
|
+
* into a temporary keychain and provisioning profiles are parsed with the
|
|
262
|
+
* macOS `security` tool.
|
|
263
|
+
*/
|
|
264
|
+
async function createIosFastlaneOptionsAsync({ buildCredentials, logger, useAppEntitlements, entitlementsPath, }) {
|
|
245
265
|
const credentialsManager = new manager_1.default(buildCredentials);
|
|
246
266
|
const credentials = await credentialsManager.prepare(logger);
|
|
247
267
|
const provisioningProfile = {};
|
|
@@ -256,3 +276,31 @@ async function resolveIosSigningOptionsAsync({ job, logger, useAppEntitlements,
|
|
|
256
276
|
entitlementsPath,
|
|
257
277
|
};
|
|
258
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Creates signing options for the zsign backend. The distribution certificate
|
|
281
|
+
* secret is already a PKCS#12 file, so it goes to disk as-is together with the
|
|
282
|
+
* provisioning profiles.
|
|
283
|
+
*/
|
|
284
|
+
async function createIosZsignOptionsAsync({ buildCredentials, logger, useAppEntitlements, entitlementsPath, tmpDir, }) {
|
|
285
|
+
const targets = Object.entries(buildCredentials);
|
|
286
|
+
const [targetName, targetCredentials] = targets[0];
|
|
287
|
+
logger.info(`Using the distribution certificate from target '${targetName}' for zsign`);
|
|
288
|
+
const certificatePath = node_path_1.default.join(tmpDir, `dist-cert-${(0, node_crypto_1.randomUUID)()}.p12`);
|
|
289
|
+
await node_fs_1.default.promises.writeFile(certificatePath, new Uint8Array(Buffer.from(targetCredentials.distributionCertificate.dataBase64, 'base64')));
|
|
290
|
+
// zsign matches profiles to bundles by the app-id suffix itself, so the
|
|
291
|
+
// record keys are informational only.
|
|
292
|
+
const provisioningProfile = {};
|
|
293
|
+
for (const [target, credentials] of targets) {
|
|
294
|
+
const profilePath = node_path_1.default.join(tmpDir, `profile-${target}-${(0, node_crypto_1.randomUUID)()}.mobileprovision`);
|
|
295
|
+
await node_fs_1.default.promises.writeFile(profilePath, new Uint8Array(Buffer.from(credentials.provisioningProfileBase64, 'base64')));
|
|
296
|
+
provisioningProfile[target] = profilePath;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
backend: 'zsign',
|
|
300
|
+
certificatePath,
|
|
301
|
+
keyPassword: targetCredentials.distributionCertificate.password,
|
|
302
|
+
provisioningProfile,
|
|
303
|
+
useAppEntitlements,
|
|
304
|
+
entitlementsPath,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Platform } from '@expo/eas-build-job';
|
|
2
1
|
import { bunyan } from '@expo/logger';
|
|
3
2
|
import { BuildFunction } from '@expo/steps';
|
|
3
|
+
import { CcacheBuildTarget } from '../../utils/cacheKey';
|
|
4
4
|
export declare function createRestoreBuildCacheFunction(): BuildFunction;
|
|
5
5
|
export declare function createCacheStatsBuildFunction(): BuildFunction;
|
|
6
|
-
export declare function restoreCcacheAsync({ logger, workingDirectory,
|
|
6
|
+
export declare function restoreCcacheAsync({ logger, workingDirectory, target, env, secrets, }: {
|
|
7
7
|
logger: bunyan;
|
|
8
8
|
workingDirectory: string;
|
|
9
|
-
|
|
9
|
+
target: CcacheBuildTarget;
|
|
10
10
|
env: Record<string, string | undefined>;
|
|
11
11
|
secrets?: {
|
|
12
12
|
robotAccessToken?: string;
|
|
@@ -33,6 +33,11 @@ function createRestoreBuildCacheFunction() {
|
|
|
33
33
|
required: false,
|
|
34
34
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
35
35
|
}),
|
|
36
|
+
steps_1.BuildStepInput.createProvider({
|
|
37
|
+
id: 'simulator',
|
|
38
|
+
required: false,
|
|
39
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
|
|
40
|
+
}),
|
|
36
41
|
],
|
|
37
42
|
fn: async (stepCtx, { env, inputs }) => {
|
|
38
43
|
const { logger } = stepCtx;
|
|
@@ -42,10 +47,18 @@ function createRestoreBuildCacheFunction() {
|
|
|
42
47
|
if (!platform || ![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
|
|
43
48
|
throw new Error(`Unsupported platform: ${platform}. Platform must be "${eas_build_job_1.Platform.ANDROID}" or "${eas_build_job_1.Platform.IOS}"`);
|
|
44
49
|
}
|
|
50
|
+
const target = platform === eas_build_job_1.Platform.IOS
|
|
51
|
+
? {
|
|
52
|
+
platform,
|
|
53
|
+
simulator: inputs.simulator.value ??
|
|
54
|
+
(stepCtx.global.staticContext.job.platform === eas_build_job_1.Platform.IOS &&
|
|
55
|
+
stepCtx.global.staticContext.job.simulator === true),
|
|
56
|
+
}
|
|
57
|
+
: { platform };
|
|
45
58
|
await restoreCcacheAsync({
|
|
46
59
|
logger,
|
|
47
60
|
workingDirectory,
|
|
48
|
-
|
|
61
|
+
target,
|
|
49
62
|
env,
|
|
50
63
|
secrets: stepCtx.global.staticContext.job.secrets,
|
|
51
64
|
});
|
|
@@ -80,7 +93,7 @@ function createCacheStatsBuildFunction() {
|
|
|
80
93
|
},
|
|
81
94
|
});
|
|
82
95
|
}
|
|
83
|
-
async function restoreCcacheAsync({ logger, workingDirectory,
|
|
96
|
+
async function restoreCcacheAsync({ logger, workingDirectory, target, env, secrets, }) {
|
|
84
97
|
const enabled = env.EAS_RESTORE_CACHE === '1' || (env.EAS_USE_CACHE === '1' && env.EAS_RESTORE_CACHE !== '0');
|
|
85
98
|
if (!enabled) {
|
|
86
99
|
return;
|
|
@@ -103,7 +116,7 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
|
|
|
103
116
|
env,
|
|
104
117
|
stdio: 'pipe',
|
|
105
118
|
}));
|
|
106
|
-
const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory,
|
|
119
|
+
const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory, target);
|
|
107
120
|
logger.info(`Restoring cache key: ${cacheKey}`);
|
|
108
121
|
const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
|
109
122
|
const { archivePath, matchedKey } = await (0, restoreCache_1.downloadCacheAsync)({
|
|
@@ -113,8 +126,8 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
|
|
|
113
126
|
robotAccessToken,
|
|
114
127
|
paths: [cachePath],
|
|
115
128
|
key: cacheKey,
|
|
116
|
-
keyPrefixes: [cacheKey_1.
|
|
117
|
-
platform,
|
|
129
|
+
keyPrefixes: [(0, cacheKey_1.getCcacheKeyPrefix)(target)],
|
|
130
|
+
platform: target.platform,
|
|
118
131
|
});
|
|
119
132
|
await (0, restoreCache_1.decompressCacheAsync)({
|
|
120
133
|
archivePath,
|
|
@@ -139,7 +152,7 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
|
|
|
139
152
|
expoApiServerURL,
|
|
140
153
|
robotAccessToken,
|
|
141
154
|
paths: [cachePath],
|
|
142
|
-
platform,
|
|
155
|
+
platform: target.platform,
|
|
143
156
|
});
|
|
144
157
|
await (0, restoreCache_1.decompressCacheAsync)({
|
|
145
158
|
archivePath,
|
|
@@ -201,12 +214,13 @@ async function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets,
|
|
|
201
214
|
logger.info(`Restoring Gradle cache key: ${cacheKey}`);
|
|
202
215
|
const gradleCachesPath = path_1.default.join(os_1.default.homedir(), '.gradle', 'caches');
|
|
203
216
|
const buildCachePath = path_1.default.join(gradleCachesPath, 'build-cache-1');
|
|
217
|
+
const journalPath = path_1.default.join(gradleCachesPath, 'journal-1');
|
|
204
218
|
const { archivePath, matchedKey } = await (0, restoreCache_1.downloadCacheAsync)({
|
|
205
219
|
logger,
|
|
206
220
|
jobId,
|
|
207
221
|
expoApiServerURL,
|
|
208
222
|
robotAccessToken,
|
|
209
|
-
paths: [buildCachePath],
|
|
223
|
+
paths: [buildCachePath, journalPath],
|
|
210
224
|
key: cacheKey,
|
|
211
225
|
keyPrefixes: [gradleCacheKey_1.GRADLE_CACHE_KEY_PREFIX],
|
|
212
226
|
platform: eas_build_job_1.Platform.ANDROID,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Platform } from '@expo/eas-build-job';
|
|
2
1
|
import { bunyan } from '@expo/logger';
|
|
3
2
|
import { BuildFunction } from '@expo/steps';
|
|
3
|
+
import { CcacheBuildTarget } from '../../utils/cacheKey';
|
|
4
4
|
export declare function createSaveBuildCacheFunction(evictUsedBefore: Date): BuildFunction;
|
|
5
|
-
export declare function saveCcacheAsync({ logger, workingDirectory,
|
|
5
|
+
export declare function saveCcacheAsync({ logger, workingDirectory, target, evictUsedBefore, env, secrets, }: {
|
|
6
6
|
logger: bunyan;
|
|
7
7
|
workingDirectory: string;
|
|
8
|
-
|
|
8
|
+
target: CcacheBuildTarget;
|
|
9
9
|
evictUsedBefore: Date;
|
|
10
10
|
env: Record<string, string | undefined>;
|
|
11
11
|
secrets?: {
|