@expo/build-tools 20.1.0 → 20.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/builders/custom.js +23 -0
  2. package/dist/common/installDependencies.d.ts +10 -1
  3. package/dist/common/installDependencies.js +95 -1
  4. package/dist/common/prebuild.js +2 -3
  5. package/dist/{ios → common}/xcpretty.d.ts +1 -0
  6. package/dist/{ios → common}/xcpretty.js +18 -3
  7. package/dist/datadog.js +7 -3
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/ios/fastlane.js +1 -1
  11. package/dist/ios/pod.js +1 -1
  12. package/dist/runtimeSettings.d.ts +12 -0
  13. package/dist/runtimeSettings.js +120 -0
  14. package/dist/steps/easFunctions.js +1 -1
  15. package/dist/steps/functions/downloadBuild.d.ts +5 -3
  16. package/dist/steps/functions/downloadBuild.js +34 -4
  17. package/dist/steps/functions/findAndUploadBuildArtifacts.js +2 -2
  18. package/dist/steps/functions/installMaestro.js +13 -2
  19. package/dist/steps/functions/maestroResultParser.d.ts +18 -0
  20. package/dist/steps/functions/maestroResultParser.js +132 -3
  21. package/dist/steps/functions/maestroTests.js +26 -13
  22. package/dist/steps/functions/repack.d.ts +3 -1
  23. package/dist/steps/functions/repack.js +15 -1
  24. package/dist/steps/functions/reportMaestroTestResults.js +39 -20
  25. package/dist/steps/functions/restoreBuildCache.js +9 -6
  26. package/dist/steps/functions/startAgentDeviceRemoteSession.d.ts +1 -1
  27. package/dist/steps/functions/startAgentDeviceRemoteSession.js +101 -22
  28. package/dist/steps/functions/startArgentRemoteSession.js +1 -1
  29. package/dist/steps/functions/startServeSimRemoteSession.js +1 -1
  30. package/dist/steps/functions/uploadArtifact.js +1 -1
  31. package/dist/steps/utils/ios/fastlane.js +1 -1
  32. package/dist/steps/utils/remoteDeviceRunSession.d.ts +29 -1
  33. package/dist/steps/utils/remoteDeviceRunSession.js +75 -1
  34. package/package.json +3 -3
  35. package/dist/steps/utils/ios/xcpretty.d.ts +0 -15
  36. package/dist/steps/utils/ios/xcpretty.js +0 -92
@@ -13,6 +13,7 @@ const path_1 = __importDefault(require("path"));
13
13
  const easBuildInternal_1 = require("../common/easBuildInternal");
14
14
  const projectSources_1 = require("../common/projectSources");
15
15
  const customBuildContext_1 = require("../customBuildContext");
16
+ const datadog_1 = require("../datadog");
16
17
  const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
17
18
  const easFunctionGroups_1 = require("../steps/easFunctionGroups");
18
19
  const easFunctions_1 = require("../steps/easFunctions");
@@ -68,6 +69,7 @@ async function runCustomBuildAsync(ctx) {
68
69
  throw parseError;
69
70
  }
70
71
  });
