@expo/build-tools 18.2.0 → 18.5.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 (65) hide show
  1. package/dist/android/gradle.js +1 -1
  2. package/dist/buildErrors/detectError.d.ts +1 -1
  3. package/dist/buildErrors/detectError.js +8 -12
  4. package/dist/buildErrors/userErrorHandlers.d.ts +2 -2
  5. package/dist/buildErrors/userErrorHandlers.js +20 -20
  6. package/dist/builders/android.js +25 -3
  7. package/dist/builders/custom.js +1 -0
  8. package/dist/builders/ios.js +1 -1
  9. package/dist/common/setup.js +8 -9
  10. package/dist/context.d.ts +3 -1
  11. package/dist/context.js +3 -1
  12. package/dist/customBuildContext.d.ts +5 -1
  13. package/dist/customBuildContext.js +22 -0
  14. package/dist/generic.d.ts +1 -3
  15. package/dist/generic.js +13 -15
  16. package/dist/ios/credentials/provisioningProfile.d.ts +2 -1
  17. package/dist/ios/credentials/provisioningProfile.js +10 -11
  18. package/dist/steps/easFunctions.js +0 -2
  19. package/dist/steps/functionGroups/maestroTest.js +0 -3
  20. package/dist/steps/functions/calculateEASUpdateRuntimeVersion.js +2 -2
  21. package/dist/steps/functions/configureEASUpdateIfInstalled.js +2 -2
  22. package/dist/steps/functions/downloadArtifact.js +3 -3
  23. package/dist/steps/functions/downloadBuild.js +2 -2
  24. package/dist/steps/functions/maestroResultParser.js +134 -80
  25. package/dist/steps/functions/readIpaInfo.js +9 -9
  26. package/dist/steps/functions/repack.js +20 -3
  27. package/dist/steps/functions/reportMaestroTestResults.js +14 -7
  28. package/dist/steps/functions/restoreBuildCache.d.ts +8 -0
  29. package/dist/steps/functions/restoreBuildCache.js +85 -0
  30. package/dist/steps/functions/saveBuildCache.d.ts +8 -0
  31. package/dist/steps/functions/saveBuildCache.js +57 -0
  32. package/dist/steps/functions/startAndroidEmulator.js +19 -0
  33. package/dist/steps/functions/uploadToAsc.d.ts +3 -0
  34. package/dist/steps/functions/uploadToAsc.js +28 -5
  35. package/dist/steps/utils/android/gradle.js +1 -1
  36. package/dist/steps/utils/android/gradleConfig.d.ts +3 -0
  37. package/dist/steps/utils/android/gradleConfig.js +15 -1
  38. package/dist/steps/utils/ios/AscApiClient.d.ts +1 -1
  39. package/dist/steps/utils/ios/AscApiUtils.d.ts +5 -0
  40. package/dist/steps/utils/ios/AscApiUtils.js +19 -16
  41. package/dist/steps/utils/ios/credentials/provisioningProfile.d.ts +2 -1
  42. package/dist/steps/utils/ios/credentials/provisioningProfile.js +10 -11
  43. package/dist/utils/AndroidEmulatorUtils.d.ts +5 -0
  44. package/dist/utils/AndroidEmulatorUtils.js +17 -0
  45. package/dist/utils/appConfig.d.ts +4 -2
  46. package/dist/utils/appConfig.js +36 -5
  47. package/dist/utils/expoCli.d.ts +4 -0
  48. package/dist/utils/expoCli.js +37 -0
  49. package/dist/utils/expoUpdates.d.ts +1 -1
  50. package/dist/utils/expoUpdates.js +4 -4
  51. package/dist/utils/gradleCacheKey.d.ts +2 -0
  52. package/dist/utils/gradleCacheKey.js +56 -0
  53. package/dist/utils/packageManager.d.ts +10 -0
  54. package/dist/utils/packageManager.js +21 -0
  55. package/dist/utils/stepMetrics.d.ts +2 -2
  56. package/dist/utils/stepMetrics.js +4 -10
  57. package/package.json +14 -13
  58. package/dist/android/gradleConfig.d.ts +0 -3
  59. package/dist/android/gradleConfig.js +0 -47
  60. package/dist/steps/functions/internalMaestroTest.d.ts +0 -9
  61. package/dist/steps/functions/internalMaestroTest.js +0 -538
  62. package/dist/templates/EasBuildGradle.d.ts +0 -1
  63. package/dist/templates/EasBuildGradle.js +0 -57
  64. package/dist/utils/findMaestroPathsFlowsToExecuteAsync.d.ts +0 -8
  65. package/dist/utils/findMaestroPathsFlowsToExecuteAsync.js +0 -184
