@expo/build-tools 21.8.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/builders/common.js +4 -0
- package/dist/builders/ios.js +11 -6
- package/dist/steps/easFunctions.js +2 -0
- package/dist/steps/functions/installMaestro.d.ts +6 -1
- package/dist/steps/functions/installMaestro.js +181 -55
- package/dist/steps/functions/maestroBackend.d.ts +11 -0
- package/dist/steps/functions/maestroBackend.js +14 -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/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/dist/utils/sourceMaps.d.ts +5 -0
- package/dist/utils/sourceMaps.js +105 -0
- package/package.json +8 -8
package/dist/builders/common.js
CHANGED
|
@@ -5,6 +5,7 @@ const eas_build_job_1 = require("@expo/eas-build-job");
|
|
|
5
5
|
const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
|
|
6
6
|
const artifacts_1 = require("../utils/artifacts");
|
|
7
7
|
const hooks_1 = require("../utils/hooks");
|
|
8
|
+
const sourceMaps_1 = require("../utils/sourceMaps");
|
|
8
9
|
async function runBuilderWithHooksAsync(ctx, builderAsync) {
|
|
9
10
|
try {
|
|
10
11
|
let buildSuccess = true;
|
|
@@ -35,6 +36,9 @@ async function runBuilderWithHooksAsync(ctx, builderAsync) {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_BUILD_ARTIFACTS, async () => {
|
|
39
|
+
if (buildSuccess) {
|
|
40
|
+
await (0, sourceMaps_1.maybeUploadSourceMapAsync)(ctx);
|
|
41
|
+
}
|
|
38
42
|
await (0, artifacts_1.maybeFindAndUploadBuildArtifacts)(ctx, {
|
|
39
43
|
logger: ctx.logger,
|
|
40
44
|
});
|
package/dist/builders/ios.js
CHANGED
|
@@ -30,6 +30,7 @@ const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
|
|
|
30
30
|
const hooks_1 = require("../utils/hooks");
|
|
31
31
|
const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
|
|
32
32
|
const processes_1 = require("../utils/processes");
|
|
33
|
+
const sourceMaps_1 = require("../utils/sourceMaps");
|
|
33
34
|
const INSTALL_PODS_WARN_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
34
35
|
const INSTALL_PODS_KILL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
35
36
|
class InstallPodsTimeoutError extends Error {
|
|
@@ -151,19 +152,23 @@ async function buildInnerAsync(ctx, jobHooksRef) {
|
|
|
151
152
|
fastlaneResult = await ctx.runBuildPhase(eas_build_job_1.BuildPhase.RUN_FASTLANE, async () => {
|
|
152
153
|
const scheme = (0, resolve_1.resolveScheme)(ctx);
|
|
153
154
|
const entitlements = await readEntitlementsAsync(ctx, { scheme, buildConfiguration });
|
|
155
|
+
const sourceMapPath = (0, sourceMaps_1.isSourceMapUploadEnabled)(ctx)
|
|
156
|
+
? await (0, sourceMaps_1.resolveIosSourceMapPathAsync)(ctx)
|
|
157
|
+
: null;
|
|
154
158
|
return await (0, fastlane_1.runFastlaneGym)(ctx, {
|
|
155
159
|
credentials,
|
|
156
160
|
scheme,
|
|
157
161
|
buildConfiguration,
|
|
158
162
|
entitlements,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
extraEnv: {
|
|
164
|
+
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
|
|
165
|
+
? {
|
|
162
166
|
EXPO_UPDATES_FINGERPRINT_OVERRIDE: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
|
|
163
167
|
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
: null),
|
|
168
|
+
}
|
|
169
|
+
: null),
|
|
170
|
+
...(sourceMapPath ? { SOURCEMAP_FILE: sourceMapPath } : null),
|
|
171
|
+
},
|
|
167
172
|
});
|
|
168
173
|
});
|
|
169
174
|
}
|
|
@@ -43,6 +43,7 @@ const sendSlackMessage_1 = require("./functions/sendSlackMessage");
|
|
|
43
43
|
const startAgentDeviceRemoteSession_1 = require("./functions/startAgentDeviceRemoteSession");
|
|
44
44
|
const startAndroidEmulator_1 = require("./functions/startAndroidEmulator");
|
|
45
45
|
const startArgentRemoteSession_1 = require("./functions/startArgentRemoteSession");
|
|
46
|
+
const startAppiumRemoteSession_1 = require("./functions/startAppiumRemoteSession");
|
|
46
47
|
const startCuttlefishDevice_1 = require("./functions/startCuttlefishDevice");
|
|
47
48
|
const startIosSimulator_1 = require("./functions/startIosSimulator");
|
|
48
49
|
const startIosSimulatorRecordings_1 = require("./functions/startIosSimulatorRecordings");
|
|
@@ -90,6 +91,7 @@ function getEasFunctions(ctx) {
|
|
|
90
91
|
(0, parseXcactivitylog_1.parseXcactivitylogFunction)(),
|
|
91
92
|
(0, startAgentDeviceRemoteSession_1.createStartAgentDeviceRemoteSessionBuildFunction)(ctx),
|
|
92
93
|
(0, startArgentRemoteSession_1.createStartArgentRemoteSessionBuildFunction)(ctx),
|
|
94
|
+
(0, startAppiumRemoteSession_1.createStartAppiumRemoteSessionBuildFunction)(ctx),
|
|
93
95
|
(0, startAndroidEmulator_1.createStartAndroidEmulatorBuildFunction)(),
|
|
94
96
|
(0, startCuttlefishDevice_1.createStartCuttlefishDeviceBuildFunction)(),
|
|
95
97
|
(0, startIosSimulator_1.createStartIosSimulatorBuildFunction)(),
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bunyan } from '@expo/logger';
|
|
2
|
+
import { BuildFunction, BuildStepEnv } from '@expo/steps';
|
|
2
3
|
export declare function createInstallMaestroBuildFunction(): BuildFunction;
|
|
4
|
+
export declare function installIdbFromBrew({ logger, env, }: {
|
|
5
|
+
logger: bunyan;
|
|
6
|
+
env: BuildStepEnv;
|
|
7
|
+
}): Promise<void>;
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createInstallMaestroBuildFunction = createInstallMaestroBuildFunction;
|
|
7
|
+
exports.installIdbFromBrew = installIdbFromBrew;
|
|
8
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
7
9
|
const results_1 = require("@expo/results");
|
|
8
10
|
const steps_1 = require("@expo/steps");
|
|
9
11
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
@@ -11,6 +13,8 @@ const assert_1 = __importDefault(require("assert"));
|
|
|
11
13
|
const fs_1 = __importDefault(require("fs"));
|
|
12
14
|
const os_1 = __importDefault(require("os"));
|
|
13
15
|
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const semver_1 = __importDefault(require("semver"));
|
|
17
|
+
const maestroBackend_1 = require("./maestroBackend");
|
|
14
18
|
const datadog_1 = require("../../datadog");
|
|
15
19
|
function createInstallMaestroBuildFunction() {
|
|
16
20
|
return new steps_1.BuildFunction({
|
|
@@ -24,6 +28,11 @@ function createInstallMaestroBuildFunction() {
|
|
|
24
28
|
required: false,
|
|
25
29
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
26
30
|
}),
|
|
31
|
+
steps_1.BuildStepInput.createProvider({
|
|
32
|
+
id: 'backend',
|
|
33
|
+
required: false,
|
|
34
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
35
|
+
}),
|
|
27
36
|
],
|
|
28
37
|
outputProviders: [
|
|
29
38
|
steps_1.BuildStepOutput.createProvider({
|
|
@@ -32,35 +41,43 @@ function createInstallMaestroBuildFunction() {
|
|
|
32
41
|
}),
|
|
33
42
|
],
|
|
34
43
|
fn: async ({ logger, global }, { inputs, env, outputs }) => {
|
|
35
|
-
const
|
|
36
|
-
|
|
44
|
+
const backend = (0, maestroBackend_1.resolveMaestroBackend)({
|
|
45
|
+
input: inputs.backend.value,
|
|
46
|
+
env,
|
|
47
|
+
});
|
|
48
|
+
const requestedVersion = inputs.maestro_version.value;
|
|
49
|
+
const { value: currentMaestroVersion } = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
|
|
37
50
|
// When not running in EAS Build VM, do not modify local environment.
|
|
38
51
|
if (env.EAS_BUILD_RUNNER !== 'eas-build') {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
52
|
+
const needsToInstallJava = backend === 'maestro' && !(await isJavaInstalled({ env }));
|
|
53
|
+
const needsToInstallIdb = backend === 'maestro' && !(await isIdbInstalled({ env }));
|
|
54
|
+
if (needsToInstallJava) {
|
|
42
55
|
logger.warn('It seems Java is not installed. It is required to run Maestro. If the job fails, this may be the reason.');
|
|
43
56
|
logger.info('');
|
|
44
57
|
}
|
|
45
|
-
if (
|
|
58
|
+
if (needsToInstallIdb) {
|
|
46
59
|
logger.warn('It seems IDB is not installed. Maestro requires it to run flows on iOS Simulator. If the job fails, this may be the reason.');
|
|
47
60
|
logger.info('');
|
|
48
61
|
}
|
|
49
62
|
if (!currentMaestroVersion) {
|
|
50
|
-
logger.warn(
|
|
63
|
+
logger.warn(`It seems ${backend} is not installed. Please install it manually and rerun the job.`);
|
|
51
64
|
logger.info('');
|
|
52
65
|
}
|
|
53
66
|
// Guide is helpful in these two cases, it doesn't mention Java.
|
|
54
|
-
if (
|
|
67
|
+
if (backend === 'maestro' && (needsToInstallIdb || !currentMaestroVersion)) {
|
|
55
68
|
logger.warn('For more info, check out Maestro installation guide: https://maestro.mobile.dev/getting-started/installing-maestro');
|
|
56
69
|
}
|
|
70
|
+
else if (backend === 'maestro-runner' && !currentMaestroVersion) {
|
|
71
|
+
logger.warn('For more info, check out maestro-runner installation guide: https://github.com/devicelab-dev/maestro-runner#install');
|
|
72
|
+
}
|
|
57
73
|
if (currentMaestroVersion) {
|
|
58
74
|
outputs.maestro_version.set(currentMaestroVersion);
|
|
59
|
-
logger.info(
|
|
75
|
+
logger.info(`${backend} ${currentMaestroVersion} is ready.`);
|
|
60
76
|
}
|
|
61
77
|
return;
|
|
62
78
|
}
|
|
63
|
-
|
|
79
|
+
const needsToInstallJava = backend === 'maestro' && !(await isJavaInstalled({ env }));
|
|
80
|
+
if (needsToInstallJava) {
|
|
64
81
|
if (global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
65
82
|
logger.info('Installing Java');
|
|
66
83
|
await installJavaFromGcs({ logger, env });
|
|
@@ -73,45 +90,129 @@ function createInstallMaestroBuildFunction() {
|
|
|
73
90
|
}
|
|
74
91
|
}
|
|
75
92
|
// IDB is only a requirement on macOS.
|
|
76
|
-
|
|
77
|
-
|
|
93
|
+
const needsToInstallIdb = backend === 'maestro' &&
|
|
94
|
+
global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN &&
|
|
95
|
+
!(await isIdbInstalled({ env }));
|
|
96
|
+
if (needsToInstallIdb) {
|
|
78
97
|
logger.info('Installing IDB');
|
|
79
98
|
await installIdbFromBrew({ logger, env });
|
|
80
99
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
switch (backend) {
|
|
101
|
+
case 'maestro':
|
|
102
|
+
// Skip installing if the input sets a specific Maestro version to install
|
|
103
|
+
// and it is already installed, either on a build image or a local computer.
|
|
104
|
+
if (!currentMaestroVersion ||
|
|
105
|
+
(requestedVersion && requestedVersion !== currentMaestroVersion)) {
|
|
106
|
+
await installMaestro({ version: requestedVersion, global, logger, env });
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'maestro-runner': {
|
|
110
|
+
let maestroRunnerVersionToInstall = requestedVersion;
|
|
111
|
+
const currentMaestroRunnerVersion = semver_1.default.coerce(currentMaestroVersion)?.version;
|
|
112
|
+
const requestedMaestroRunnerVersion = requestedVersion && requestedVersion !== 'latest'
|
|
113
|
+
? semver_1.default.valid(requestedVersion)
|
|
114
|
+
: null;
|
|
115
|
+
if (global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN &&
|
|
116
|
+
((requestedMaestroRunnerVersion &&
|
|
117
|
+
semver_1.default.gte(requestedMaestroRunnerVersion, '1.1.16')) ||
|
|
118
|
+
requestedVersion === 'latest' ||
|
|
119
|
+
(requestedVersion === undefined &&
|
|
120
|
+
(!currentMaestroRunnerVersion || semver_1.default.gt(currentMaestroRunnerVersion, '1.1.15'))))) {
|
|
121
|
+
const xcodeVersion = await getXcodeVersion({ env });
|
|
122
|
+
// maestro-runner 1.1.16 added `arch` to its xcodebuild destination. Xcode versions
|
|
123
|
+
// below 26 reject that option, so use the last compatible maestro-runner version.
|
|
124
|
+
if (semver_1.default.lt(xcodeVersion, '26.0.0')) {
|
|
125
|
+
if (requestedMaestroRunnerVersion) {
|
|
126
|
+
throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', `maestro-runner ${requestedVersion} is not compatible with Xcode ${xcodeVersion}. Use maestro-runner 1.1.15 or an Xcode 26+ image.`);
|
|
127
|
+
}
|
|
128
|
+
maestroRunnerVersionToInstall = '1.1.15';
|
|
129
|
+
logger.info(`Xcode ${xcodeVersion} requires maestro-runner ${maestroRunnerVersionToInstall}.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!currentMaestroVersion ||
|
|
133
|
+
(maestroRunnerVersionToInstall &&
|
|
134
|
+
maestroRunnerVersionToInstall !== currentMaestroVersion)) {
|
|
135
|
+
await installMaestroRunner({
|
|
136
|
+
version: maestroRunnerVersionToInstall,
|
|
137
|
+
global,
|
|
138
|
+
logger,
|
|
139
|
+
env,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
91
144
|
}
|
|
92
|
-
const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env }));
|
|
145
|
+
const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
|
|
93
146
|
if (!maestroVersionResult.ok) {
|
|
94
147
|
logger.error(maestroVersionResult.reason, 'Failed to get Maestro version.');
|
|
95
|
-
throw new Error(
|
|
148
|
+
throw new Error(`Failed to ensure ${backend} is installed.`);
|
|
96
149
|
}
|
|
97
|
-
logger.info(
|
|
150
|
+
logger.info(`${backend} ${maestroVersionResult.value} is ready.`);
|
|
98
151
|
outputs.maestro_version.set(maestroVersionResult.value);
|
|
99
152
|
datadog_1.Datadog.distribution('eas.maestro.install', 1, {
|
|
100
153
|
maestro_version: maestroVersionResult.value,
|
|
154
|
+
maestro_backend: backend,
|
|
101
155
|
});
|
|
102
156
|
},
|
|
103
157
|
});
|
|
104
158
|
}
|
|
105
|
-
async function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
159
|
+
async function getXcodeVersion({ env }) {
|
|
160
|
+
let stdout;
|
|
161
|
+
try {
|
|
162
|
+
({ stdout } = await (0, turtle_spawn_1.default)('xcodebuild', ['-version'], { stdio: 'pipe', env }));
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw new eas_build_job_1.SystemError('Failed to get Xcode version', { cause: error });
|
|
166
|
+
}
|
|
167
|
+
const xcodeVersion = semver_1.default.coerce(/^Xcode\s+(\S+)/m.exec(stdout)?.[1])?.version;
|
|
168
|
+
if (!xcodeVersion) {
|
|
169
|
+
throw new eas_build_job_1.SystemError(`Failed to parse Xcode version from xcodebuild output: ${stdout.trim()}`);
|
|
170
|
+
}
|
|
171
|
+
return xcodeVersion;
|
|
172
|
+
}
|
|
173
|
+
async function getMaestroVersion({ env, backend, }) {
|
|
174
|
+
switch (backend) {
|
|
175
|
+
case 'maestro': {
|
|
176
|
+
const { stdout } = await (0, turtle_spawn_1.default)('maestro', ['--version'], { stdio: 'pipe', env });
|
|
177
|
+
// `maestro --version` can print an analytics notice to stdout before the version,
|
|
178
|
+
// e.g. "Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS...\n2.0.10".
|
|
179
|
+
// Take the last version-looking token: the real version is printed after the notice.
|
|
180
|
+
const versions = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g);
|
|
181
|
+
return versions?.at(-1) ?? stdout.trim();
|
|
182
|
+
}
|
|
183
|
+
case 'maestro-runner': {
|
|
184
|
+
const { stdout } = await (0, turtle_spawn_1.default)('maestro-runner', ['--version'], { stdio: 'pipe', env });
|
|
185
|
+
// maestro-runner prints build information after its version. The Go runtime version in
|
|
186
|
+
// that output is also semver-shaped, so read only the prefixed runner version.
|
|
187
|
+
return /^maestro-runner\s+(\S+)/m.exec(stdout)?.[1] ?? stdout.trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function installMaestroRunner({ global, version, logger, env, }) {
|
|
192
|
+
logger.info('Fetching maestro-runner install script');
|
|
193
|
+
const tempDirectory = await fs_1.default.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'install_maestro_runner'));
|
|
194
|
+
try {
|
|
195
|
+
const installMaestroRunnerScriptResponse = await fetch('https://open.devicelab.dev/install/maestro-runner');
|
|
196
|
+
const installMaestroRunnerScript = await installMaestroRunnerScriptResponse.text();
|
|
197
|
+
const scriptPath = path_1.default.join(tempDirectory, 'install_maestro_runner.sh');
|
|
198
|
+
await fs_1.default.promises.writeFile(scriptPath, installMaestroRunnerScript, { mode: 0o777 });
|
|
199
|
+
logger.info('Installing maestro-runner');
|
|
200
|
+
(0, assert_1.default)(env.HOME, 'Failed to infer directory to install maestro-runner in: $HOME environment variable is empty.');
|
|
201
|
+
await (0, turtle_spawn_1.default)(scriptPath, version && version !== 'latest' ? ['--version', version] : [], {
|
|
202
|
+
logger,
|
|
203
|
+
env,
|
|
204
|
+
});
|
|
205
|
+
const binDir = path_1.default.join(env.HOME, '.maestro-runner', 'bin');
|
|
206
|
+
global.updateEnv({
|
|
207
|
+
...global.env,
|
|
208
|
+
PATH: `${binDir}:${global.env.PATH}`,
|
|
209
|
+
});
|
|
210
|
+
env.PATH = `${binDir}:${env.PATH}`;
|
|
211
|
+
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
|
|
215
|
+
}
|
|
115
216
|
}
|
|
116
217
|
async function installMaestro({ global, version, logger, env, }) {
|
|
117
218
|
logger.info('Fetching install script');
|
|
@@ -141,10 +242,10 @@ async function installMaestro({ global, version, logger, env, }) {
|
|
|
141
242
|
const maestroBinDir = path_1.default.join(maestroDir, 'bin');
|
|
142
243
|
global.updateEnv({
|
|
143
244
|
...global.env,
|
|
144
|
-
PATH: `${global.env.PATH}
|
|
245
|
+
PATH: `${maestroBinDir}:${global.env.PATH}`,
|
|
145
246
|
});
|
|
146
|
-
env.PATH = `${env.PATH}
|
|
147
|
-
process.env.PATH = `${process.env.PATH}
|
|
247
|
+
env.PATH = `${maestroBinDir}:${env.PATH}`;
|
|
248
|
+
process.env.PATH = `${maestroBinDir}:${process.env.PATH}`;
|
|
148
249
|
}
|
|
149
250
|
finally {
|
|
150
251
|
await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
|
|
@@ -152,7 +253,7 @@ async function installMaestro({ global, version, logger, env, }) {
|
|
|
152
253
|
}
|
|
153
254
|
async function isIdbInstalled({ env }) {
|
|
154
255
|
try {
|
|
155
|
-
await (0, turtle_spawn_1.default)('
|
|
256
|
+
await (0, turtle_spawn_1.default)('idb_companion', ['--version'], { ignoreStdio: true, env });
|
|
156
257
|
return true;
|
|
157
258
|
}
|
|
158
259
|
catch {
|
|
@@ -160,22 +261,47 @@ async function isIdbInstalled({ env }) {
|
|
|
160
261
|
}
|
|
161
262
|
}
|
|
162
263
|
async function installIdbFromBrew({ logger, env, }) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
264
|
+
try {
|
|
265
|
+
// Unfortunately our Mac images sometimes have two Homebrew
|
|
266
|
+
// installations. We should use the ARM64 one, located in /opt/homebrew.
|
|
267
|
+
const brewPath = '/opt/homebrew/bin/brew';
|
|
268
|
+
const localEnv = {
|
|
269
|
+
...env,
|
|
270
|
+
HOMEBREW_NO_AUTO_UPDATE: '1',
|
|
271
|
+
HOMEBREW_NO_INSTALL_CLEANUP: '1',
|
|
272
|
+
};
|
|
273
|
+
logger.info('Tapping facebook/fb...');
|
|
274
|
+
await (0, turtle_spawn_1.default)(brewPath, ['tap', 'facebook/fb'], {
|
|
275
|
+
env: localEnv,
|
|
276
|
+
logger,
|
|
277
|
+
});
|
|
278
|
+
const brewRepo = await (0, turtle_spawn_1.default)(brewPath, ['--repo', 'facebook/fb'], {
|
|
279
|
+
env: localEnv,
|
|
280
|
+
});
|
|
281
|
+
const tapPath = brewRepo.stdout.trim();
|
|
282
|
+
// c0386793f59da10c619787f2aa18d938ef1d69c9 is hash for 1.1.8 release,
|
|
283
|
+
// last known compatible version + post_install fix.
|
|
284
|
+
const gitSha = 'c0386793f59da10c619787f2aa18d938ef1d69c9';
|
|
285
|
+
logger.info('Checking out facebook/fb at idb_companion@1.1.8...');
|
|
286
|
+
await (0, turtle_spawn_1.default)('git', ['fetch', 'origin', gitSha], {
|
|
287
|
+
cwd: tapPath,
|
|
288
|
+
logger,
|
|
289
|
+
});
|
|
290
|
+
await (0, turtle_spawn_1.default)('git', ['checkout', gitSha], {
|
|
291
|
+
cwd: tapPath,
|
|
292
|
+
logger,
|
|
293
|
+
});
|
|
294
|
+
logger.info('Installing idb_companion v1.1.8...');
|
|
295
|
+
await (0, turtle_spawn_1.default)(brewPath, ['install', 'facebook/fb/idb-companion'], {
|
|
296
|
+
env: localEnv,
|
|
297
|
+
logger,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
throw new eas_build_job_1.SystemError('Failed to install idb-companion required for Maestro to run.', {
|
|
302
|
+
cause: err,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
179
305
|
}
|
|
180
306
|
async function isJavaInstalled({ env }) {
|
|
181
307
|
try {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BuildStepEnv } from '@expo/steps';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const MaestroBackendSchema: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
maestro: "maestro";
|
|
5
|
+
"maestro-runner": "maestro-runner";
|
|
6
|
+
}>>;
|
|
7
|
+
export type MaestroBackend = z.output<typeof MaestroBackendSchema>;
|
|
8
|
+
export declare function resolveMaestroBackend({ input, env, }: {
|
|
9
|
+
input: unknown;
|
|
10
|
+
env: BuildStepEnv;
|
|
11
|
+
}): MaestroBackend;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaestroBackendSchema = void 0;
|
|
4
|
+
exports.resolveMaestroBackend = resolveMaestroBackend;
|
|
5
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
exports.MaestroBackendSchema = zod_1.z.enum(['maestro', 'maestro-runner']).default('maestro');
|
|
8
|
+
function resolveMaestroBackend({ input, env, }) {
|
|
9
|
+
const result = exports.MaestroBackendSchema.safeParse(input || env.EAS_MAESTRO_BACKEND || undefined);
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', 'backend and EAS_MAESTRO_BACKEND must be either "maestro" or "maestro-runner".', { cause: result.error });
|
|
12
|
+
}
|
|
13
|
+
return result.data;
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
export interface MaestroFlowResult {
|
|
2
3
|
name: string;
|
|
3
4
|
path: string;
|
|
@@ -23,6 +24,34 @@ export declare function isFileAttrRun(testcases: JUnitTestCaseResult[]): testcas
|
|
|
23
24
|
file: string;
|
|
24
25
|
})[];
|
|
25
26
|
export declare function junitFileHasFileAttrs(junitFile: string): Promise<boolean>;
|
|
27
|
+
declare const MaestroRunnerReportSchema: z.ZodObject<{
|
|
28
|
+
flows: z.ZodPipe<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
29
|
+
name: z.ZodString;
|
|
30
|
+
sourceFile: z.ZodString;
|
|
31
|
+
status: z.ZodEnum<{
|
|
32
|
+
failed: "failed";
|
|
33
|
+
passed: "passed";
|
|
34
|
+
}>;
|
|
35
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
36
|
+
status: z.ZodLiteral<"skipped">;
|
|
37
|
+
}, z.core.$strip>], "status">>, z.ZodTransform<{
|
|
38
|
+
name: string;
|
|
39
|
+
sourceFile: string;
|
|
40
|
+
status: "failed" | "passed";
|
|
41
|
+
}[], ({
|
|
42
|
+
name: string;
|
|
43
|
+
sourceFile: string;
|
|
44
|
+
status: "failed" | "passed";
|
|
45
|
+
} | {
|
|
46
|
+
status: "skipped";
|
|
47
|
+
})[]>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
type MaestroRunnerReport = z.infer<typeof MaestroRunnerReportSchema>;
|
|
50
|
+
export declare function parseFailedFlowsFromMaestroRunnerReport(args: {
|
|
51
|
+
reportDirectory: string;
|
|
52
|
+
workingDirectory: string;
|
|
53
|
+
}): Promise<string[] | null>;
|
|
54
|
+
export declare function parseMaestroRunnerReport(reportDirectory: string): Promise<MaestroRunnerReport | null>;
|
|
26
55
|
export declare function parseMaestroResultsFromFileAttrs(junitDirectory: string): Promise<MaestroFlowResult[]>;
|
|
27
56
|
/**
|
|
28
57
|
* Returns the `file=` paths of the failing testcases in the given attempt's
|
|
@@ -75,3 +104,4 @@ export declare function copyLatestAttemptXml(args: {
|
|
|
75
104
|
sourceDir: string;
|
|
76
105
|
outputPath: string;
|
|
77
106
|
}): Promise<void>;
|
|
107
|
+
export {};
|
|
@@ -7,6 +7,8 @@ exports.parseFailedFlowNamesFromJUnitFile = parseFailedFlowNamesFromJUnitFile;
|
|
|
7
7
|
exports.parseJUnitTestCases = parseJUnitTestCases;
|
|
8
8
|
exports.isFileAttrRun = isFileAttrRun;
|
|
9
9
|
exports.junitFileHasFileAttrs = junitFileHasFileAttrs;
|
|
10
|
+
exports.parseFailedFlowsFromMaestroRunnerReport = parseFailedFlowsFromMaestroRunnerReport;
|
|
11
|
+
exports.parseMaestroRunnerReport = parseMaestroRunnerReport;
|
|
10
12
|
exports.parseMaestroResultsFromFileAttrs = parseMaestroResultsFromFileAttrs;
|
|
11
13
|
exports.parseFailedFlowsFromFileAttrs = parseFailedFlowsFromFileAttrs;
|
|
12
14
|
exports.parseMaestroResults = parseMaestroResults;
|
|
@@ -17,6 +19,7 @@ const results_1 = require("@expo/results");
|
|
|
17
19
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
18
20
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
19
21
|
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const zod_1 = require("zod");
|
|
20
23
|
// Per-attempt JUnit XML files use `*-attempt-N.xml` names; this extracts N.
|
|
21
24
|
const ATTEMPT_PATTERN = /attempt-(\d+)/;
|
|
22
25
|
const xmlParser = new fast_xml_parser_1.XMLParser({
|
|
@@ -25,10 +28,16 @@ const xmlParser = new fast_xml_parser_1.XMLParser({
|
|
|
25
28
|
// Ensure single-element arrays are always arrays
|
|
26
29
|
isArray: name => ['testsuite', 'testcase', 'property'].includes(name),
|
|
27
30
|
});
|
|
28
|
-
//
|
|
31
|
+
// Official Maestro writes the flow path as a `file=` testcase attribute. maestro-runner writes
|
|
32
|
+
// the same value as a `<property name="file" value="..."/>` child.
|
|
29
33
|
function fileAttrOf(tc) {
|
|
30
34
|
const f = tc?.['@_file'];
|
|
31
|
-
|
|
35
|
+
if (typeof f === 'string' && f.length > 0) {
|
|
36
|
+
return f;
|
|
37
|
+
}
|
|
38
|
+
const properties = tc?.properties?.property ?? [];
|
|
39
|
+
const fileProperty = properties.find(property => property['@_name'] === 'file')?.['@_value'];
|
|
40
|
+
return typeof fileProperty === 'string' && fileProperty.length > 0 ? fileProperty : undefined;
|
|
32
41
|
}
|
|
33
42
|
function parseJUnitContent(content) {
|
|
34
43
|
const results = [];
|
|
@@ -48,22 +57,40 @@ function parseJUnitContent(content) {
|
|
|
48
57
|
if (!name) {
|
|
49
58
|
continue;
|
|
50
59
|
}
|
|
60
|
+
// Standard JUnit marks skipped tests with a <skipped/> child (no failure/error). Exclude
|
|
61
|
+
// them so they aren't miscounted as passed, matching the report.json path which drops
|
|
62
|
+
// skipped flows.
|
|
63
|
+
if (tc.skipped != null) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
51
66
|
const file = fileAttrOf(tc);
|
|
52
67
|
const timeStr = tc['@_time'];
|
|
53
68
|
const timeSeconds = timeStr ? parseFloat(timeStr) : 0;
|
|
54
69
|
const duration = Number.isFinite(timeSeconds) ? Math.round(timeSeconds * 1000) : 0;
|
|
55
|
-
|
|
70
|
+
// maestro-runner puts the real error in the `message` attribute and the command
|
|
71
|
+
// label (e.g. `tapOn`) in the body; official Maestro only writes the body. Prefer
|
|
72
|
+
// `@_message` and fall back to `#text` so both stay correct.
|
|
56
73
|
const failureText = tc.failure != null
|
|
57
74
|
? typeof tc.failure === 'string'
|
|
58
75
|
? tc.failure
|
|
59
|
-
: (tc.failure?.['#text'] ?? null)
|
|
76
|
+
: (tc.failure?.['@_message'] ?? tc.failure?.['#text'] ?? null)
|
|
60
77
|
: null;
|
|
61
78
|
const errorText = tc.error != null
|
|
62
79
|
? typeof tc.error === 'string'
|
|
63
80
|
? tc.error
|
|
64
|
-
: (tc.error?.['#text'] ?? null)
|
|
81
|
+
: (tc.error?.['@_message'] ?? tc.error?.['#text'] ?? null)
|
|
65
82
|
: null;
|
|
66
83
|
const errorMessage = failureText ?? errorText ?? null;
|
|
84
|
+
// Official Maestro uses status="SUCCESS". maestro-runner uses standard JUnit semantics:
|
|
85
|
+
// a testcase passes when it has no failure or error child.
|
|
86
|
+
const statusAttribute = tc['@_status'];
|
|
87
|
+
const status = typeof statusAttribute === 'string'
|
|
88
|
+
? statusAttribute === 'SUCCESS'
|
|
89
|
+
? 'passed'
|
|
90
|
+
: 'failed'
|
|
91
|
+
: tc.failure == null && tc.error == null
|
|
92
|
+
? 'passed'
|
|
93
|
+
: 'failed';
|
|
67
94
|
const rawProperties = tc.properties?.property ?? [];
|
|
68
95
|
const properties = {};
|
|
69
96
|
for (const prop of rawProperties) {
|
|
@@ -143,6 +170,52 @@ async function junitFileHasFileAttrs(junitFile) {
|
|
|
143
170
|
async function fileExists(absPath) {
|
|
144
171
|
return (await (0, results_1.asyncResult)(promises_1.default.stat(absPath))).ok;
|
|
145
172
|
}
|
|
173
|
+
// maestro-runner writes report.json as the source of truth for a run. Use it for runner control
|
|
174
|
+
// flow because sourceFile preserves the exact flow path, while older runner JUnit reports flatten
|
|
175
|
+
// it to a basename. Keep only passed and failed flows so the result matches Maestro JUnit reports,
|
|
176
|
+
// which do not include skipped flows.
|
|
177
|
+
const MaestroRunnerRecordedFlowSchema = zod_1.z.object({
|
|
178
|
+
name: zod_1.z.string().min(1),
|
|
179
|
+
sourceFile: zod_1.z.string().min(1),
|
|
180
|
+
status: zod_1.z.enum(['passed', 'failed']),
|
|
181
|
+
});
|
|
182
|
+
const MaestroRunnerReportSchema = zod_1.z.object({
|
|
183
|
+
flows: zod_1.z
|
|
184
|
+
.array(zod_1.z.discriminatedUnion('status', [
|
|
185
|
+
MaestroRunnerRecordedFlowSchema,
|
|
186
|
+
zod_1.z.object({ status: zod_1.z.literal('skipped') }),
|
|
187
|
+
]))
|
|
188
|
+
.transform(flows => flows.filter((flow) => flow.status !== 'skipped')),
|
|
189
|
+
});
|
|
190
|
+
async function parseFailedFlowsFromMaestroRunnerReport(args) {
|
|
191
|
+
const report = await parseMaestroRunnerReport(args.reportDirectory);
|
|
192
|
+
if (report === null) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const failedPaths = [
|
|
196
|
+
...new Set(report.flows.filter(flow => flow.status === 'failed').map(flow => flow.sourceFile)),
|
|
197
|
+
];
|
|
198
|
+
if (failedPaths.length === 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
for (const flowPath of failedPaths) {
|
|
202
|
+
if (!(await fileExists(path_1.default.resolve(args.workingDirectory, flowPath)))) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return failedPaths;
|
|
207
|
+
}
|
|
208
|
+
async function parseMaestroRunnerReport(reportDirectory) {
|
|
209
|
+
let report;
|
|
210
|
+
try {
|
|
211
|
+
report = JSON.parse(await promises_1.default.readFile(path_1.default.join(reportDirectory, 'report.json'), 'utf8'));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const result = MaestroRunnerReportSchema.safeParse(report);
|
|
217
|
+
return result.success ? result.data : null;
|
|
218
|
+
}
|
|
146
219
|
// Group by `file=` so two same-named flows in different files stay separate.
|
|
147
220
|
async function parseMaestroResultsFromFileAttrs(junitDirectory) {
|
|
148
221
|
let junitEntries;
|
|
@@ -21,6 +21,12 @@ export declare function harvestFailureScreenshotsAsync(args: {
|
|
|
21
21
|
failedFlowNames: ReadonlySet<string>;
|
|
22
22
|
logger: bunyan;
|
|
23
23
|
}): Promise<HarvestedScreenshot[]>;
|
|
24
|
+
export declare function harvestMaestroRunnerFailureScreenshotsAsync(args: {
|
|
25
|
+
reportDirectory: string;
|
|
26
|
+
capturedSinceMs: number;
|
|
27
|
+
attemptIndex: number;
|
|
28
|
+
logger: bunyan;
|
|
29
|
+
}): Promise<HarvestedScreenshot[]>;
|
|
24
30
|
export declare function computePureFailureFlowNames(testCases: readonly {
|
|
25
31
|
name: string;
|
|
26
32
|
status: 'passed' | 'failed';
|