@expo/build-tools 20.3.0 → 20.5.1

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.
Files changed (29) hide show
  1. package/dist/common/projectSources.js +1 -1
  2. package/dist/context.d.ts +3 -2
  3. package/dist/ios/fastlane.d.ts +1 -1
  4. package/dist/steps/easFunctions.js +1 -1
  5. package/dist/steps/functions/createSubmissionEntity.js +25 -1
  6. package/dist/steps/functions/downloadArtifact.js +13 -3
  7. package/dist/steps/functions/maestroScreenshots.d.ts +27 -0
  8. package/dist/steps/functions/maestroScreenshots.js +134 -0
  9. package/dist/steps/functions/maestroTests.d.ts +2 -1
  10. package/dist/steps/functions/maestroTests.js +77 -1
  11. package/dist/steps/functions/repack.js +12 -7
  12. package/dist/steps/functions/restoreCache.js +2 -1
  13. package/dist/steps/functions/saveCache.js +3 -2
  14. package/dist/steps/functions/startAgentDeviceRemoteSession.js +1 -3
  15. package/dist/steps/functions/startArgentRemoteSession.d.ts +1 -0
  16. package/dist/steps/functions/startArgentRemoteSession.js +17 -8
  17. package/dist/steps/functions/startIosSimulator.js +19 -0
  18. package/dist/steps/functions/startServeSimRemoteSession.js +1 -7
  19. package/dist/steps/functions/uploadArtifact.js +13 -5
  20. package/dist/steps/utils/argentArtifacts.d.ts +28 -0
  21. package/dist/steps/utils/argentArtifacts.js +119 -0
  22. package/dist/steps/utils/deviceRunSessionArtifacts.d.ts +9 -0
  23. package/dist/steps/utils/deviceRunSessionArtifacts.js +87 -0
  24. package/dist/steps/utils/remoteDeviceRunSession.d.ts +4 -0
  25. package/dist/steps/utils/remoteDeviceRunSession.js +15 -0
  26. package/dist/utils/IosSimulatorUtils.d.ts +4 -0
  27. package/dist/utils/IosSimulatorUtils.js +40 -0
  28. package/dist/utils/expoUpdates.js +6 -1
  29. package/package.json +4 -4
@@ -169,7 +169,7 @@ async function uploadProjectMetadataAsync(ctx, { projectDirectory }) {
169
169
  }
170
170
  }