@@ -35,6 +35,7 @@ class ProvisioningProfile {
35
35
  }
36
36
  profilePath;
37
37
  profileData;
38
+ developerCertificates = [];
38
39
  constructor(ctx, profile, keychainPath, target, certificateCommonName) {
39
40
  this.ctx = ctx;
40
41
  this.profile = profile;
@@ -60,10 +61,11 @@ class ProvisioningProfile {
60
61
  await fs_extra_1.default.remove(this.profilePath);
61
62
  }
62
63
  verifyCertificate(fingerprint) {
63
- const devCertFingerprint = this.genDerCertFingerprint();
64
- if (devCertFingerprint !== fingerprint) {
65
- throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match.
66
- Profile's certificate fingerprint = ${devCertFingerprint}, distribution certificate fingerprint = ${fingerprint}`);
64
+ const devCertFingerprints = this.getAllDerCertFingerprints();
65
+ if (!devCertFingerprints.includes(fingerprint)) {
66
+ throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match.\n` +
67
+ `Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
68
+ `distribution certificate fingerprint = ${fingerprint}`);
67
69
  }
68
70
  }
69
71
  async load() {
@@ -87,6 +89,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
87
89
  }
88
90
  const applicationIdentifier = plistData.Entitlements['application-identifier'];
89
91
  const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
92
+ this.developerCertificates = plistData.DeveloperCertificates.map((cert) => Buffer.from(cert, 'base64'));
90
93
  this.profileData = {
91
94
  path: this.profilePath,
92
95
  target: this.target,
@@ -94,7 +97,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
94
97
  teamId: plistData.TeamIdentifier[0],
95
98
  uuid: plistData.UUID,
96
99
  name: plistData.Name,
97
- developerCertificate: Buffer.from(plistData.DeveloperCertificates[0], 'base64'),
100
+ developerCertificate: this.developerCertificates[0],
98
101
  certificateCommonName: this.certificateCommonName,
99
102
  distributionType: this.resolveDistributionType(plistData),
100
103
  };
@@ -110,12 +113,8 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
110
113
  return DistributionType.APP_STORE;
111
114
  }
112
115
  }
113
- genDerCertFingerprint() {
114
- return crypto_1.default
115
- .createHash('sha1')
116
- .update(new Uint8Array(this.data.developerCertificate))
117
- .digest('hex')
118
- .toUpperCase();
116
+ getAllDerCertFingerprints() {
117
+ return this.developerCertificates.map(cert => crypto_1.default.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase());
119
118
  }
120
119
  }
121
120
  exports.default = ProvisioningProfile;
