@expo/build-tools 20.3.0 → 20.4.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/context.d.ts +1 -0
- package/dist/steps/easFunctions.js +1 -1
- package/dist/steps/functions/maestroScreenshots.d.ts +27 -0
- package/dist/steps/functions/maestroScreenshots.js +134 -0
- package/dist/steps/functions/maestroTests.d.ts +2 -1
- package/dist/steps/functions/maestroTests.js +77 -1
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +1 -3
- package/dist/steps/functions/startArgentRemoteSession.d.ts +1 -0
- package/dist/steps/functions/startArgentRemoteSession.js +17 -8
- package/dist/steps/functions/startServeSimRemoteSession.js +1 -7
- package/dist/steps/functions/uploadArtifact.js +13 -5
- package/dist/steps/utils/argentArtifacts.d.ts +28 -0
- package/dist/steps/utils/argentArtifacts.js +119 -0
- package/dist/steps/utils/deviceRunSessionArtifacts.d.ts +9 -0
- package/dist/steps/utils/deviceRunSessionArtifacts.js +87 -0
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +4 -0
- package/dist/steps/utils/remoteDeviceRunSession.js +15 -0
- package/package.json +2 -2
package/dist/context.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ function getEasFunctions(ctx) {
|
|
|
90
90
|
(0, createSubmissionEntity_1.createSubmissionEntityFunction)(),
|
|
91
91
|
(0, uploadToAsc_1.createUploadToAscBuildFunction)(),
|
|
92
92
|
(0, reportMaestroTestResults_1.createReportMaestroTestResultsFunction)(ctx),
|
|
93
|
-
(0, maestroTests_1.createMaestroTestsBuildFunction)(),
|
|
93
|
+
(0, maestroTests_1.createMaestroTestsBuildFunction)(ctx),
|
|
94
94
|
];
|
|
95
95
|
if (ctx.hasBuildJob()) {
|
|
96
96
|
functions.push(...[
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { bunyan } from '@expo/logger';
|
|
2
|
+
export interface ParsedFailureScreenshot {
|
|
3
|
+
flowName: string;
|
|
4
|
+
capturedAtMs: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseFailureScreenshotFilename(filename: string): ParsedFailureScreenshot | null;
|
|
7
|
+
export interface HarvestedScreenshot {
|
|
8
|
+
fileAbsPath: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
metadata: {
|
|
11
|
+
kind: 'maestro-test-screenshot';
|
|
12
|
+
flowName: string;
|
|
13
|
+
attemptIndex: number;
|
|
14
|
+
capturedAtMs: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function harvestFailureScreenshotsAsync(args: {
|
|
18
|
+
testsDirectory: string;
|
|
19
|
+
capturedSinceMs: number;
|
|
20
|
+
attemptIndex: number;
|
|
21
|
+
logger: bunyan;
|
|
22
|
+
}): Promise<HarvestedScreenshot[]>;
|
|
23
|
+
export declare function computePureFailureFlowNames(testCases: readonly {
|
|
24
|
+
name: string;
|
|
25
|
+
status: 'passed' | 'failed';
|
|
26
|
+
}[]): Set<string>;
|
|
27
|
+
export declare function selectFailureScreenshots(harvested: readonly HarvestedScreenshot[], pureFailureFlowNames: ReadonlySet<string>): HarvestedScreenshot[];
|
|
@@ -0,0 +1,134 @@
|
|
|
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.parseFailureScreenshotFilename = parseFailureScreenshotFilename;
|
|
7
|
+
exports.harvestFailureScreenshotsAsync = harvestFailureScreenshotsAsync;
|
|
8
|
+
exports.computePureFailureFlowNames = computePureFailureFlowNames;
|
|
9
|
+
exports.selectFailureScreenshots = selectFailureScreenshots;
|
|
10
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
// Maestro failure screenshot: `screenshot-[shard-N-]❌-<epochMillis>-(<flowName>).png`.
|
|
13
|
+
// The flow name may contain parentheses, so anchor on the `-(` after the epoch and the
|
|
14
|
+
// `).png` suffix rather than scanning for parens.
|
|
15
|
+
const FAILURE_SCREENSHOT_PATTERN = /^screenshot-(?:shard-\d+-)?❌-(\d+)-\((.+)\)\.png$/u;
|
|
16
|
+
function parseFailureScreenshotFilename(filename) {
|
|
17
|
+
const match = filename.match(FAILURE_SCREENSHOT_PATTERN);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const capturedAtMs = Number(match[1]);
|
|
22
|
+
if (!Number.isFinite(capturedAtMs)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
flowName: match[2],
|
|
27
|
+
capturedAtMs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Scans every debug dir under testsDirectory whose mtime is recent enough (mtime-based to cover
|
|
31
|
+
// the maestro <2.5.0 bug that splits one invocation across two dirs), then keeps only screenshots
|
|
32
|
+
// whose own capturedAtMs is at/after capturedSinceMs. Never throws: an unreadable dir is logged
|
|
33
|
+
// and skipped so a screenshot harvest can't affect the maestro step outcome.
|
|
34
|
+
async function harvestFailureScreenshotsAsync(args) {
|
|
35
|
+
// Gate dirs by mtime floored to the second, so a dir created within the same second as the
|
|
36
|
+
// attempt start isn't stat'd below the baseline. The exact capturedAtMs gate below is what
|
|
37
|
+
// prevents mis-attributing an earlier attempt's screenshot.
|
|
38
|
+
const dirSinceMtimeMs = Math.floor(args.capturedSinceMs / 1000) * 1000;
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await promises_1.default.readdir(args.testsDirectory, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
args.logger.info({ err }, `Skipping screenshot harvest: cannot read ${args.testsDirectory}.`);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isDirectory()) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const dirPath = path_1.default.join(args.testsDirectory, entry.name);
|
|
53
|
+
let filenames;
|
|
54
|
+
try {
|
|
55
|
+
if ((await promises_1.default.stat(dirPath)).mtimeMs < dirSinceMtimeMs) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
filenames = await promises_1.default.readdir(dirPath);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
args.logger.info({ err }, `Skipping unreadable debug dir ${dirPath}.`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const filename of filenames) {
|
|
65
|
+
const parsed = parseFailureScreenshotFilename(filename);
|
|
66
|
+
// Re-check capture time per file: a dir's mtime can advance after this attempt began (or
|
|
67
|
+
// fall within the floored-second dir baseline), so the dir gate alone would re-attribute an
|
|
68
|
+
// earlier attempt's screenshot to this one.
|
|
69
|
+
if (parsed === null || parsed.capturedAtMs < args.capturedSinceMs) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
results.push({
|
|
73
|
+
fileAbsPath: path_1.default.join(dirPath, filename),
|
|
74
|
+
displayName: `Failure Screenshot: ${parsed.flowName} (attempt ${args.attemptIndex + 1})`,
|
|
75
|
+
metadata: {
|
|
76
|
+
kind: 'maestro-test-screenshot',
|
|
77
|
+
flowName: parsed.flowName,
|
|
78
|
+
attemptIndex: args.attemptIndex,
|
|
79
|
+
capturedAtMs: parsed.capturedAtMs,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
// Maestro substitutes '/' -> '_' in screenshot filenames (so HarvestedScreenshot.flowName
|
|
87
|
+
// is already in that form) but keeps '/' in the JUnit testcase name. Normalize the JUnit
|
|
88
|
+
// name the same way before comparing — matching the website's own normalization.
|
|
89
|
+
function normalizeFlowName(name) {
|
|
90
|
+
return name.replace(/\//g, '_');
|
|
91
|
+
}
|
|
92
|
+
// A flow is a "pure failure" when it failed and never passed across all attempts. The website
|
|
93
|
+
// surfaces only the final screenshot for such a flow, so keeping its earlier attempts would
|
|
94
|
+
// spend the per-job artifact budget on screenshots that are never shown — we keep only the final
|
|
95
|
+
// one (selectFailureScreenshots). A flow that passed at least once (flaky / fail-pass-fail) is
|
|
96
|
+
// NOT pure: each of its failed attempts is a distinct failure and is kept.
|
|
97
|
+
function computePureFailureFlowNames(testCases) {
|
|
98
|
+
const passed = new Set();
|
|
99
|
+
const failed = new Set();
|
|
100
|
+
for (const testCase of testCases) {
|
|
101
|
+
const flowName = normalizeFlowName(testCase.name);
|
|
102
|
+
if (testCase.status === 'passed') {
|
|
103
|
+
passed.add(flowName);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
failed.add(flowName);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const pure = new Set();
|
|
110
|
+
for (const flowName of failed) {
|
|
111
|
+
if (!passed.has(flowName)) {
|
|
112
|
+
pure.add(flowName);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return pure;
|
|
116
|
+
}
|
|
117
|
+
// Keep every harvested screenshot for flaky/mixed flows; for pure-failure flows keep only
|
|
118
|
+
// the final (highest attemptIndex) screenshot.
|
|
119
|
+
function selectFailureScreenshots(harvested, pureFailureFlowNames) {
|
|
120
|
+
const finalAttemptByPureFlow = new Map();
|
|
121
|
+
for (const shot of harvested) {
|
|
122
|
+
if (!pureFailureFlowNames.has(shot.metadata.flowName)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const finalAttempt = finalAttemptByPureFlow.get(shot.metadata.flowName);
|
|
126
|
+
if (finalAttempt === undefined || shot.metadata.attemptIndex > finalAttempt) {
|
|
127
|
+
finalAttemptByPureFlow.set(shot.metadata.flowName, shot.metadata.attemptIndex);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return harvested.filter(shot => {
|
|
131
|
+
const finalAttempt = finalAttemptByPureFlow.get(shot.metadata.flowName);
|
|
132
|
+
return finalAttempt === undefined || shot.metadata.attemptIndex === finalAttempt;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { BuildFunction } from '@expo/steps';
|
|
2
|
-
|
|
2
|
+
import { CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
export declare function createMaestroTestsBuildFunction(ctx: CustomBuildContext): BuildFunction;
|
|
@@ -8,10 +8,12 @@ const eas_build_job_1 = require("@expo/eas-build-job");
|
|
|
8
8
|
const steps_1 = require("@expo/steps");
|
|
9
9
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
10
10
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
13
|
const zod_1 = require("zod");
|
|
13
14
|
const maestroFlowDiscovery_1 = require("./maestroFlowDiscovery");
|
|
14
15
|
const maestroResultParser_1 = require("./maestroResultParser");
|
|
16
|
+
const maestroScreenshots_1 = require("./maestroScreenshots");
|
|
15
17
|
const retry_1 = require("../../utils/retry");
|
|
16
18
|
const FlowPathSchema = zod_1.z.array(zod_1.z.string().min(1)).min(1);
|
|
17
19
|
const RetriesSchema = zod_1.z.number().int().min(0).default(0);
|
|
@@ -56,7 +58,7 @@ function buildMaestroArgs(args) {
|
|
|
56
58
|
out.push(...args.flow_path);
|
|
57
59
|
return out;
|
|
58
60
|
}
|
|
59
|
-
function createMaestroTestsBuildFunction() {
|
|
61
|
+
function createMaestroTestsBuildFunction(ctx) {
|
|
60
62
|
return new steps_1.BuildFunction({
|
|
61
63
|
namespace: 'eas',
|
|
62
64
|
id: 'maestro_tests',
|
|
@@ -171,6 +173,7 @@ function createMaestroTestsBuildFunction() {
|
|
|
171
173
|
// trusted; we then fall through to dumb retry (re-run everything).
|
|
172
174
|
let flowsToRun = flowPaths;
|
|
173
175
|
let lastAttemptExitCode = null;
|
|
176
|
+
const harvested = [];
|
|
174
177
|
const totalAttempts = retries + 1;
|
|
175
178
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
176
179
|
const outputPath = outputFormat === 'junit'
|
|
@@ -187,6 +190,7 @@ function createMaestroTestsBuildFunction() {
|
|
|
187
190
|
exclude_tags: excludeTags,
|
|
188
191
|
});
|
|
189
192
|
logger.info(`Running maestro (attempt ${attempt + 1}/${totalAttempts}): maestro ${maestroArgs.join(' ')}`);
|
|
193
|
+
const attemptStartedAtMs = Date.now();
|
|
190
194
|
try {
|
|
191
195
|
await (0, turtle_spawn_1.default)('maestro', maestroArgs, {
|
|
192
196
|
cwd: stepCtx.workingDirectory,
|
|
@@ -207,6 +211,17 @@ function createMaestroTestsBuildFunction() {
|
|
|
207
211
|
throw new eas_build_job_1.SystemError('Unexpected spawn failure invoking maestro', { cause: err });
|
|
208
212
|
}
|
|
209
213
|
}
|
|
214
|
+
// Harvest this attempt's failure screenshots before any retry subsetting. Gated on
|
|
215
|
+
// junit: test-case-result rows (and therefore the summary icons) only exist for junit
|
|
216
|
+
// runs, so harvesting other formats would just create orphan artifacts the website hides.
|
|
217
|
+
if (outputFormat === 'junit') {
|
|
218
|
+
harvested.push(...(await (0, maestroScreenshots_1.harvestFailureScreenshotsAsync)({
|
|
219
|
+
testsDirectory,
|
|
220
|
+
capturedSinceMs: attemptStartedAtMs,
|
|
221
|
+
attemptIndex: attempt,
|
|
222
|
+
logger,
|
|
223
|
+
})));
|
|
224
|
+
}
|
|
210
225
|
if (lastAttemptExitCode === 0 || attempt === retries) {
|
|
211
226
|
break;
|
|
212
227
|
}
|
|
@@ -276,6 +291,11 @@ function createMaestroTestsBuildFunction() {
|
|
|
276
291
|
}
|
|
277
292
|
}
|
|
278
293
|
}
|
|
294
|
+
// Upload before the ERR_MAESTRO_TESTS_FAILED throw below so fully-failed runs (which need
|
|
295
|
+
// screenshots most) still upload. Harvest only ran for junit, so guard the same way.
|
|
296
|
+
if (outputFormat === 'junit') {
|
|
297
|
+
await uploadFailureScreenshotsAsync({ harvested, junitReportDirectory, ctx, logger });
|
|
298
|
+
}
|
|
279
299
|
// The retry loop exits via success (0), numeric status (retryable),
|
|
280
300
|
// or throw (infra). A non-null non-zero status means the user's tests
|
|
281
301
|
// failed every attempt.
|
|
@@ -285,3 +305,59 @@ function createMaestroTestsBuildFunction() {
|
|
|
285
305
|
},
|
|
286
306
|
});
|
|
287
307
|
}
|
|
308
|
+
// Reduce harvested failure screenshots to what's worth uploading, then upload them as workflow
|
|
309
|
+
// artifacts. Best-effort and verdict-neutral: never throws, so a screenshot problem can't mask
|
|
310
|
+
// the maestro test result. Caller guards on junit (harvest only runs for junit).
|
|
311
|
+
async function uploadFailureScreenshotsAsync({ harvested, junitReportDirectory, ctx, logger, }) {
|
|
312
|
+
// Reduce to the attempts worth uploading — every failed attempt for flaky flows, only the final
|
|
313
|
+
// attempt for all-failed flows. See computePureFailureFlowNames / selectFailureScreenshots.
|
|
314
|
+
// Guard the JUnit re-parse so a malformed/missing report can't throw past here and mask the
|
|
315
|
+
// test verdict (the whole step is verdict-neutral for screenshots).
|
|
316
|
+
let selected;
|
|
317
|
+
try {
|
|
318
|
+
const pureFailureFlowNames = (0, maestroScreenshots_1.computePureFailureFlowNames)(await (0, maestroResultParser_1.parseJUnitTestCases)(junitReportDirectory));
|
|
319
|
+
selected = (0, maestroScreenshots_1.selectFailureScreenshots)(harvested, pureFailureFlowNames);
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
logger.warn({ err }, 'Failed to classify failure screenshots; skipping screenshot upload.');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (selected.length === 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Cap well under www's 50-artifact-per-job limit.
|
|
329
|
+
const MAX_SCREENSHOT_UPLOADS = 30;
|
|
330
|
+
const toUpload = selected.slice(0, MAX_SCREENSHOT_UPLOADS);
|
|
331
|
+
if (selected.length > toUpload.length) {
|
|
332
|
+
logger.warn(`Found ${selected.length} failure screenshots; uploading only the first ${toUpload.length}.`);
|
|
333
|
+
}
|
|
334
|
+
// Copy each shot to an ASCII-safe name outside testsDirectory (the originals contain a
|
|
335
|
+
// non-ASCII marker and testsDirectory is uploaded wholesale as the tarball).
|
|
336
|
+
let safeScreenshotDir;
|
|
337
|
+
try {
|
|
338
|
+
safeScreenshotDir = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'eas-maestro-screenshots-'));
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
logger.warn({ err }, 'Failed to create the failure-screenshot staging dir; skipping screenshot upload.');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await Promise.all(toUpload.map(async (shot, index) => {
|
|
345
|
+
try {
|
|
346
|
+
// `index` disambiguates two flows that fail within the same millisecond of an attempt.
|
|
347
|
+
const safePath = path_1.default.join(safeScreenshotDir, `failure-attempt-${shot.metadata.attemptIndex}-${index}-${shot.metadata.capturedAtMs}.png`);
|
|
348
|
+
await promises_1.default.copyFile(shot.fileAbsPath, safePath);
|
|
349
|
+
await ctx.runtimeApi.uploadArtifact({
|
|
350
|
+
artifact: {
|
|
351
|
+
type: eas_build_job_1.GenericArtifactType.OTHER,
|
|
352
|
+
name: shot.displayName,
|
|
353
|
+
paths: [safePath],
|
|
354
|
+
metadata: shot.metadata,
|
|
355
|
+
},
|
|
356
|
+
logger,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
logger.warn({ err }, 'Failed to upload failure screenshot.');
|
|
361
|
+
}
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
@@ -16,7 +16,6 @@ const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
|
|
|
16
16
|
const AGENT_DEVICE_REPO_URL = 'https://github.com/callstackincubator/agent-device.git';
|
|
17
17
|
const SRC_DIR = '/tmp/agent-device-src';
|
|
18
18
|
const DAEMON_JSON_PATH = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device', 'daemon.json');
|
|
19
|
-
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
20
19
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
21
20
|
function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
22
21
|
return new steps_1.BuildFunction({
|
|
@@ -43,8 +42,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
43
42
|
const { runtimePlatform } = global;
|
|
44
43
|
logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
|
|
45
44
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
46
|
-
|
|
47
|
-
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
45
|
+
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
48
46
|
}
|
|
49
47
|
logger.info('Launching agent-device daemon.');
|
|
50
48
|
const daemonProcess = await startAgentDeviceDaemonAsync({ packageVersion, env, logger });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type bunyan } from '@expo/logger';
|
|
2
2
|
import { BuildFunction } from '@expo/steps';
|
|
3
3
|
import { CustomBuildContext } from '../../customBuildContext';
|
|
4
|
+
export declare const MIN_ARGENT_REMOTE_SESSION_VERSION = "0.12.0";
|
|
4
5
|
export declare function createStartArgentRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
|
|
5
6
|
export declare function warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger, }: {
|
|
6
7
|
packageVersion: string | undefined;
|
|
@@ -3,6 +3,7 @@ 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.MIN_ARGENT_REMOTE_SESSION_VERSION = void 0;
|
|
6
7
|
exports.createStartArgentRemoteSessionBuildFunction = createStartArgentRemoteSessionBuildFunction;
|
|
7
8
|
exports.warnIfArgentPackageVersionCannotBeVerified = warnIfArgentPackageVersionCannotBeVerified;
|
|
8
9
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
@@ -13,11 +14,12 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
13
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
15
|
const semver_1 = __importDefault(require("semver"));
|
|
15
16
|
const zod_1 = require("zod");
|
|
17
|
+
const argentArtifacts_1 = require("../utils/argentArtifacts");
|
|
16
18
|
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
17
19
|
const ARGENT_PACKAGE_NAME = '@swmansion/argent';
|
|
18
|
-
|
|
20
|
+
exports.MIN_ARGENT_REMOTE_SESSION_VERSION = '0.12.0';
|
|
21
|
+
const ARGENT_ARTIFACTS_LIST_ENDPOINT_FLAG = 'artifacts-list-endpoint';
|
|
19
22
|
const ARGENT_STATE_FILE = node_path_1.default.join(node_os_1.default.homedir(), '.argent', 'tool-server.json');
|
|
20
|
-
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
21
23
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
22
24
|
const ArgentToolServerStateSchema = zod_1.z.object({
|
|
23
25
|
port: zod_1.z.number(),
|
|
@@ -50,11 +52,12 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
50
52
|
const { runtimePlatform } = global;
|
|
51
53
|
logger.info(`Starting argent remote session (version: ${versionSpec}, runtime: ${runtimePlatform}).`);
|
|
52
54
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
53
|
-
|
|
54
|
-
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
55
|
+
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
55
56
|
}
|
|
56
57
|
// Stale state from a previous run would mask the new server's port.
|
|
57
58
|
await node_fs_1.default.promises.rm(ARGENT_STATE_FILE, { force: true });
|
|
59
|
+
logger.info('Enabling the Argent artifacts list endpoint flag.');
|
|
60
|
+
await (0, turtle_spawn_1.default)('bunx', [`${ARGENT_PACKAGE_NAME}@${versionSpec}`, 'enable', ARGENT_ARTIFACTS_LIST_ENDPOINT_FLAG], { env, logger });
|
|
58
61
|
logger.info(`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} tool-server via bunx.`);
|
|
59
62
|
const argentServer = (0, remoteDeviceRunSession_1.spawnDetached)({
|
|
60
63
|
command: 'bunx',
|
|
@@ -88,6 +91,12 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
88
91
|
throw new eas_build_job_1.SystemError(`${err instanceof Error ? err.message : `Timed out waiting for argent tool-server state.`}${output ? `\nArgent tool-server output:\n${output}` : ''}`);
|
|
89
92
|
}
|
|
90
93
|
logger.info(`Argent tool-server is listening on port ${toolServerPort}.`);
|
|
94
|
+
void (0, argentArtifacts_1.pollArgentArtifactsForUploadAsync)(ctx, {
|
|
95
|
+
deviceRunSessionId,
|
|
96
|
+
toolsUrl: `http://127.0.0.1:${toolServerPort}`,
|
|
97
|
+
toolsAuthToken: toolServerToken,
|
|
98
|
+
logger,
|
|
99
|
+
});
|
|
91
100
|
const publicToolsUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
|
|
92
101
|
port: toolServerPort,
|
|
93
102
|
subdomainPrefix: 'argent',
|
|
@@ -132,15 +141,15 @@ function warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger, })
|
|
|
132
141
|
}
|
|
133
142
|
const validVersion = semver_1.default.valid(packageVersion);
|
|
134
143
|
if (!validVersion) {
|
|
135
|
-
logger.warn(`Argent remote simulator sessions require ${ARGENT_PACKAGE_NAME}@${MIN_ARGENT_REMOTE_SESSION_VERSION} or newer, ` +
|
|
144
|
+
logger.warn(`Argent remote simulator sessions require ${ARGENT_PACKAGE_NAME}@${exports.MIN_ARGENT_REMOTE_SESSION_VERSION} or newer, ` +
|
|
136
145
|
`but package_version "${packageVersion}" is not an exact semver version that EAS can verify. ` +
|
|
137
146
|
`Continuing and letting bunx resolve it.`);
|
|
138
147
|
return;
|
|
139
148
|
}
|
|
140
|
-
if (semver_1.default.lt(validVersion, MIN_ARGENT_REMOTE_SESSION_VERSION)) {
|
|
141
|
-
throw new eas_build_job_1.SystemError(`Argent remote simulator sessions require ${ARGENT_PACKAGE_NAME}@${MIN_ARGENT_REMOTE_SESSION_VERSION} or newer. ` +
|
|
149
|
+
if (semver_1.default.lt(validVersion, exports.MIN_ARGENT_REMOTE_SESSION_VERSION)) {
|
|
150
|
+
throw new eas_build_job_1.SystemError(`Argent remote simulator sessions require ${ARGENT_PACKAGE_NAME}@${exports.MIN_ARGENT_REMOTE_SESSION_VERSION} or newer. ` +
|
|
142
151
|
`The requested package_version "${packageVersion}" is too old for the EAS remote-session API. ` +
|
|
143
|
-
`Use "latest" or pass an exact version >= ${MIN_ARGENT_REMOTE_SESSION_VERSION}.`);
|
|
152
|
+
`Use "latest" or pass an exact version >= ${exports.MIN_ARGENT_REMOTE_SESSION_VERSION}.`);
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
function parseArgentToolServerState(raw) {
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.createStartServeSimRemoteSessionBuildFunction = createStartServeSimRemoteSessionBuildFunction;
|
|
7
4
|
const steps_1 = require("@expo/steps");
|
|
8
|
-
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
9
5
|
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
10
|
-
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
11
6
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
12
7
|
function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
13
8
|
return new steps_1.BuildFunction({
|
|
@@ -20,8 +15,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
|
20
15
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
21
16
|
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
22
17
|
logger.info('Starting serve-sim remote session.');
|
|
23
|
-
|
|
24
|
-
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
18
|
+
await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
|
|
25
19
|
const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
|
|
26
20
|
baseDomain: ngrokTunnelDomain,
|
|
27
21
|
env,
|
|
@@ -62,6 +62,11 @@ function createUploadArtifactBuildFunction(ctx) {
|
|
|
62
62
|
required: false,
|
|
63
63
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
|
|
64
64
|
}),
|
|
65
|
+
steps_1.BuildStepInput.createProvider({
|
|
66
|
+
id: 'metadata',
|
|
67
|
+
required: false,
|
|
68
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.JSON,
|
|
69
|
+
}),
|
|
65
70
|
],
|
|
66
71
|
outputProviders: [
|
|
67
72
|
steps_1.BuildStepOutput.createProvider({
|
|
@@ -93,7 +98,7 @@ function createUploadArtifactBuildFunction(ctx) {
|
|
|
93
98
|
}
|
|
94
99
|
throw result.reason;
|
|
95
100
|
});
|
|
96
|
-
const
|
|
101
|
+
const artifactSpec = {
|
|
97
102
|
type: parseArtifactTypeInput({
|
|
98
103
|
platform: ctx.job.platform,
|
|
99
104
|
inputValue: `${inputs.type.value ?? ''}`,
|
|
@@ -101,11 +106,14 @@ function createUploadArtifactBuildFunction(ctx) {
|
|
|
101
106
|
paths: artifactPaths,
|
|
102
107
|
name: (inputs.name.value || inputs.key.value),
|
|
103
108
|
};
|
|
109
|
+
const artifact = (0, eas_build_job_1.isGenericArtifact)(artifactSpec)
|
|
110
|
+
? {
|
|
111
|
+
...artifactSpec,
|
|
112
|
+
metadata: inputs.metadata.value,
|
|
113
|
+
}
|
|
114
|
+
: artifactSpec;
|
|
104
115
|
try {
|
|
105
|
-
const { artifactId } = await ctx.runtimeApi.uploadArtifact({
|
|
106
|
-
artifact,
|
|
107
|
-
logger,
|
|
108
|
-
});
|
|
116
|
+
const { artifactId } = await ctx.runtimeApi.uploadArtifact({ artifact, logger });
|
|
109
117
|
if (artifactId) {
|
|
110
118
|
outputs.artifact_id.set(artifactId);
|
|
111
119
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type bunyan } from '@expo/logger';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { CustomBuildContext } from '../../customBuildContext';
|
|
4
|
+
declare const ArgentArtifactSchema: z.ZodObject<{
|
|
5
|
+
id: z.ZodString;
|
|
6
|
+
filename: z.ZodString;
|
|
7
|
+
mimeType: z.ZodString;
|
|
8
|
+
isDirectory: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
type ArgentArtifact = z.infer<typeof ArgentArtifactSchema>;
|
|
11
|
+
export declare function pollArgentArtifactsForUploadAsync(ctx: CustomBuildContext, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, }: {
|
|
12
|
+
deviceRunSessionId: string;
|
|
13
|
+
toolsUrl: string;
|
|
14
|
+
toolsAuthToken?: string;
|
|
15
|
+
logger: bunyan;
|
|
16
|
+
}): Promise<never>;
|
|
17
|
+
export declare function listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, }: {
|
|
18
|
+
toolsUrl: string;
|
|
19
|
+
toolsAuthToken?: string;
|
|
20
|
+
}): Promise<ArgentArtifact[]>;
|
|
21
|
+
export declare function uploadArgentArtifactAsync(ctx: CustomBuildContext, { deviceRunSessionId, toolsUrl, toolsAuthToken, artifact, logger, }: {
|
|
22
|
+
deviceRunSessionId: string;
|
|
23
|
+
toolsUrl: string;
|
|
24
|
+
toolsAuthToken?: string;
|
|
25
|
+
artifact: ArgentArtifact;
|
|
26
|
+
logger: bunyan;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
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.pollArgentArtifactsForUploadAsync = pollArgentArtifactsForUploadAsync;
|
|
7
|
+
exports.listArgentArtifactsAsync = listArgentArtifactsAsync;
|
|
8
|
+
exports.uploadArgentArtifactAsync = uploadArgentArtifactAsync;
|
|
9
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const promises_2 = require("node:stream/promises");
|
|
16
|
+
const zod_1 = require("zod");
|
|
17
|
+
const sentry_1 = require("../../sentry");
|
|
18
|
+
const artifacts_1 = require("../../utils/artifacts");
|
|
19
|
+
const retry_1 = require("../../utils/retry");
|
|
20
|
+
const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
|
|
21
|
+
const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
|
|
22
|
+
const ArgentArtifactSchema = zod_1.z.object({
|
|
23
|
+
id: zod_1.z.string(),
|
|
24
|
+
filename: zod_1.z.string(),
|
|
25
|
+
mimeType: zod_1.z.string(),
|
|
26
|
+
isDirectory: zod_1.z.boolean().optional(),
|
|
27
|
+
});
|
|
28
|
+
const ArgentArtifactsListResponseSchema = zod_1.z.object({
|
|
29
|
+
artifacts: zod_1.z.array(ArgentArtifactSchema),
|
|
30
|
+
});
|
|
31
|
+
async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, }) {
|
|
32
|
+
logger.info('Started polling Argent tool-server for artifacts.');
|
|
33
|
+
const seenArtifactIds = new Set();
|
|
34
|
+
let listArtifactsErrorCount = 0;
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
const artifacts = await listArgentArtifactsAsync({ toolsUrl, toolsAuthToken });
|
|
38
|
+
listArtifactsErrorCount = 0;
|
|
39
|
+
for (const artifact of artifacts) {
|
|
40
|
+
if (seenArtifactIds.has(artifact.id)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
seenArtifactIds.add(artifact.id);
|
|
44
|
+
void uploadArgentArtifactAsync(ctx, {
|
|
45
|
+
deviceRunSessionId,
|
|
46
|
+
toolsUrl,
|
|
47
|
+
toolsAuthToken,
|
|
48
|
+
artifact,
|
|
49
|
+
logger,
|
|
50
|
+
}).catch(err => {
|
|
51
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
52
|
+
sentry_1.Sentry.capture('Could not upload Argent remote session artifact', error);
|
|
53
|
+
logger.warn({ err: error }, 'Could not upload Argent remote session artifact.');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
59
|
+
listArtifactsErrorCount += 1;
|
|
60
|
+
if (listArtifactsErrorCount === 1 || listArtifactsErrorCount % 5 === 0) {
|
|
61
|
+
sentry_1.Sentry.capture('Could not list Argent remote session artifacts', error);
|
|
62
|
+
logger.warn({ err: error, failedArtifactListCount: listArtifactsErrorCount }, 'Could not list Argent remote session artifacts.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await (0, retry_1.sleepAsync)(ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function listArgentArtifactsAsync({ toolsUrl, toolsAuthToken, }) {
|
|
69
|
+
const response = await (0, node_fetch_1.default)(new URL('/artifacts', toolsUrl).toString(), {
|
|
70
|
+
headers: toolsAuthToken ? { Authorization: `Bearer ${toolsAuthToken}` } : {},
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new eas_build_job_1.SystemError(`Failed to list Argent artifacts: ${response.status} ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
const result = ArgentArtifactsListResponseSchema.safeParse(await response.json());
|
|
76
|
+
if (!result.success) {
|
|
77
|
+
throw new eas_build_job_1.SystemError(`Invalid Argent artifacts response: ${result.error.message}`);
|
|
78
|
+
}
|
|
79
|
+
return result.data.artifacts;
|
|
80
|
+
}
|
|
81
|
+
async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, artifact, logger, }) {
|
|
82
|
+
const filename = artifact.isDirectory ? `${artifact.filename}.tar.gz` : artifact.filename;
|
|
83
|
+
logger.info(`Downloading artifact ${filename}.`);
|
|
84
|
+
const temporaryDirectory = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), 'argent-artifact-'));
|
|
85
|
+
try {
|
|
86
|
+
const temporaryArtifactPath = node_path_1.default.join(temporaryDirectory, node_path_1.default.basename(filename));
|
|
87
|
+
await downloadArgentArtifactToFileAsync({
|
|
88
|
+
artifact,
|
|
89
|
+
toolsUrl,
|
|
90
|
+
toolsAuthToken,
|
|
91
|
+
destinationPath: temporaryArtifactPath,
|
|
92
|
+
});
|
|
93
|
+
const { size } = await (0, promises_1.stat)(temporaryArtifactPath);
|
|
94
|
+
logger.info(`Uploading artifact ${filename} (${(0, artifacts_1.formatBytes)(size)}).`);
|
|
95
|
+
await (0, deviceRunSessionArtifacts_1.uploadDeviceRunSessionArtifactAsync)(ctx, {
|
|
96
|
+
deviceRunSessionId,
|
|
97
|
+
artifactId: artifact.id,
|
|
98
|
+
name: `${filename} (${artifact.id})`,
|
|
99
|
+
filename,
|
|
100
|
+
size,
|
|
101
|
+
stream: (0, node_fs_1.createReadStream)(temporaryArtifactPath),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await (0, promises_1.rm)(temporaryDirectory, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function downloadArgentArtifactToFileAsync({ artifact, toolsUrl, toolsAuthToken, destinationPath, }) {
|
|
109
|
+
const response = await (0, node_fetch_1.default)(new URL(`/artifacts/${artifact.id}`, toolsUrl).toString(), {
|
|
110
|
+
headers: toolsAuthToken ? { Authorization: `Bearer ${toolsAuthToken}` } : {},
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new eas_build_job_1.SystemError(`Failed to download Argent artifact ${artifact.id}: ${response.status} ${response.statusText}`);
|
|
114
|
+
}
|
|
115
|
+
if (!response.body) {
|
|
116
|
+
throw new eas_build_job_1.SystemError(`Argent artifact ${artifact.id} response did not include a readable body.`);
|
|
117
|
+
}
|
|
118
|
+
await (0, promises_2.pipeline)(response.body, (0, node_fs_1.createWriteStream)(destinationPath));
|
|
119
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CustomBuildContext } from '../../customBuildContext';
|
|
2
|
+
export declare function uploadDeviceRunSessionArtifactAsync(ctx: CustomBuildContext, { deviceRunSessionId, artifactId, name, filename, size, stream, }: {
|
|
3
|
+
deviceRunSessionId: string;
|
|
4
|
+
artifactId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
filename: string;
|
|
7
|
+
size: number;
|
|
8
|
+
stream: NodeJS.ReadableStream;
|
|
9
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.uploadDeviceRunSessionArtifactAsync = uploadDeviceRunSessionArtifactAsync;
|
|
37
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
38
|
+
const gql_tada_1 = require("gql.tada");
|
|
39
|
+
const node_fetch_1 = __importStar(require("node-fetch"));
|
|
40
|
+
const CREATE_DEVICE_RUN_SESSION_ARTIFACT_UPLOAD_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
41
|
+
mutation CreateDeviceRunSessionArtifactUploadSession(
|
|
42
|
+
$deviceRunSessionId: ID!
|
|
43
|
+
$input: CreateDeviceRunSessionArtifactUploadSessionInput!
|
|
44
|
+
) {
|
|
45
|
+
deviceRunSession {
|
|
46
|
+
createArtifactUploadSession(deviceRunSessionId: $deviceRunSessionId, input: $input) {
|
|
47
|
+
uploadSession {
|
|
48
|
+
url
|
|
49
|
+
headers
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`);
|
|
55
|
+
async function uploadDeviceRunSessionArtifactAsync(ctx, { deviceRunSessionId, artifactId, name, filename, size, stream, }) {
|
|
56
|
+
const uploadSession = await createDeviceRunSessionArtifactUploadSessionAsync(ctx, {
|
|
57
|
+
deviceRunSessionId,
|
|
58
|
+
artifactId,
|
|
59
|
+
name,
|
|
60
|
+
filename,
|
|
61
|
+
size,
|
|
62
|
+
});
|
|
63
|
+
const response = await (0, node_fetch_1.default)(uploadSession.url, {
|
|
64
|
+
method: 'PUT',
|
|
65
|
+
headers: new node_fetch_1.Headers(uploadSession.headers),
|
|
66
|
+
body: stream,
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new eas_build_job_1.SystemError(`Failed to upload device run session artifact ${artifactId}: ${response.status} ${response.statusText}`, { cause: response });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function createDeviceRunSessionArtifactUploadSessionAsync(ctx, { deviceRunSessionId, artifactId, name, filename, size, }) {
|
|
73
|
+
const result = await ctx.graphqlClient
|
|
74
|
+
.mutation(CREATE_DEVICE_RUN_SESSION_ARTIFACT_UPLOAD_SESSION_MUTATION, {
|
|
75
|
+
deviceRunSessionId,
|
|
76
|
+
input: {
|
|
77
|
+
name,
|
|
78
|
+
filename,
|
|
79
|
+
size,
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
.toPromise();
|
|
83
|
+
if (result.error) {
|
|
84
|
+
throw new eas_build_job_1.SystemError(`Failed to create upload session for device run session artifact ${artifactId}: ${result.error.message}`, { cause: result.error });
|
|
85
|
+
}
|
|
86
|
+
return result.data.deviceRunSession.createArtifactUploadSession.uploadSession;
|
|
87
|
+
}
|
|
@@ -11,6 +11,10 @@ declare const TurnIceServersSchema: z.ZodArray<z.ZodObject<{
|
|
|
11
11
|
credential: z.ZodOptional<z.ZodString>;
|
|
12
12
|
}, z.core.$strip>>;
|
|
13
13
|
export type TurnIceServers = z.infer<typeof TurnIceServersSchema>;
|
|
14
|
+
export declare function selectXcodeDeveloperDirectoryAsync({ env, logger, }: {
|
|
15
|
+
env: BuildStepEnv;
|
|
16
|
+
logger: bunyan;
|
|
17
|
+
}): Promise<void>;
|
|
14
18
|
/**
|
|
15
19
|
* Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
|
|
16
20
|
* credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
|
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
|
|
40
40
|
exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
|
|
41
41
|
exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
|
|
42
|
+
exports.selectXcodeDeveloperDirectoryAsync = selectXcodeDeveloperDirectoryAsync;
|
|
42
43
|
exports.turnIceServersToServeSimArgs = turnIceServersToServeSimArgs;
|
|
43
44
|
exports.fetchServeSimTurnArgsAsync = fetchServeSimTurnArgsAsync;
|
|
44
45
|
exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
|
|
@@ -47,6 +48,7 @@ exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
|
|
|
47
48
|
exports.startNgrokTunnelAsync = startNgrokTunnelAsync;
|
|
48
49
|
exports.waitForFileAsync = waitForFileAsync;
|
|
49
50
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
51
|
+
const steps_1 = require("@expo/steps");
|
|
50
52
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
51
53
|
const ngrok = __importStar(require("@ngrok/ngrok"));
|
|
52
54
|
const gql_tada_1 = require("gql.tada");
|
|
@@ -57,6 +59,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
57
59
|
const sentry_1 = require("../../sentry");
|
|
58
60
|
const retry_1 = require("../../utils/retry");
|
|
59
61
|
const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
62
|
+
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
60
63
|
const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
61
64
|
mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
|
|
62
65
|
deviceRunSession {
|
|
@@ -102,6 +105,18 @@ const TurnIceServersSchema = zod_1.z.array(zod_1.z.object({
|
|
|
102
105
|
username: zod_1.z.string().optional(),
|
|
103
106
|
credential: zod_1.z.string().optional(),
|
|
104
107
|
}));
|
|
108
|
+
async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
|
|
109
|
+
if (process.env.ENVIRONMENT === 'development') {
|
|
110
|
+
logger.info('Job running outside of EAS, not selecting Xcode developer directory.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
|
114
|
+
await (0, steps_1.spawnAsync)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], {
|
|
115
|
+
env,
|
|
116
|
+
logger,
|
|
117
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
105
120
|
const TurnIceServersResponseSchema = zod_1.z.object({
|
|
106
121
|
data: zod_1.z.object({
|
|
107
122
|
iceServers: TurnIceServersSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.4.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"typescript": "^5.5.4",
|
|
101
101
|
"uuid": "^9.0.1"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "f867b7b9f2641fe1db6924711cb705e663d40397"
|
|
104
104
|
}
|