171
171
  `), {
172
- buildId: ctx.env.EAS_BUILD_ID,
172
+ buildId: (0, nullthrows_1.default)(ctx.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set'),
173
173
  projectMetadataFile: {
174
174
  type: 'GCS',
175
175
  bucketKey: uploadSession.bucketKey,
package/dist/context.d.ts CHANGED
@@ -19,6 +19,7 @@ export type ArtifactToUpload = {
19
19
  type: GenericArtifactType;
20
20
  name: string;
21
21
  paths: string[];
22
+ metadata?: Record<string, unknown>;
22
23
  };
23
24
  export interface BuildContextOptions {
24
25
  workingdir: string;
@@ -42,7 +43,7 @@ export interface BuildContextOptions {
42
43
  }>;
43
44
  reportError?: (msg: string, err?: Error, options?: {
44
45
  tags?: Record<string, string>;
45
- extras?: Record<string, string>;
46
+ extras?: Record<string, string | undefined>;
46
47
  }) => void;
47
48
  skipNativeBuild?: boolean;
48
49
  metadata?: Metadata;
@@ -57,7 +58,7 @@ export declare class BuildContext<TJob extends Job = Job> {
57
58
  readonly cacheManager?: CacheManager;
58
59
  readonly reportError?: (msg: string, err?: Error, options?: {
59
60
  tags?: Record<string, string>;
60
- extras?: Record<string, string>;
61
+ extras?: Record<string, string | undefined>;
61
62
  }) => void;
62
63
  readonly skipNativeBuild?: boolean;
63
64
  readonly expoApiV2BaseUrl?: string;
@@ -19,6 +19,6 @@ export declare function runFastlaneResign<TJob extends Ios.Job>(ctx: BuildContex
19
19
  }): Promise<void>;
20
20
  export declare function runFastlane(fastlaneArgs: string[], { logger, env, cwd, }?: {
21
21
  logger?: bunyan;
22
- env?: Record<string, string>;
22
+ env?: Env;
23
23
  cwd?: string;
24
24
  }): Promise<SpawnResult>;
@@ -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(...[
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createSubmissionEntityFunction = createSubmissionEntityFunction;
4
4
  const results_1 = require("@expo/results");
5
5
  const steps_1 = require("@expo/steps");
6
+ const sentry_1 = require("../../sentry");
6
7
  const retryOnDNSFailure_1 = require("../../utils/retryOnDNSFailure");
7
8
  function createSubmissionEntityFunction() {
8
9
  return new steps_1.BuildFunction({
@@ -53,16 +54,39 @@ function createSubmissionEntityFunction() {
53
54
  const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
54
55
  if (!robotAccessToken) {
55
56
  stepsCtx.logger.error('Failed to create submission entity: no robot access token found');
57
+ sentry_1.Sentry.capture('Failed to create submission entity: missing robot access token');
56
58
  return;
57
59
  }
58
60
  const buildId = inputs.build_id.value;
59
61
  if (!buildId) {
60
62
  stepsCtx.logger.error('Failed to create submission entity: no build ID provided');
63
+ sentry_1.Sentry.capture('Failed to create submission entity: missing build ID', {
64
+ extras: {
65
+ buildId,
66
+ },
67
+ });
61
68
  return;
62
69
  }
63
70
  const workflowJobId = stepsCtx.global.env.__WORKFLOW_JOB_ID;
64
71
  if (!workflowJobId) {
65
72
  stepsCtx.logger.error('Failed to create submission entity: no workflow job ID found');
73
+ sentry_1.Sentry.capture('Failed to create submission entity: missing workflow job ID', {
74
+ extras: {
75
+ buildId,
76
+ workflowJobId,
77
+ },
78
+ });
79
+ return;
80
+ }
81
+ const expoApiServerURL = stepsCtx.global.staticContext.expoApiServerURL;
82
+ if (!expoApiServerURL) {
83
+ stepsCtx.logger.error('Failed to create submission entity: no Expo API server URL found');
84
+ sentry_1.Sentry.capture('Failed to create submission entity: missing Expo API server URL', {
85
+ extras: {
86
+ buildId,
87
+ workflowJobId,
88
+ },
89
+ });
66
90
  return;
67
91
  }
68
92
  // This is supposed to provide fallback for `''` -> `undefined`.
@@ -76,7 +100,7 @@ function createSubmissionEntityFunction() {
76
100
  const ascAppIdentifier = inputs.asc_app_identifier.value || undefined;
77
101
  /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */
78
102
  try {
79
- const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(fetch)(new URL('/v2/app-store-submissions/', stepsCtx.global.staticContext.expoApiServerURL), {
103
+ const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(fetch)(new URL('/v2/app-store-submissions/', expoApiServerURL), {
80
104
  method: 'POST',
81
105
  headers: {
82
106
  Authorization: `Bearer ${robotAccessToken}`,
@@ -52,11 +52,21 @@ function createDownloadArtifactFunction() {
52
52
  });
53
53
  const interpolationContext = stepsCtx.global.getInterpolationContext();
54
54
  if (!('workflow' in interpolationContext)) {
55
- throw new eas_build_job_1.UserError('EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW', 'No workflow found in the interpolation context.');
55
+ throw new eas_build_job_1.SystemError('No workflow found in the interpolation context.', {
56
+ trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW',
57
+ });
56
58
  }
57
59
  const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
58
60
  if (!robotAccessToken) {
59
- throw new eas_build_job_1.UserError('EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN', 'No robot access token found in the job secrets.');
61
+ throw new eas_build_job_1.SystemError('No robot access token found in the job secrets.', {
62
+ trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN',
63
+ });
64
+ }
65
+ const expoApiServerURL = stepsCtx.global.staticContext.expoApiServerURL;
66
+ if (!expoApiServerURL) {
67
+ throw new eas_build_job_1.SystemError('Missing Expo API server URL.', {
68
+ trackingCode: 'EAS_DOWNLOAD_ARTIFACT_NO_EXPO_API_SERVER_URL',
69
+ });
60
70
  }
61
71
  const workflowRunId = interpolationContext.workflow.id;
62
72
  const { logger } = stepsCtx;
@@ -69,7 +79,7 @@ function createDownloadArtifactFunction() {
69
79
  const { artifactPath } = await downloadArtifactAsync({
70
80
  logger,
71
81
  workflowRunId,
72
- expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
82
+ expoApiServerURL,
73
83
  robotAccessToken,
74
84
  params,
75
85
  });
@@ -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
- export declare function createMaestroTestsBuildFunction(): BuildFunction;
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
+ }
@@ -33,11 +33,6 @@ function createRepackBuildFunction() {
33
33
  required: false,
34
34
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
35
35
  }),
36
- steps_1.BuildStepInput.createProvider({
37
- id: 'output_path',
38
- allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
39
- required: false,
40
- }),
41
36
  steps_1.BuildStepInput.createProvider({
42
37
  id: 'embed_bundle_assets',
43
38
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
@@ -93,8 +88,10 @@ function createRepackBuildFunction() {
93
88
  await node_fs_1.default.promises.mkdir(workingDirectory);
94
89
  stepsCtx.logger.info(`Created temporary working directory: ${workingDirectory}`);
95
90
  const sourceAppPath = inputs.source_app_path.value;
96
- const outputPath = inputs.output_path.value ??
97
- node_path_1.default.join(tmpDir, `repacked-${(0, node_crypto_1.randomUUID)()}${node_path_1.default.extname(sourceAppPath)}`);
91
+ const outputPath = createOutputPath({
92
+ sourceAppPath,
93
+ tmpDir,
94
+ });
98
95
  const exportEmbedOptions = inputs.embed_bundle_assets.value
99
96
  ? {
100
97
  sourcemapOutput: undefined,
@@ -174,6 +171,14 @@ function createRepackBuildFunction() {
174
171
  },
175
172
  });
176
173
  }
174
+ function createOutputPath({ sourceAppPath, tmpDir, }) {
175
+ const outputPath = node_path_1.default.join(tmpDir, `repacked-${(0, node_crypto_1.randomUUID)()}${node_path_1.default.extname(sourceAppPath)}`);
176
+ const extension = node_path_1.default.extname(outputPath);
177
+ if (extension.toLowerCase() !== '.aab') {
178
+ return outputPath;
179
+ }
180
+ return `${outputPath.slice(0, -extension.length)}.apk`;
181
+ }
177
182
  /**
178
183
  * Install `@expo/repack-app` in a sandbox directory and import it.
179
184
  */
@@ -99,11 +99,12 @@ function createRestoreCacheFunction() {
99
99
  .parse((inputs.restore_keys.value ?? '').split(/[\r\n]+/))
100
100
  .filter(key => key !== '');
101
101
  const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
102
+ const expoApiServerURL = (0, nullthrows_1.default)(stepsCtx.global.staticContext.expoApiServerURL, 'expoApiServerURL is not set');
102
103
  const robotAccessToken = (0, nullthrows_1.default)(stepsCtx.global.staticContext.job.secrets?.robotAccessToken, 'robotAccessToken is not set');
103
104
  const { archivePath, matchedKey } = await downloadCacheAsync({
104
105
  logger,
105
106
  jobId,
106
- expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
107
+ expoApiServerURL,
107
108
  robotAccessToken,
108
109
  paths,
109
110
  key,
@@ -80,6 +80,7 @@ function createSaveCacheFunction() {
80
80
  .filter(path => path.length > 0);
81
81
  const key = zod_1.default.string().parse(inputs.key.value);
82
82
  const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
83
+ const expoApiServerURL = (0, nullthrows_1.default)(stepsCtx.global.staticContext.expoApiServerURL, 'expoApiServerURL is not set');
83
84
  const robotAccessToken = (0, nullthrows_1.default)(stepsCtx.global.staticContext.job.secrets?.robotAccessToken, 'robotAccessToken is not set');
84
85
  const { archivePath } = await compressCacheAsync({
85
86
  paths,
@@ -92,7 +93,7 @@ function createSaveCacheFunction() {
92
93
  await uploadPublicCacheAsync({
93
94
  logger,
94
95
  jobId,
95
- expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
96
+ expoApiServerURL,
96
97
  robotAccessToken,
97
98
  archivePath,
98
99
  key,
@@ -105,7 +106,7 @@ function createSaveCacheFunction() {
105
106
  await uploadCacheAsync({
106
107
  logger,
107
108
  jobId,
108
- expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
109
+ expoApiServerURL,
109
110
  robotAccessToken,
110
111
  archivePath,
111
112
  key,
@@ -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
- logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
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
- const MIN_ARGENT_REMOTE_SESSION_VERSION = '0.11.0';
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
- logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
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) {
@@ -26,6 +26,12 @@ function createStartIosSimulatorBuildFunction() {
26
26
  defaultValue: 1,
27
27
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
28
28
  }),
29
+ steps_1.BuildStepInput.createProvider({
30
+ id: 'enable_accessibility_settings',
31
+ required: false,
32
+ defaultValue: false,
33
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
34
+ }),
29
35
  ],
30
36
  fn: async ({ logger }, { inputs, env }) => {
31
37
  try {
@@ -45,9 +51,16 @@ function createStartIosSimulatorBuildFunction() {
45
51
  }
46
52
  const deviceIdentifierInput = inputs.device_identifier.value?.toString();
47
53
  const originalDeviceIdentifier = deviceIdentifierInput ?? (await findMostGenericIphoneUuidAsync({ env }));
54
+ const enableAccessibilitySettings = Boolean(inputs.enable_accessibility_settings.value);
48
55
  if (!originalDeviceIdentifier) {
49
56
  throw new Error('Could not find an iPhone among available simulator devices.');
50
57
  }
58
+ if (enableAccessibilitySettings) {
59
+ await IosSimulatorUtils_1.IosSimulatorUtils.enableAccessibilitySettingsAsync({
60
+ deviceIdentifier: originalDeviceIdentifier,
61
+ env,
62
+ });
63
+ }
51
64
  const { udid } = await IosSimulatorUtils_1.IosSimulatorUtils.startAsync({
52
65
  deviceIdentifier: originalDeviceIdentifier,
53
66
  env,
@@ -78,6 +91,12 @@ function createStartIosSimulatorBuildFunction() {
78
91
  destinationDeviceName: cloneDeviceName,
79
92
  env,
80
93
  });
94
+ if (enableAccessibilitySettings) {
95
+ await IosSimulatorUtils_1.IosSimulatorUtils.enableAccessibilitySettingsAsync({
96
+ deviceIdentifier: cloneDeviceName,
97
+ env,
98
+ });
99
+ }
81
100
  const { udid: cloneUdid } = await IosSimulatorUtils_1.IosSimulatorUtils.startAsync({
82
101
  deviceIdentifier: cloneDeviceName,
83
102
  env,
@@ -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
- logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
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 artifact = {
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,
@@ -39,6 +39,10 @@ export declare namespace IosSimulatorUtils {
39
39
  destinationDeviceName: IosSimulatorName;
40
40
  env: NodeJS.ProcessEnv;
41
41
  }): Promise<void>;
42
+ export function enableAccessibilitySettingsAsync({ deviceIdentifier, env, }: {
43
+ deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
44
+ env: NodeJS.ProcessEnv;
45
+ }): Promise<void>;
42
46
  export function startAsync({ deviceIdentifier, env, }: {
43
47
  deviceIdentifier: IosSimulatorUuid | IosSimulatorName;
44
48
  env: NodeJS.ProcessEnv;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.IosSimulatorUtils = void 0;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
7
8
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
9
10
  const node_os_1 = __importDefault(require("node:os"));
@@ -37,6 +38,45 @@ var IosSimulatorUtils;
37
38
  });
38
39
  }
39
40
  IosSimulatorUtils.cloneAsync = cloneAsync;
41
+ async function enableAccessibilitySettingsAsync({ deviceIdentifier, env, }) {
42
+ try {
43
+ const devices = await getAvailableDevicesAsync({ env, filter: 'available' });
44
+ const device = devices.find(device => device.isAvailable &&
45
+ (device.udid === deviceIdentifier || device.name === deviceIdentifier));
46
+ if (!device) {
47
+ throw new eas_build_job_1.UserError('EAS_IOS_SIMULATOR_NOT_FOUND', `Failed to find available iOS Simulator "${deviceIdentifier}" to update accessibility settings.`);
48
+ }
49
+ if (device.state !== 'Shutdown') {
50
+ throw new eas_build_job_1.UserError('EAS_IOS_SIMULATOR_NOT_SHUTDOWN', `Expected iOS Simulator "${deviceIdentifier}" to be shutdown before updating accessibility settings, but it is ${device.state}.`);
51
+ }
52
+ const plistPath = node_path_1.default.join(device.dataPath, 'Library', 'Preferences', 'com.apple.Accessibility.plist');
53
+ await node_fs_1.default.promises.mkdir(node_path_1.default.dirname(plistPath), { recursive: true });
54
+ const plistExists = await node_fs_1.default.promises
55
+ .access(plistPath)
56
+ .then(() => true)
57
+ .catch(() => false);
58
+ if (!plistExists) {
59
+ await (0, turtle_spawn_1.default)('plutil', ['-create', 'binary1', plistPath], { env });
60
+ }
61
+ for (const key of [
62
+ 'AutomationEnabled',
63
+ 'IgnoreAXServerEntitlements',
64
+ 'AccessibilityEnabled',
65
+ 'ApplicationAccessibilityEnabled',
66
+ ]) {
67
+ await (0, turtle_spawn_1.default)('plutil', ['-replace', key, '-bool', 'true', plistPath], { env });
68
+ }
69
+ }
70
+ catch (err) {
71
+ if (err instanceof eas_build_job_1.ExpoError) {
72
+ throw err;
73
+ }
74
+ throw new eas_build_job_1.SystemError('Failed to update iOS Simulator accessibility settings.', {
75
+ cause: err,
76
+ });
77
+ }
78
+ }
79
+ IosSimulatorUtils.enableAccessibilitySettingsAsync = enableAccessibilitySettingsAsync;
40
80
  async function startAsync({ deviceIdentifier, env, }) {
41
81
  const bootstatusResult = await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'bootstatus', deviceIdentifier, '-b'], {
42
82
  env,
@@ -173,6 +173,11 @@ async function isEASUpdateConfigured(ctx) {
173
173
  }
174
174
  async function logDiffFingerprints({ resolvedRuntime, ctx, }) {
175
175
  const { resolvedRuntimeVersion, resolvedFingerprintSources } = resolvedRuntime;
176
+ const buildId = ctx.env.EAS_BUILD_ID;
177
+ if (!buildId) {
178
+ ctx.logger.warn('Skipping fingerprint diff because EAS_BUILD_ID is not set');
179
+ return;
180
+ }
176
181
  const fingerprintInfo = await ctx.graphqlClient
177
182
  .query((0, gql_tada_1.graphql)(`
