@expo/build-tools 18.8.1 → 18.10.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/ios/pod.js +57 -0
- package/dist/steps/easFunctions.js +2 -0
- package/dist/steps/functions/maestroResultParser.d.ts +12 -0
- package/dist/steps/functions/maestroResultParser.js +35 -2
- package/dist/steps/functions/maestroTests.d.ts +2 -0
- package/dist/steps/functions/maestroTests.js +208 -0
- package/package.json +3 -3
package/dist/ios/pod.js
CHANGED
|
@@ -5,11 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.installPods = installPods;
|
|
7
7
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const semver_1 = __importDefault(require("semver"));
|
|
11
|
+
const MIN_PRECOMPILED_MODULES_EXPO_VERSION = '55.0.18';
|
|
12
|
+
// const PRECOMPILED_MODULES_BASE_URL = 'https://storage.googleapis.com/eas-build-precompiled-modules/';
|
|
9
13
|
async function installPods(ctx, { infoCallbackFn }) {
|
|
10
14
|
const iosDir = path_1.default.join(ctx.getReactNativeProjectDirectory(), 'ios');
|
|
11
15
|
const verboseFlag = ctx.env['EAS_VERBOSE'] === '1' ? ['--verbose'] : [];
|
|
12
16
|
const cocoapodsDeploymentFlag = ctx.env['POD_INSTALL_DEPLOYMENT'] === '1' ? ['--deployment'] : [];
|
|
17
|
+
const precompiledModulesEnv = await resolvePrecompiledModulesPodInstallEnvAsync(ctx);
|
|
13
18
|
return {
|
|
14
19
|
spawnPromise: (0, turtle_spawn_1.default)('pod', ['install', ...verboseFlag, ...cocoapodsDeploymentFlag], {
|
|
15
20
|
cwd: iosDir,
|
|
@@ -17,6 +22,7 @@ async function installPods(ctx, { infoCallbackFn }) {
|
|
|
17
22
|
env: {
|
|
18
23
|
...ctx.env,
|
|
19
24
|
LANG: 'en_US.UTF-8',
|
|
25
|
+
...precompiledModulesEnv,
|
|
20
26
|
},
|
|
21
27
|
lineTransformer: (line) => {
|
|
22
28
|
if (!line ||
|
|
@@ -31,3 +37,54 @@ async function installPods(ctx, { infoCallbackFn }) {
|
|
|
31
37
|
}),
|
|
32
38
|
};
|
|
33
39
|
}
|
|
40
|
+
async function resolvePrecompiledModulesPodInstallEnvAsync(ctx) {
|
|
41
|
+
if (ctx.job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES !== '1') {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
let expoPackageVersion;
|
|
45
|
+
try {
|
|
46
|
+
expoPackageVersion = await getInstalledExpoPackageVersionAsync(ctx);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
ctx.logger.info({ err }, 'Failed to detect installed Expo package version; not enabling precompiled modules use.');
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
const validExpoPackageVersion = semver_1.default.valid(expoPackageVersion);
|
|
53
|
+
if (!validExpoPackageVersion) {
|
|
54
|
+
ctx.logger.info(`Detected expo=${expoPackageVersion}; not enabling precompiled modules use because the installed Expo package version is not a valid semver version.`);
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
if (semver_1.default.lt(validExpoPackageVersion, MIN_PRECOMPILED_MODULES_EXPO_VERSION)) {
|
|
58
|
+
ctx.logger.info(`Detected expo=${validExpoPackageVersion}; not enabling precompiled modules use because precompiled modules require expo>=${MIN_PRECOMPILED_MODULES_EXPO_VERSION}.`);
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
// Start rollout with Expo precompiled modules only. Add third-party modules after this is stable.
|
|
62
|
+
const env = {
|
|
63
|
+
EXPO_USE_PRECOMPILED_MODULES: '1',
|
|
64
|
+
// EXPO_PRECOMPILED_MODULES_BASE_URL: getPrecompiledModulesBaseUrl(),
|
|
65
|
+
};
|
|
66
|
+
ctx.logger.info(`Detected expo=${validExpoPackageVersion}; enabling precompiled modules use. Installing pods with additional environment variables.\n${Object.entries(env)
|
|
67
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
68
|
+
.join('\n')}\nPrecompiled modules pod install environment is configured.`);
|
|
69
|
+
return env;
|
|
70
|
+
}
|
|
71
|
+
async function getInstalledExpoPackageVersionAsync(ctx) {
|
|
72
|
+
const { stdout } = await (0, turtle_spawn_1.default)('node', ['--print', "require.resolve('expo/package.json')"], {
|
|
73
|
+
cwd: ctx.getReactNativeProjectDirectory(),
|
|
74
|
+
env: ctx.env,
|
|
75
|
+
stdio: 'pipe',
|
|
76
|
+
});
|
|
77
|
+
const expoPackageJsonPath = stdout.toString().trim();
|
|
78
|
+
return (await fs_extra_1.default.readJson(expoPackageJsonPath)).version;
|
|
79
|
+
}
|
|
80
|
+
// function getPrecompiledModulesBaseUrl<TJob extends Ios.Job>(ctx: BuildContext<TJob>): string {
|
|
81
|
+
// if (!ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL) {
|
|
82
|
+
// return PRECOMPILED_MODULES_BASE_URL;
|
|
83
|
+
// }
|
|
84
|
+
//
|
|
85
|
+
// const parsedUrl = new URL(PRECOMPILED_MODULES_BASE_URL);
|
|
86
|
+
// return PRECOMPILED_MODULES_BASE_URL.replace(
|
|
87
|
+
// `${parsedUrl.protocol}//${parsedUrl.host}`,
|
|
88
|
+
// `${ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL.replace(/\/$/, '')}/${parsedUrl.host}`
|
|
89
|
+
// );
|
|
90
|
+
// }
|
|
@@ -33,6 +33,7 @@ const restoreCache_1 = require("./functions/restoreCache");
|
|
|
33
33
|
const parseXcactivitylog_1 = require("./functions/parseXcactivitylog");
|
|
34
34
|
const runFastlane_1 = require("./functions/runFastlane");
|
|
35
35
|
const runGradle_1 = require("./functions/runGradle");
|
|
36
|
+
const maestroTests_1 = require("./functions/maestroTests");
|
|
36
37
|
const saveBuildCache_1 = require("./functions/saveBuildCache");
|
|
37
38
|
const saveCache_1 = require("./functions/saveCache");
|
|
38
39
|
const sendSlackMessage_1 = require("./functions/sendSlackMessage");
|
|
@@ -85,6 +86,7 @@ function getEasFunctions(ctx) {
|
|
|
85
86
|
(0, createSubmissionEntity_1.createSubmissionEntityFunction)(),
|
|
86
87
|
(0, uploadToAsc_1.createUploadToAscBuildFunction)(),
|
|
87
88
|
(0, reportMaestroTestResults_1.createReportMaestroTestResultsFunction)(ctx),
|
|
89
|
+
(0, maestroTests_1.createMaestroTestsBuildFunction)(),
|
|
88
90
|
];
|
|
89
91
|
if (ctx.hasBuildJob()) {
|
|
90
92
|
functions.push(...[
|
|
@@ -39,4 +39,16 @@ type FlowMetadata = z.output<typeof FlowMetadataFileSchema>;
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function parseFlowMetadata(filePath: string): Promise<FlowMetadata | null>;
|
|
41
41
|
export declare function parseMaestroResults(junitDirectory: string, testsDirectory: string, projectRoot: string): Promise<MaestroFlowResult[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Copies the highest-attempt-index *.xml file from sourceDir to outputPath.
|
|
44
|
+
* After the maestro retry loop completes, this produces a single canonical
|
|
45
|
+
* JUnit report at final_report_path matching the bash step's "cp latest
|
|
46
|
+
* attempt" semantics.
|
|
47
|
+
*
|
|
48
|
+
* Throws if sourceDir contains no *.xml files or if the copy fails.
|
|
49
|
+
*/
|
|
50
|
+
export declare function copyLatestAttemptXml(args: {
|
|
51
|
+
sourceDir: string;
|
|
52
|
+
outputPath: string;
|
|
53
|
+
}): Promise<void>;
|
|
42
54
|
export {};
|
|
@@ -7,12 +7,15 @@ exports.extractFlowKey = extractFlowKey;
|
|
|
7
7
|
exports.parseJUnitTestCases = parseJUnitTestCases;
|
|
8
8
|
exports.parseFlowMetadata = parseFlowMetadata;
|
|
9
9
|
exports.parseMaestroResults = parseMaestroResults;
|
|
10
|
+
exports.copyLatestAttemptXml = copyLatestAttemptXml;
|
|
10
11
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
11
12
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
13
14
|
const zod_1 = require("zod");
|
|
14
15
|
// Maestro's TestDebugReporter creates timestamped directories, e.g. "2024-06-15_143022"
|
|
15
16
|
const TIMESTAMP_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{6}$/;
|
|
17
|
+
// Per-attempt JUnit XML files use `*-attempt-N.xml` names; this extracts N.
|
|
18
|
+
const ATTEMPT_PATTERN = /attempt-(\d+)/;
|
|
16
19
|
function extractFlowKey(filename, prefix) {
|
|
17
20
|
const match = filename.match(new RegExp(`^${prefix}-(.+)\\.json$`));
|
|
18
21
|
return match?.[1] ?? null;
|
|
@@ -190,8 +193,6 @@ async function parseMaestroResults(junitDirectory, testsDirectory, projectRoot)
|
|
|
190
193
|
}
|
|
191
194
|
// 3. Merge: JUnit results + ai-*.json metadata
|
|
192
195
|
const results = [];
|
|
193
|
-
// Parse attempt index from filename pattern: *-attempt-N.*
|
|
194
|
-
const ATTEMPT_PATTERN = /attempt-(\d+)/;
|
|
195
196
|
// Group results by flow name
|
|
196
197
|
const resultsByName = new Map();
|
|
197
198
|
for (const entry of junitResultsWithSource) {
|
|
@@ -267,3 +268,35 @@ async function relativizePathAsync(flowFilePath, projectRoot) {
|
|
|
267
268
|
}
|
|
268
269
|
return relative;
|
|
269
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Copies the highest-attempt-index *.xml file from sourceDir to outputPath.
|
|
273
|
+
* After the maestro retry loop completes, this produces a single canonical
|
|
274
|
+
* JUnit report at final_report_path matching the bash step's "cp latest
|
|
275
|
+
* attempt" semantics.
|
|
276
|
+
*
|
|
277
|
+
* Throws if sourceDir contains no *.xml files or if the copy fails.
|
|
278
|
+
*/
|
|
279
|
+
async function copyLatestAttemptXml(args) {
|
|
280
|
+
const entries = await promises_1.default.readdir(args.sourceDir);
|
|
281
|
+
const xmlFiles = entries.filter(f => f.endsWith('.xml')).sort();
|
|
282
|
+
if (xmlFiles.length === 0) {
|
|
283
|
+
throw new Error(`No *.xml files found in ${args.sourceDir}`);
|
|
284
|
+
}
|
|
285
|
+
// Pick the file with the highest attempt index. Files without the marker are
|
|
286
|
+
// treated as attempt 0. Ties are broken by sorted filename — later wins.
|
|
287
|
+
let winner = xmlFiles[0];
|
|
288
|
+
let winnerAttempt = (() => {
|
|
289
|
+
const m = winner.match(ATTEMPT_PATTERN);
|
|
290
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
291
|
+
})();
|
|
292
|
+
for (let i = 1; i < xmlFiles.length; i++) {
|
|
293
|
+
const candidate = xmlFiles[i];
|
|
294
|
+
const match = candidate.match(ATTEMPT_PATTERN);
|
|
295
|
+
const attempt = match ? parseInt(match[1], 10) : 0;
|
|
296
|
+
if (attempt >= winnerAttempt) {
|
|
297
|
+
winner = candidate;
|
|
298
|
+
winnerAttempt = attempt;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
await promises_1.default.copyFile(path_1.default.join(args.sourceDir, winner), args.outputPath);
|
|
302
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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.createMaestroTestsBuildFunction = createMaestroTestsBuildFunction;
|
|
7
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
8
|
+
const steps_1 = require("@expo/steps");
|
|
9
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
10
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const maestroResultParser_1 = require("./maestroResultParser");
|
|
14
|
+
const retry_1 = require("../../utils/retry");
|
|
15
|
+
const FlowPathSchema = zod_1.z.array(zod_1.z.string().min(1)).min(1);
|
|
16
|
+
const RetriesSchema = zod_1.z.number().int().min(0).default(0);
|
|
17
|
+
const ShardsSchema = zod_1.z.number().int().min(1).optional();
|
|
18
|
+
function parseInput(schema, value, message) {
|
|
19
|
+
const result = schema.safeParse(value);
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', message, { cause: result.error });
|
|
22
|
+
}
|
|
23
|
+
return result.data;
|
|
24
|
+
}
|
|
25
|
+
// `outputPath: null` means "let maestro pick" (no --output flag). Junit and
|
|
26
|
+
// other declared formats pass an explicit path so downstream upload steps
|
|
27
|
+
// know where to find the result.
|
|
28
|
+
function buildMaestroArgs(args) {
|
|
29
|
+
const out = ['test'];
|
|
30
|
+
if (args.output_format) {
|
|
31
|
+
out.push(`--format=${args.output_format.toUpperCase()}`);
|
|
32
|
+
}
|
|
33
|
+
if (args.outputPath) {
|
|
34
|
+
out.push(`--output=${args.outputPath}`);
|
|
35
|
+
}
|
|
36
|
+
if (args.shards !== undefined) {
|
|
37
|
+
out.push(`--shard-split=${args.shards}`);
|
|
38
|
+
}
|
|
39
|
+
if (args.include_tags) {
|
|
40
|
+
out.push(`--include-tags=${args.include_tags}`);
|
|
41
|
+
}
|
|
42
|
+
if (args.exclude_tags) {
|
|
43
|
+
out.push(`--exclude-tags=${args.exclude_tags}`);
|
|
44
|
+
}
|
|
45
|
+
out.push(...args.flow_path);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function createMaestroTestsBuildFunction() {
|
|
49
|
+
return new steps_1.BuildFunction({
|
|
50
|
+
namespace: 'eas',
|
|
51
|
+
id: 'maestro_tests',
|
|
52
|
+
name: 'Run Maestro Tests',
|
|
53
|
+
__metricsId: 'eas/maestro_tests',
|
|
54
|
+
inputProviders: [
|
|
55
|
+
steps_1.BuildStepInput.createProvider({
|
|
56
|
+
id: 'flow_path',
|
|
57
|
+
required: true,
|
|
58
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.JSON,
|
|
59
|
+
}),
|
|
60
|
+
steps_1.BuildStepInput.createProvider({
|
|
61
|
+
id: 'retries',
|
|
62
|
+
required: false,
|
|
63
|
+
defaultValue: 0,
|
|
64
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
65
|
+
}),
|
|
66
|
+
steps_1.BuildStepInput.createProvider({
|
|
67
|
+
id: 'shards',
|
|
68
|
+
required: false,
|
|
69
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
70
|
+
}),
|
|
71
|
+
steps_1.BuildStepInput.createProvider({
|
|
72
|
+
id: 'include_tags',
|
|
73
|
+
required: false,
|
|
74
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
75
|
+
}),
|
|
76
|
+
steps_1.BuildStepInput.createProvider({
|
|
77
|
+
id: 'exclude_tags',
|
|
78
|
+
required: false,
|
|
79
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
80
|
+
}),
|
|
81
|
+
steps_1.BuildStepInput.createProvider({
|
|
82
|
+
id: 'output_format',
|
|
83
|
+
required: false,
|
|
84
|
+
defaultValue: 'junit',
|
|
85
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
86
|
+
}),
|
|
87
|
+
steps_1.BuildStepInput.createProvider({
|
|
88
|
+
id: 'platform',
|
|
89
|
+
required: false,
|
|
90
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
outputProviders: [
|
|
94
|
+
steps_1.BuildStepOutput.createProvider({ id: 'junit_report_directory', required: true }),
|
|
95
|
+
steps_1.BuildStepOutput.createProvider({ id: 'final_report_path', required: false }),
|
|
96
|
+
steps_1.BuildStepOutput.createProvider({ id: 'tests_directory', required: true }),
|
|
97
|
+
],
|
|
98
|
+
fn: async (stepCtx, { inputs, outputs, env, signal }) => {
|
|
99
|
+
const { logger, global } = stepCtx;
|
|
100
|
+
const platformInput = inputs.platform.value;
|
|
101
|
+
const outputFormat = inputs.output_format.value?.toLowerCase();
|
|
102
|
+
const includeTags = inputs.include_tags.value;
|
|
103
|
+
const excludeTags = inputs.exclude_tags.value;
|
|
104
|
+
const platform = platformInput === 'ios' || platformInput === 'android'
|
|
105
|
+
? platformInput
|
|
106
|
+
: global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN
|
|
107
|
+
? 'ios'
|
|
108
|
+
: 'android';
|
|
109
|
+
// Paths derive from env.HOME (not os.homedir()). Maestro is spawned with
|
|
110
|
+
// this env and writes debug output under $HOME/.maestro/tests; the step
|
|
111
|
+
// must read from the same place or stale files leak across runs.
|
|
112
|
+
const home = env.HOME;
|
|
113
|
+
if (!home) {
|
|
114
|
+
throw new eas_build_job_1.SystemError('HOME env var is not set');
|
|
115
|
+
}
|
|
116
|
+
const testsDirectory = path_1.default.join(home, '.maestro', 'tests');
|
|
117
|
+
const junitReportDirectory = path_1.default.join(testsDirectory, 'junit-reports');
|
|
118
|
+
const finalReportPath = outputFormat === 'junit'
|
|
119
|
+
? path_1.default.join(testsDirectory, `${platform}-maestro-junit.xml`)
|
|
120
|
+
: undefined;
|
|
121
|
+
// Public docs (EAS workflows pre-packaged-jobs) document
|
|
122
|
+
// `${MAESTRO_TESTS_DIR}` for users to save screenshots/recordings into
|
|
123
|
+
// the uploaded dir.
|
|
124
|
+
const spawnEnv = { ...env, MAESTRO_TESTS_DIR: testsDirectory };
|
|
125
|
+
// Outputs are published BEFORE any throw below so downstream
|
|
126
|
+
// `if: always()` upload steps still see populated values when this
|
|
127
|
+
// step fails early.
|
|
128
|
+
outputs.tests_directory.set(testsDirectory);
|
|
129
|
+
outputs.junit_report_directory.set(junitReportDirectory);
|
|
130
|
+
if (finalReportPath !== undefined) {
|
|
131
|
+
outputs.final_report_path.set(finalReportPath);
|
|
132
|
+
}
|
|
133
|
+
const flowPaths = parseInput(FlowPathSchema, inputs.flow_path.value, 'flow_path must be a non-empty array of non-empty strings.');
|
|
134
|
+
const retries = parseInput(RetriesSchema, inputs.retries.value, 'retries must be a non-negative integer.');
|
|
135
|
+
const shards = parseInput(ShardsSchema, inputs.shards.value, 'shards must be a positive integer.');
|
|
136
|
+
try {
|
|
137
|
+
await promises_1.default.mkdir(junitReportDirectory, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new eas_build_job_1.SystemError('Failed to create JUnit report directory', { cause: err });
|
|
141
|
+
}
|
|
142
|
+
// Retry loop. spawn-async error shapes:
|
|
143
|
+
// ENOENT/EACCES → infra (binary missing/not executable) → SystemError.
|
|
144
|
+
// numeric err.status → maestro exited non-zero → retry.
|
|
145
|
+
// else (signal-only, OOM kill, unknown) → infra → SystemError, never
|
|
146
|
+
// downgraded to "tests failed".
|
|
147
|
+
let lastAttemptExitCode = null;
|
|
148
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
149
|
+
const outputPath = outputFormat === 'junit'
|
|
150
|
+
? path_1.default.join(junitReportDirectory, `${platform}-maestro-junit-attempt-${attempt}.xml`)
|
|
151
|
+
: outputFormat
|
|
152
|
+
? path_1.default.join(testsDirectory, `${platform}-maestro-${outputFormat}.${outputFormat}`)
|
|
153
|
+
: null;
|
|
154
|
+
try {
|
|
155
|
+
await (0, turtle_spawn_1.default)('maestro', buildMaestroArgs({
|
|
156
|
+
flow_path: flowPaths,
|
|
157
|
+
outputPath,
|
|
158
|
+
output_format: outputFormat,
|
|
159
|
+
shards,
|
|
160
|
+
include_tags: includeTags,
|
|
161
|
+
exclude_tags: excludeTags,
|
|
162
|
+
}), { cwd: stepCtx.workingDirectory, env: spawnEnv, logger, signal });
|
|
163
|
+
lastAttemptExitCode = 0;
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (err && (err.code === 'ENOENT' || err.code === 'EACCES')) {
|
|
167
|
+
throw new eas_build_job_1.SystemError('Failed to invoke maestro', { cause: err });
|
|
168
|
+
}
|
|
169
|
+
if (err && typeof err.status === 'number') {
|
|
170
|
+
lastAttemptExitCode = err.status;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
throw new eas_build_job_1.SystemError('Unexpected spawn failure invoking maestro', { cause: err });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (lastAttemptExitCode === 0 || attempt === retries) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
logger.info('Test failed, retrying...');
|
|
180
|
+
await (0, retry_1.sleepAsync)(2000);
|
|
181
|
+
}
|
|
182
|
+
// Copy the latest attempt's JUnit to the final report path so downstream
|
|
183
|
+
// upload/report steps have a single canonical file.
|
|
184
|
+
if (finalReportPath !== undefined) {
|
|
185
|
+
try {
|
|
186
|
+
await (0, maestroResultParser_1.copyLatestAttemptXml)({
|
|
187
|
+
sourceDir: junitReportDirectory,
|
|
188
|
+
outputPath: finalReportPath,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (copyErr) {
|
|
192
|
+
// Swallow: a copy failure usually means maestro itself failed early
|
|
193
|
+
// (bad YAML wrote no *.xml). Throwing SystemError here would mask
|
|
194
|
+
// the real reason and cancel billing for a user-side failure — let
|
|
195
|
+
// the lastAttemptExitCode check below surface ERR_MAESTRO_TESTS_FAILED.
|
|
196
|
+
logger.warn(`Failed to produce final_report_path at ${finalReportPath}: ${copyErr?.message ?? copyErr}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// The retry loop exits via success (0), numeric status (retryable),
|
|
200
|
+
// or throw (infra). A non-null non-zero status means the user's tests
|
|
201
|
+
// failed every attempt.
|
|
202
|
+
if (lastAttemptExitCode !== 0) {
|
|
203
|
+
const totalAttempts = retries + 1;
|
|
204
|
+
throw new eas_build_job_1.UserError('ERR_MAESTRO_TESTS_FAILED', `Maestro tests failed after ${totalAttempts} attempt${totalAttempts === 1 ? '' : 's'}.`);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.10.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@expo/plist": "^0.2.0",
|
|
46
46
|
"@expo/results": "^1.0.0",
|
|
47
47
|
"@expo/spawn-async": "1.7.2",
|
|
48
|
-
"@expo/steps": "18.
|
|
48
|
+
"@expo/steps": "18.9.0",
|
|
49
49
|
"@expo/template-file": "18.5.0",
|
|
50
50
|
"@expo/turtle-spawn": "18.5.0",
|
|
51
51
|
"@expo/xcpretty": "^4.3.1",
|
|
@@ -98,5 +98,5 @@
|
|
|
98
98
|
"typescript": "^5.5.4",
|
|
99
99
|
"uuid": "^9.0.1"
|
|
100
100
|
},
|
|
101
|
-
"gitHead": "
|
|
101
|
+
"gitHead": "cacd48bf669342be783037607b02e5f9b1b148c0"
|
|
102
102
|
}
|