@expo/build-tools 18.2.0 → 18.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/gradle.js +1 -1
- package/dist/buildErrors/detectError.d.ts +1 -1
- package/dist/buildErrors/detectError.js +8 -12
- package/dist/buildErrors/userErrorHandlers.d.ts +2 -2
- package/dist/buildErrors/userErrorHandlers.js +20 -20
- package/dist/builders/android.js +12 -2
- package/dist/builders/custom.js +1 -0
- package/dist/common/setup.js +4 -6
- package/dist/context.d.ts +2 -0
- package/dist/context.js +2 -0
- package/dist/customBuildContext.d.ts +5 -1
- package/dist/customBuildContext.js +22 -0
- package/dist/generic.d.ts +1 -3
- package/dist/generic.js +13 -15
- package/dist/ios/credentials/provisioningProfile.d.ts +2 -1
- package/dist/ios/credentials/provisioningProfile.js +10 -11
- package/dist/steps/functions/downloadArtifact.js +3 -3
- package/dist/steps/functions/downloadBuild.js +2 -2
- package/dist/steps/functions/internalMaestroTest.js +8 -12
- package/dist/steps/functions/maestroResultParser.js +134 -80
- package/dist/steps/functions/readIpaInfo.js +9 -9
- package/dist/steps/functions/reportMaestroTestResults.js +14 -7
- package/dist/steps/functions/uploadToAsc.js +4 -4
- package/dist/steps/utils/android/gradle.js +1 -1
- package/dist/steps/utils/android/gradleConfig.d.ts +3 -0
- package/dist/steps/utils/android/gradleConfig.js +15 -1
- package/dist/steps/utils/ios/AscApiUtils.js +3 -3
- package/dist/steps/utils/ios/credentials/provisioningProfile.d.ts +2 -1
- package/dist/steps/utils/ios/credentials/provisioningProfile.js +10 -11
- package/dist/utils/stepMetrics.d.ts +2 -2
- package/dist/utils/stepMetrics.js +4 -10
- package/package.json +4 -4
- package/dist/android/gradleConfig.d.ts +0 -3
- package/dist/android/gradleConfig.js +0 -47
- package/dist/templates/EasBuildGradle.d.ts +0 -1
- package/dist/templates/EasBuildGradle.js +0 -57
|
@@ -177,14 +177,14 @@ function createInternalEasMaestroTestFunction(ctx) {
|
|
|
177
177
|
const failedFlows = [];
|
|
178
178
|
for (const [flowIndex, flowPath] of flowPathsToExecute.entries()) {
|
|
179
179
|
stepCtx.logger.info('');
|
|
180
|
-
// If output_format is empty or noop, we won't use this.
|
|
181
|
-
const outputPath = node_path_1.default.join(maestroReportsDir, [
|
|
182
|
-
`${output_format ? output_format + '-' : ''}report-flow-${flowIndex + 1}`,
|
|
183
|
-
MaestroOutputFormatToExtensionMap[output_format ?? 'noop'],
|
|
184
|
-
]
|
|
185
|
-
.filter(Boolean)
|
|
186
|
-
.join('.'));
|
|
187
180
|
for (let attemptCount = 0; attemptCount < retries; attemptCount++) {
|
|
181
|
+
// Generate unique report path per attempt (not overwritten on retry)
|
|
182
|
+
const outputPath = node_path_1.default.join(maestroReportsDir, [
|
|
183
|
+
`${output_format ? output_format + '-' : ''}report-flow-${flowIndex + 1}-attempt-${attemptCount}`,
|
|
184
|
+
MaestroOutputFormatToExtensionMap[output_format ?? 'noop'],
|
|
185
|
+
]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join('.'));
|
|
188
188
|
const localDeviceName = `eas-simulator-${flowIndex}-${attemptCount}`;
|
|
189
189
|
// If the test passes, but the recording fails, we don't want to make the test fail,
|
|
190
190
|
// so we return two separate results.
|
|
@@ -227,11 +227,7 @@ function createInternalEasMaestroTestFunction(ctx) {
|
|
|
227
227
|
if (logsResult?.ok) {
|
|
228
228
|
try {
|
|
229
229
|
const extension = node_path_1.default.extname(logsResult.value.outputPath);
|
|
230
|
-
const destinationPath = node_path_1.default.join(deviceLogsDir, `flow-${flowIndex}${extension}`);
|
|
231
|
-
await node_fs_1.default.promises.rm(destinationPath, {
|
|
232
|
-
force: true,
|
|
233
|
-
recursive: true,
|
|
234
|
-
});
|
|
230
|
+
const destinationPath = node_path_1.default.join(deviceLogsDir, `flow-${flowIndex}-attempt-${attemptCount}${extension}`);
|
|
235
231
|
await node_fs_1.default.promises.rename(logsResult.value.outputPath, destinationPath);
|
|
236
232
|
}
|
|
237
233
|
catch (err) {
|
|
@@ -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
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
80
|
+
if (error instanceof eas_build_job_1.UserError) {
|
|
81
81
|
throw error;
|
|
82
82
|
}
|
|
83
|
-
throw new
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -56,13 +56,20 @@ function createReportMaestroTestResultsFunction(ctx) {
|
|
|
56
56
|
logger.info('No maestro test results found, skipping report');
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
|
-
//
|
|
60
|
-
// the same name
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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 => {
|
|
@@ -41,7 +41,7 @@ exports.isClosedVersionTrainError = isClosedVersionTrainError;
|
|
|
41
41
|
exports.isInvalidBundleIdentifierError = isInvalidBundleIdentifierError;
|
|
42
42
|
exports.isMissingPurposeStringError = isMissingPurposeStringError;
|
|
43
43
|
exports.parseMissingUsageDescriptionKeys = parseMissingUsageDescriptionKeys;
|
|
44
|
-
const
|
|
44
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
45
45
|
const results_1 = require("@expo/results");
|
|
46
46
|
const steps_1 = require("@expo/steps");
|
|
47
47
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
@@ -252,7 +252,7 @@ function createUploadToAscBuildFunction() {
|
|
|
252
252
|
const ipaBundleIdentifier = ipaInfoResult.ok
|
|
253
253
|
? ipaInfoResult.value.bundleIdentifier
|
|
254
254
|
: null;
|
|
255
|
-
throw new
|
|
255
|
+
throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID', `Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` +
|
|
256
256
|
`IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` +
|
|
257
257
|
`App Store Connect app bundle identifier: ${ascAppBundleIdentifier}\n\n` +
|
|
258
258
|
'Bundle identifier cannot be changed for an existing App Store Connect app. ' +
|
|
@@ -261,7 +261,7 @@ function createUploadToAscBuildFunction() {
|
|
|
261
261
|
}
|
|
262
262
|
if (isMissingPurposeStringError(errors)) {
|
|
263
263
|
const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors);
|
|
264
|
-
throw new
|
|
264
|
+
throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING', `Build upload was rejected by App Store Connect because Info.plist is missing one or more privacy purpose strings.\n\n` +
|
|
265
265
|
`${missingUsageDescriptionKeys.length > 0
|
|
266
266
|
? `Missing keys reported by App Store Connect:\n- ${missingUsageDescriptionKeys.join('\n- ')}\n\n`
|
|
267
267
|
: ''}` +
|
|
@@ -272,7 +272,7 @@ function createUploadToAscBuildFunction() {
|
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
274
|
if (isClosedVersionTrainError(errors)) {
|
|
275
|
-
throw new
|
|
275
|
+
throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', `Build upload was rejected by App Store Connect because the ${bundleShortVersion} app version is not accepted for new build submissions. ` +
|
|
276
276
|
'This usually means the version train is closed or lower than a previously approved version. ' +
|
|
277
277
|
'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version, then rebuild and submit again.');
|
|
278
278
|
}
|
|
@@ -25,7 +25,7 @@ async function runGradleCommand({ logger, gradleCommand, androidDir, env, extraE
|
|
|
25
25
|
return line;
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
-
env: { ...env, ...extraEnv },
|
|
28
|
+
env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' },
|
|
29
29
|
});
|
|
30
30
|
if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
|
|
31
31
|
adjustOOMScore(spawnPromise, logger);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Android } from '@expo/eas-build-job';
|
|
1
2
|
import { bunyan } from '@expo/logger';
|
|
3
|
+
import { BuildContext } from '../../../context';
|
|
2
4
|
export declare function injectCredentialsGradleConfig(logger: bunyan, workingDir: string): Promise<void>;
|
|
3
5
|
export declare function injectConfigureVersionGradleConfig(logger: bunyan, workingDir: string, { versionCode, versionName }: {
|
|
4
6
|
versionCode?: string;
|
|
5
7
|
versionName?: string;
|
|
6
8
|
}): Promise<void>;
|
|
9
|
+
export declare function warnIfLegacyEasBuildGradleExists(ctx: BuildContext<Android.Job>): Promise<void>;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.injectCredentialsGradleConfig = injectCredentialsGradleConfig;
|
|
7
7
|
exports.injectConfigureVersionGradleConfig = injectConfigureVersionGradleConfig;
|
|
8
|
+
exports.warnIfLegacyEasBuildGradleExists = warnIfLegacyEasBuildGradleExists;
|
|
8
9
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
9
10
|
const template_file_1 = require("@expo/template-file");
|
|
10
11
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
@@ -41,6 +42,19 @@ async function deleteEasBuildConfigureVersionGradle(workingDir) {
|
|
|
41
42
|
const targetPath = getEasBuildConfigureVersionGradlePath(workingDir);
|
|
42
43
|
await fs_extra_1.default.remove(targetPath);
|
|
43
44
|
}
|
|
45
|
+
let legacyEasBuildGradleWarningEmitted = false;
|
|
46
|
+
async function warnIfLegacyEasBuildGradleExists(ctx) {
|
|
47
|
+
const legacyGradlePath = getLegacyEasBuildGradlePath(ctx.getReactNativeProjectDirectory());
|
|
48
|
+
if ((await fs_extra_1.default.pathExists(legacyGradlePath)) &&
|
|
49
|
+
(process.env.NODE_ENV === 'test' || !legacyEasBuildGradleWarningEmitted)) {
|
|
50
|
+
legacyEasBuildGradleWarningEmitted = true;
|
|
51
|
+
ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
|
|
52
|
+
ctx.markBuildPhaseHasWarnings();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getLegacyEasBuildGradlePath(projectRoot) {
|
|
56
|
+
return path_1.default.join(projectRoot, 'android/app/eas-build.gradle');
|
|
57
|
+
}
|
|
44
58
|
function getEasBuildInjectCredentialsGradlePath(workingDir) {
|
|
45
59
|
return path_1.default.join(workingDir, 'android/app/eas-build-inject-android-credentials.gradle');
|
|
46
60
|
}
|
|
@@ -83,6 +97,6 @@ function hasLine(haystack, needle) {
|
|
|
83
97
|
return (haystack
|
|
84
98
|
.replace(/\r\n/g, '\n')
|
|
85
99
|
.split('\n')
|
|
86
|
-
// Check for both single and double quotes
|
|
100
|
+
// Check for both single and double quotes.
|
|
87
101
|
.some(line => line === needle || line === needle.replace(/"/g, "'")));
|
|
88
102
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AscApiUtils = void 0;
|
|
4
|
-
const
|
|
4
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
5
5
|
const AscApiClient_1 = require("./AscApiClient");
|
|
6
6
|
var AscApiUtils;
|
|
7
7
|
(function (AscApiUtils) {
|
|
@@ -25,7 +25,7 @@ var AscApiUtils;
|
|
|
25
25
|
// Don't hide the original NOT_FOUND error with a secondary lookup failure.
|
|
26
26
|
throw error;
|
|
27
27
|
}
|
|
28
|
-
throw new
|
|
28
|
+
throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_APP_NOT_FOUND', `App Store Connect app for application identifier ${appleAppIdentifier} was not found. ` +
|
|
29
29
|
'Verify the configured application identifier and that the App Store Connect API key has access to the application in the correct App Store Connect account.' +
|
|
30
30
|
(visibleAppsSummary
|
|
31
31
|
? `\n\nExample applications visible to this API key:\n${visibleAppsSummary}`
|
|
@@ -64,7 +64,7 @@ var AscApiUtils;
|
|
|
64
64
|
const isDuplicateVersionError = errors.length > 0 &&
|
|
65
65
|
errors.every(item => item.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE');
|
|
66
66
|
if (isDuplicateVersionError) {
|
|
67
|
-
throw new
|
|
67
|
+
throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE', `Increment Build Number: Build number ${bundleVersion} for app version ${bundleShortVersion} has already been used. ` +
|
|
68
68
|
'App Store Connect requires unique build numbers within each app version (version train). ' +
|
|
69
69
|
'Increment it by setting ios.buildNumber in app.json, or set "autoIncrement": true in eas.json (recommended). Then rebuild and resubmit.', {
|
|
70
70
|
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
|
@@ -23,11 +23,12 @@ export default class ProvisioningProfile {
|
|
|
23
23
|
get data(): ProvisioningProfileData;
|
|
24
24
|
private readonly profilePath;
|
|
25
25
|
private profileData?;
|
|
26
|
+
private developerCertificates;
|
|
26
27
|
constructor(profile: Buffer, keychainPath: string, target: string, certificateCommonName: string);
|
|
27
28
|
init(logger: bunyan): Promise<void>;
|
|
28
29
|
destroy(logger: bunyan): Promise<void>;
|
|
29
30
|
verifyCertificate(fingerprint: string): void;
|
|
30
31
|
private load;
|
|
31
32
|
private resolveDistributionType;
|
|
32
|
-
private
|
|
33
|
+
private getAllDerCertFingerprints;
|
|
33
34
|
}
|
|
@@ -34,6 +34,7 @@ class ProvisioningProfile {
|
|
|
34
34
|
}
|
|
35
35
|
profilePath;
|
|
36
36
|
profileData;
|
|
37
|
+
developerCertificates = [];
|
|
37
38
|
constructor(profile, keychainPath, target, certificateCommonName) {
|
|
38
39
|
this.profile = profile;
|
|
39
40
|
this.keychainPath = keychainPath;
|
|
@@ -58,10 +59,11 @@ class ProvisioningProfile {
|
|
|
58
59
|
await fs_extra_1.default.remove(this.profilePath);
|
|
59
60
|
}
|
|
60
61
|
verifyCertificate(fingerprint) {
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match
|
|
64
|
-
Profile's certificate
|
|
62
|
+
const devCertFingerprints = this.getAllDerCertFingerprints();
|
|
63
|
+
if (!devCertFingerprints.includes(fingerprint)) {
|
|
64
|
+
throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match.\n` +
|
|
65
|
+
`Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
|
|
66
|
+
`distribution certificate fingerprint = ${fingerprint}`);
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
async load() {
|
|
@@ -85,6 +87,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
|
|
85
87
|
}
|
|
86
88
|
const applicationIdentifier = plistData.Entitlements['application-identifier'];
|
|
87
89
|
const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
|
|
90
|
+
this.developerCertificates = plistData.DeveloperCertificates.map((cert) => Buffer.from(cert, 'base64'));
|
|
88
91
|
this.profileData = {
|
|
89
92
|
path: this.profilePath,
|
|
90
93
|
target: this.target,
|
|
@@ -92,7 +95,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
|
|
92
95
|
teamId: plistData.TeamIdentifier[0],
|
|
93
96
|
uuid: plistData.UUID,
|
|
94
97
|
name: plistData.Name,
|
|
95
|
-
developerCertificate:
|
|
98
|
+
developerCertificate: this.developerCertificates[0],
|
|
96
99
|
certificateCommonName: this.certificateCommonName,
|
|
97
100
|
distributionType: this.resolveDistributionType(plistData),
|
|
98
101
|
};
|
|
@@ -108,12 +111,8 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
|
|
|
108
111
|
return DistributionType.APP_STORE;
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
|
-
|
|
112
|
-
return crypto_1.default
|
|
113
|
-
.createHash('sha1')
|
|
114
|
-
.update(new Uint8Array(this.data.developerCertificate))
|
|
115
|
-
.digest('hex')
|
|
116
|
-
.toUpperCase();
|
|
114
|
+
getAllDerCertFingerprints() {
|
|
115
|
+
return this.developerCertificates.map(cert => crypto_1.default.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase());
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
exports.default = ProvisioningProfile;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
|
-
import {
|
|
2
|
+
import { StepMetric } from '@expo/steps';
|
|
3
3
|
export declare function uploadStepMetricsToWwwAsync({ workflowJobId, robotAccessToken, expoApiV2BaseUrl, stepMetrics, logger, }: {
|
|
4
4
|
workflowJobId: string;
|
|
5
5
|
robotAccessToken: string;
|
|
6
6
|
expoApiV2BaseUrl: string;
|
|
7
|
-
stepMetrics:
|
|
7
|
+
stepMetrics: StepMetric[];
|
|
8
8
|
logger: bunyan;
|
|
9
9
|
}): Promise<void>;
|
|
@@ -3,22 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.uploadStepMetricsToWwwAsync = uploadStepMetricsToWwwAsync;
|
|
4
4
|
const turtleFetch_1 = require("./turtleFetch");
|
|
5
5
|
async function uploadStepMetricsToWwwAsync({ workflowJobId, robotAccessToken, expoApiV2BaseUrl, stepMetrics, logger, }) {
|
|
6
|
-
if (stepMetrics.length === 0) {
|
|
7
|
-
logger.debug('No step metrics to upload');
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
6
|
try {
|
|
11
7
|
await (0, turtleFetch_1.turtleFetch)(new URL(`workflows/${workflowJobId}/metrics`, expoApiV2BaseUrl).toString(), 'POST', {
|
|
12
8
|
json: { stepMetrics },
|
|
13
|
-
headers: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
timeout: 20000,
|
|
9
|
+
headers: { Authorization: `Bearer ${robotAccessToken}` },
|
|
10
|
+
timeout: 5000,
|
|
11
|
+
retries: 2,
|
|
17
12
|
logger,
|
|
18
13
|
});
|
|
19
|
-
logger.info(`Uploaded ${stepMetrics.length} step metrics`);
|
|
20
14
|
}
|
|
21
15
|
catch {
|
|
22
|
-
// Don't
|
|
16
|
+
// Don't fail the build for metrics — silently give up
|
|
23
17
|
}
|
|
24
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.4.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"@expo/config": "10.0.6",
|
|
39
39
|
"@expo/config-plugins": "9.0.12",
|
|
40
40
|
"@expo/downloader": "18.0.1",
|
|
41
|
-
"@expo/eas-build-job": "18.
|
|
41
|
+
"@expo/eas-build-job": "18.4.0",
|
|
42
42
|
"@expo/env": "^0.4.0",
|
|
43
43
|
"@expo/logger": "18.0.1",
|
|
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": "18.
|
|
48
|
+
"@expo/steps": "18.4.0",
|
|
49
49
|
"@expo/template-file": "18.0.1",
|
|
50
50
|
"@expo/turtle-spawn": "18.0.1",
|
|
51
51
|
"@expo/xcpretty": "^4.3.1",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript": "^5.5.4",
|
|
98
98
|
"uuid": "^9.0.1"
|
|
99
99
|
},
|
|
100
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "4e202db843be2dca6450af4b45ee76b226a662ea"
|
|
101
101
|
}
|