@expo/build-tools 18.0.2 → 18.1.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/customBuildContext.d.ts +2 -0
- package/dist/customBuildContext.js +2 -0
- package/dist/steps/easFunctions.js +2 -0
- package/dist/steps/functions/internalMaestroTest.js +7 -0
- package/dist/steps/functions/maestroResultParser.d.ts +42 -0
- package/dist/steps/functions/maestroResultParser.js +215 -0
- package/dist/steps/functions/reportMaestroTestResults.d.ts +3 -0
- package/dist/steps/functions/reportMaestroTestResults.js +105 -0
- package/dist/steps/functions/uploadToAsc.d.ts +3 -0
- package/dist/steps/functions/uploadToAsc.js +25 -19
- package/dist/steps/utils/ios/AscApiClient.d.ts +46 -3
- package/dist/steps/utils/ios/AscApiClient.js +52 -2
- package/dist/steps/utils/ios/AscApiUtils.d.ts +13 -0
- package/dist/steps/utils/ios/AscApiUtils.js +90 -0
- package/package.json +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BuildJob, Env, Job, Metadata, StaticJobInterpolationContext } from '@expo/eas-build-job';
|
|
2
2
|
import { bunyan } from '@expo/logger';
|
|
3
3
|
import { BuildRuntimePlatform, ExternalBuildContextProvider } from '@expo/steps';
|
|
4
|
+
import { Client } from '@urql/core';
|
|
4
5
|
import { ArtifactToUpload, BuildContext } from './context';
|
|
5
6
|
export interface BuilderRuntimeApi {
|
|
6
7
|
uploadArtifact: (spec: {
|
|
@@ -20,6 +21,7 @@ export declare class CustomBuildContext<TJob extends Job = Job> implements Exter
|
|
|
20
21
|
*/
|
|
21
22
|
readonly startTime: Date;
|
|
22
23
|
readonly logger: bunyan;
|
|
24
|
+
readonly graphqlClient: Client;
|
|
23
25
|
readonly runtimeApi: BuilderRuntimeApi;
|
|
24
26
|
job: TJob;
|
|
25
27
|
metadata?: Metadata;
|
|
@@ -34,6 +34,7 @@ class CustomBuildContext {
|
|
|
34
34
|
*/
|
|
35
35
|
startTime;
|
|
36
36
|
logger;
|
|
37
|
+
graphqlClient;
|
|
37
38
|
runtimeApi;
|
|
38
39
|
job;
|
|
39
40
|
metadata;
|
|
@@ -43,6 +44,7 @@ class CustomBuildContext {
|
|
|
43
44
|
this.job = buildCtx.job;
|
|
44
45
|
this.metadata = buildCtx.metadata;
|
|
45
46
|
this.logger = buildCtx.logger.child({ phase: eas_build_job_1.BuildPhase.CUSTOM });
|
|
47
|
+
this.graphqlClient = buildCtx.graphqlClient;
|
|
46
48
|
this.projectSourceDirectory = path_1.default.join(buildCtx.workingdir, 'temporary-custom-build');
|
|
47
49
|
this.projectTargetDirectory = path_1.default.join(buildCtx.workingdir, 'build');
|
|
48
50
|
this.defaultWorkingDirectory = buildCtx.getReactNativeProjectDirectory();
|
|
@@ -22,6 +22,7 @@ const internalMaestroTest_1 = require("./functions/internalMaestroTest");
|
|
|
22
22
|
const prebuild_1 = require("./functions/prebuild");
|
|
23
23
|
const readIpaInfo_1 = require("./functions/readIpaInfo");
|
|
24
24
|
const repack_1 = require("./functions/repack");
|
|
25
|
+
const reportMaestroTestResults_1 = require("./functions/reportMaestroTestResults");
|
|
25
26
|
const resolveAppleTeamIdFromCredentials_1 = require("./functions/resolveAppleTeamIdFromCredentials");
|
|
26
27
|
const resolveBuildConfig_1 = require("./functions/resolveBuildConfig");
|
|
27
28
|
const restoreBuildCache_1 = require("./functions/restoreBuildCache");
|
|
@@ -73,6 +74,7 @@ function getEasFunctions(ctx) {
|
|
|
73
74
|
(0, createSubmissionEntity_1.createSubmissionEntityFunction)(),
|
|
74
75
|
(0, uploadToAsc_1.createUploadToAscBuildFunction)(),
|
|
75
76
|
(0, internalMaestroTest_1.createInternalEasMaestroTestFunction)(ctx),
|
|
77
|
+
(0, reportMaestroTestResults_1.createReportMaestroTestResultsFunction)(ctx),
|
|
76
78
|
];
|
|
77
79
|
if (ctx.hasBuildJob()) {
|
|
78
80
|
functions.push(...[
|
|
@@ -74,6 +74,10 @@ function createInternalEasMaestroTestFunction(ctx) {
|
|
|
74
74
|
id: 'test_reports_artifact_id',
|
|
75
75
|
required: false,
|
|
76
76
|
}),
|
|
77
|
+
steps_1.BuildStepOutput.createProvider({
|
|
78
|
+
id: 'junit_report_directory',
|
|
79
|
+
required: false,
|
|
80
|
+
}),
|
|
77
81
|
],
|
|
78
82
|
fn: async (stepCtx, { inputs: _inputs, env, outputs }) => {
|
|
79
83
|
// inputs come in form of { value: unknown }. Here we parse them into a typed and validated object.
|
|
@@ -293,6 +297,9 @@ function createInternalEasMaestroTestFunction(ctx) {
|
|
|
293
297
|
stepCtx.logger.error({ err }, 'Failed to upload reports.');
|
|
294
298
|
}
|
|
295
299
|
}
|
|
300
|
+
if (output_format === 'junit') {
|
|
301
|
+
outputs.junit_report_directory.set(maestroReportsDir);
|
|
302
|
+
}
|
|
296
303
|
const generatedDeviceLogs = await node_fs_1.default.promises.readdir(deviceLogsDir);
|
|
297
304
|
if (generatedDeviceLogs.length === 0) {
|
|
298
305
|
stepCtx.logger.warn('No device logs were successfully collected.');
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export interface MaestroFlowResult {
|
|
3
|
+
name: string;
|
|
4
|
+
path: string;
|
|
5
|
+
status: 'passed' | 'failed';
|
|
6
|
+
errorMessage: string | null;
|
|
7
|
+
duration: number;
|
|
8
|
+
retryCount: number;
|
|
9
|
+
tags: string[];
|
|
10
|
+
properties: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function extractFlowKey(filename: string, prefix: string): string | null;
|
|
13
|
+
export interface JUnitTestCaseResult {
|
|
14
|
+
name: string;
|
|
15
|
+
status: 'passed' | 'failed';
|
|
16
|
+
duration: number;
|
|
17
|
+
errorMessage: string | null;
|
|
18
|
+
tags: string[];
|
|
19
|
+
properties: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseJUnitTestCases(junitDirectory: string): Promise<JUnitTestCaseResult[]>;
|
|
22
|
+
declare const FlowMetadataFileSchema: z.ZodObject<{
|
|
23
|
+
flow_name: z.ZodString;
|
|
24
|
+
flow_file_path: z.ZodString;
|
|
25
|
+
}, z.core.$strip>;
|
|
26
|
+
type FlowMetadata = z.output<typeof FlowMetadataFileSchema>;
|
|
27
|
+
/**
|
|
28
|
+
* Parses an `ai-*.json` file produced by Maestro's TestDebugReporter.
|
|
29
|
+
*
|
|
30
|
+
* The file contains:
|
|
31
|
+
* - `flow_name`: derived from the YAML `config.name` field if present, otherwise
|
|
32
|
+
* the flow filename without extension.
|
|
33
|
+
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/runner/TestRunner.kt#L70
|
|
34
|
+
* - `flow_file_path`: absolute path to the original flow YAML file.
|
|
35
|
+
* - `outputs`: screenshot defect data (unused here).
|
|
36
|
+
*
|
|
37
|
+
* Filename format: `ai-(flowName).json` where `/` in flowName is replaced with `_`.
|
|
38
|
+
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/report/TestDebugReporter.kt#L67
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseFlowMetadata(filePath: string): Promise<FlowMetadata | null>;
|
|
41
|
+
export declare function parseMaestroResults(junitDirectory: string, testsDirectory: string, projectRoot: string): Promise<MaestroFlowResult[]>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractFlowKey = extractFlowKey;
|
|
7
|
+
exports.parseJUnitTestCases = parseJUnitTestCases;
|
|
8
|
+
exports.parseFlowMetadata = parseFlowMetadata;
|
|
9
|
+
exports.parseMaestroResults = parseMaestroResults;
|
|
10
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
11
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const zod_1 = require("zod");
|
|
14
|
+
// Maestro's TestDebugReporter creates timestamped directories, e.g. "2024-06-15_143022"
|
|
15
|
+
const TIMESTAMP_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}_\d{6}$/;
|
|
16
|
+
function extractFlowKey(filename, prefix) {
|
|
17
|
+
const match = filename.match(new RegExp(`^${prefix}-(.+)\\.json$`));
|
|
18
|
+
return match?.[1] ?? null;
|
|
19
|
+
}
|
|
20
|
+
const xmlParser = new fast_xml_parser_1.XMLParser({
|
|
21
|
+
ignoreAttributes: false,
|
|
22
|
+
attributeNamePrefix: '@_',
|
|
23
|
+
// Ensure single-element arrays are always arrays
|
|
24
|
+
isArray: name => ['testsuite', 'testcase', 'property'].includes(name),
|
|
25
|
+
});
|
|
26
|
+
async function parseJUnitTestCases(junitDirectory) {
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = await promises_1.default.readdir(junitDirectory);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const xmlFiles = entries.filter(f => f.endsWith('.xml'));
|
|
35
|
+
if (xmlFiles.length === 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const results = [];
|
|
39
|
+
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
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
const FlowMetadataFileSchema = zod_1.z.object({
|
|
106
|
+
flow_name: zod_1.z.string(),
|
|
107
|
+
flow_file_path: zod_1.z.string(),
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Parses an `ai-*.json` file produced by Maestro's TestDebugReporter.
|
|
111
|
+
*
|
|
112
|
+
* The file contains:
|
|
113
|
+
* - `flow_name`: derived from the YAML `config.name` field if present, otherwise
|
|
114
|
+
* the flow filename without extension.
|
|
115
|
+
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/runner/TestRunner.kt#L70
|
|
116
|
+
* - `flow_file_path`: absolute path to the original flow YAML file.
|
|
117
|
+
* - `outputs`: screenshot defect data (unused here).
|
|
118
|
+
*
|
|
119
|
+
* Filename format: `ai-(flowName).json` where `/` in flowName is replaced with `_`.
|
|
120
|
+
* See: https://github.com/mobile-dev-inc/Maestro/blob/c0e95fd/maestro-cli/src/main/java/maestro/cli/report/TestDebugReporter.kt#L67
|
|
121
|
+
*/
|
|
122
|
+
async function parseFlowMetadata(filePath) {
|
|
123
|
+
try {
|
|
124
|
+
const content = await promises_1.default.readFile(filePath, 'utf-8');
|
|
125
|
+
const data = JSON.parse(content);
|
|
126
|
+
return FlowMetadataFileSchema.parse(data);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
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) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
// 2. Parse ai-*.json from debug output for flow_file_path + retryCount
|
|
139
|
+
const flowPathMap = new Map(); // flowName → flowFilePath
|
|
140
|
+
const flowOccurrences = new Map(); // flowName → count
|
|
141
|
+
let entries;
|
|
142
|
+
try {
|
|
143
|
+
entries = await promises_1.default.readdir(testsDirectory);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
entries = [];
|
|
147
|
+
}
|
|
148
|
+
const timestampDirs = entries.filter(name => TIMESTAMP_DIR_PATTERN.test(name)).sort();
|
|
149
|
+
for (const dir of timestampDirs) {
|
|
150
|
+
const dirPath = path_1.default.join(testsDirectory, dir);
|
|
151
|
+
let files;
|
|
152
|
+
try {
|
|
153
|
+
files = await promises_1.default.readdir(dirPath);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
const flowKey = extractFlowKey(file, 'ai');
|
|
160
|
+
if (!flowKey) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const metadata = await parseFlowMetadata(path_1.default.join(dirPath, file));
|
|
164
|
+
if (!metadata) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Track latest path (last timestamp dir wins)
|
|
168
|
+
flowPathMap.set(metadata.flow_name, metadata.flow_file_path);
|
|
169
|
+
// Count occurrences for retryCount
|
|
170
|
+
flowOccurrences.set(metadata.flow_name, (flowOccurrences.get(metadata.flow_name) ?? 0) + 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// 3. Merge: JUnit results + ai-*.json metadata
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const junit of junitResults) {
|
|
176
|
+
const flowFilePath = flowPathMap.get(junit.name);
|
|
177
|
+
const relativePath = flowFilePath
|
|
178
|
+
? 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
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
async function relativizePathAsync(flowFilePath, projectRoot) {
|
|
196
|
+
if (!path_1.default.isAbsolute(flowFilePath)) {
|
|
197
|
+
return flowFilePath;
|
|
198
|
+
}
|
|
199
|
+
// Resolve symlinks (e.g., /tmp -> /private/tmp on macOS) for consistent comparison
|
|
200
|
+
let resolvedRoot = projectRoot;
|
|
201
|
+
let resolvedFlow = flowFilePath;
|
|
202
|
+
try {
|
|
203
|
+
resolvedRoot = await promises_1.default.realpath(projectRoot);
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
try {
|
|
207
|
+
resolvedFlow = await promises_1.default.realpath(flowFilePath);
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
const relative = path_1.default.relative(resolvedRoot, resolvedFlow);
|
|
211
|
+
if (relative.startsWith('..')) {
|
|
212
|
+
return flowFilePath;
|
|
213
|
+
}
|
|
214
|
+
return relative;
|
|
215
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createReportMaestroTestResultsFunction = createReportMaestroTestResultsFunction;
|
|
4
|
+
const steps_1 = require("@expo/steps");
|
|
5
|
+
const gql_tada_1 = require("gql.tada");
|
|
6
|
+
const maestroResultParser_1 = require("./maestroResultParser");
|
|
7
|
+
const CREATE_MUTATION = (0, gql_tada_1.graphql)(`
|
|
8
|
+
mutation CreateWorkflowDeviceTestCaseResults($input: CreateWorkflowDeviceTestCaseResultsInput!) {
|
|
9
|
+
workflowDeviceTestCaseResult {
|
|
10
|
+
createWorkflowDeviceTestCaseResults(input: $input) {
|
|
11
|
+
id
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
`);
|
|
16
|
+
const FLOW_STATUS_TO_TEST_CASE_RESULT_STATUS = {
|
|
17
|
+
passed: 'PASSED',
|
|
18
|
+
failed: 'FAILED',
|
|
19
|
+
};
|
|
20
|
+
function createReportMaestroTestResultsFunction(ctx) {
|
|
21
|
+
return new steps_1.BuildFunction({
|
|
22
|
+
namespace: 'eas',
|
|
23
|
+
id: 'report_maestro_test_results',
|
|
24
|
+
name: 'Report Maestro Test Results',
|
|
25
|
+
__metricsId: 'eas/report_maestro_test_results',
|
|
26
|
+
inputProviders: [
|
|
27
|
+
steps_1.BuildStepInput.createProvider({
|
|
28
|
+
id: 'junit_report_directory',
|
|
29
|
+
required: false,
|
|
30
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
31
|
+
defaultValue: '${{ env.HOME }}/.maestro/tests',
|
|
32
|
+
}),
|
|
33
|
+
steps_1.BuildStepInput.createProvider({
|
|
34
|
+
id: 'tests_directory',
|
|
35
|
+
required: false,
|
|
36
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
37
|
+
defaultValue: '${{ env.HOME }}/.maestro/tests',
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
fn: async (stepsCtx, { inputs }) => {
|
|
41
|
+
const { logger } = stepsCtx;
|
|
42
|
+
const workflowJobId = stepsCtx.global.env.__WORKFLOW_JOB_ID;
|
|
43
|
+
if (!workflowJobId) {
|
|
44
|
+
logger.info('Not running in a workflow job, skipping test results report');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const junitDirectory = inputs.junit_report_directory.value ?? '';
|
|
48
|
+
if (!junitDirectory) {
|
|
49
|
+
logger.info('No JUnit directory provided, skipping test results report');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const testsDirectory = inputs.tests_directory.value;
|
|
53
|
+
try {
|
|
54
|
+
const flowResults = await (0, maestroResultParser_1.parseMaestroResults)(junitDirectory, testsDirectory, stepsCtx.workingDirectory);
|
|
55
|
+
if (flowResults.length === 0) {
|
|
56
|
+
logger.info('No maestro test results found, skipping report');
|
|
57
|
+
return;
|
|
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.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const testCaseResults = flowResults.flatMap(f => {
|
|
69
|
+
const status = FLOW_STATUS_TO_TEST_CASE_RESULT_STATUS[f.status];
|
|
70
|
+
if (!status) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
name: f.name,
|
|
76
|
+
path: f.path,
|
|
77
|
+
status,
|
|
78
|
+
errorMessage: f.errorMessage,
|
|
79
|
+
duration: f.duration,
|
|
80
|
+
retryCount: f.retryCount,
|
|
81
|
+
tags: f.tags,
|
|
82
|
+
properties: f.properties,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
});
|
|
86
|
+
const result = await ctx.graphqlClient
|
|
87
|
+
.mutation(CREATE_MUTATION, {
|
|
88
|
+
input: {
|
|
89
|
+
workflowJobId,
|
|
90
|
+
testCaseResults,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
.toPromise();
|
|
94
|
+
if (result.error) {
|
|
95
|
+
logger.error({ error: result.error }, 'GraphQL error creating test case results');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
logger.info(`Reported ${testCaseResults.length} test case result(s).`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.error({ err: error }, 'Failed to create test case results');
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.createUploadToAscBuildFunction = createUploadToAscBuildFunction;
|
|
40
|
+
exports.isClosedVersionTrainError = isClosedVersionTrainError;
|
|
41
|
+
const errors_1 = require("@expo/eas-build-job/dist/errors");
|
|
40
42
|
const steps_1 = require("@expo/steps");
|
|
41
43
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
42
44
|
const jose = __importStar(require("jose"));
|
|
@@ -45,6 +47,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
45
47
|
const promises_1 = require("node:timers/promises");
|
|
46
48
|
const zod_1 = require("zod");
|
|
47
49
|
const AscApiClient_1 = require("../utils/ios/AscApiClient");
|
|
50
|
+
const AscApiUtils_1 = require("../utils/ios/AscApiUtils");
|
|
48
51
|
function createUploadToAscBuildFunction() {
|
|
49
52
|
return new steps_1.BuildFunction({
|
|
50
53
|
namespace: 'eas',
|
|
@@ -128,26 +131,14 @@ function createUploadToAscBuildFunction() {
|
|
|
128
131
|
.sign(privateKey);
|
|
129
132
|
const client = new AscApiClient_1.AscApiClient({ token, logger: stepsCtx.logger });
|
|
130
133
|
stepsCtx.logger.info('Reading App information...');
|
|
131
|
-
const appResponse = await
|
|
134
|
+
const appResponse = await AscApiUtils_1.AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
|
|
132
135
|
stepsCtx.logger.info(`Uploading Build to "${appResponse.data.attributes.name}" (${appResponse.data.attributes.bundleId})...`);
|
|
133
136
|
stepsCtx.logger.info('Creating Build Upload...');
|
|
134
|
-
const buildUploadResponse = await
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
cfBundleShortVersionString: bundleShortVersion,
|
|
140
|
-
cfBundleVersion: bundleVersion,
|
|
141
|
-
},
|
|
142
|
-
relationships: {
|
|
143
|
-
app: {
|
|
144
|
-
data: {
|
|
145
|
-
type: 'apps',
|
|
146
|
-
id: appleAppIdentifier,
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
137
|
+
const buildUploadResponse = await AscApiUtils_1.AscApiUtils.createBuildUploadAsync({
|
|
138
|
+
client,
|
|
139
|
+
appleAppIdentifier,
|
|
140
|
+
bundleShortVersion,
|
|
141
|
+
bundleVersion,
|
|
151
142
|
});
|
|
152
143
|
const buildUploadId = buildUploadResponse.data.id;
|
|
153
144
|
const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/ios/${buildUploadId}`;
|
|
@@ -223,10 +214,18 @@ function createUploadToAscBuildFunction() {
|
|
|
223
214
|
}
|
|
224
215
|
stepsCtx.logger.info('Checking build upload status...');
|
|
225
216
|
const waitingForBuildStartedAt = Date.now();
|
|
217
|
+
const waitingLogIntervalMs = 10 * 1000;
|
|
218
|
+
let lastWaitLogTime = 0;
|
|
219
|
+
let lastWaitLogState = null;
|
|
226
220
|
while (Date.now() - waitingForBuildStartedAt < 30 * 60 * 1000 /* 30 minutes */) {
|
|
227
221
|
const { data: { attributes: { state }, }, } = await client.getAsync(`/v1/buildUploads/:id`, { 'fields[buildUploads]': ['state', 'build'], include: ['build'] }, { id: buildUploadId });
|
|
228
222
|
if (state.state === 'AWAITING_UPLOAD' || state.state === 'PROCESSING') {
|
|
229
|
-
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (lastWaitLogState !== state.state || now - lastWaitLogTime >= waitingLogIntervalMs) {
|
|
225
|
+
stepsCtx.logger.info(`Waiting for build upload to complete... (status = ${state.state})`);
|
|
226
|
+
lastWaitLogTime = now;
|
|
227
|
+
lastWaitLogState = state.state;
|
|
228
|
+
}
|
|
230
229
|
await (0, promises_1.setTimeout)(2000);
|
|
231
230
|
continue;
|
|
232
231
|
}
|
|
@@ -242,6 +241,10 @@ function createUploadToAscBuildFunction() {
|
|
|
242
241
|
stepsCtx.logger.error(`Errors:\n${itemizeMessages(errors)}\n`);
|
|
243
242
|
}
|
|
244
243
|
if (state.state === 'FAILED') {
|
|
244
|
+
if (isClosedVersionTrainError(errors)) {
|
|
245
|
+
throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', `Build upload was rejected by App Store Connect because the ${bundleShortVersion} version train is closed. ` +
|
|
246
|
+
'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version and submit again.');
|
|
247
|
+
}
|
|
245
248
|
throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
|
|
246
249
|
}
|
|
247
250
|
else if (state.state === 'COMPLETE') {
|
|
@@ -255,6 +258,9 @@ function createUploadToAscBuildFunction() {
|
|
|
255
258
|
function itemizeMessages(messages) {
|
|
256
259
|
return `- ${messages.map(m => `${m.description} (${m.code})`).join('\n- ')}`;
|
|
257
260
|
}
|
|
261
|
+
function isClosedVersionTrainError(messages) {
|
|
262
|
+
return (messages.length > 0 && messages.every(message => ['90062', '90186'].includes(message.code)));
|
|
263
|
+
}
|
|
258
264
|
async function uploadChunksAsync({ uploadOperations, ipaPath, logger, }) {
|
|
259
265
|
const fd = await fs_extra_1.default.open(ipaPath, 'r');
|
|
260
266
|
try {
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
declare const AscErrorResponseSchema: z.ZodObject<{
|
|
4
|
+
errors: z.ZodArray<z.ZodObject<{
|
|
5
|
+
id: z.ZodOptional<z.ZodString>;
|
|
6
|
+
status: z.ZodOptional<z.ZodString>;
|
|
7
|
+
code: z.ZodOptional<z.ZodString>;
|
|
8
|
+
title: z.ZodOptional<z.ZodString>;
|
|
9
|
+
detail: z.ZodOptional<z.ZodString>;
|
|
10
|
+
source: z.ZodOptional<z.ZodUnknown>;
|
|
11
|
+
}, z.core.$strip>>;
|
|
12
|
+
}, z.core.$strip>;
|
|
3
13
|
declare const GetApi: {
|
|
14
|
+
'/v1/apps': {
|
|
15
|
+
path: z.ZodObject<{}, z.core.$strip>;
|
|
16
|
+
request: z.ZodObject<{
|
|
17
|
+
'fields[apps]': z.ZodArray<z.ZodEnum<{
|
|
18
|
+
name: "name";
|
|
19
|
+
bundleId: "bundleId";
|
|
20
|
+
}>>;
|
|
21
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
response: z.ZodObject<{
|
|
24
|
+
data: z.ZodArray<z.ZodObject<{
|
|
25
|
+
type: z.ZodLiteral<"apps">;
|
|
26
|
+
id: z.ZodString;
|
|
27
|
+
attributes: z.ZodObject<{
|
|
28
|
+
bundleId: z.ZodString;
|
|
29
|
+
name: z.ZodString;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
};
|
|
4
34
|
'/v1/apps/:id': {
|
|
5
35
|
path: z.ZodObject<{
|
|
6
36
|
id: z.ZodString;
|
|
@@ -38,10 +68,10 @@ declare const GetApi: {
|
|
|
38
68
|
attributes: z.ZodObject<{
|
|
39
69
|
assetDeliveryState: z.ZodObject<{
|
|
40
70
|
state: z.ZodEnum<{
|
|
71
|
+
FAILED: "FAILED";
|
|
41
72
|
AWAITING_UPLOAD: "AWAITING_UPLOAD";
|
|
42
73
|
UPLOAD_COMPLETE: "UPLOAD_COMPLETE";
|
|
43
74
|
COMPLETE: "COMPLETE";
|
|
44
|
-
FAILED: "FAILED";
|
|
45
75
|
}>;
|
|
46
76
|
errors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
47
77
|
code: z.ZodString;
|
|
@@ -76,9 +106,9 @@ declare const GetApi: {
|
|
|
76
106
|
attributes: z.ZodObject<{
|
|
77
107
|
state: z.ZodObject<{
|
|
78
108
|
state: z.ZodEnum<{
|
|
109
|
+
FAILED: "FAILED";
|
|
79
110
|
AWAITING_UPLOAD: "AWAITING_UPLOAD";
|
|
80
111
|
COMPLETE: "COMPLETE";
|
|
81
|
-
FAILED: "FAILED";
|
|
82
112
|
PROCESSING: "PROCESSING";
|
|
83
113
|
}>;
|
|
84
114
|
infos: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -203,10 +233,10 @@ declare const PatchApi: {
|
|
|
203
233
|
attributes: z.ZodObject<{
|
|
204
234
|
assetDeliveryState: z.ZodObject<{
|
|
205
235
|
state: z.ZodEnum<{
|
|
236
|
+
FAILED: "FAILED";
|
|
206
237
|
AWAITING_UPLOAD: "AWAITING_UPLOAD";
|
|
207
238
|
UPLOAD_COMPLETE: "UPLOAD_COMPLETE";
|
|
208
239
|
COMPLETE: "COMPLETE";
|
|
209
|
-
FAILED: "FAILED";
|
|
210
240
|
}>;
|
|
211
241
|
errors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
212
242
|
code: z.ZodString;
|
|
@@ -222,6 +252,12 @@ declare const PatchApi: {
|
|
|
222
252
|
}, z.core.$strip>;
|
|
223
253
|
};
|
|
224
254
|
};
|
|
255
|
+
export type AscApiClientGetApi = {
|
|
256
|
+
[Path in keyof typeof GetApi]: {
|
|
257
|
+
request: z.input<(typeof GetApi)[Path]['request']>;
|
|
258
|
+
response: z.output<(typeof GetApi)[Path]['response']>;
|
|
259
|
+
};
|
|
260
|
+
};
|
|
225
261
|
export type AscApiClientPostApi = {
|
|
226
262
|
[Path in keyof typeof PostApi]: {
|
|
227
263
|
request: z.input<(typeof PostApi)[Path]['request']>;
|
|
@@ -234,6 +270,13 @@ export type AscApiClientPatchApi = {
|
|
|
234
270
|
response: z.output<(typeof PatchApi)[Path]['response']>;
|
|
235
271
|
};
|
|
236
272
|
};
|
|
273
|
+
export declare class AscApiRequestError extends Error {
|
|
274
|
+
readonly status: number;
|
|
275
|
+
readonly responseJson: z.output<typeof AscErrorResponseSchema>;
|
|
276
|
+
constructor(message: string, status: number, responseJson: z.output<typeof AscErrorResponseSchema>, options?: {
|
|
277
|
+
cause?: unknown;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
237
280
|
export declare class AscApiClient {
|
|
238
281
|
private readonly baseUrl;
|
|
239
282
|
private readonly token;
|
|
@@ -3,11 +3,42 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AscApiClient = void 0;
|
|
6
|
+
exports.AscApiClient = exports.AscApiRequestError = void 0;
|
|
7
7
|
const results_1 = require("@expo/results");
|
|
8
8
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
9
9
|
const zod_1 = require("zod");
|
|
10
|
+
const AscErrorResponseSchema = zod_1.z.object({
|
|
11
|
+
errors: zod_1.z
|
|
12
|
+
.array(zod_1.z.object({
|
|
13
|
+
id: zod_1.z.string().optional(),
|
|
14
|
+
status: zod_1.z.string().optional(),
|
|
15
|
+
code: zod_1.z.string().optional(),
|
|
16
|
+
title: zod_1.z.string().optional(),
|
|
17
|
+
detail: zod_1.z.string().optional(),
|
|
18
|
+
source: zod_1.z.unknown().optional(),
|
|
19
|
+
}))
|
|
20
|
+
.min(1),
|
|
21
|
+
});
|
|
10
22
|
const GetApi = {
|
|
23
|
+
'/v1/apps': {
|
|
24
|
+
path: zod_1.z.object({}),
|
|
25
|
+
request: zod_1.z.object({
|
|
26
|
+
'fields[apps]': zod_1.z.array(zod_1.z.enum(['bundleId', 'name'])).refine(opts => {
|
|
27
|
+
return opts.includes('bundleId') && opts.includes('name');
|
|
28
|
+
}),
|
|
29
|
+
limit: zod_1.z.number().int().min(1).max(200).optional(),
|
|
30
|
+
}),
|
|
31
|
+
response: zod_1.z.object({
|
|
32
|
+
data: zod_1.z.array(zod_1.z.object({
|
|
33
|
+
type: zod_1.z.literal('apps'),
|
|
34
|
+
id: zod_1.z.string(),
|
|
35
|
+
attributes: zod_1.z.object({
|
|
36
|
+
bundleId: zod_1.z.string(),
|
|
37
|
+
name: zod_1.z.string(),
|
|
38
|
+
}),
|
|
39
|
+
})),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
11
42
|
'/v1/apps/:id': {
|
|
12
43
|
path: zod_1.z.object({
|
|
13
44
|
id: zod_1.z.string(),
|
|
@@ -212,6 +243,16 @@ const PatchApi = {
|
|
|
212
243
|
}),
|
|
213
244
|
},
|
|
214
245
|
};
|
|
246
|
+
class AscApiRequestError extends Error {
|
|
247
|
+
status;
|
|
248
|
+
responseJson;
|
|
249
|
+
constructor(message, status, responseJson, options) {
|
|
250
|
+
super(message, { cause: options?.cause });
|
|
251
|
+
this.status = status;
|
|
252
|
+
this.responseJson = responseJson;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
exports.AscApiRequestError = AscApiRequestError;
|
|
215
256
|
class AscApiClient {
|
|
216
257
|
baseUrl = 'https://api.appstoreconnect.apple.com';
|
|
217
258
|
token;
|
|
@@ -283,11 +324,20 @@ class AscApiClient {
|
|
|
283
324
|
});
|
|
284
325
|
if (!response.ok) {
|
|
285
326
|
const text = await response.text();
|
|
327
|
+
const parsedAscErrorResponse = await (0, results_1.asyncResult)((async () => AscErrorResponseSchema.parse(JSON.parse(text)))());
|
|
328
|
+
if (parsedAscErrorResponse.ok) {
|
|
329
|
+
throw new AscApiRequestError(`Unexpected response (${response.status}) from App Store Connect: ${text}`, response.status, parsedAscErrorResponse.value, { cause: response });
|
|
330
|
+
}
|
|
286
331
|
throw new Error(`Unexpected response (${response.status}) from App Store Connect: ${text}`, {
|
|
287
332
|
cause: response,
|
|
288
333
|
});
|
|
289
334
|
}
|
|
290
|
-
const
|
|
335
|
+
const text = await response.text();
|
|
336
|
+
const parsedJson = await (0, results_1.asyncResult)((async () => JSON.parse(text))());
|
|
337
|
+
if (!parsedJson.ok) {
|
|
338
|
+
throw new Error(`Malformed JSON response from App Store Connect (${response.status}): ${text}`);
|
|
339
|
+
}
|
|
340
|
+
const json = parsedJson.value;
|
|
291
341
|
this.logger?.debug(`Response from App Store Connect: ${JSON.stringify(json, null, 2)}`);
|
|
292
342
|
const parsedResponse = await (0, results_1.asyncResult)((async () => responseSchema.parse(json))());
|
|
293
343
|
if (!parsedResponse.ok) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AscApiClient, AscApiClientGetApi, AscApiClientPostApi } from './AscApiClient';
|
|
2
|
+
export declare namespace AscApiUtils {
|
|
3
|
+
function getAppInfoAsync({ client, appleAppIdentifier, }: {
|
|
4
|
+
client: Pick<AscApiClient, 'getAsync'>;
|
|
5
|
+
appleAppIdentifier: string;
|
|
6
|
+
}): Promise<AscApiClientGetApi['/v1/apps/:id']['response']>;
|
|
7
|
+
function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, }: {
|
|
8
|
+
client: Pick<AscApiClient, 'postAsync'>;
|
|
9
|
+
appleAppIdentifier: string;
|
|
10
|
+
bundleShortVersion: string;
|
|
11
|
+
bundleVersion: string;
|
|
12
|
+
}): Promise<AscApiClientPostApi['/v1/buildUploads']['response']>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AscApiUtils = void 0;
|
|
4
|
+
const errors_1 = require("@expo/eas-build-job/dist/errors");
|
|
5
|
+
const AscApiClient_1 = require("./AscApiClient");
|
|
6
|
+
var AscApiUtils;
|
|
7
|
+
(function (AscApiUtils) {
|
|
8
|
+
async function getAppInfoAsync({ client, appleAppIdentifier, }) {
|
|
9
|
+
try {
|
|
10
|
+
return await client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appleAppIdentifier });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
const notFoundErrors = error instanceof AscApiClient_1.AscApiRequestError && error.status === 404
|
|
14
|
+
? error.responseJson.errors
|
|
15
|
+
: [];
|
|
16
|
+
const isAppNotFoundError = notFoundErrors.length > 0 && notFoundErrors.every(item => item.code === 'NOT_FOUND');
|
|
17
|
+
if (!isAppNotFoundError) {
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
let visibleAppsSummary = null;
|
|
21
|
+
try {
|
|
22
|
+
visibleAppsSummary = await getVisibleAppsSummaryAsync(client);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Don't hide the original NOT_FOUND error with a secondary lookup failure.
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_APP_NOT_FOUND', `App Store Connect app for application identifier ${appleAppIdentifier} was not found. ` +
|
|
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
|
+
(visibleAppsSummary
|
|
31
|
+
? `\n\nExample applications visible to this API key:\n${visibleAppsSummary}`
|
|
32
|
+
: ''), {
|
|
33
|
+
cause: error,
|
|
34
|
+
docsUrl: 'https://expo.fyi/asc-app-id',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
AscApiUtils.getAppInfoAsync = getAppInfoAsync;
|
|
39
|
+
async function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, }) {
|
|
40
|
+
try {
|
|
41
|
+
return await client.postAsync('/v1/buildUploads', {
|
|
42
|
+
data: {
|
|
43
|
+
type: 'buildUploads',
|
|
44
|
+
attributes: {
|
|
45
|
+
platform: 'IOS',
|
|
46
|
+
cfBundleShortVersionString: bundleShortVersion,
|
|
47
|
+
cfBundleVersion: bundleVersion,
|
|
48
|
+
},
|
|
49
|
+
relationships: {
|
|
50
|
+
app: {
|
|
51
|
+
data: {
|
|
52
|
+
type: 'apps',
|
|
53
|
+
id: appleAppIdentifier,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const errors = error instanceof AscApiClient_1.AscApiRequestError && error.status === 409
|
|
62
|
+
? error.responseJson.errors
|
|
63
|
+
: [];
|
|
64
|
+
const isDuplicateVersionError = errors.length > 0 &&
|
|
65
|
+
errors.every(item => item.code === 'ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE');
|
|
66
|
+
if (isDuplicateVersionError) {
|
|
67
|
+
throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_VERSION_DUPLICATE', `Increment Build Number: Build number ${bundleVersion} for app version ${bundleShortVersion} has already been used. ` +
|
|
68
|
+
'App Store Connect requires unique build numbers within each app version (version train). ' +
|
|
69
|
+
'Increment it by setting ios.buildNumber in app.json, or set "autoIncrement": true in eas.json (recommended). Then rebuild and resubmit.', {
|
|
70
|
+
cause: error,
|
|
71
|
+
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
AscApiUtils.createBuildUploadAsync = createBuildUploadAsync;
|
|
78
|
+
})(AscApiUtils || (exports.AscApiUtils = AscApiUtils = {}));
|
|
79
|
+
async function getVisibleAppsSummaryAsync(client) {
|
|
80
|
+
const appsResponse = await client.getAsync('/v1/apps', {
|
|
81
|
+
'fields[apps]': ['bundleId', 'name'],
|
|
82
|
+
limit: 10,
|
|
83
|
+
});
|
|
84
|
+
if (appsResponse.data.length === 0) {
|
|
85
|
+
return ' (none)';
|
|
86
|
+
}
|
|
87
|
+
return appsResponse.data
|
|
88
|
+
.map(app => `- ${app.attributes.name} (${app.attributes.bundleId}) (ID: ${app.id})`)
|
|
89
|
+
.join('\n');
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "18.0
|
|
3
|
+
"version": "18.1.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@urql/core": "^6.0.1",
|
|
54
54
|
"bplist-parser": "0.3.2",
|
|
55
55
|
"fast-glob": "^3.3.2",
|
|
56
|
+
"fast-xml-parser": "^4.4.1",
|
|
56
57
|
"fs-extra": "^11.2.0",
|
|
57
58
|
"gql.tada": "^1.8.13",
|
|
58
59
|
"joi": "^17.13.1",
|
|
@@ -96,5 +97,5 @@
|
|
|
96
97
|
"typescript": "^5.5.4",
|
|
97
98
|
"uuid": "^9.0.1"
|
|
98
99
|
},
|
|
99
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "61b601de883b5c65d87163d8477b8a9250bc2de9"
|
|
100
101
|
}
|