@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.
- 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 +25 -3
- package/dist/builders/custom.js +1 -0
- package/dist/builders/ios.js +1 -1
- package/dist/common/setup.js +8 -9
- package/dist/context.d.ts +3 -1
- package/dist/context.js +3 -1
- 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/easFunctions.js +0 -2
- package/dist/steps/functionGroups/maestroTest.js +0 -3
- package/dist/steps/functions/calculateEASUpdateRuntimeVersion.js +2 -2
- package/dist/steps/functions/configureEASUpdateIfInstalled.js +2 -2
- package/dist/steps/functions/downloadArtifact.js +3 -3
- package/dist/steps/functions/downloadBuild.js +2 -2
- package/dist/steps/functions/maestroResultParser.js +134 -80
- package/dist/steps/functions/readIpaInfo.js +9 -9
- package/dist/steps/functions/repack.js +20 -3
- package/dist/steps/functions/reportMaestroTestResults.js +14 -7
- package/dist/steps/functions/restoreBuildCache.d.ts +8 -0
- package/dist/steps/functions/restoreBuildCache.js +85 -0
- package/dist/steps/functions/saveBuildCache.d.ts +8 -0
- package/dist/steps/functions/saveBuildCache.js +57 -0
- package/dist/steps/functions/startAndroidEmulator.js +19 -0
- package/dist/steps/functions/uploadToAsc.d.ts +3 -0
- package/dist/steps/functions/uploadToAsc.js +28 -5
- 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/AscApiClient.d.ts +1 -1
- package/dist/steps/utils/ios/AscApiUtils.d.ts +5 -0
- package/dist/steps/utils/ios/AscApiUtils.js +19 -16
- package/dist/steps/utils/ios/credentials/provisioningProfile.d.ts +2 -1
- package/dist/steps/utils/ios/credentials/provisioningProfile.js +10 -11
- package/dist/utils/AndroidEmulatorUtils.d.ts +5 -0
- package/dist/utils/AndroidEmulatorUtils.js +17 -0
- package/dist/utils/appConfig.d.ts +4 -2
- package/dist/utils/appConfig.js +36 -5
- package/dist/utils/expoCli.d.ts +4 -0
- package/dist/utils/expoCli.js +37 -0
- package/dist/utils/expoUpdates.d.ts +1 -1
- package/dist/utils/expoUpdates.js +4 -4
- package/dist/utils/gradleCacheKey.d.ts +2 -0
- package/dist/utils/gradleCacheKey.js +56 -0
- package/dist/utils/packageManager.d.ts +10 -0
- package/dist/utils/packageManager.js +21 -0
- package/dist/utils/stepMetrics.d.ts +2 -2
- package/dist/utils/stepMetrics.js +4 -10
- package/package.json +14 -13
- package/dist/android/gradleConfig.d.ts +0 -3
- package/dist/android/gradleConfig.js +0 -47
- package/dist/steps/functions/internalMaestroTest.d.ts +0 -9
- package/dist/steps/functions/internalMaestroTest.js +0 -538
- package/dist/templates/EasBuildGradle.d.ts +0 -1
- package/dist/templates/EasBuildGradle.js +0 -57
- package/dist/utils/findMaestroPathsFlowsToExecuteAsync.d.ts +0 -8
- 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
|
|
64
|
-
if (
|
|
65
|
-
throw new eas_build_job_1.errors.CredentialsDistCertMismatchError(`Provisioning profile and distribution certificate don't match
|
|
66
|
-
Profile's certificate
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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({
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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 => {
|
|
@@ -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>;
|