@expo/build-tools 21.5.1 → 21.7.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/custom.js +5 -4
- package/dist/common/jobHooks.js +1 -2
- package/dist/generic.js +5 -4
- package/dist/steps/functions/startAgentDeviceRemoteSession.d.ts +0 -1
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +21 -8
- package/dist/steps/functions/startArgentRemoteSession.js +13 -0
- package/dist/steps/utils/agentDeviceEvents.d.ts +2 -3
- package/dist/steps/utils/argentArtifacts.js +22 -1
- package/dist/steps/utils/argentEvents.d.ts +2 -3
- package/dist/steps/utils/deviceRunSessionEvents.d.ts +11 -3
- package/dist/steps/utils/deviceRunSessionEvents.js +10 -1
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +12 -1
- package/dist/steps/utils/remoteDeviceRunSession.js +47 -1
- package/dist/steps/utils/serveSimMetricsArtifacts.js +4 -2
- package/dist/utils/easCli.d.ts +2 -0
- package/dist/utils/easCli.js +43 -3
- package/package.json +4 -4
- package/dist/steps/compositeFunctions.d.ts +0 -11
- package/dist/steps/compositeFunctions.js +0 -62
package/dist/builders/custom.js
CHANGED
|
@@ -15,7 +15,6 @@ const projectSources_1 = require("../common/projectSources");
|
|
|
15
15
|
const customBuildContext_1 = require("../customBuildContext");
|
|
16
16
|
const datadog_1 = require("../datadog");
|
|
17
17
|
const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
|
|
18
|
-
const compositeFunctions_1 = require("../steps/compositeFunctions");
|
|
19
18
|
const easFunctionGroups_1 = require("../steps/easFunctionGroups");
|
|
20
19
|
const easFunctions_1 = require("../steps/easFunctions");
|
|
21
20
|
const retry_1 = require("../utils/retry");
|
|
@@ -55,11 +54,13 @@ async function runCustomBuildAsync(ctx) {
|
|
|
55
54
|
steps: ctx.job.steps,
|
|
56
55
|
hooks: ctx.job.hooks,
|
|
57
56
|
// Eager for job steps (always run), lazy loader for hooks (running anchors only).
|
|
58
|
-
compositeFunctionCatalog: await (0,
|
|
59
|
-
|
|
57
|
+
compositeFunctionCatalog: await (0, steps_1.buildLocalCompositeFunctionCatalogAsync)(projectRoot, {
|
|
58
|
+
rootSteps: ctx.job.steps,
|
|
59
|
+
logger: ctx.logger,
|
|
60
|
+
}),
|
|
61
|
+
loadCompositeFunction: (0, steps_1.createLocalCompositeFunctionLoader)(projectRoot, {
|
|
60
62
|
logger: ctx.logger,
|
|
61
63
|
}),
|
|
62
|
-
loadCompositeFunction: (0, compositeFunctions_1.createCompositeFunctionLoader)(projectRoot, ctx.logger),
|
|
63
64
|
})
|
|
64
65
|
: new steps_1.BuildConfigParser(globalContext, {
|
|
65
66
|
externalFunctions: easFunctions,
|
package/dist/common/jobHooks.js
CHANGED
|
@@ -4,7 +4,6 @@ exports.parseJobHooksAsync = parseJobHooksAsync;
|
|
|
4
4
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
5
5
|
const steps_1 = require("@expo/steps");
|
|
6
6
|
const customBuildContext_1 = require("../customBuildContext");
|
|
7
|
-
const compositeFunctions_1 = require("../steps/compositeFunctions");
|
|
8
7
|
const easFunctionGroups_1 = require("../steps/easFunctionGroups");
|
|
9
8
|
const easFunctions_1 = require("../steps/easFunctions");
|
|
10
9
|
/**
|
|
@@ -60,7 +59,7 @@ async function parseJobHooksAsync(ctx, wrappedAnchors) {
|
|
|
60
59
|
const hookEntriesByKey = {};
|
|
61
60
|
const orderedSteps = [];
|
|
62
61
|
const compositeFunctionCatalog = {};
|
|
63
|
-
const loadCompositeFunction = (0,
|
|
62
|
+
const loadCompositeFunction = (0, steps_1.createLocalCompositeFunctionLoader)(ctx.getReactNativeProjectDirectory(), { logger: ctx.logger });
|
|
64
63
|
for (const anchor of wrappedAnchors) {
|
|
65
64
|
for (const side of ['before', 'after']) {
|
|
66
65
|
const key = `${side}_${anchor}`;
|
package/dist/generic.js
CHANGED
|
@@ -11,7 +11,6 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
11
11
|
const nullthrows_1 = __importDefault(require("nullthrows"));
|
|
12
12
|
const projectSources_1 = require("./common/projectSources");
|
|
13
13
|
const customBuildContext_1 = require("./customBuildContext");
|
|
14
|
-
const compositeFunctions_1 = require("./steps/compositeFunctions");
|
|
15
14
|
const easFunctionGroups_1 = require("./steps/easFunctionGroups");
|
|
16
15
|
const easFunctions_1 = require("./steps/easFunctions");
|
|
17
16
|
const outputs_1 = require("./utils/outputs");
|
|
@@ -38,8 +37,8 @@ async function runGenericJobAsync(ctx) {
|
|
|
38
37
|
try {
|
|
39
38
|
const projectRoot = ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory);
|
|
40
39
|
// Eager for job steps (always run), lazy loader for hooks (running anchors only).
|
|
41
|
-
const compositeFunctionCatalog = await (0,
|
|
42
|
-
|
|
40
|
+
const compositeFunctionCatalog = await (0, steps_1.buildLocalCompositeFunctionCatalogAsync)(projectRoot, {
|
|
41
|
+
rootSteps: ctx.job.steps,
|
|
43
42
|
logger: ctx.logger,
|
|
44
43
|
});
|
|
45
44
|
const parser = new steps_1.StepsConfigParser(globalContext, {
|
|
@@ -48,7 +47,9 @@ async function runGenericJobAsync(ctx) {
|
|
|
48
47
|
steps: ctx.job.steps,
|
|
49
48
|
hooks: ctx.job.hooks,
|
|
50
49
|
compositeFunctionCatalog,
|
|
51
|
-
loadCompositeFunction: (0,
|
|
50
|
+
loadCompositeFunction: (0, steps_1.createLocalCompositeFunctionLoader)(projectRoot, {
|
|
51
|
+
logger: ctx.logger,
|
|
52
|
+
}),
|
|
52
53
|
});
|
|
53
54
|
return await parser.parseAsync();
|
|
54
55
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type bunyan } from '@expo/logger';
|
|
2
2
|
import { BuildFunction } from '@expo/steps';
|
|
3
3
|
import { type CustomBuildContext } from '../../customBuildContext';
|
|
4
|
-
export declare const DEFAULT_AGENT_DEVICE_VERSION = "0.20.3";
|
|
5
4
|
export declare function createStartAgentDeviceRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
|
|
6
5
|
export declare function stopAgentDeviceEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }: {
|
|
7
6
|
eventCollection: {
|
|
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEFAULT_AGENT_DEVICE_VERSION = void 0;
|
|
7
6
|
exports.createStartAgentDeviceRemoteSessionBuildFunction = createStartAgentDeviceRemoteSessionBuildFunction;
|
|
8
7
|
exports.stopAgentDeviceEventCollectionSafelyAsync = stopAgentDeviceEventCollectionSafelyAsync;
|
|
9
8
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
@@ -17,7 +16,6 @@ const agentDeviceArtifacts_1 = require("../utils/agentDeviceArtifacts");
|
|
|
17
16
|
const agentDeviceEvents_1 = require("../utils/agentDeviceEvents");
|
|
18
17
|
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
19
18
|
const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
|
|
20
|
-
exports.DEFAULT_AGENT_DEVICE_VERSION = '0.20.3';
|
|
21
19
|
const AGENT_DEVICE_REPO_URL = 'https://github.com/callstack/agent-device.git';
|
|
22
20
|
const SRC_DIR = '/tmp/agent-device-src';
|
|
23
21
|
const AGENT_DEVICE_STATE_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device');
|
|
@@ -39,6 +37,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
39
37
|
required: false,
|
|
40
38
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
41
39
|
}),
|
|
40
|
+
steps_1.BuildStepInput.createProvider({
|
|
41
|
+
id: 'max_idle_time_minutes',
|
|
42
|
+
required: false,
|
|
43
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
44
|
+
}),
|
|
42
45
|
],
|
|
43
46
|
fn: async ({ logger, global }, { inputs, env, signal }) => {
|
|
44
47
|
// Fail fast before any expensive setup if the injected env
|
|
@@ -48,9 +51,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
48
51
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
49
52
|
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
50
53
|
const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
|
|
51
|
-
const packageVersion = inputs.package_version.value
|
|
54
|
+
const packageVersion = inputs.package_version.value;
|
|
55
|
+
// A missing or non-positive value disables the idle timeout (opt-in feature).
|
|
56
|
+
const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
|
|
52
57
|
const { runtimePlatform } = global;
|
|
53
|
-
logger.info(`Starting agent-device remote session (version: ${packageVersion}, runtime: ${runtimePlatform}).`);
|
|
58
|
+
logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
|
|
54
59
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
55
60
|
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
56
61
|
}
|
|
@@ -111,6 +116,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
111
116
|
deviceRunSessionId,
|
|
112
117
|
logger,
|
|
113
118
|
signal,
|
|
119
|
+
idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
|
|
120
|
+
? {
|
|
121
|
+
maxIdleTimeMinutes,
|
|
122
|
+
getLastEventObservedAt: eventCollection.getLastEventObservedAt,
|
|
123
|
+
}
|
|
124
|
+
: undefined,
|
|
114
125
|
});
|
|
115
126
|
}
|
|
116
127
|
finally {
|
|
@@ -174,7 +185,7 @@ async function startAgentDeviceDaemonAsync({ packageVersion, env, logger, }) {
|
|
|
174
185
|
},
|
|
175
186
|
extras: {
|
|
176
187
|
packageSpec,
|
|
177
|
-
packageVersion,
|
|
188
|
+
packageVersion: packageVersion ?? 'latest',
|
|
178
189
|
bunVersion,
|
|
179
190
|
bunInstallConfigured: Boolean(env.BUN_INSTALL?.trim()),
|
|
180
191
|
},
|
|
@@ -184,7 +195,9 @@ async function startAgentDeviceDaemonAsync({ packageVersion, env, logger, }) {
|
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
async function startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger, }) {
|
|
187
|
-
logger.info(
|
|
198
|
+
logger.info(packageVersion
|
|
199
|
+
? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
|
|
200
|
+
: `Cloning agent-device (latest) into ${SRC_DIR}.`);
|
|
188
201
|
await cloneAgentDeviceAsync({ packageVersion, env, logger });
|
|
189
202
|
logger.info('Installing agent-device dependencies.');
|
|
190
203
|
await (0, turtle_spawn_1.default)('bun', ['install', '--production'], {
|
|
@@ -201,7 +214,7 @@ async function startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger,
|
|
|
201
214
|
});
|
|
202
215
|
}
|
|
203
216
|
async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
|
|
204
|
-
const branchArgs = ['--branch', `v${packageVersion}`];
|
|
217
|
+
const branchArgs = packageVersion ? ['--branch', `v${packageVersion}`] : [];
|
|
205
218
|
await (0, turtle_spawn_1.default)('git', ['clone', '--depth', '1', ...branchArgs, AGENT_DEVICE_REPO_URL, SRC_DIR], {
|
|
206
219
|
env,
|
|
207
220
|
logger,
|
|
@@ -233,7 +246,7 @@ async function getBunVersionForDiagnosticsAsync(env) {
|
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
function createAgentDevicePackageSpec(packageVersion) {
|
|
236
|
-
const versionSpec = packageVersion.replace(/^v(?=\d)/, '');
|
|
249
|
+
const versionSpec = packageVersion ? packageVersion.replace(/^v(?=\d)/, '') : 'latest';
|
|
237
250
|
return `${AGENT_DEVICE_PACKAGE_NAME}@${versionSpec}`;
|
|
238
251
|
}
|
|
239
252
|
function getGlobalAgentDeviceDaemonPath(env) {
|
|
@@ -52,6 +52,11 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
52
52
|
required: false,
|
|
53
53
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
54
54
|
}),
|
|
55
|
+
steps_1.BuildStepInput.createProvider({
|
|
56
|
+
id: 'max_idle_time_minutes',
|
|
57
|
+
required: false,
|
|
58
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
59
|
+
}),
|
|
55
60
|
],
|
|
56
61
|
fn: async ({ logger, global }, { inputs, env, signal }) => {
|
|
57
62
|
// Fail fast before any expensive setup if the injected env
|
|
@@ -62,6 +67,8 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
62
67
|
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
63
68
|
const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
|
|
64
69
|
const packageVersion = inputs.package_version.value;
|
|
70
|
+
// A missing or non-positive value disables the idle timeout (opt-in feature).
|
|
71
|
+
const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
|
|
65
72
|
warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger });
|
|
66
73
|
const versionSpec = packageVersion ?? 'latest';
|
|
67
74
|
const { runtimePlatform } = global;
|
|
@@ -172,6 +179,12 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
172
179
|
deviceRunSessionId,
|
|
173
180
|
logger,
|
|
174
181
|
signal,
|
|
182
|
+
idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
|
|
183
|
+
? {
|
|
184
|
+
maxIdleTimeMinutes,
|
|
185
|
+
getLastEventObservedAt: eventCollection.getLastEventObservedAt,
|
|
186
|
+
}
|
|
187
|
+
: undefined,
|
|
175
188
|
});
|
|
176
189
|
}
|
|
177
190
|
finally {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { type bunyan } from '@expo/logger';
|
|
2
2
|
import { type CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
|
|
3
4
|
export declare function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }: {
|
|
4
5
|
ctx: CustomBuildContext;
|
|
5
6
|
deviceRunSessionId: string;
|
|
6
7
|
stateDir: string;
|
|
7
8
|
logger: bunyan;
|
|
8
9
|
pollIntervalMs?: number;
|
|
9
|
-
}): Promise<
|
|
10
|
-
stopAsync: () => Promise<void>;
|
|
11
|
-
}>;
|
|
10
|
+
}): Promise<DeviceRunSessionEventCollection>;
|
|
@@ -21,6 +21,17 @@ const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
|
|
|
21
21
|
const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
|
|
22
22
|
const ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS = 30_000;
|
|
23
23
|
const ARGENT_ARTIFACT_FETCH_TIMEOUT_MS = 10_000;
|
|
24
|
+
// Kinds the EAS dashboard groups device run session artifacts by. Only media types the dashboard
|
|
25
|
+
// renders specially are mapped; anything else stays unclassified and lands in its "Other" group.
|
|
26
|
+
// Note that these kinds only affect grouping and labelling. The dashboard gates its inline video
|
|
27
|
+
// player on the `__eas_screen_recording` metadata flag, which Argent artifacts deliberately do not
|
|
28
|
+
// set, so a `screen-recording` kind here does not add a player to the session page.
|
|
29
|
+
const ARGENT_ARTIFACT_KIND_BY_MIME_TYPE = new Map([
|
|
30
|
+
['image/png', 'screenshot'],
|
|
31
|
+
['image/jpeg', 'screenshot'],
|
|
32
|
+
['video/mp4', 'screen-recording'],
|
|
33
|
+
['video/quicktime', 'screen-recording'],
|
|
34
|
+
]);
|
|
24
35
|
const ArgentArtifactSchema = zod_1.z.object({
|
|
25
36
|
id: zod_1.z.string(),
|
|
26
37
|
filename: zod_1.z.string(),
|
|
@@ -30,6 +41,16 @@ const ArgentArtifactSchema = zod_1.z.object({
|
|
|
30
41
|
const ArgentArtifactsListResponseSchema = zod_1.z.object({
|
|
31
42
|
artifacts: zod_1.z.array(ArgentArtifactSchema),
|
|
32
43
|
});
|
|
44
|
+
function getArgentArtifactKind(artifact) {
|
|
45
|
+
// Directories are repackaged as a tarball before upload, so the reported media type describes the
|
|
46
|
+
// contents rather than the file we actually store.
|
|
47
|
+
if (artifact.isDirectory) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
// Media types are case-insensitive and may carry parameters, e.g. `image/png; charset=binary`.
|
|
51
|
+
const mediaType = artifact.mimeType.split(';')[0].trim().toLowerCase();
|
|
52
|
+
return ARGENT_ARTIFACT_KIND_BY_MIME_TYPE.get(mediaType);
|
|
53
|
+
}
|
|
33
54
|
async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, signal, }) {
|
|
34
55
|
logger.info('Started polling Argent tool-server for artifacts.');
|
|
35
56
|
const seenArtifactIds = new Set();
|
|
@@ -126,7 +147,7 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
|
|
|
126
147
|
artifactId: artifact.id,
|
|
127
148
|
name: `${filename} (${artifact.id})`,
|
|
128
149
|
filename,
|
|
129
|
-
kind:
|
|
150
|
+
kind: getArgentArtifactKind(artifact),
|
|
130
151
|
size,
|
|
131
152
|
stream: (0, node_fs_1.createReadStream)(temporaryArtifactPath),
|
|
132
153
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type bunyan } from '@expo/logger';
|
|
2
2
|
import { type CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
|
|
3
4
|
export declare const ARGENT_EVENT_LOG_FILENAME = "tool-server-events.jsonl";
|
|
4
5
|
export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionId, eventLogPath, logger, pollIntervalMs, }: {
|
|
5
6
|
ctx: CustomBuildContext;
|
|
@@ -7,6 +8,4 @@ export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionI
|
|
|
7
8
|
eventLogPath: string;
|
|
8
9
|
logger: bunyan;
|
|
9
10
|
pollIntervalMs?: number;
|
|
10
|
-
}): Promise<
|
|
11
|
-
stopAsync: () => Promise<void>;
|
|
12
|
-
}>;
|
|
11
|
+
}): Promise<DeviceRunSessionEventCollection>;
|
|
@@ -53,12 +53,20 @@ export type DeviceRunSessionEventSource = {
|
|
|
53
53
|
deviceRunSessionId: string;
|
|
54
54
|
}) => DeviceRunSessionEventParseResult;
|
|
55
55
|
};
|
|
56
|
+
export type DeviceRunSessionEventCollection = {
|
|
57
|
+
stopAsync: () => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Local arrival time of the most recently collected event, or `undefined`
|
|
60
|
+
* when no event has been observed yet. Uses the collector's clock rather
|
|
61
|
+
* than the event's embedded timestamp so producer clock skew cannot affect
|
|
62
|
+
* idle detection.
|
|
63
|
+
*/
|
|
64
|
+
getLastEventObservedAt: () => Date | undefined;
|
|
65
|
+
};
|
|
56
66
|
export declare function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSessionId, source, logger, pollIntervalMs, }: {
|
|
57
67
|
ctx: CustomBuildContext;
|
|
58
68
|
deviceRunSessionId: string;
|
|
59
69
|
source: DeviceRunSessionEventSource;
|
|
60
70
|
logger: bunyan;
|
|
61
71
|
pollIntervalMs?: number;
|
|
62
|
-
}): Promise<
|
|
63
|
-
stopAsync: () => Promise<void>;
|
|
64
|
-
}>;
|
|
72
|
+
}): Promise<DeviceRunSessionEventCollection>;
|
|
@@ -58,7 +58,13 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
|
|
|
58
58
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
59
59
|
logger.warn({ err: error }, 'Could not persist device run session events to the artifact.');
|
|
60
60
|
reportEventLogFailure(error, 'setup');
|
|
61
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
stopAsync: async () => { },
|
|
63
|
+
// Collection is disabled, so session activity is invisible here. Report
|
|
64
|
+
// fresh activity so an enabled idle timeout never stops a session it
|
|
65
|
+
// cannot observe.
|
|
66
|
+
getLastEventObservedAt: () => new Date(),
|
|
67
|
+
};
|
|
62
68
|
}
|
|
63
69
|
let didReportRealtimeLogFailure = false;
|
|
64
70
|
const reportRealtimeLogFailure = (error, operation) => {
|
|
@@ -83,6 +89,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
|
|
|
83
89
|
}
|
|
84
90
|
const states = new Map();
|
|
85
91
|
const controller = new AbortController();
|
|
92
|
+
let lastEventObservedAt;
|
|
86
93
|
let parseFailureCount = 0;
|
|
87
94
|
const parseFailureCounts = {
|
|
88
95
|
'invalid-json': 0,
|
|
@@ -106,6 +113,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
|
|
|
106
113
|
source,
|
|
107
114
|
deviceRunSessionId,
|
|
108
115
|
writeEvent: event => {
|
|
116
|
+
lastEventObservedAt = new Date();
|
|
109
117
|
eventLogStream.write(event);
|
|
110
118
|
realtimeLogStream?.write({ ...event, logId: event.eventId });
|
|
111
119
|
},
|
|
@@ -170,6 +178,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
|
|
|
170
178
|
// A diagnostics failure must not recreate an unhandled rejection in this best-effort poller.
|
|
171
179
|
});
|
|
172
180
|
return {
|
|
181
|
+
getLastEventObservedAt: () => lastEventObservedAt,
|
|
173
182
|
stopAsync: async () => {
|
|
174
183
|
controller.abort();
|
|
175
184
|
await pollingPromise;
|
|
@@ -15,11 +15,22 @@ export declare function selectXcodeDeveloperDirectoryAsync({ env, logger, }: {
|
|
|
15
15
|
env: BuildStepEnv;
|
|
16
16
|
logger: bunyan;
|
|
17
17
|
}): Promise<void>;
|
|
18
|
-
export
|
|
18
|
+
export type DeviceRunSessionIdleTimeout = {
|
|
19
|
+
/** Stop the session after this many minutes without observed activity. */
|
|
20
|
+
maxIdleTimeMinutes: number;
|
|
21
|
+
/**
|
|
22
|
+
* Local arrival time of the most recent session event, or `undefined` when
|
|
23
|
+
* no event has been observed yet. The idle clock starts when the wait
|
|
24
|
+
* begins, so a session nobody ever connects to still times out.
|
|
25
|
+
*/
|
|
26
|
+
getLastEventObservedAt: () => Date | undefined;
|
|
27
|
+
};
|
|
28
|
+
export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }: {
|
|
19
29
|
ctx: CustomBuildContext;
|
|
20
30
|
deviceRunSessionId: string;
|
|
21
31
|
logger: bunyan;
|
|
22
32
|
signal?: AbortSignal;
|
|
33
|
+
idleTimeout?: DeviceRunSessionIdleTimeout;
|
|
23
34
|
}): Promise<void>;
|
|
24
35
|
/**
|
|
25
36
|
* Install ffmpeg when the runtime does not already provide it, so argent's
|
|
@@ -97,6 +97,16 @@ const DEVICE_RUN_SESSION_STATUS_QUERY = (0, gql_tada_1.graphql)(`
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
`);
|
|
100
|
+
const ENSURE_DEVICE_RUN_SESSION_STOPPED_MUTATION = (0, gql_tada_1.graphql)(`
|
|
101
|
+
mutation EnsureDeviceRunSessionStopped($deviceRunSessionId: ID!) {
|
|
102
|
+
deviceRunSession {
|
|
103
|
+
ensureDeviceRunSessionStopped(deviceRunSessionId: $deviceRunSessionId) {
|
|
104
|
+
id
|
|
105
|
+
status
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`);
|
|
100
110
|
const DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS = 5_000;
|
|
101
111
|
function getDeviceRunSessionIdOrThrow(env) {
|
|
102
112
|
const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
|
|
@@ -142,10 +152,26 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
|
|
|
142
152
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
143
153
|
});
|
|
144
154
|
}
|
|
145
|
-
async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }) {
|
|
155
|
+
async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }) {
|
|
146
156
|
logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
|
|
157
|
+
if (idleTimeout) {
|
|
158
|
+
logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
|
|
159
|
+
}
|
|
147
160
|
let pollErrorCount = 0;
|
|
161
|
+
let lastActivityAt = new Date();
|
|
148
162
|
while (!signal?.aborted) {
|
|
163
|
+
if (idleTimeout) {
|
|
164
|
+
const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
|
|
165
|
+
if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
|
|
166
|
+
lastActivityAt = lastEventObservedAt;
|
|
167
|
+
}
|
|
168
|
+
if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
|
|
169
|
+
logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
|
|
170
|
+
`${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
|
|
171
|
+
await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
149
175
|
try {
|
|
150
176
|
const result = await ctx.graphqlClient
|
|
151
177
|
.query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
|
|
@@ -180,6 +206,26 @@ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, lo
|
|
|
180
206
|
await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
|
|
181
207
|
}
|
|
182
208
|
}
|
|
209
|
+
// Best effort: when this fails, the caller still tears the session down and the
|
|
210
|
+
// job run finishes, which clients also treat as the session ending.
|
|
211
|
+
async function ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger, }) {
|
|
212
|
+
try {
|
|
213
|
+
const result = await ctx.graphqlClient
|
|
214
|
+
.mutation(ENSURE_DEVICE_RUN_SESSION_STOPPED_MUTATION, { deviceRunSessionId })
|
|
215
|
+
.toPromise();
|
|
216
|
+
if (result.error) {
|
|
217
|
+
throw result.error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
222
|
+
sentry_1.Sentry.capture('Could not mark idle device run session as stopped', error, {
|
|
223
|
+
level: 'warning',
|
|
224
|
+
extras: { deviceRunSessionId },
|
|
225
|
+
});
|
|
226
|
+
logger.warn({ err: error }, `Could not mark device run session ${deviceRunSessionId} as stopped. The session job ends anyway.`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
183
229
|
async function sleepUntilAbortedAsync(timeoutMs, signal) {
|
|
184
230
|
try {
|
|
185
231
|
await (0, promises_1.setTimeout)(timeoutMs, undefined, signal ? { signal } : undefined);
|
|
@@ -20,17 +20,19 @@ async function uploadServeSimMetricsFileAsync(ctx, { deviceRunSessionId, udid, f
|
|
|
20
20
|
logger.info(`serve-sim metrics file for ${udid} is empty; skipping upload.`);
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
+
const deviceName = typeof metadata?.deviceName === 'string' ? metadata.deviceName : undefined;
|
|
23
24
|
try {
|
|
24
|
-
logger.info(`Uploading serve-sim metrics for ${udid} (${(0, artifacts_1.formatBytes)(size)}).`);
|
|
25
|
+
logger.info(`Uploading serve-sim metrics for ${deviceName ?? udid} (${(0, artifacts_1.formatBytes)(size)}).`);
|
|
25
26
|
await (0, deviceRunSessionArtifacts_1.uploadDeviceRunSessionArtifactAsync)(ctx, {
|
|
26
27
|
deviceRunSessionId,
|
|
27
28
|
artifactId: `metrics-${udid}`,
|
|
28
|
-
name: `Performance metrics (${udid.slice(0, 8)})`,
|
|
29
|
+
name: `Performance metrics (${deviceName ?? udid.slice(0, 8)})`,
|
|
29
30
|
filename: METRICS_ARTIFACT_FILENAME,
|
|
30
31
|
kind: 'performance-metrics',
|
|
31
32
|
metadata: {
|
|
32
33
|
__eas_type: 'performance-metrics',
|
|
33
34
|
udid,
|
|
35
|
+
...(deviceName && { deviceName }),
|
|
34
36
|
...(metadata && {
|
|
35
37
|
hostCores: metadata.hostCores,
|
|
36
38
|
sampleIntervalMs: metadata.sampleIntervalMs,
|
package/dist/utils/easCli.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SpawnOptions, SpawnResult } from '@expo/turtle-spawn';
|
|
2
2
|
import { Env } from '@expo/eas-build-job';
|
|
3
|
+
/** Test-only: clears the memoized versions promise so each test fetches fresh. */
|
|
4
|
+
export declare function resetCachedEasCliVersionsForTest(): void;
|
|
3
5
|
export declare function resolveEasCommandPrefixAndEnvAsync(): Promise<{
|
|
4
6
|
cmd: string;
|
|
5
7
|
args: string[];
|
package/dist/utils/easCli.js
CHANGED
|
@@ -3,11 +3,48 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resetCachedEasCliVersionsForTest = resetCachedEasCliVersionsForTest;
|
|
6
7
|
exports.resolveEasCommandPrefixAndEnvAsync = resolveEasCommandPrefixAndEnvAsync;
|
|
7
8
|
exports.runEasCliCommand = runEasCliCommand;
|
|
8
9
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
9
10
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
10
11
|
const packageManager_1 = require("./packageManager");
|
|
12
|
+
const sentry_1 = require("../sentry");
|
|
13
|
+
let cachedEasCliVersionsPromise;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the `eas-cli` versions to install for staging/production. Prefers the
|
|
16
|
+
* versions committed to `cli-versions.json` (fetched from expo/eas-cli); on any
|
|
17
|
+
* failure (e.g. GitHub is unreachable or slow) reports to Sentry and falls back
|
|
18
|
+
* to the `latest-eas-build*` npm dist-tags so the build can still proceed.
|
|
19
|
+
*
|
|
20
|
+
* Memoized: `resolveEasCommandPrefixAndEnvAsync` runs more than once per build,
|
|
21
|
+
* so the (never-rejecting) result promise is cached to fetch GitHub at most once
|
|
22
|
+
* per process.
|
|
23
|
+
*/
|
|
24
|
+
async function resolveEasCliVersionsAsync() {
|
|
25
|
+
if (!cachedEasCliVersionsPromise) {
|
|
26
|
+
cachedEasCliVersionsPromise = fetchEasCliVersionsWithFallbackAsync();
|
|
27
|
+
}
|
|
28
|
+
return await cachedEasCliVersionsPromise;
|
|
29
|
+
}
|
|
30
|
+
async function fetchEasCliVersionsWithFallbackAsync() {
|
|
31
|
+
try {
|
|
32
|
+
return await (0, eas_build_job_1.fetchEasCliVersionsAsync)();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof eas_build_job_1.EasCliVersionsFetchTimeoutError
|
|
36
|
+
? 'Timed out fetching cli-versions.json; falling back to npm dist-tags'
|
|
37
|
+
: 'Failed to fetch cli-versions.json; falling back to npm dist-tags';
|
|
38
|
+
sentry_1.Sentry.capture(message, error instanceof Error ? error : new Error(String(error)), {
|
|
39
|
+
level: 'warning',
|
|
40
|
+
});
|
|
41
|
+
return { STAGING: eas_build_job_1.EasCliNpmTags.STAGING, PRODUCTION: eas_build_job_1.EasCliNpmTags.PRODUCTION };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Test-only: clears the memoized versions promise so each test fetches fresh. */
|
|
45
|
+
function resetCachedEasCliVersionsForTest() {
|
|
46
|
+
cachedEasCliVersionsPromise = undefined;
|
|
47
|
+
}
|
|
11
48
|
async function probeEasdAsync() {
|
|
12
49
|
try {
|
|
13
50
|
const result = await (0, turtle_spawn_1.default)('easd', ['--help'], {
|
|
@@ -25,23 +62,26 @@ async function resolveEasCommandPrefixAndEnvAsync() {
|
|
|
25
62
|
if (await probeEasdAsync()) {
|
|
26
63
|
return { cmd: 'easd', args: [], extraEnv: {} };
|
|
27
64
|
}
|
|
65
|
+
const versions = await resolveEasCliVersionsAsync();
|
|
28
66
|
return {
|
|
29
67
|
cmd: 'npx',
|
|
30
|
-
args: [...npxArgsPrefix, `eas-cli@${
|
|
68
|
+
args: [...npxArgsPrefix, `eas-cli@${versions.STAGING}`],
|
|
31
69
|
extraEnv: {},
|
|
32
70
|
};
|
|
33
71
|
}
|
|
34
72
|
else if (process.env.ENVIRONMENT === 'staging') {
|
|
73
|
+
const versions = await resolveEasCliVersionsAsync();
|
|
35
74
|
return {
|
|
36
75
|
cmd: 'npx',
|
|
37
|
-
args: [...npxArgsPrefix, `eas-cli@${
|
|
76
|
+
args: [...npxArgsPrefix, `eas-cli@${versions.STAGING}`],
|
|
38
77
|
extraEnv: { EXPO_STAGING: '1' },
|
|
39
78
|
};
|
|
40
79
|
}
|
|
41
80
|
else {
|
|
81
|
+
const versions = await resolveEasCliVersionsAsync();
|
|
42
82
|
return {
|
|
43
83
|
cmd: 'npx',
|
|
44
|
-
args: [...npxArgsPrefix, `eas-cli@${
|
|
84
|
+
args: [...npxArgsPrefix, `eas-cli@${versions.PRODUCTION}`],
|
|
45
85
|
extraEnv: {},
|
|
46
86
|
};
|
|
47
87
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "21.
|
|
3
|
+
"version": "21.7.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"@expo/config": "55.0.10",
|
|
40
40
|
"@expo/config-plugins": "55.0.7",
|
|
41
41
|
"@expo/downloader": "21.0.0",
|
|
42
|
-
"@expo/eas-build-job": "21.
|
|
42
|
+
"@expo/eas-build-job": "21.6.0",
|
|
43
43
|
"@expo/env": "^0.4.0",
|
|
44
44
|
"@expo/logger": "21.0.0",
|
|
45
45
|
"@expo/package-manager": "1.9.10",
|
|
46
46
|
"@expo/plist": "^0.3.5",
|
|
47
47
|
"@expo/results": "^1.0.0",
|
|
48
48
|
"@expo/spawn-async": "1.7.2",
|
|
49
|
-
"@expo/steps": "21.
|
|
49
|
+
"@expo/steps": "21.6.0",
|
|
50
50
|
"@expo/template-file": "21.0.2",
|
|
51
51
|
"@expo/turtle-spawn": "21.0.0",
|
|
52
52
|
"@expo/xcpretty": "^4.3.1",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"typescript": "^5.5.4",
|
|
101
101
|
"uuid": "^9.0.1"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "f923181857de80919ec7ee9861c9b71c2ee5635d"
|
|
104
104
|
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Loads local function.yml files from the project and builds the catalog consumed by
|
|
3
|
-
* {@link StepsConfigParser}. Keeps filesystem I/O in build-tools; expansion logic lives in @expo/steps.
|
|
4
|
-
*/
|
|
5
|
-
import { bunyan } from '@expo/logger';
|
|
6
|
-
import { CompositeFunctionCatalog, CompositeFunctionConfig, Step } from '@expo/eas-build-job';
|
|
7
|
-
export declare function createCompositeFunctionLoader(projectRoot: string, logger?: bunyan): (compositeFunctionPath: string) => Promise<CompositeFunctionConfig>;
|
|
8
|
-
export declare function buildCompositeFunctionCatalogAsync(projectRoot: string, { steps, logger }: {
|
|
9
|
-
steps: readonly Step[];
|
|
10
|
-
logger?: bunyan;
|
|
11
|
-
}): Promise<CompositeFunctionCatalog>;
|
|
@@ -1,62 +0,0 @@
|
|
|
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.createCompositeFunctionLoader = createCompositeFunctionLoader;
|
|
7
|
-
exports.buildCompositeFunctionCatalogAsync = buildCompositeFunctionCatalogAsync;
|
|
8
|
-
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
9
|
-
const steps_1 = require("@expo/steps");
|
|
10
|
-
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
-
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const yaml_1 = __importDefault(require("yaml"));
|
|
13
|
-
const zod_1 = require("zod");
|
|
14
|
-
async function loadLocalCompositeFunctionConfigAsync(projectRoot, compositeFunctionPath, { logger } = {}) {
|
|
15
|
-
const resolvedPath = (0, steps_1.resolveLocalCompositeFunctionPath)(projectRoot, compositeFunctionPath);
|
|
16
|
-
for (const ext of ['yml', 'yaml']) {
|
|
17
|
-
const absolutePath = path_1.default.join(resolvedPath, `function.${ext}`);
|
|
18
|
-
let rawContents;
|
|
19
|
-
try {
|
|
20
|
-
rawContents = await promises_1.default.readFile(absolutePath, 'utf-8');
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
if (err?.code === 'ENOENT') {
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
throw new Error(`Failed to read local composite function "${compositeFunctionPath}" from ${absolutePath}`, {
|
|
27
|
-
cause: err,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
let parsed;
|
|
31
|
-
try {
|
|
32
|
-
parsed = yaml_1.default.parse(rawContents);
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
throw new Error(`Failed to parse local composite function "${compositeFunctionPath}" YAML at ${absolutePath}`, {
|
|
36
|
-
cause: err,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
let config;
|
|
40
|
-
try {
|
|
41
|
-
config = eas_build_job_1.CompositeFunctionConfigZ.parse(parsed);
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
if (err instanceof zod_1.ZodError) {
|
|
45
|
-
throw new Error(`Invalid composite function "${compositeFunctionPath}": ${zod_1.z.prettifyError(err)}`);
|
|
46
|
-
}
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
logger?.debug(`Loaded local composite function "${compositeFunctionPath}" from ${path_1.default.relative(projectRoot, absolutePath)}`);
|
|
50
|
-
return config;
|
|
51
|
-
}
|
|
52
|
-
throw new Error(`Local composite function "${compositeFunctionPath}" was referenced by a step but no such composite function exists. A local composite function is resolved from a "function.yml" (or "function.yaml") file at the referenced path relative to the EAS project root (e.g. "uses: ${compositeFunctionPath}" resolves "${compositeFunctionPath}/function.yml"). The recommended convention is to keep composite functions under ".eas/functions/<name>".`);
|
|
53
|
-
}
|
|
54
|
-
function createCompositeFunctionLoader(projectRoot, logger) {
|
|
55
|
-
return compositeFunctionPath => loadLocalCompositeFunctionConfigAsync(projectRoot, compositeFunctionPath, { logger });
|
|
56
|
-
}
|
|
57
|
-
async function buildCompositeFunctionCatalogAsync(projectRoot, { steps, logger }) {
|
|
58
|
-
return (0, steps_1.buildCompositeFunctionCatalogFromStepsAsync)({
|
|
59
|
-
rootSteps: steps,
|
|
60
|
-
loadCompositeFunction: createCompositeFunctionLoader(projectRoot, logger),
|
|
61
|
-
});
|
|
62
|
-
}
|