@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
|
@@ -29,6 +29,11 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
|
|
|
29
29
|
required: false,
|
|
30
30
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
31
31
|
}),
|
|
32
|
+
steps_1.BuildStepInput.createProvider({
|
|
33
|
+
id: 'simulator',
|
|
34
|
+
required: false,
|
|
35
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
|
|
36
|
+
}),
|
|
32
37
|
],
|
|
33
38
|
fn: async (stepCtx, { env, inputs }) => {
|
|
34
39
|
const { logger } = stepCtx;
|
|
@@ -38,10 +43,18 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
|
|
|
38
43
|
if (!platform || ![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
|
|
39
44
|
throw new Error(`Unsupported platform: ${platform}. Platform must be "${eas_build_job_1.Platform.ANDROID}" or "${eas_build_job_1.Platform.IOS}"`);
|
|
40
45
|
}
|
|
46
|
+
const target = platform === eas_build_job_1.Platform.IOS
|
|
47
|
+
? {
|
|
48
|
+
platform,
|
|
49
|
+
simulator: inputs.simulator.value ??
|
|
50
|
+
(stepCtx.global.staticContext.job.platform === eas_build_job_1.Platform.IOS &&
|
|
51
|
+
stepCtx.global.staticContext.job.simulator === true),
|
|
52
|
+
}
|
|
53
|
+
: { platform };
|
|
41
54
|
await saveCcacheAsync({
|
|
42
55
|
logger,
|
|
43
56
|
workingDirectory,
|
|
44
|
-
|
|
57
|
+
target,
|
|
45
58
|
evictUsedBefore,
|
|
46
59
|
env,
|
|
47
60
|
secrets: stepCtx.global.staticContext.job.secrets,
|
|
@@ -57,7 +70,7 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
|
|
|
57
70
|
},
|
|
58
71
|
});
|
|
59
72
|
}
|
|
60
|
-
async function saveCcacheAsync({ logger, workingDirectory,
|
|
73
|
+
async function saveCcacheAsync({ logger, workingDirectory, target, evictUsedBefore, env, secrets, }) {
|
|
61
74
|
const enabled = env.EAS_SAVE_CACHE === '1' || (env.EAS_USE_CACHE === '1' && env.EAS_SAVE_CACHE !== '0');
|
|
62
75
|
if (!enabled) {
|
|
63
76
|
return;
|
|
@@ -72,7 +85,7 @@ async function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBe
|
|
|
72
85
|
return;
|
|
73
86
|
}
|
|
74
87
|
try {
|
|
75
|
-
const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory,
|
|
88
|
+
const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory, target);
|
|
76
89
|
logger.info(`Saving cache key: ${cacheKey}`);
|
|
77
90
|
const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
|
78
91
|
const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
|
|
@@ -104,7 +117,7 @@ async function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBe
|
|
|
104
117
|
key: cacheKey,
|
|
105
118
|
paths: [cachePath],
|
|
106
119
|
size,
|
|
107
|
-
platform,
|
|
120
|
+
platform: target.platform,
|
|
108
121
|
});
|
|
109
122
|
}
|
|
110
123
|
catch (err) {
|
|
@@ -117,6 +130,7 @@ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, })
|
|
|
117
130
|
}
|
|
118
131
|
const gradleCachesPath = path_1.default.join(os_1.default.homedir(), '.gradle', 'caches');
|
|
119
132
|
const buildCachePath = path_1.default.join(gradleCachesPath, 'build-cache-1');
|
|
133
|
+
const journalPath = path_1.default.join(gradleCachesPath, 'journal-1');
|
|
120
134
|
try {
|
|
121
135
|
await fs_1.default.promises.access(buildCachePath);
|
|
122
136
|
}
|
|
@@ -130,9 +144,10 @@ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, })
|
|
|
130
144
|
const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
|
131
145
|
const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
|
|
132
146
|
const expoApiServerURL = (0, nullthrows_1.default)(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
|
|
147
|
+
await fs_1.default.promises.mkdir(journalPath, { recursive: true });
|
|
133
148
|
logger.info('Compressing Gradle build cache...');
|
|
134
149
|
const { archivePath } = await (0, saveCache_1.compressCacheAsync)({
|
|
135
|
-
paths: [buildCachePath],
|
|
150
|
+
paths: [buildCachePath, journalPath],
|
|
136
151
|
workingDirectory: gradleCachesPath,
|
|
137
152
|
verbose: env.EXPO_DEBUG === '1',
|
|
138
153
|
logger,
|
|
@@ -146,7 +161,7 @@ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, })
|
|
|
146
161
|
robotAccessToken,
|
|
147
162
|
archivePath,
|
|
148
163
|
key: cacheKey,
|
|
149
|
-
paths: [buildCachePath],
|
|
164
|
+
paths: [buildCachePath, journalPath],
|
|
150
165
|
size,
|
|
151
166
|
platform: eas_build_job_1.Platform.ANDROID,
|
|
152
167
|
});
|
|
@@ -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 {};
|
|
@@ -0,0 +1,244 @@
|
|
|
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.createStartAppiumRemoteSessionBuildFunction = createStartAppiumRemoteSessionBuildFunction;
|
|
7
|
+
exports.resolveAppium3VersionSpec = resolveAppium3VersionSpec;
|
|
8
|
+
exports.resolveAppiumDeviceAsync = resolveAppiumDeviceAsync;
|
|
9
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
10
|
+
const steps_1 = require("@expo/steps");
|
|
11
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const semver_1 = __importDefault(require("semver"));
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
|
|
18
|
+
const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
|
|
19
|
+
const retry_1 = require("../../utils/retry");
|
|
20
|
+
const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
21
|
+
const appiumEvents_1 = require("../utils/appiumEvents");
|
|
22
|
+
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
23
|
+
const APPIUM_HOST = '127.0.0.1';
|
|
24
|
+
const APPIUM_PORT = 4723;
|
|
25
|
+
const APPIUM_STARTUP_TIMEOUT_MS = 120_000;
|
|
26
|
+
const DEFAULT_APPIUM_VERSION = '^3';
|
|
27
|
+
const AppiumInstalledDriversSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.object({ installed: zod_1.z.boolean().optional() }).passthrough());
|
|
28
|
+
function createStartAppiumRemoteSessionBuildFunction(ctx) {
|
|
29
|
+
return new steps_1.BuildFunction({
|
|
30
|
+
namespace: 'eas',
|
|
31
|
+
id: 'start_appium_remote_session',
|
|
32
|
+
name: 'Start Appium remote session',
|
|
33
|
+
__metricsId: 'eas/start_appium_remote_session',
|
|
34
|
+
inputProviders: [
|
|
35
|
+
steps_1.BuildStepInput.createProvider({
|
|
36
|
+
id: 'package_version',
|
|
37
|
+
required: false,
|
|
38
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
39
|
+
}),
|
|
40
|
+
steps_1.BuildStepInput.createProvider({
|
|
41
|
+
id: 'max_idle_time_minutes',
|
|
42
|
+
required: false,
|
|
43
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
fn: async ({ logger, global }, { inputs, env, signal }) => {
|
|
47
|
+
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
48
|
+
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
49
|
+
const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
|
|
50
|
+
const packageVersion = inputs.package_version.value;
|
|
51
|
+
const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
|
|
52
|
+
const { runtimePlatform } = global;
|
|
53
|
+
const versionSpec = resolveAppium3VersionSpec(packageVersion);
|
|
54
|
+
logger.info(`Starting Appium remote session (version: ${versionSpec}, runtime: ${runtimePlatform}).`);
|
|
55
|
+
const device = await resolveAppiumDeviceAsync({ runtimePlatform, env, logger });
|
|
56
|
+
const { appiumHome, appiumBinPath, appiumEnv } = await installAppiumAsync({
|
|
57
|
+
versionSpec,
|
|
58
|
+
driverName: device.driverName,
|
|
59
|
+
env,
|
|
60
|
+
logger,
|
|
61
|
+
});
|
|
62
|
+
const appiumProcess = (0, remoteDeviceRunSession_1.spawnDetached)({
|
|
63
|
+
command: appiumBinPath,
|
|
64
|
+
args: [
|
|
65
|
+
'--address',
|
|
66
|
+
APPIUM_HOST,
|
|
67
|
+
'--port',
|
|
68
|
+
String(APPIUM_PORT),
|
|
69
|
+
'--base-path',
|
|
70
|
+
'/',
|
|
71
|
+
'--log-level',
|
|
72
|
+
'error',
|
|
73
|
+
// Appium 3 gates session listing (GET /appium/sessions) behind the
|
|
74
|
+
// session_discovery insecure feature. We rely on it to poll for
|
|
75
|
+
// Appium Event Timings, so enable it for all drivers.
|
|
76
|
+
'--allow-insecure',
|
|
77
|
+
'*:session_discovery',
|
|
78
|
+
'--default-capabilities',
|
|
79
|
+
JSON.stringify({ 'appium:eventTimings': true }),
|
|
80
|
+
],
|
|
81
|
+
env: appiumEnv,
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
await waitForAppiumReadyAsync({ appiumProcess, logger });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
await appiumProcess.stopAsync();
|
|
88
|
+
await node_fs_1.default.promises.rm(appiumHome, { recursive: true, force: true });
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
const eventCollection = await (0, appiumEvents_1.startAppiumEventCollectionAsync)({
|
|
92
|
+
ctx,
|
|
93
|
+
deviceRunSessionId,
|
|
94
|
+
appiumUrl: `http://${APPIUM_HOST}:${APPIUM_PORT}/`,
|
|
95
|
+
logger,
|
|
96
|
+
});
|
|
97
|
+
let appiumTunnel;
|
|
98
|
+
let serveSim;
|
|
99
|
+
try {
|
|
100
|
+
appiumTunnel = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
|
|
101
|
+
port: APPIUM_PORT,
|
|
102
|
+
subdomainPrefix: 'appium',
|
|
103
|
+
baseDomain: ngrokTunnelDomain,
|
|
104
|
+
authtoken: ngrokAuthtoken,
|
|
105
|
+
logger,
|
|
106
|
+
});
|
|
107
|
+
switch (runtimePlatform) {
|
|
108
|
+
case steps_1.BuildRuntimePlatform.DARWIN:
|
|
109
|
+
serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
|
|
110
|
+
baseDomain: ngrokTunnelDomain,
|
|
111
|
+
env,
|
|
112
|
+
logger,
|
|
113
|
+
timeoutMs: APPIUM_STARTUP_TIMEOUT_MS,
|
|
114
|
+
});
|
|
115
|
+
break;
|
|
116
|
+
case steps_1.BuildRuntimePlatform.LINUX:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
|
|
120
|
+
ctx,
|
|
121
|
+
deviceRunSessionId,
|
|
122
|
+
remoteConfig: {
|
|
123
|
+
appiumUrl: appiumTunnel.url,
|
|
124
|
+
capabilities: {
|
|
125
|
+
platformName: device.platformName,
|
|
126
|
+
'appium:automationName': device.automationName,
|
|
127
|
+
'appium:udid': device.udid,
|
|
128
|
+
},
|
|
129
|
+
...(serveSim ? { webPreviewUrl: serveSim.previewUrl } : {}),
|
|
130
|
+
},
|
|
131
|
+
logger,
|
|
132
|
+
});
|
|
133
|
+
await (0, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
|
|
134
|
+
ctx,
|
|
135
|
+
deviceRunSessionId,
|
|
136
|
+
logger,
|
|
137
|
+
signal,
|
|
138
|
+
idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
|
|
139
|
+
? {
|
|
140
|
+
maxIdleTimeMinutes,
|
|
141
|
+
getLastEventObservedAt: eventCollection.getLastEventObservedAt,
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
if (serveSim) {
|
|
148
|
+
await serveSim.stopAsync();
|
|
149
|
+
}
|
|
150
|
+
if (appiumTunnel) {
|
|
151
|
+
await appiumTunnel.stopAsync();
|
|
152
|
+
}
|
|
153
|
+
await eventCollection.stopAsync();
|
|
154
|
+
await appiumProcess.stopAsync();
|
|
155
|
+
await node_fs_1.default.promises.rm(appiumHome, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function resolveAppium3VersionSpec(packageVersion) {
|
|
161
|
+
const versionSpec = packageVersion ?? DEFAULT_APPIUM_VERSION;
|
|
162
|
+
const range = semver_1.default.validRange(versionSpec);
|
|
163
|
+
if (!range || !semver_1.default.subset(range, '>=3.0.0 <4.0.0-0')) {
|
|
164
|
+
throw new eas_build_job_1.SystemError(`Appium 3 is required for EAS Simulator sessions. Received package version "${versionSpec}".`);
|
|
165
|
+
}
|
|
166
|
+
return versionSpec;
|
|
167
|
+
}
|
|
168
|
+
async function resolveAppiumDeviceAsync({ runtimePlatform, env, logger, }) {
|
|
169
|
+
switch (runtimePlatform) {
|
|
170
|
+
case steps_1.BuildRuntimePlatform.DARWIN: {
|
|
171
|
+
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
172
|
+
const [bootedDevice] = await IosSimulatorUtils_1.IosSimulatorUtils.getAvailableDevicesAsync({
|
|
173
|
+
env,
|
|
174
|
+
filter: 'booted',
|
|
175
|
+
});
|
|
176
|
+
if (!bootedDevice) {
|
|
177
|
+
throw new eas_build_job_1.SystemError('Could not find a booted iOS simulator for the Appium session.');
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
platformName: 'iOS',
|
|
181
|
+
automationName: 'XCUITest',
|
|
182
|
+
driverName: 'xcuitest',
|
|
183
|
+
udid: bootedDevice.udid,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
case steps_1.BuildRuntimePlatform.LINUX: {
|
|
187
|
+
const attachedDevices = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.getAttachedDevicesAsync({ env });
|
|
188
|
+
const bootedDevice = attachedDevices.find(device => device.state === 'device');
|
|
189
|
+
if (!bootedDevice) {
|
|
190
|
+
throw new eas_build_job_1.SystemError('Could not find a booted Android emulator for the Appium session.');
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
platformName: 'Android',
|
|
194
|
+
automationName: 'UiAutomator2',
|
|
195
|
+
driverName: 'uiautomator2',
|
|
196
|
+
udid: bootedDevice.serialId,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function installAppiumAsync({ versionSpec, driverName, env, logger, }) {
|
|
202
|
+
const appiumHome = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-appium-home-'));
|
|
203
|
+
await node_fs_1.default.promises.writeFile(node_path_1.default.join(appiumHome, 'package.json'), `${JSON.stringify({ name: 'eas-appium-home', private: true })}\n`);
|
|
204
|
+
const appiumEnv = { ...env, APPIUM_HOME: appiumHome };
|
|
205
|
+
const appiumBinPath = node_path_1.default.join(appiumHome, 'node_modules', '.bin', 'appium');
|
|
206
|
+
logger.info(`Installing appium@${versionSpec}.`);
|
|
207
|
+
await (0, turtle_spawn_1.default)('npm', ['install', '--prefix', appiumHome, `appium@${versionSpec}`], {
|
|
208
|
+
env: appiumEnv,
|
|
209
|
+
logger,
|
|
210
|
+
});
|
|
211
|
+
const { stdout } = await (0, turtle_spawn_1.default)(appiumBinPath, ['driver', 'list', '--installed', '--json'], {
|
|
212
|
+
env: appiumEnv,
|
|
213
|
+
stdio: 'pipe',
|
|
214
|
+
});
|
|
215
|
+
const installedDrivers = AppiumInstalledDriversSchema.parse(JSON.parse(stdout));
|
|
216
|
+
if (installedDrivers[driverName]?.installed) {
|
|
217
|
+
logger.info(`Updating the installed Appium ${driverName} driver.`);
|
|
218
|
+
await (0, turtle_spawn_1.default)(appiumBinPath, ['driver', 'update', driverName], { env: appiumEnv, logger });
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
logger.info(`Installing the Appium ${driverName} driver.`);
|
|
222
|
+
await (0, turtle_spawn_1.default)(appiumBinPath, ['driver', 'install', driverName], { env: appiumEnv, logger });
|
|
223
|
+
}
|
|
224
|
+
return { appiumHome, appiumBinPath, appiumEnv };
|
|
225
|
+
}
|
|
226
|
+
async function waitForAppiumReadyAsync({ appiumProcess, logger, }) {
|
|
227
|
+
const deadline = Date.now() + APPIUM_STARTUP_TIMEOUT_MS;
|
|
228
|
+
while (Date.now() < deadline) {
|
|
229
|
+
try {
|
|
230
|
+
const response = await (0, turtleFetch_1.turtleFetch)(`http://${APPIUM_HOST}:${APPIUM_PORT}/status`, 'GET', {
|
|
231
|
+
timeout: 2_000,
|
|
232
|
+
retries: 0,
|
|
233
|
+
logger,
|
|
234
|
+
});
|
|
235
|
+
if (response.ok) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
await (0, retry_1.sleepAsync)(1_000);
|
|
241
|
+
}
|
|
242
|
+
const output = appiumProcess.getOutput();
|
|
243
|
+
throw new eas_build_job_1.SystemError(`Timed out waiting for Appium to become ready.${output ? `\nAppium output:\n${output}` : ''}`);
|
|
244
|
+
}
|
|
@@ -57,6 +57,11 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
57
57
|
required: false,
|
|
58
58
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
59
59
|
}),
|
|
60
|
+
steps_1.BuildStepInput.createProvider({
|
|
61
|
+
id: 'max_duration_seconds',
|
|
62
|
+
required: false,
|
|
63
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
64
|
+
}),
|
|
60
65
|
],
|
|
61
66
|
fn: async ({ logger, global }, { inputs, env, signal }) => {
|
|
62
67
|
// Fail fast before any expensive setup if the injected env
|
|
@@ -69,6 +74,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
69
74
|
const packageVersion = inputs.package_version.value;
|
|
70
75
|
// A missing or non-positive value disables the idle timeout (opt-in feature).
|
|
71
76
|
const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
|
|
77
|
+
const maxDurationSeconds = inputs.max_duration_seconds?.value;
|
|
72
78
|
warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger });
|
|
73
79
|
const versionSpec = packageVersion ?? 'latest';
|
|
74
80
|
const { runtimePlatform } = global;
|
|
@@ -178,6 +184,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
178
184
|
ctx,
|
|
179
185
|
deviceRunSessionId,
|
|
180
186
|
logger,
|
|
187
|
+
maxDurationSeconds,
|
|
181
188
|
signal,
|
|
182
189
|
idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
|
|
183
190
|
? {
|
|
@@ -11,9 +11,23 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
|
11
11
|
name: 'Start serve-sim remote session',
|
|
12
12
|
__metricsId: 'eas/start_serve_sim_remote_session',
|
|
13
13
|
supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
|
|
14
|
-
|
|
14
|
+
inputProviders: [
|
|
15
|
+
steps_1.BuildStepInput.createProvider({
|
|
16
|
+
id: 'package_version',
|
|
17
|
+
required: false,
|
|
18
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
19
|
+
}),
|
|
20
|
+
steps_1.BuildStepInput.createProvider({
|
|
21
|
+
id: 'max_duration_seconds',
|
|
22
|
+
required: false,
|
|
23
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
fn: async ({ logger }, { inputs, env, signal }) => {
|
|
15
27
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
16
28
|
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
29
|
+
const maxDurationSeconds = inputs.max_duration_seconds?.value;
|
|
30
|
+
const packageVersion = inputs.package_version?.value;
|
|
17
31
|
logger.info('Starting serve-sim remote session.');
|
|
18
32
|
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
19
33
|
const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
|
|
@@ -21,6 +35,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
|
21
35
|
env,
|
|
22
36
|
logger,
|
|
23
37
|
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
38
|
+
packageVersion,
|
|
24
39
|
});
|
|
25
40
|
logger.info(`Preview URL: ${serveSim.previewUrl}`);
|
|
26
41
|
try {
|
|
@@ -34,6 +49,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
|
34
49
|
ctx,
|
|
35
50
|
deviceRunSessionId,
|
|
36
51
|
logger,
|
|
52
|
+
maxDurationSeconds,
|
|
37
53
|
signal,
|
|
38
54
|
});
|
|
39
55
|
}
|
|
@@ -141,15 +141,25 @@ function createUploadToAscBuildFunction() {
|
|
|
141
141
|
const appResponse = await AscApiUtils_1.AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
|
|
142
142
|
const ascAppBundleIdentifier = appResponse.data.attributes.bundleId;
|
|
143
143
|
stepsCtx.logger.info(`Uploading Build to "${appResponse.data.attributes.name}" (${ascAppBundleIdentifier})...`);
|
|
144
|
+
// Derive the App Store Connect platform from the IPA itself so tvOS (and
|
|
145
|
+
// other) binaries are uploaded to the correct version train instead of the
|
|
146
|
+
// iOS one. Falls back to `IOS` if the platform cannot be read.
|
|
147
|
+
const ipaInfoResult = await (0, results_1.asyncResult)((0, readIpaInfo_1.readIpaInfoAsync)(ipaPath));
|
|
148
|
+
const platform = ipaInfoResult.ok
|
|
149
|
+
? AscApiUtils_1.AscApiUtils.ascPlatformFromDtPlatformName(ipaInfoResult.value.dtPlatformName)
|
|
150
|
+
: 'IOS';
|
|
151
|
+
stepsCtx.logger.info(`Detected App Store Connect platform: ${platform}`);
|
|
144
152
|
stepsCtx.logger.info('Creating Build Upload...');
|
|
145
153
|
const buildUploadResponse = await AscApiUtils_1.AscApiUtils.createBuildUploadAsync({
|
|
146
154
|
client,
|
|
147
155
|
appleAppIdentifier,
|
|
148
156
|
bundleShortVersion,
|
|
149
157
|
bundleVersion,
|
|
158
|
+
platform,
|
|
150
159
|
});
|
|
151
160
|
const buildUploadId = buildUploadResponse.data.id;
|
|
152
|
-
const
|
|
161
|
+
const platformPathSegment = AscApiUtils_1.AscApiUtils.testFlightPlatformPathSegment(platform);
|
|
162
|
+
const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/${platformPathSegment}/${buildUploadId}`;
|
|
153
163
|
outputs.build_upload_id.set(buildUploadId);
|
|
154
164
|
outputs.build_upload_url.set(buildUploadUrl);
|
|
155
165
|
stepsCtx.logger.info(`Build Upload initialized (ID: ${buildUploadId}). Preparing IPA upload...`);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type bunyan } from '@expo/logger';
|
|
2
|
+
import { type CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
|
|
4
|
+
export declare function startAppiumEventCollectionAsync({ ctx, deviceRunSessionId, appiumUrl, logger, pollIntervalMs, }: {
|
|
5
|
+
ctx: CustomBuildContext;
|
|
6
|
+
deviceRunSessionId: string;
|
|
7
|
+
appiumUrl: string;
|
|
8
|
+
logger: bunyan;
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
}): Promise<DeviceRunSessionEventCollection>;
|
|
@@ -0,0 +1,175 @@
|
|
|
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.startAppiumEventCollectionAsync = startAppiumEventCollectionAsync;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const promises_1 = require("node:timers/promises");
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
13
|
+
const deviceRunSessionEvents_1 = require("./deviceRunSessionEvents");
|
|
14
|
+
const APPIUM_REQUEST_TIMEOUT_MS = 10_000;
|
|
15
|
+
const POLL_INTERVAL_MS = 1_000;
|
|
16
|
+
const GET_LOG_EVENTS_COMMAND = 'getLogEvents';
|
|
17
|
+
const AppiumSessionsResponseSchema = zod_1.z.object({
|
|
18
|
+
value: zod_1.z.array(zod_1.z.object({ id: zod_1.z.string() }).passthrough()),
|
|
19
|
+
});
|
|
20
|
+
const AppiumCommandSchema = zod_1.z.object({
|
|
21
|
+
appiumSessionId: zod_1.z.string(),
|
|
22
|
+
cmd: zod_1.z.string(),
|
|
23
|
+
startTime: zod_1.z.number(),
|
|
24
|
+
endTime: zod_1.z.number(),
|
|
25
|
+
});
|
|
26
|
+
const AppiumEventsResponseSchema = zod_1.z.object({
|
|
27
|
+
value: zod_1.z.object({
|
|
28
|
+
commands: zod_1.z.array(AppiumCommandSchema.omit({ appiumSessionId: true })),
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
async function startAppiumEventCollectionAsync({ ctx, deviceRunSessionId, appiumUrl, logger, pollIntervalMs = POLL_INTERVAL_MS, }) {
|
|
32
|
+
const eventDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-appium-events-'));
|
|
33
|
+
const eventFile = node_path_1.default.join(eventDirectory, 'commands.ndjson');
|
|
34
|
+
await node_fs_1.default.promises.writeFile(eventFile, '');
|
|
35
|
+
let eventCollection;
|
|
36
|
+
try {
|
|
37
|
+
eventCollection = await (0, deviceRunSessionEvents_1.startDeviceRunSessionEventCollectionAsync)({
|
|
38
|
+
ctx,
|
|
39
|
+
deviceRunSessionId,
|
|
40
|
+
logger,
|
|
41
|
+
pollIntervalMs,
|
|
42
|
+
source: createAppiumEventSource(eventFile),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
await node_fs_1.default.promises.rm(eventDirectory, { recursive: true, force: true });
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const observedCommandKeys = new Set();
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
let didReportCollectionFailure = false;
|
|
52
|
+
const collectSafelyAsync = async () => {
|
|
53
|
+
try {
|
|
54
|
+
await collectAppiumCommandsAsync({
|
|
55
|
+
appiumUrl,
|
|
56
|
+
eventFile,
|
|
57
|
+
observedCommandKeys,
|
|
58
|
+
logger,
|
|
59
|
+
});
|
|
60
|
+
didReportCollectionFailure = false;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!didReportCollectionFailure) {
|
|
64
|
+
didReportCollectionFailure = true;
|
|
65
|
+
logger.warn({ err: error }, 'Could not collect Appium events.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const pollingPromise = (async () => {
|
|
70
|
+
while (!controller.signal.aborted) {
|
|
71
|
+
await collectSafelyAsync();
|
|
72
|
+
try {
|
|
73
|
+
await (0, promises_1.setTimeout)(pollIntervalMs, undefined, { signal: controller.signal });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (!controller.signal.aborted) {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})().catch(error => {
|
|
82
|
+
logger.warn({ err: error }, 'Appium event collection poller failed.');
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
getLastEventObservedAt: eventCollection.getLastEventObservedAt,
|
|
86
|
+
stopAsync: async () => {
|
|
87
|
+
controller.abort();
|
|
88
|
+
await pollingPromise;
|
|
89
|
+
await collectSafelyAsync();
|
|
90
|
+
try {
|
|
91
|
+
await eventCollection.stopAsync();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await node_fs_1.default.promises.rm(eventDirectory, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function createAppiumEventSource(eventFile) {
|
|
100
|
+
return {
|
|
101
|
+
producer: 'appium',
|
|
102
|
+
findEventFilesAsync: async () => [eventFile],
|
|
103
|
+
sourceKeyForFile: file => node_path_1.default.basename(file, node_path_1.default.extname(file)),
|
|
104
|
+
parseLine: ({ line, sourceKey, sequenceNumber, deviceRunSessionId }) => {
|
|
105
|
+
if (!line.trim()) {
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(line);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { failure: 'invalid-json' };
|
|
114
|
+
}
|
|
115
|
+
const result = AppiumCommandSchema.safeParse(parsed);
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
return { failure: 'invalid-event' };
|
|
118
|
+
}
|
|
119
|
+
const command = result.data;
|
|
120
|
+
const operationId = `${sourceKey}:${sequenceNumber}`;
|
|
121
|
+
return {
|
|
122
|
+
event: {
|
|
123
|
+
v: 1,
|
|
124
|
+
eventId: `appium:${deviceRunSessionId}:${operationId}`,
|
|
125
|
+
ts: new Date(command.endTime).toISOString(),
|
|
126
|
+
producer: 'appium',
|
|
127
|
+
type: 'operation.completed',
|
|
128
|
+
operationId,
|
|
129
|
+
durationMs: Math.max(0, command.endTime - command.startTime),
|
|
130
|
+
summary: command.cmd,
|
|
131
|
+
data: {
|
|
132
|
+
command: command.cmd,
|
|
133
|
+
appiumSessionId: command.appiumSessionId,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async function collectAppiumCommandsAsync({ appiumUrl, eventFile, observedCommandKeys, logger, }) {
|
|
141
|
+
const response = await (0, turtleFetch_1.turtleFetch)(new URL('appium/sessions', appiumUrl).toString(), 'GET', {
|
|
142
|
+
timeout: APPIUM_REQUEST_TIMEOUT_MS,
|
|
143
|
+
retries: 0,
|
|
144
|
+
logger,
|
|
145
|
+
});
|
|
146
|
+
const sessions = AppiumSessionsResponseSchema.parse(await response.json()).value;
|
|
147
|
+
const newCommands = [];
|
|
148
|
+
const pendingCommandKeys = new Set();
|
|
149
|
+
for (const { id: appiumSessionId } of sessions) {
|
|
150
|
+
const eventsResponse = await (0, turtleFetch_1.turtleFetch)(new URL(`session/${encodeURIComponent(appiumSessionId)}/appium/events`, appiumUrl).toString(), 'POST', {
|
|
151
|
+
json: {},
|
|
152
|
+
timeout: APPIUM_REQUEST_TIMEOUT_MS,
|
|
153
|
+
retries: 0,
|
|
154
|
+
logger,
|
|
155
|
+
});
|
|
156
|
+
const commands = AppiumEventsResponseSchema.parse(await eventsResponse.json()).value.commands;
|
|
157
|
+
for (const command of commands) {
|
|
158
|
+
const key = [appiumSessionId, command.cmd, command.startTime, command.endTime].join(':');
|
|
159
|
+
if (command.cmd === GET_LOG_EVENTS_COMMAND ||
|
|
160
|
+
observedCommandKeys.has(key) ||
|
|
161
|
+
pendingCommandKeys.has(key)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
pendingCommandKeys.add(key);
|
|
165
|
+
newCommands.push({ key, command: { appiumSessionId, ...command } });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (newCommands.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await node_fs_1.default.promises.appendFile(eventFile, `${newCommands.map(({ command }) => JSON.stringify(command)).join('\n')}\n`);
|
|
172
|
+
for (const { key } of newCommands) {
|
|
173
|
+
observedCommandKeys.add(key);
|
|
174
|
+
}
|
|
175
|
+
}
|