178
183
  query GetFingerprintUrl($id: ID!) {
@@ -184,7 +189,7 @@ async function logDiffFingerprints({ resolvedRuntime, ctx, }) {
184
189
  }
185
190
  }
186
191
  }
187
- `), { id: ctx.env.EAS_BUILD_ID })
192
+ `), { id: buildId })
188
193
  .toPromise();
189
194
  if (fingerprintInfo.error) {
190
195
  ctx.logger.warn('Failed to fetch current fingerprint info', fingerprintInfo.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "20.3.0",
3
+ "version": "20.5.1",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -38,14 +38,14 @@
38
38
  "@expo/config": "55.0.10",
39
39
  "@expo/config-plugins": "55.0.7",
40
40
  "@expo/downloader": "20.0.0",
41
- "@expo/eas-build-job": "20.1.0",
41
+ "@expo/eas-build-job": "20.5.1",
42
42
  "@expo/env": "^0.4.0",
43
43
  "@expo/logger": "20.0.0",
44
44
  "@expo/package-manager": "1.9.10",
45
45
  "@expo/plist": "^0.2.0",
46
46
  "@expo/results": "^1.0.0",
47
47
  "@expo/spawn-async": "1.7.2",
48
- "@expo/steps": "20.1.0",
48
+ "@expo/steps": "20.5.1",
49
49
  "@expo/template-file": "20.0.0",
50
50
  "@expo/turtle-spawn": "20.0.0",
51
51
  "@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": "490457976c996d06447e6442fbd1aec1ace09f1b"
103
+ "gitHead": "cd47185f29446299b7682d52cd917eb9a01038e7"
104
104
  }