@expo/build-tools 18.0.6 → 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/steps/functions/uploadToAsc.d.ts +3 -0
- package/dist/steps/functions/uploadToAsc.js +25 -19
- package/dist/steps/utils/ios/AscApiClient.d.ts +43 -0
- 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 +2 -2
|
@@ -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;
|
|
@@ -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>",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript": "^5.5.4",
|
|
98
98
|
"uuid": "^9.0.1"
|
|
99
99
|
},
|
|
100
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "61b601de883b5c65d87163d8477b8a9250bc2de9"
|
|
101
101
|
}
|