@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.
@@ -1,2 +1,5 @@
1
1
  import { BuildFunction } from '@expo/steps';
2
2
  export declare function createUploadToAscBuildFunction(): BuildFunction;
3
+ export declare function isClosedVersionTrainError(messages: {
4
+ code: string;
5
+ }[]): boolean;
@@ -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 client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appleAppIdentifier });
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 client.postAsync('/v1/buildUploads', {
135
- data: {
136
- type: 'buildUploads',
137
- attributes: {
138
- platform: 'IOS',
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
- stepsCtx.logger.info(`Waiting for build upload to complete... (status = ${state.state})`);
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 json = await response.json();
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.6",
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": "df8ebc8f84809e52032661f9f08768650440e5c0"
100
+ "gitHead": "61b601de883b5c65d87163d8477b8a9250bc2de9"
101
101
  }