72
+ logUserProvidedCustomFunctions(workflow);
71
73
  try {
72
74
  try {
73
75
  await workflow.executeAsync();
@@ -92,3 +94,24 @@ async function runCustomBuildAsync(ctx) {
92
94
  }
93
95
  return ctx.artifacts;
94
96
  }
97
+ function logUserProvidedCustomFunctions(workflow) {
98
+ for (const buildFunction of Object.values(workflow.buildFunctions)) {
99
+ if (!buildFunction.customFunctionModulePath) {
100
+ continue;
101
+ }
102
+ datadog_1.Datadog.log('Custom build saw user-provided function', {
103
+ event: 'custom_build_user_provided_function',
104
+ custom_function_id: buildFunction.getFullId(),
105
+ custom_function_module_path: buildFunction.customFunctionModulePath,
106
+ custom_function_input_count: String(buildFunction.inputProviders?.length ?? 0),
107
+ custom_function_output_count: String(buildFunction.outputProviders?.length ?? 0),
108
+ ...(buildFunction.name ? { custom_function_name: buildFunction.name } : {}),
109
+ ...(buildFunction.shell ? { custom_function_shell: buildFunction.shell } : {}),
110
+ ...(buildFunction.supportedRuntimePlatforms
111
+ ? {
112
+ custom_function_supported_runtime_platforms: buildFunction.supportedRuntimePlatforms.join(','),
113
+ }
114
+ : {}),
115
+ });
116
+ }
117
+ }
@@ -2,14 +2,23 @@ import { Job } from '@expo/eas-build-job';
2
2
  import { SpawnOptions, SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
3
3
  import { BuildContext } from '../context';
4
4
  import { PackageManager } from '../utils/packageManager';
5
- export declare function installDependenciesAsync({ packageManager, env, logger, infoCallbackFn, cwd, useFrozenLockfile, }: {
5
+ export declare function installDependenciesAsync({ packageManager, env, logger, infoCallbackFn, lineTransformer, cwd, useFrozenLockfile, }: {
6
6
  packageManager: PackageManager;
7
7
  env: Record<string, string | undefined>;
8
8
  cwd: string;
9
9
  logger: Exclude<SpawnOptions['logger'], undefined>;
10
10
  infoCallbackFn?: SpawnOptions['infoCallbackFn'];
11
+ lineTransformer?: SpawnOptions['lineTransformer'];
11
12
  useFrozenLockfile: boolean;
12
13
  }): Promise<{
13
14
  spawnPromise: SpawnPromise<SpawnResult>;
14
15
  }>;
16
+ export declare function installDependenciesWithNpmCacheFallbackAsync({ packageManager, env, logger, infoCallbackFn, cwd, useFrozenLockfile, }: {
17
+ packageManager: PackageManager;
18
+ env: Record<string, string | undefined>;
19
+ cwd: string;
20
+ logger: Exclude<SpawnOptions['logger'], undefined>;
21
+ infoCallbackFn?: SpawnOptions['infoCallbackFn'];
22
+ useFrozenLockfile: boolean;
23
+ }): Promise<void>;
15
24
  export declare function resolvePackagerDir(ctx: BuildContext<Job>): string;
@@ -4,12 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.installDependenciesAsync = installDependenciesAsync;
7
+ exports.installDependenciesWithNpmCacheFallbackAsync = installDependenciesWithNpmCacheFallbackAsync;
7
8
  exports.resolvePackagerDir = resolvePackagerDir;
9
+ const eas_build_job_1 = require("@expo/eas-build-job");
8
10
  const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
9
11
  const path_1 = __importDefault(require("path"));
12
+ const sentry_1 = require("../sentry");
10
13
  const packageManager_1 = require("../utils/packageManager");
11
14
  const project_1 = require("../utils/project");
12
- async function installDependenciesAsync({ packageManager, env, logger, infoCallbackFn, cwd, useFrozenLockfile, }) {
15
+ async function installDependenciesAsync({ packageManager, env, logger, infoCallbackFn, lineTransformer, cwd, useFrozenLockfile, }) {
13
16
  let args;
14
17
  switch (packageManager) {
15
18
  case packageManager_1.PackageManager.NPM: {
@@ -60,10 +63,67 @@ async function installDependenciesAsync({ packageManager, env, logger, infoCallb
60
63
  cwd,
61
64
  logger,
62
65
  infoCallbackFn,
66
+ lineTransformer,
63
67
  env,
64
68
  }),
65
69
  };
66
70
  }
71
+ async function installDependenciesWithNpmCacheFallbackAsync({ packageManager, env, logger, infoCallbackFn, cwd, useFrozenLockfile, }) {
72
+ const npmCacheUrl = env.NPM_CONFIG_REGISTRY;
73
+ let firstErrorLine;
74
+ let errorLineCount = 0;
75
+ try {
76
+ await (await installDependenciesAsync({
77
+ packageManager,
78
+ env,
79
+ logger,
80
+ infoCallbackFn,
81
+ lineTransformer: (line) => {
82
+ if (isNpmCacheRegistryErrorLine(line, { env, npmCacheUrl })) {
83
+ firstErrorLine ??= line;
84
+ errorLineCount += 1;
85
+ }
86
+ return line;
87
+ },
88
+ cwd,
89
+ useFrozenLockfile,
90
+ })).spawnPromise;
91
+ if (firstErrorLine) {
92
+ sentry_1.Sentry.capture(new NpmCacheRegistryNonFatalError(), {
93
+ level: 'warning',
94
+ tags: { packageManager },
95
+ extras: { cwd, npmCacheUrl, useFrozenLockfile, firstErrorLine, errorLineCount },
96
+ });
97
+ }
98
+ }
99
+ catch (err) {
100
+ if (!isNpmCacheInstallFailure(err, { env, npmCacheUrl })) {
101
+ throw err;
102
+ }
103
+ logger.warn(`Failed to install dependencies using the npm cache registry (${npmCacheUrl}). Retrying without the npm cache registry.`);
104
+ sentry_1.Sentry.capture(new NpmCacheRegistryInstallError(err), {
105
+ level: 'warning',
106
+ tags: { packageManager },
107
+ extras: {
108
+ cwd,
109
+ npmCacheUrl,
110
+ useFrozenLockfile,
111
+ originalErrorMessage: err instanceof Error ? err.message : String(err),
112
+ status: err instanceof Error ? err.status : undefined,
113
+ signal: err instanceof Error ? err.signal : undefined,
114
+ },
115
+ });
116
+ const { NPM_CONFIG_REGISTRY: _NPM_CONFIG_REGISTRY, ...fallbackEnv } = env;
117
+ await (await installDependenciesAsync({
118
+ packageManager,
119
+ env: fallbackEnv,
120
+ logger,
121
+ infoCallbackFn,
122
+ cwd,
123
+ useFrozenLockfile,
124
+ })).spawnPromise;
125
+ }
126
+ }
67
127
  function resolvePackagerDir(ctx) {
68
128
  const packagerRunDir = (0, packageManager_1.findPackagerRootDir)(ctx.getReactNativeProjectDirectory());
69
129
  if (packagerRunDir !== ctx.getReactNativeProjectDirectory()) {
@@ -72,3 +132,37 @@ function resolvePackagerDir(ctx) {
72
132
  }
73
133
  return packagerRunDir;
74
134
  }
135
+ function isNpmCacheInstallFailure(err, { env, npmCacheUrl }) {
136
+ if (!isNpmCacheRegistryEnabled(env, npmCacheUrl)) {
137
+ return false;
138
+ }
139
+ const errorOutput = getErrorOutput(err);
140
+ return errorOutput.includes(npmCacheUrl);
141
+ }
142
+ function isNpmCacheRegistryErrorLine(line, { env, npmCacheUrl }) {
143
+ return (isNpmCacheRegistryEnabled(env, npmCacheUrl) &&
144
+ line.includes(npmCacheUrl) &&
145
+ /(?:error|failed|ENOTFOUND|ECONN|ETIMEDOUT|EAI_AGAIN|FetchError)/i.test(line));
146
+ }
147
+ function isNpmCacheRegistryEnabled(env, npmCacheUrl) {
148
+ return env.EAS_USE_NPM_CACHE === '1' && !!npmCacheUrl;
149
+ }
150
+ function getErrorOutput(err) {
151
+ if (!(err instanceof Error)) {
152
+ return '';
153
+ }
154
+ const { stdout, stderr } = err;
155
+ return [stdout, stderr].filter(Boolean).join('\n');
156
+ }
157
+ class NpmCacheRegistryNonFatalError extends eas_build_job_1.SystemError {
158
+ name = 'NpmCacheRegistryNonFatalError';
159
+ constructor() {
160
+ super('Non-fatal npm cache registry error during dependency install');
161
+ }
162
+ }
163
+ class NpmCacheRegistryInstallError extends eas_build_job_1.SystemError {
164
+ name = 'NpmCacheRegistryInstallError';
165
+ constructor(cause) {
166
+ super('Failed to install dependencies using npm cache registry', { cause });
167
+ }
168
+ }
@@ -19,15 +19,14 @@ async function prebuildAsync(ctx, { logger, workingDir, options }) {
19
19
  options: spawnOptions,
20
20
  packageManager: ctx.packageManager,
21
21
  });
22
- const installDependenciesSpawnPromise = (await (0, installDependencies_1.installDependenciesAsync)({
22
+ await (0, installDependencies_1.installDependenciesWithNpmCacheFallbackAsync)({
23
23
  packageManager: ctx.packageManager,
24
24
  env: ctx.env,
25
25
  logger,
26
26
  cwd: (0, installDependencies_1.resolvePackagerDir)(ctx),
27
27
  // prebuild sometimes modifies package.json, so we don't want to use frozen lockfile
28
28
  useFrozenLockfile: false,
29
- })).spawnPromise;
30
- await installDependenciesSpawnPromise;
29
+ });
31
30
  }
32
31
  function getPrebuildCommandArgs(ctx) {
33
32
  let prebuildCommand = ctx.job.experimental?.prebuildCommand ?? `prebuild --no-install --platform ${ctx.job.platform}`;
@@ -4,6 +4,7 @@ export declare class XcodeBuildLogger {
4
4
  private readonly projectRoot;
5
5
  private loggerError?;
6
6
  private flushing;
7
+ private reportedFormatterError;
7
8
  private logReaderPromise?;
8
9
  private logsPath?;
9
10
  constructor(logger: bunyan, projectRoot: string);
@@ -10,12 +10,14 @@ const assert_1 = __importDefault(require("assert"));
10
10
  const fast_glob_1 = __importDefault(require("fast-glob"));
11
11
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
12
  const path_1 = __importDefault(require("path"));
13
+ const sentry_1 = require("../sentry");
13
14
  const CHECK_FILE_INTERVAL_MS = 1000;
14
15
  class XcodeBuildLogger {
15
16
  logger;
16
17
  projectRoot;
17
18
  loggerError;
18
19
  flushing = false;
20
+ reportedFormatterError = false;
19
21
  logReaderPromise;
20
22
  logsPath;
21
23
  constructor(logger, projectRoot) {
@@ -62,9 +64,22 @@ class XcodeBuildLogger {
62
64
  this.logReaderPromise = (0, spawn_async_1.default)('tail', ['-n', '+0', '-f', logsPath], { stdio: 'pipe' });
63
65
  (0, assert_1.default)(this.logReaderPromise.child.stdout, 'stdout is not available');
64
66
  this.logReaderPromise.child.stdout.on('data', (data) => {
65
- const lines = formatter.pipe(data.toString());
66
- for (const line of lines) {
67
- this.logger.info(line);
67
+ try {
68
+ const lines = formatter.pipe(data.toString());
69
+ for (const line of lines) {
70
+ this.logger.info(line);
71
+ }
72
+ }
73
+ catch (err) {
74
+ // The formatter can throw on unexpected xcodebuild output. Fall back to
75
+ // raw logs instead of crashing the process with an uncaught exception.
76
+ if (!this.reportedFormatterError) {
77
+ this.reportedFormatterError = true;
78
+ sentry_1.Sentry.capture('xcpretty formatter failed to parse xcodebuild logs', err, {
79
+ extras: { data: data.toString().slice(-2000) },
80
+ });
81
+ }
82
+ this.logger.info(data.toString());
68
83
  }
69
84
  });
70
85
  await this.logReaderPromise;
package/dist/datadog.js CHANGED
@@ -5,6 +5,9 @@ const sentry_1 = require("./sentry");
5
5
  const turtleFetch_1 = require("./utils/turtleFetch");
6
6
  let setupOptions = null;
7
7
  let pendingUploads = [];
8
+ function withTargetTag(tags, target) {
9
+ return { ...tags, target: target.kind === 'build' ? 'build' : 'job_run' };
10
+ }
8
11
  exports.Datadog = {
9
12
  setup(opts) {
10
13
  setupOptions = opts;
@@ -19,7 +22,7 @@ exports.Datadog = {
19
22
  name,
20
23
  type: 'distribution',
21
24
  value,
22
- tags,
25
+ tags: withTargetTag(tags, target),
23
26
  },
24
27
  ];
25
28
  const metricsPath = target.kind === 'build'
@@ -43,9 +46,10 @@ exports.Datadog = {
43
46
  return;
44
47
  }
45
48
  const { expoApiV2BaseUrl, robotAccessToken, target } = setupOptions;
49
+ const tagsWithTarget = withTargetTag(tags, target);
46
50
  const log = target.kind === 'build'
47
- ? { buildId: target.turtleBuildId, message, tags }
48
- : { jobRunId: target.turtleJobRunId, message, tags };
51
+ ? { buildId: target.turtleBuildId, message, tags: tagsWithTarget }
52
+ : { jobRunId: target.turtleJobRunId, message, tags: tagsWithTarget };
49
53
  const logsPath = target.kind === 'build' ? 'turtle-builds/logs' : 'turtle-job-runs/logs';
50
54
  const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL(logsPath, expoApiV2BaseUrl).toString(), 'POST', {
51
55
  json: log,
package/dist/index.d.ts CHANGED
@@ -11,3 +11,4 @@ export type { GradleProfileTask } from './android/gradleProfile';
11
11
  export * from './generic';
12
12
  export { Datadog } from './datadog';
13
13
  export { Sentry } from './sentry';
14
+ export * from './runtimeSettings';
package/dist/index.js CHANGED
@@ -64,3 +64,4 @@ var datadog_1 = require("./datadog");
64
64
  Object.defineProperty(exports, "Datadog", { enumerable: true, get: function () { return datadog_1.Datadog; } });
65
65
  var sentry_1 = require("./sentry");
66
66
  Object.defineProperty(exports, "Sentry", { enumerable: true, get: function () { return sentry_1.Sentry; } });
67
+ __exportStar(require("./runtimeSettings"), exports);
@@ -14,8 +14,8 @@ const path_1 = __importDefault(require("path"));
14
14
  const fastfile_1 = require("./fastfile");
15
15
  const gymfile_1 = require("./gymfile");
16
16
  const tvos_1 = require("./tvos");
17
- const xcpretty_1 = require("./xcpretty");
18
17
  const fastlane_1 = require("../common/fastlane");
18
+ const xcpretty_1 = require("../common/xcpretty");
19
19
  const context_1 = require("../context");
20
20
  async function runFastlaneGym(ctx, { scheme, buildConfiguration, credentials, entitlements, extraEnv, }) {
21
21
  const workspacePath = path_1.default.join(ctx.getReactNativeProjectDirectory(), 'ios');
package/dist/ios/pod.js CHANGED
@@ -42,7 +42,7 @@ async function resolvePrecompiledModulesPodInstallEnvAsync(ctx) {
42
42
  ctx.logger.info('EXPO_USE_PRECOMPILED_MODULES=0 is set; not enabling precompiled modules use.');
43
43
  return {};
44
44
  }
45
- if (ctx.job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES !== '1') {
45
+ if (ctx.env.EAS_USE_PRECOMPILED_MODULES !== '1') {
46
46
  return {};
47
47
  }
48
48
  let expoPackageVersion;
@@ -0,0 +1,12 @@
1
+ import { Env } from '@expo/eas-build-job';
2
+ export declare namespace RuntimeSettings {
3
+ function loadAsync({ environment, env: nextRuntimeEnvironment, }: {
4
+ environment: string;
5
+ env?: Env;
6
+ }): Promise<void>;
7
+ function isUsingIosPrecompiledModulesEnabled(): boolean;
8
+ function getNpmCacheUrl(): string | null;
9
+ function getNodeJsCacheUrl(): string | null;
10
+ function getMavenCacheUrl(): string | null;
11
+ function getCocoapodsCacheUrl(): string | null;
12
+ }
@@ -0,0 +1,120 @@
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.RuntimeSettings = void 0;
7
+ const node_fetch_1 = __importDefault(require("node-fetch"));
8
+ const zod_1 = require("zod");
9
+ const sentry_1 = require("./sentry");
10
+ const ENVIRONMENT_TO_RUNTIME_SETTINGS_URL = {
11
+ staging: 'https://storage.googleapis.com/eas-workflows-staging/runtime-settings.json',
12
+ production: 'https://storage.googleapis.com/eas-workflows-production/runtime-settings.json',
13
+ };
14
+ const RuntimeSettingsSchema = zod_1.z
15
+ .object({
16
+ caches: zod_1.z.record(zod_1.z.string(), zod_1.z
17
+ .object({
18
+ npm: zod_1.z.boolean(),
19
+ nodejs: zod_1.z.boolean(),
20
+ maven: zod_1.z.boolean(),
21
+ cocoapods: zod_1.z.boolean(),
22
+ })
23
+ .partial()),
24
+ iosPrecompiledModules: zod_1.z.boolean(),
25
+ })
26
+ .partial();
27
+ let runtimeSettings = {};
28
+ let runtimeEnvironment = {};
29
+ var RuntimeSettings;
30
+ (function (RuntimeSettings) {
31
+ async function loadAsync({ environment, env: nextRuntimeEnvironment = {}, }) {
32
+ runtimeEnvironment = nextRuntimeEnvironment;
33
+ const url = ENVIRONMENT_TO_RUNTIME_SETTINGS_URL[environment];
34
+ if (!url) {
35
+ return;
36
+ }
37
+ try {
38
+ const response = await (0, node_fetch_1.default)(url, {
39
+ signal: AbortSignal.timeout(1000),
40
+ });
41
+ if (!response.ok) {
42
+ sentry_1.Sentry.capture('Failed to fetch worker runtime settings', {
43
+ extras: { url, status: response.status },
44
+ level: 'warning',
45
+ });
46
+ return;
47
+ }
48
+ runtimeSettings = RuntimeSettingsSchema.parse(await response.json());
49
+ if (runtimeSettings.caches && !runtimeSettings.caches[process.platform]) {
50
+ sentry_1.Sentry.capture(new Error(`Runtime settings are missing cache settings for platform "${process.platform}"`), {
51
+ extras: {
52
+ configuredPlatforms: Object.keys(runtimeSettings.caches),
53
+ platform: process.platform,
54
+ },
55
+ level: 'error',
56
+ });
57
+ }
58
+ }
59
+ catch (err) {
60
+ sentry_1.Sentry.capture('Failed to load worker runtime settings', err instanceof Error ? err : new Error(String(err)), {
61
+ extras: { url },
62
+ level: 'warning',
63
+ });
64
+ }
65
+ }
66
+ RuntimeSettings.loadAsync = loadAsync;
67
+ function isUsingIosPrecompiledModulesEnabled() {
68
+ return runtimeSettings.iosPrecompiledModules ?? false;
69
+ }
70
+ RuntimeSettings.isUsingIosPrecompiledModulesEnabled = isUsingIosPrecompiledModulesEnabled;
71
+ function getNpmCacheUrl() {
72
+ if (runtimeEnvironment['EAS_BUILD_DISABLE_NPM_CACHE'] === '1') {
73
+ return null;
74
+ }
75
+ const runtimeEnabled = runtimeSettings.caches?.[process.platform]?.npm;
76
+ if (runtimeEnabled === false) {
77
+ return null;
78
+ }
79
+ const envOverride = runtimeEnvironment['EAS_USE_NPM_CACHE'];
80
+ const enabled = envOverride === '1' || (envOverride !== '0' && runtimeEnabled);
81
+ return enabled ? process.env.EAS_NPM_CACHE_URL || null : null;
82
+ }
83
+ RuntimeSettings.getNpmCacheUrl = getNpmCacheUrl;
84
+ function getNodeJsCacheUrl() {
85
+ const runtimeEnabled = runtimeSettings.caches?.[process.platform]?.nodejs;
86
+ if (runtimeEnabled === false) {
87
+ return null;
88
+ }
89
+ const envOverride = runtimeEnvironment['EAS_USE_NODEJS_CACHE'];
90
+ const enabled = envOverride === '1' || (envOverride !== '0' && runtimeEnabled);
91
+ return enabled ? process.env.EAS_NODEJS_CACHE_URL || null : null;
92
+ }
93
+ RuntimeSettings.getNodeJsCacheUrl = getNodeJsCacheUrl;
94
+ function getMavenCacheUrl() {
95
+ if (runtimeEnvironment['EAS_BUILD_DISABLE_MAVEN_CACHE'] === '1') {
96
+ return null;
97
+ }
98
+ const runtimeEnabled = runtimeSettings.caches?.[process.platform]?.maven;
99
+ if (runtimeEnabled === false) {
100
+ return null;
101
+ }
102
+ const envOverride = runtimeEnvironment['EAS_USE_MAVEN_CACHE'];
103
+ const enabled = envOverride === '1' || (envOverride !== '0' && runtimeEnabled);
104
+ return enabled ? process.env.EAS_MAVEN_CACHE_URL || null : null;
105
+ }
106
+ RuntimeSettings.getMavenCacheUrl = getMavenCacheUrl;
107
+ function getCocoapodsCacheUrl() {
108
+ if (runtimeEnvironment['EAS_BUILD_DISABLE_COCOAPODS_CACHE'] === '1') {
109
+ return null;
110
+ }
111
+ const runtimeEnabled = runtimeSettings.caches?.[process.platform]?.cocoapods;
112
+ if (runtimeEnabled === false) {
113
+ return null;
114
+ }
115
+ const envOverride = runtimeEnvironment['EAS_USE_COCOAPODS_CACHE'];
116
+ const enabled = envOverride === '1' || (envOverride !== '0' && runtimeEnabled);
117
+ return enabled ? process.env.EAS_COCOAPODS_CACHE_URL || null : null;
118
+ }
119
+ RuntimeSettings.getCocoapodsCacheUrl = getCocoapodsCacheUrl;
120
+ })(RuntimeSettings || (exports.RuntimeSettings = RuntimeSettings = {}));
@@ -57,7 +57,7 @@ function getEasFunctions(ctx) {
57
57
  (0, installNodeModules_1.createInstallNodeModulesBuildFunction)(),
58
58
  (0, prebuild_1.createPrebuildBuildFunction)(),
59
59
  (0, readIpaInfo_1.createReadIpaInfoBuildFunction)(),
60
- (0, downloadBuild_1.createDownloadBuildFunction)(),
60
+ (0, downloadBuild_1.createDownloadBuildFunction)(ctx),
61
61
  (0, export_1.createEasExportBuildFunction)(),
62
62
  (0, deploy_1.createEasDeployBuildFunction)(),
63
63
  (0, repack_1.createRepackBuildFunction)(),
@@ -1,10 +1,12 @@
1
1
  import { bunyan } from '@expo/logger';
2
2
  import { BuildFunction } from '@expo/steps';
3
- export declare function createDownloadBuildFunction(): BuildFunction;
4
- export declare function downloadBuildAsync({ logger, buildId, expoApiServerURL, robotAccessToken, extensions, }: {
3
+ import { Client } from '@urql/core';
4
+ import { CustomBuildContext } from '../../customBuildContext';
5
+ export declare function createDownloadBuildFunction(ctx: CustomBuildContext): BuildFunction;
6
+ export declare function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }: {
5
7
  logger: bunyan;
6
8
  buildId: string;
7
- expoApiServerURL: string;
9
+ graphqlClient: Client;
8
10
  robotAccessToken: string | null;
9
11
  extensions: string[];
10
12
  }): Promise<{
@@ -9,6 +9,7 @@ const eas_build_job_1 = require("@expo/eas-build-job");
9
9
  const results_1 = require("@expo/results");
10
10
  const steps_1 = require("@expo/steps");
11
11
  const fast_glob_1 = require("fast-glob");
12
+ const gql_tada_1 = require("gql.tada");
12
13
  const node_fetch_1 = __importDefault(require("node-fetch"));
13
14
  const node_fs_1 = __importDefault(require("node:fs"));
14
15
  const node_os_1 = __importDefault(require("node:os"));
@@ -21,7 +22,20 @@ const files_1 = require("../../utils/files");
21
22
  const retryOnDNSFailure_1 = require("../../utils/retryOnDNSFailure");
22
23
  const strings_1 = require("../../utils/strings");
23
24
  const streamPipeline = (0, util_1.promisify)(stream_1.default.pipeline);
24
- function createDownloadBuildFunction() {
25
+ const BUILD_BY_ID_QUERY = (0, gql_tada_1.graphql)(`
26
+ query DownloadBuildByIdQuery($buildId: ID!) {
27
+ builds {
28
+ byId(buildId: $buildId) {
29
+ id
30
+ platform
31
+ artifacts {
32
+ applicationArchiveUrl
33
+ }
34
+ }
35
+ }
36
+ }
37
+ `);
38
+ function createDownloadBuildFunction(ctx) {
25
39
  return new steps_1.BuildFunction({
26
40
  namespace: 'eas',
27
41
  id: 'download_build',
@@ -55,7 +69,7 @@ function createDownloadBuildFunction() {
55
69
  const { artifactPath } = await downloadBuildAsync({
56
70
  logger,
57
71
  buildId,
58
- expoApiServerURL: stepsCtx.global.staticContext.expoApiServerURL,
72
+ graphqlClient: ctx.graphqlClient,
59
73
  robotAccessToken: stepsCtx.global.staticContext.job.secrets?.robotAccessToken ?? null,
60
74
  extensions,
61
75
  });
@@ -63,9 +77,25 @@ function createDownloadBuildFunction() {
63
77
  },
64
78
  });
65
79
  }
66
- async function downloadBuildAsync({ logger, buildId, expoApiServerURL, robotAccessToken, extensions, }) {
80
+ async function fetchApplicationArchiveUrlAsync({ buildId, graphqlClient, }) {
81
+ const result = await graphqlClient.query(BUILD_BY_ID_QUERY, { buildId }).toPromise();
82
+ if (result.error) {
83
+ const { error } = result;
84
+ const message = `Could not fetch build ${buildId}: ${error.message}`;
85
+ throw error.networkError || result.error.response?.status >= 500
86
+ ? new eas_build_job_1.SystemError(message, { cause: error })
87
+ : new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_FETCH_FAILED', message, { cause: error });
88
+ }
89
+ const applicationArchiveUrl = result.data?.builds.byId?.artifacts?.applicationArchiveUrl;
90
+ if (!applicationArchiveUrl) {
91
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_NO_APPLICATION_ARCHIVE', 'Build does not have an application archive url');
92
+ }
93
+ return applicationArchiveUrl;
94
+ }
95
+ async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }) {
67
96
  const downloadDestinationDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'download_build-downloaded-'));
68
- const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(new URL(`/v2/artifacts/eas/${buildId}`, expoApiServerURL), {
97
+ const downloadUrl = await fetchApplicationArchiveUrlAsync({ buildId, graphqlClient });
98
+ const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(downloadUrl, {
69
99
  headers: robotAccessToken ? { Authorization: `Bearer ${robotAccessToken}` } : undefined,
70
100
  });
71
101
  if (!response.ok) {
@@ -23,14 +23,14 @@ function createFindAndUploadBuildArtifactsBuildFunction(ctx) {
23
23
  await uploadApplicationArchivesAsync({ ctx, stepCtx });
24
24
  }
25
25
  catch (err) {
26
- stepCtx.logger.error(`Failed to upload application archives.`, err);
26
+ stepCtx.logger.error({ err }, `Failed to upload application archives.`);
27
27
  firstError ||= err;
28
28
  }
29
29
  try {
30
30
  await uploadBuildArtifacts({ ctx, stepCtx });
31
31
  }
32
32
  catch (err) {
33
- stepCtx.logger.error(`Failed to upload build artifacts.`, err);
33
+ stepCtx.logger.error({ err }, `Failed to upload build artifacts.`);
34
34
  firstError ||= err;
35
35
  }
36
36
  if (ctx.job.platform === eas_build_job_1.Platform.IOS) {
@@ -11,6 +11,7 @@ const assert_1 = __importDefault(require("assert"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const os_1 = __importDefault(require("os"));
13
13
  const path_1 = __importDefault(require("path"));
14
+ const datadog_1 = require("../../datadog");
14
15
  function createInstallMaestroBuildFunction() {
15
16
  return new steps_1.BuildFunction({
16
17
  namespace: 'eas',
@@ -95,12 +96,22 @@ function createInstallMaestroBuildFunction() {
95
96
  }
96
97
  logger.info(`Maestro ${maestroVersionResult.value} is ready.`);
97
98
  outputs.maestro_version.set(maestroVersionResult.value);
99
+ datadog_1.Datadog.distribution('eas.maestro.install', 1, {
100
+ maestro_version: maestroVersionResult.value,
101
+ });
98
102
  },
99
103
  });
100
104
  }
101
105
  async function getMaestroVersion({ env }) {
102
- const maestroVersion = await (0, turtle_spawn_1.default)('maestro', ['--version'], { stdio: 'pipe', env });
103
- return maestroVersion.stdout.trim();
106
+ const { stdout } = await (0, turtle_spawn_1.default)('maestro', ['--version'], { stdio: 'pipe', env });
107
+ // `maestro --version` can print an analytics notice to stdout before the version,
108
+ // e.g. "Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS...\n2.0.10".
109
+ // Take the last version-looking token: the real version is printed after the notice, so
110
+ // this stays correct even if the notice itself contains a version-like string. Keeps the
111
+ // step output and the eas.maestro.install metric tag clean. Best-effort only: fall back to
112
+ // the raw output if none is found, so we never fail the build over a version string.
113
+ const versions = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g);
114
+ return versions?.at(-1) ?? stdout.trim();
104
115
  }
105
116
  async function installMaestro({ global, version, logger, env, }) {
106
117
  logger.info('Fetching install script');
@@ -10,6 +10,7 @@ export interface MaestroFlowResult {
10
10
  }
11
11
  export interface JUnitTestCaseResult {
12
12
  name: string;
13
+ file: string | undefined;
13
14
  status: 'passed' | 'failed';
14
15
  duration: number;
15
16
  errorMessage: string | null;
@@ -17,6 +18,23 @@ export interface JUnitTestCaseResult {
17
18
  properties: Record<string, string>;
18
19
  }
19
20
  export declare function parseJUnitTestCases(junitDirectory: string): Promise<JUnitTestCaseResult[]>;
21
+ export declare function isFileAttrRun(testcases: JUnitTestCaseResult[]): testcases is (JUnitTestCaseResult & {
22
+ file: string;
23
+ })[];
24
+ export declare function junitFileHasFileAttrs(junitFile: string): Promise<boolean>;
25
+ export declare function parseMaestroResultsFromFileAttrs(junitDirectory: string): Promise<MaestroFlowResult[]>;
26
+ /**
27
+ * Returns the `file=` paths of the failing testcases in the given attempt's
28
+ * JUnit file, or `null` when the result cannot be trusted (caller then falls
29
+ * back to dumb retry — re-run everything; never the legacy scan).
30
+ *
31
+ * Callers verify the report carries `file=` attributes first
32
+ * (junitFileHasFileAttrs).
33
+ */
34
+ export declare function parseFailedFlowsFromFileAttrs(args: {
35
+ junitFile: string;
36
+ workingDirectory: string;
37
+ }): Promise<string[] | null>;
20
38
  export declare function parseMaestroResults(junitDirectory: string, nameToPath: Map<string, string> | null): Promise<MaestroFlowResult[]>;
21
39
  /**
22
40
  * Returns the subset of `inputFlowPaths` whose testcases failed in the given