@@ -18,7 +18,6 @@ const injectAndroidCredentials_1 = require("./functions/injectAndroidCredentials
18
18
  const installMaestro_1 = require("./functions/installMaestro");
19
19
  const installNodeModules_1 = require("./functions/installNodeModules");
20
20
  const installPods_1 = require("./functions/installPods");
21
- const internalMaestroTest_1 = require("./functions/internalMaestroTest");
22
21
  const prebuild_1 = require("./functions/prebuild");
23
22
  const readIpaInfo_1 = require("./functions/readIpaInfo");
24
23
  const repack_1 = require("./functions/repack");
@@ -73,7 +72,6 @@ function getEasFunctions(ctx) {
73
72
  (0, calculateEASUpdateRuntimeVersion_1.calculateEASUpdateRuntimeVersionFunction)(),
74
73
  (0, createSubmissionEntity_1.createSubmissionEntityFunction)(),
75
74
  (0, uploadToAsc_1.createUploadToAscBuildFunction)(),
76
- (0, internalMaestroTest_1.createInternalEasMaestroTestFunction)(ctx),
77
75
  (0, reportMaestroTestResults_1.createReportMaestroTestResultsFunction)(ctx),
78
76
  ];
79
77
  if (ctx.hasBuildJob()) {
@@ -39,7 +39,6 @@ function createEasMaestroTestFunctionGroup(buildToolsContext) {
39
39
  }) ?? 'ios/build/Build/Products/*simulator/*.app';
40
40
  steps.push(new steps_1.BuildStep(globalCtx, {
41
41
  id: steps_1.BuildStep.getNewId(),
42
- name: 'install_app',
43
42
  displayName: `Install app to Simulator`,
44
43
  command: `
45
44
  # shopt -s nullglob is necessary not to try to install
@@ -78,7 +77,6 @@ function createEasMaestroTestFunctionGroup(buildToolsContext) {
78
77
  }) ?? 'android/app/build/outputs/**/*.apk';
79
78
  steps.push(new steps_1.BuildStep(globalCtx, {
80
79
  id: steps_1.BuildStep.getNewId(),
81
- name: 'install_app',
82
80
  displayName: `Install app to Emulator`,
83
81
  command: `
84
82
  # shopt -s globstar is necessary to add /**/ support
@@ -111,7 +109,6 @@ function createEasMaestroTestFunctionGroup(buildToolsContext) {
111
109
  for (const flowPath of flowPaths) {
112
110
  steps.push(new steps_1.BuildStep(globalCtx, {
113
111
  id: steps_1.BuildStep.getNewId(),
114
- name: 'maestro_test',
115
112
  ifCondition: '${ always() }',
116
113
  displayName: `maestro test ${flowPath}`,
117
114
  command: `maestro test ${flowPath}`,
@@ -30,7 +30,7 @@ function calculateEASUpdateRuntimeVersionFunction() {
30
30
  }),
31
31
  ],
32
32
  fn: async (stepCtx, { env, inputs, outputs }) => {
33
- const appConfig = (0, appConfig_1.readAppConfig)({
33
+ const appConfig = (await (0, appConfig_1.readAppConfig)({
34
34
  projectDir: stepCtx.workingDirectory,
35
35
  env: Object.keys(env).reduce((acc, key) => {
36
36
  acc[key] = env[key] ?? '';
@@ -38,7 +38,7 @@ function calculateEASUpdateRuntimeVersionFunction() {
38
38
  }, {}),
39
39
  logger: stepCtx.logger,
40
40
  sdkVersion: stepCtx.global.staticContext.metadata?.sdkVersion,
41
- }).exp;
41
+ })).exp;
42
42
  const platform = inputs.platform.value ?? stepCtx.global.staticContext.job.platform;
43
43
  const workflow = inputs.workflow.value ?? stepCtx.global.staticContext.job.type;
44
44
  if (![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
@@ -44,7 +44,7 @@ function configureEASUpdateIfInstalledFunction() {
44
44
  const job = stepCtx.global.staticContext.job;
45
45
  (0, assert_1.default)(job.platform, 'Configuring EAS Update in generic jobs is not supported.');
46
46
  const metadata = stepCtx.global.staticContext.metadata;
47
- const appConfig = (0, appConfig_1.readAppConfig)({
47
+ const appConfig = (await (0, appConfig_1.readAppConfig)({
48
48
  projectDir: stepCtx.workingDirectory,
49
49
  env: Object.keys(env).reduce((acc, key) => {
50
50
  acc[key] = env[key] ?? '';
@@ -52,7 +52,7 @@ function configureEASUpdateIfInstalledFunction() {
52
52
  }, {}),
53
53
  logger: stepCtx.logger,
54
54
  sdkVersion: metadata?.sdkVersion,
55
- }).exp;
55
+ })).exp;
56
56
  const channelInput = inputs.channel.value;
57
57
  const runtimeVersionInput = inputs.runtime_version.value;
58
58
  const resolvedRuntimeVersionInput = inputs.resolved_eas_update_runtime_version.value;
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createDownloadArtifactFunction = createDownloadArtifactFunction;
7
7
  exports.downloadArtifactAsync = downloadArtifactAsync;
8
- const errors_1 = require("@expo/eas-build-job/dist/errors");
8
+ 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 node_fetch_1 = __importDefault(require("node-fetch"));
@@ -52,11 +52,11 @@ function createDownloadArtifactFunction() {
52
52
  });
53
53
  const interpolationContext = stepsCtx.global.getInterpolationContext();
54
54
  if (!('workflow' in interpolationContext)) {
55
- throw new errors_1.UserFacingError('EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW', 'No workflow found in the interpolation context.');
55
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_ARTIFACT_NO_WORKFLOW', 'No workflow found in the interpolation context.');
56
56
  }
57
57
  const robotAccessToken = stepsCtx.global.staticContext.job.secrets?.robotAccessToken;
58
58
  if (!robotAccessToken) {
59
- throw new errors_1.UserFacingError('EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN', 'No robot access token found in the job secrets.');
59
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_ARTIFACT_NO_ROBOT_ACCESS_TOKEN', 'No robot access token found in the job secrets.');
60
60
  }
61
61
  const workflowRunId = interpolationContext.workflow.id;
62
62
  const { logger } = stepsCtx;
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createDownloadBuildFunction = createDownloadBuildFunction;
7
7
  exports.downloadBuildAsync = downloadBuildAsync;
8
- const errors_1 = require("@expo/eas-build-job/dist/errors");
8
+ 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");
@@ -98,7 +98,7 @@ async function downloadBuildAsync({ logger, buildId, expoApiServerURL, robotAcce
98
98
  onlyDirectories: false,
99
99
  });
100
100
  if (matchingFiles.length === 0) {
101
- throw new errors_1.UserFacingError('EAS_DOWNLOAD_BUILD_NO_MATCHING_FILES', `No ${extensions.map(ext => `.${ext}`).join(', ')} entries found in the archive.`);
101
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_NO_MATCHING_FILES', `No ${extensions.map(ext => `.${ext}`).join(', ')} entries found in the archive.`);
102
102
  }
103
103
  logger.info(`Found ${matchingFiles.length} matching ${(0, strings_1.pluralize)(matchingFiles.length, 'entry')}:\n${matchingFiles.map(f => `- ${node_path_1.default.relative(extractionDirectory, f)}`).join('\n')}`);
104
104
  return { artifactPath: matchingFiles[0] };
@@ -23,6 +23,68 @@ const xmlParser = new fast_xml_parser_1.XMLParser({
23
23
  // Ensure single-element arrays are always arrays
24
24
  isArray: name => ['testsuite', 'testcase', 'property'].includes(name),
25
25
  });
26
+ // Internal helper — not exported. Parses a single JUnit XML file.
27
+ async function parseJUnitFile(filePath) {
28
+ const results = [];
29
+ try {
30
+ const content = await promises_1.default.readFile(filePath, 'utf-8');
31
+ const parsed = xmlParser.parse(content);
32
+ const testsuites = parsed?.testsuites?.testsuite;
33
+ if (!Array.isArray(testsuites)) {
34
+ return results;
35
+ }
36
+ for (const suite of testsuites) {
37
+ const testcases = suite?.testcase;
38
+ if (!Array.isArray(testcases)) {
39
+ continue;
40
+ }
41
+ for (const tc of testcases) {
42
+ const name = tc['@_name'];
43
+ if (!name) {
44
+ continue;
45
+ }
46
+ const timeStr = tc['@_time'];
47
+ const timeSeconds = timeStr ? parseFloat(timeStr) : 0;
48
+ const duration = Number.isFinite(timeSeconds) ? Math.round(timeSeconds * 1000) : 0;
49
+ const status = tc['@_status'] === 'SUCCESS' ? 'passed' : 'failed';
50
+ const failureText = tc.failure != null
51
+ ? typeof tc.failure === 'string'
52
+ ? tc.failure
53
+ : (tc.failure?.['#text'] ?? null)
54
+ : null;
55
+ const errorText = tc.error != null
56
+ ? typeof tc.error === 'string'
57
+ ? tc.error
58
+ : (tc.error?.['#text'] ?? null)
59
+ : null;
60
+ const errorMessage = failureText ?? errorText ?? null;
61
+ const rawProperties = tc.properties?.property ?? [];
62
+ const properties = {};
63
+ for (const prop of rawProperties) {
64
+ const propName = prop['@_name'];
65
+ const value = prop['@_value'];
66
+ if (typeof propName !== 'string' || typeof value !== 'string') {
67
+ continue;
68
+ }
69
+ properties[propName] = value;
70
+ }
71
+ const tagsValue = properties['tags'];
72
+ const tags = tagsValue
73
+ ? tagsValue
74
+ .split(',')
75
+ .map(t => t.trim())
76
+ .filter(Boolean)
77
+ : [];
78
+ delete properties['tags'];
79
+ results.push({ name, status, duration, errorMessage, tags, properties });
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ // Skip malformed XML files
85
+ }
86
+ return results;
87
+ }
26
88
  async function parseJUnitTestCases(junitDirectory) {
27
89
  let entries;
28
90
  try {
@@ -37,68 +99,7 @@ async function parseJUnitTestCases(junitDirectory) {
37
99
  }
38
100
  const results = [];
39
101
  for (const xmlFile of xmlFiles) {
40
- try {
41
- const content = await promises_1.default.readFile(path_1.default.join(junitDirectory, xmlFile), 'utf-8');
42
- const parsed = xmlParser.parse(content);
43
- const testsuites = parsed?.testsuites?.testsuite;
44
- if (!Array.isArray(testsuites)) {
45
- continue;
46
- }
47
- for (const suite of testsuites) {
48
- const testcases = suite?.testcase;
49
- if (!Array.isArray(testcases)) {
50
- continue;
51
- }
52
- for (const tc of testcases) {
53
- const name = tc['@_name'];
54
- if (!name) {
55
- continue;
56
- }
57
- const timeStr = tc['@_time'];
58
- const timeSeconds = timeStr ? parseFloat(timeStr) : 0;
59
- const duration = Number.isFinite(timeSeconds) ? Math.round(timeSeconds * 1000) : 0;
60
- // Use @_status as primary indicator (more robust than checking <failure> presence)
61
- const status = tc['@_status'] === 'SUCCESS' ? 'passed' : 'failed';
62
- // Extract error message from <failure> or <error> elements
63
- const failureText = tc.failure != null
64
- ? typeof tc.failure === 'string'
65
- ? tc.failure
66
- : (tc.failure?.['#text'] ?? null)
67
- : null;
68
- const errorText = tc.error != null
69
- ? typeof tc.error === 'string'
70
- ? tc.error
71
- : (tc.error?.['#text'] ?? null)
72
- : null;
73
- const errorMessage = failureText ?? errorText ?? null;
74
- // Extract properties
75
- const rawProperties = tc.properties?.property ?? [];
76
- const properties = {};
77
- for (const prop of rawProperties) {
78
- const propName = prop['@_name'];
79
- const value = prop['@_value'];
80
- if (typeof propName !== 'string' || typeof value !== 'string') {
81
- continue;
82
- }
83
- properties[propName] = value;
84
- }
85
- // Extract tags from "tags" property (Maestro 2.2.0+, comma-separated)
86
- const tagsValue = properties['tags'];
87
- const tags = tagsValue
88
- ? tagsValue
89
- .split(',')
90
- .map(t => t.trim())
91
- .filter(Boolean)
92
- : [];
93
- delete properties['tags'];
94
- results.push({ name, status, duration, errorMessage, tags, properties });
95
- }
96
- }
97
- }
98
- catch {
99
- // Skip malformed XML files
100
- continue;
101
- }
102
+ results.push(...(await parseJUnitFile(path_1.default.join(junitDirectory, xmlFile))));
102
103
  }
103
104
  return results;
104
105
  }
@@ -130,9 +131,26 @@ async function parseFlowMetadata(filePath) {
130
131
  }
131
132
  }
132
133
  async function parseMaestroResults(junitDirectory, testsDirectory, projectRoot) {
133
- // 1. Parse JUnit XML files (primary source)
134
- const junitResults = await parseJUnitTestCases(junitDirectory);
135
- if (junitResults.length === 0) {
134
+ // 1. Parse JUnit XML files, tracking which file each result came from
135
+ let junitEntries;
136
+ try {
137
+ junitEntries = await promises_1.default.readdir(junitDirectory);
138
+ }
139
+ catch {
140
+ return [];
141
+ }
142
+ const xmlFiles = junitEntries.filter(f => f.endsWith('.xml')).sort();
143
+ if (xmlFiles.length === 0) {
144
+ return [];
145
+ }
146
+ const junitResultsWithSource = [];
147
+ for (const xmlFile of xmlFiles) {
148
+ const fileResults = await parseJUnitFile(path_1.default.join(junitDirectory, xmlFile));
149
+ for (const result of fileResults) {
150
+ junitResultsWithSource.push({ result, sourceFile: xmlFile });
151
+ }
152
+ }
153
+ if (junitResultsWithSource.length === 0) {
136
154
  return [];
137
155
  }
138
156
  // 2. Parse ai-*.json from debug output for flow_file_path + retryCount
@@ -172,23 +190,59 @@ async function parseMaestroResults(junitDirectory, testsDirectory, projectRoot)
172
190
  }
173
191
  // 3. Merge: JUnit results + ai-*.json metadata
174
192
  const results = [];
175
- for (const junit of junitResults) {
176
- const flowFilePath = flowPathMap.get(junit.name);
193
+ // Parse attempt index from filename pattern: *-attempt-N.*
194
+ const ATTEMPT_PATTERN = /attempt-(\d+)/;
195
+ // Group results by flow name
196
+ const resultsByName = new Map();
197
+ for (const entry of junitResultsWithSource) {
198
+ const group = resultsByName.get(entry.result.name) ?? [];
199
+ group.push(entry);
200
+ resultsByName.set(entry.result.name, group);
201
+ }
202
+ for (const [flowName, flowEntries] of resultsByName) {
203
+ const flowFilePath = flowPathMap.get(flowName);
177
204
  const relativePath = flowFilePath
178
205
  ? await relativizePathAsync(flowFilePath, projectRoot)
179
- : junit.name; // fallback: use flow name if ai-*.json not found
180
- const occurrences = flowOccurrences.get(junit.name) ?? 0;
181
- const retryCount = Math.max(0, occurrences - 1);
182
- results.push({
183
- name: junit.name,
184
- path: relativePath,
185
- status: junit.status,
186
- errorMessage: junit.errorMessage,
187
- duration: junit.duration,
188
- retryCount,
189
- tags: junit.tags,
190
- properties: junit.properties,
191
- });
206
+ : flowName;
207
+ if (flowEntries.length === 1) {
208
+ // Single result for this flow — use ai-*.json occurrence count for retryCount
209
+ // (backward compat with old-style single JUnit file that gets overwritten)
210
+ const { result } = flowEntries[0];
211
+ const occurrences = flowOccurrences.get(flowName) ?? 0;
212
+ const retryCount = Math.max(0, occurrences - 1);
213
+ results.push({
214
+ name: flowName,
215
+ path: relativePath,
216
+ status: result.status,
217
+ errorMessage: result.errorMessage,
218
+ duration: result.duration,
219
+ retryCount,
220
+ tags: result.tags,
221
+ properties: result.properties,
222
+ });
223
+ }
224
+ else {
225
+ // Multiple results — per-attempt JUnit files. Sort by attempt index from filename.
226
+ const sorted = flowEntries
227
+ .map(entry => {
228
+ const match = entry.sourceFile.match(ATTEMPT_PATTERN);
229
+ const attemptIndex = match ? parseInt(match[1], 10) : 0;
230
+ return { ...entry, attemptIndex };
231
+ })
232
+ .sort((a, b) => a.attemptIndex - b.attemptIndex);
233
+ for (const { result, attemptIndex } of sorted) {
234
+ results.push({
235
+ name: flowName,
236
+ path: relativePath,
237
+ status: result.status,
238
+ errorMessage: result.errorMessage,
239
+ duration: result.duration,
240
+ retryCount: attemptIndex,
241
+ tags: result.tags,
242
+ properties: result.properties,
243
+ });
244
+ }
245
+ }
192
246
  }
193
247
  return results;
194
248
  }
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createReadIpaInfoBuildFunction = createReadIpaInfoBuildFunction;
7
7
  exports.readIpaInfoAsync = readIpaInfoAsync;
8
- const errors_1 = require("@expo/eas-build-job/dist/errors");
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
9
9
  const steps_1 = require("@expo/steps");
10
10
  const fs_extra_1 = __importDefault(require("fs-extra"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
@@ -45,7 +45,7 @@ function createReadIpaInfoBuildFunction() {
45
45
  const ipaPathInput = zod_1.z.string().parse(inputs.ipa_path.value);
46
46
  const ipaPath = node_path_1.default.resolve(stepCtx.workingDirectory, ipaPathInput);
47
47
  if (!(await fs_extra_1.default.pathExists(ipaPath))) {
48
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_FILE_NOT_FOUND', `IPA file not found: ${ipaPath}`);
48
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_FILE_NOT_FOUND', `IPA file not found: ${ipaPath}`);
49
49
  }
50
50
  const ipaInfo = await readIpaInfoAsync(ipaPath);
51
51
  outputs.bundle_identifier.set(ipaInfo.bundleIdentifier);
@@ -60,15 +60,15 @@ async function readIpaInfoAsync(ipaPath) {
60
60
  const infoPlist = parseInfoPlistBuffer(infoPlistBuffer);
61
61
  const bundleIdentifier = infoPlist.CFBundleIdentifier;
62
62
  if (typeof bundleIdentifier !== 'string') {
63
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleIdentifier in Info.plist');
63
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleIdentifier in Info.plist');
64
64
  }
65
65
  const bundleShortVersion = infoPlist.CFBundleShortVersionString;
66
66
  if (typeof bundleShortVersion !== 'string') {
67
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleShortVersionString in Info.plist');
67
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleShortVersionString in Info.plist');
68
68
  }
69
69
  const bundleVersion = infoPlist.CFBundleVersion;
70
70
  if (typeof bundleVersion !== 'string') {
71
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleVersion in Info.plist');
71
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleVersion in Info.plist');
72
72
  }
73
73
  return {
74
74
  bundleIdentifier,
@@ -77,10 +77,10 @@ async function readIpaInfoAsync(ipaPath) {
77
77
  };
78
78
  }
79
79
  catch (error) {
80
- if (error instanceof errors_1.UserFacingError) {
80
+ if (error instanceof eas_build_job_1.UserError) {
81
81
  throw error;
82
82
  }
83
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_FAILED', `Failed to read IPA info: ${error.message}`, { cause: error });
83
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_FAILED', `Failed to read IPA info: ${error.message}`, { cause: error });
84
84
  }
85
85
  }
86
86
  function parseInfoPlistBuffer(data) {
@@ -89,7 +89,7 @@ function parseInfoPlistBuffer(data) {
89
89
  const parsedBinaryPlists = bplist_parser_1.default.parseBuffer(data);
90
90
  const parsedBinaryPlist = parsedBinaryPlists[0];
91
91
  if (!parsedBinaryPlist || typeof parsedBinaryPlist !== 'object') {
92
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_INVALID_BINARY_PLIST', 'Invalid binary plist in IPA');
92
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INVALID_BINARY_PLIST', 'Invalid binary plist in IPA');
93
93
  }
94
94
  return parsedBinaryPlist;
95
95
  }
@@ -101,7 +101,7 @@ async function readInfoPlistBufferFromIpaAsync(ipaPath) {
101
101
  const entries = Object.values(await zip.entries());
102
102
  const infoPlistEntry = entries.find(entry => INFO_PLIST_PATH_REGEXP.test(entry.name));
103
103
  if (!infoPlistEntry) {
104
- throw new errors_1.UserFacingError('EAS_READ_IPA_INFO_INFO_PLIST_NOT_FOUND', `Failed to read IPA info: Could not find Info.plist in ${ipaPath}`);
104
+ throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INFO_PLIST_NOT_FOUND', `Failed to read IPA info: Could not find Info.plist in ${ipaPath}`);
105
105
  }
106
106
  return await zip.entryData(infoPlistEntry.name);
107
107
  }
@@ -15,6 +15,7 @@ const node_path_1 = __importDefault(require("node:path"));
15
15
  const resolve_from_1 = __importDefault(require("resolve-from"));
16
16
  const fastlane_1 = require("../../common/fastlane");
17
17
  const manager_1 = __importDefault(require("../utils/ios/credentials/manager"));
18
+ const packageManager_1 = require("../../utils/packageManager");
18
19
  function createRepackBuildFunction() {
19
20
  return new steps_1.BuildFunction({
20
21
  namespace: 'eas',
@@ -42,6 +43,11 @@ function createRepackBuildFunction() {
42
43
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
43
44
  required: false,
44
45
  }),
46
+ steps_1.BuildStepInput.createProvider({
47
+ id: 'js_bundle_only',
48
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
49
+ required: false,
50
+ }),
45
51
  steps_1.BuildStepInput.createProvider({
46
52
  id: 'repack_version',
47
53
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
@@ -68,7 +74,10 @@ function createRepackBuildFunction() {
68
74
  if (![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
69
75
  throw new Error(`Unsupported platform: ${platform}. Platform must be "${eas_build_job_1.Platform.ANDROID}" or "${eas_build_job_1.Platform.IOS}"`);
70
76
  }
71
- const repackSpawnAsync = createSpawnAsyncStepAdapter({ verbose, logger: stepsCtx.logger });
77
+ const repackSpawnAsync = createSpawnAsyncStepAdapter({
78
+ verbose,
79
+ logger: stepsCtx.logger,
80
+ });
72
81
  const tmpDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), `repack-`));
73
82
  const workingDirectory = node_path_1.default.join(tmpDir, 'working-directory');
74
83
  await node_fs_1.default.promises.mkdir(workingDirectory);
@@ -81,10 +90,16 @@ function createRepackBuildFunction() {
81
90
  sourcemapOutput: undefined,
82
91
  }
83
92
  : undefined;
84
- stepsCtx.logger.info(`Using repack from: ${inputs.repack_package.value}@${inputs.repack_version.value}`);
93
+ const jsBundleOnly = inputs.js_bundle_only.value ?? false;
94
+ const resolvedRepackVersion = (await (0, packageManager_1.resolvePackageVersionAsync)({
95
+ logger: stepsCtx.logger,
96
+ packageName: inputs.repack_package.value,
97
+ distTag: inputs.repack_version.value,
98
+ })) ?? inputs.repack_version.value;
99
+ stepsCtx.logger.info(`Using repack from: ${inputs.repack_package.value}@${inputs.repack_version.value} (${resolvedRepackVersion})`);
85
100
  const repackApp = await installAndImportRepackAsync({
86
101
  packageName: inputs.repack_package.value,
87
- version: inputs.repack_version.value,
102
+ version: resolvedRepackVersion,
88
103
  });
89
104
  const { repackAppIosAsync, repackAppAndroidAsync } = repackApp;
90
105
  stepsCtx.logger.info('Repacking the app...');
@@ -97,6 +112,7 @@ function createRepackBuildFunction() {
97
112
  outputPath,
98
113
  workingDirectory,
99
114
  exportEmbedOptions,
115
+ jsBundleOnly,
100
116
  iosSigningOptions: await resolveIosSigningOptionsAsync({
101
117
  job: stepsCtx.global.staticContext.job,
102
118
  logger: stepsCtx.logger,
@@ -124,6 +140,7 @@ function createRepackBuildFunction() {
124
140
  outputPath,
125
141
  workingDirectory,
126
142
  exportEmbedOptions,
143
+ jsBundleOnly,
127
144
  androidSigningOptions,
128
145
  logger: stepsCtx.logger,
129
146
  spawnAsync: repackSpawnAsync,
@@ -56,13 +56,20 @@ function createReportMaestroTestResultsFunction(ctx) {
56
56
  logger.info('No maestro test results found, skipping report');
57
57
  return;
58
58
  }
59
- // Maestro allows overriding flow names via config, so different flow files can share
60
- // the same name. JUnit XML only contains names (not file paths), making it impossible
61
- // to map duplicates back to their original flow files. Skip and let the user fix it.
62
- const names = flowResults.map(r => r.name);
63
- const duplicates = names.filter((n, i) => names.indexOf(n) !== i);
64
- if (duplicates.length > 0) {
65
- logger.error(`Duplicate test case names found in JUnit output: ${[...new Set(duplicates)].join(', ')}. Skipping report. Ensure each Maestro flow has a unique name.`);
59
+ // Detect truly conflicting results: same (name, retryCount) pair means different flow files
60
+ // share the same name (Maestro config override), which we can't disambiguate.
61
+ // Same name with different retryCount is expected (per-attempt results from retries).
62
+ const seen = new Set();
63
+ const conflicting = new Set();
64
+ for (const r of flowResults) {
65
+ const key = `${r.name}:${r.retryCount}`;
66
+ if (seen.has(key)) {
67
+ conflicting.add(r.name);
68
+ }
69
+ seen.add(key);
70
+ }
71
+ if (conflicting.size > 0) {
72
+ logger.error(`Duplicate test case names found in JUnit output: ${[...conflicting].join(', ')}. Skipping report. Ensure each Maestro flow has a unique name.`);
66
73
  return;
67
74
  }
68
75
  const testCaseResults = flowResults.flatMap(f => {
@@ -12,6 +12,14 @@ export declare function restoreCcacheAsync({ logger, workingDirectory, platform,
12
12
  robotAccessToken?: string;
13
13
  };
14
14
  }): Promise<void>;
15
+ export declare function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets, }: {
16
+ logger: bunyan;
17
+ workingDirectory: string;
18
+ env: Record<string, string | undefined>;
19
+ secrets?: {
20
+ robotAccessToken?: string;
21
+ };
22
+ }): Promise<void>;
15
23
  export declare function cacheStatsAsync({ logger, env, secrets, }: {
16
24
  logger: bunyan;
17
25
  env: Record<string, string | undefined>;