@expo/build-tools 18.0.6 → 18.2.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/buildErrors/buildErrorHandlers.d.ts +2 -1
- package/dist/buildErrors/buildErrorHandlers.js +6 -2
- package/dist/buildErrors/detectError.js +8 -14
- package/dist/common/installDependencies.js +8 -2
- package/dist/context.js +1 -1
- package/dist/steps/functions/internalMaestroTest.js +62 -17
- package/dist/steps/functions/restoreCache.js +0 -4
- package/dist/steps/functions/startAndroidEmulator.js +121 -32
- package/dist/steps/functions/uploadToAsc.d.ts +12 -0
- package/dist/steps/functions/uploadToAsc.js +76 -21
- 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/dist/utils/AndroidEmulatorUtils.d.ts +9 -2
- package/dist/utils/AndroidEmulatorUtils.js +40 -11
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@ import { ErrorHandler } from './errors.types';
|
|
|
2
2
|
export declare class TrackedBuildError extends Error {
|
|
3
3
|
errorCode: string;
|
|
4
4
|
message: string;
|
|
5
|
-
|
|
5
|
+
metadata?: Record<string, unknown> | undefined;
|
|
6
|
+
constructor(errorCode: string, message: string, metadata?: Record<string, unknown> | undefined);
|
|
6
7
|
}
|
|
7
8
|
export declare const buildErrorHandlers: ErrorHandler<TrackedBuildError>[];
|
|
@@ -9,10 +9,12 @@ const escapeRegExp_1 = __importDefault(require("lodash/escapeRegExp"));
|
|
|
9
9
|
class TrackedBuildError extends Error {
|
|
10
10
|
errorCode;
|
|
11
11
|
message;
|
|
12
|
-
|
|
12
|
+
metadata;
|
|
13
|
+
constructor(errorCode, message, metadata) {
|
|
13
14
|
super(message);
|
|
14
15
|
this.errorCode = errorCode;
|
|
15
16
|
this.message = message;
|
|
17
|
+
this.metadata = metadata;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
exports.TrackedBuildError = TrackedBuildError;
|
|
@@ -130,7 +132,9 @@ exports.buildErrorHandlers = [
|
|
|
130
132
|
// example log:
|
|
131
133
|
// [stderr] WARN tarball tarball data for @typescript-eslint/typescript-estree@5.26.0 (sha512-cozo/GbwixVR0sgfHItz3t1yXu521yn71Wj6PlYCFA3WPhy51CUPkifFKfBis91bDclGmAY45hhaAXVjdn4new==) seems to be corrupted. Trying again.
|
|
132
134
|
regexp: /tarball tarball data for ([^ ]*) .* seems to be corrupted. Trying again/,
|
|
133
|
-
createError: (match) => new TrackedBuildError('NPM_CORRUPTED_PACKAGE', `npm: corrupted package ${match[1]}
|
|
135
|
+
createError: (match) => new TrackedBuildError('NPM_CORRUPTED_PACKAGE', `npm: corrupted package ${match[1]}`, {
|
|
136
|
+
packageName: match[1],
|
|
137
|
+
}),
|
|
134
138
|
})),
|
|
135
139
|
...[eas_build_job_1.BuildPhase.INSTALL_DEPENDENCIES, eas_build_job_1.BuildPhase.PREBUILD].map(phase => ({
|
|
136
140
|
phase,
|
|
@@ -58,21 +58,15 @@ async function resolveBuildPhaseErrorAsync(error, logLines, errorContext, buildL
|
|
|
58
58
|
: (resolveError(userErrorHandlers_1.userErrorHandlers, logLines, errorContext, xcodeBuildLogs) ??
|
|
59
59
|
new eas_build_job_1.errors.UnknownError(errorContext.phase));
|
|
60
60
|
const buildError = resolveError(buildErrorHandlers_1.buildErrorHandlers, logLines, errorContext, xcodeBuildLogs);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const message = (isUnknownUserError ? buildError?.message : userFacingError.message) ?? userFacingError.message;
|
|
68
|
-
const errorCode = (isUnknownUserError ? buildError?.errorCode : userFacingError.errorCode) ??
|
|
69
|
-
userFacingError.errorCode;
|
|
70
|
-
return new eas_build_job_1.errors.BuildError(message, {
|
|
71
|
-
errorCode,
|
|
72
|
-
userFacingErrorCode: userFacingError.errorCode,
|
|
73
|
-
userFacingMessage: userFacingError.message,
|
|
61
|
+
const trackingCode = buildError && buildError.errorCode !== userFacingError.errorCode
|
|
62
|
+
? buildError.errorCode
|
|
63
|
+
: undefined;
|
|
64
|
+
return new eas_build_job_1.errors.BuildError(userFacingError.message, {
|
|
65
|
+
errorCode: userFacingError.errorCode,
|
|
66
|
+
trackingCode,
|
|
74
67
|
docsUrl: userFacingError.docsUrl,
|
|
75
|
-
|
|
68
|
+
cause: error,
|
|
76
69
|
buildPhase: phase,
|
|
70
|
+
metadata: buildError?.metadata,
|
|
77
71
|
});
|
|
78
72
|
}
|
|
@@ -13,7 +13,8 @@ async function installDependenciesAsync({ packageManager, env, logger, infoCallb
|
|
|
13
13
|
let args;
|
|
14
14
|
switch (packageManager) {
|
|
15
15
|
case packageManager_1.PackageManager.NPM: {
|
|
16
|
-
args = useFrozenLockfile ? ['ci'
|
|
16
|
+
args = useFrozenLockfile ? ['ci'] : ['install'];
|
|
17
|
+
args.push('--include=dev');
|
|
17
18
|
break;
|
|
18
19
|
}
|
|
19
20
|
case packageManager_1.PackageManager.PNPM: {
|
|
@@ -35,7 +36,12 @@ async function installDependenciesAsync({ packageManager, env, logger, infoCallb
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
else {
|
|
38
|
-
args = [
|
|
39
|
+
args = [
|
|
40
|
+
'install',
|
|
41
|
+
...(useFrozenLockfile ? ['--frozen-lockfile'] : []),
|
|
42
|
+
'--production',
|
|
43
|
+
'false',
|
|
44
|
+
];
|
|
39
45
|
}
|
|
40
46
|
break;
|
|
41
47
|
}
|
package/dist/context.js
CHANGED
|
@@ -18,6 +18,7 @@ const zod_1 = require("zod");
|
|
|
18
18
|
const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
|
|
19
19
|
const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
|
|
20
20
|
const findMaestroPathsFlowsToExecuteAsync_1 = require("../../utils/findMaestroPathsFlowsToExecuteAsync");
|
|
21
|
+
const retry_1 = require("../../utils/retry");
|
|
21
22
|
const strings_1 = require("../../utils/strings");
|
|
22
23
|
function createInternalEasMaestroTestFunction(ctx) {
|
|
23
24
|
return new steps_1.BuildFunction({
|
|
@@ -344,9 +345,11 @@ const MaestroOutputFormatToExtensionMap = {
|
|
|
344
345
|
junit: 'xml',
|
|
345
346
|
html: 'html',
|
|
346
347
|
};
|
|
348
|
+
const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
|
|
349
|
+
const ANDROID_STARTUP_RETRIES_COUNT = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length - 1;
|
|
347
350
|
async function withCleanDeviceAsync({ platform, sourceDeviceIdentifier, localDeviceName, env, logger, fn, }) {
|
|
348
351
|
// Clone and start the device
|
|
349
|
-
let localDeviceIdentifier;
|
|
352
|
+
let localDeviceIdentifier = null;
|
|
350
353
|
switch (platform) {
|
|
351
354
|
case 'ios': {
|
|
352
355
|
logger.info(`Cloning iOS Simulator ${sourceDeviceIdentifier} to ${localDeviceName}...`);
|
|
@@ -369,27 +372,69 @@ async function withCleanDeviceAsync({ platform, sourceDeviceIdentifier, localDev
|
|
|
369
372
|
break;
|
|
370
373
|
}
|
|
371
374
|
case 'android': {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
await (0, retry_1.retryAsync)(async (attemptCount) => {
|
|
376
|
+
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
|
377
|
+
const attempt = attemptCount + 1;
|
|
378
|
+
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
|
379
|
+
let serialId = null;
|
|
380
|
+
try {
|
|
381
|
+
logger.info(`Cloning Android Emulator ${sourceDeviceIdentifier} to ${localDeviceName} (attempt ${attempt}/${maxAttempts})...`);
|
|
382
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.cloneAsync({
|
|
383
|
+
sourceDeviceName: sourceDeviceIdentifier,
|
|
384
|
+
destinationDeviceName: localDeviceName,
|
|
385
|
+
env,
|
|
386
|
+
logger,
|
|
387
|
+
});
|
|
388
|
+
logger.info(`Starting Android Emulator ${localDeviceName} (attempt ${attempt}/${maxAttempts})...`);
|
|
389
|
+
const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
390
|
+
deviceName: localDeviceName,
|
|
391
|
+
env,
|
|
392
|
+
});
|
|
393
|
+
serialId = startResult.serialId;
|
|
394
|
+
logger.info(`Waiting for Android Emulator ${localDeviceName} to be ready...`);
|
|
395
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
396
|
+
serialId,
|
|
397
|
+
env,
|
|
398
|
+
timeoutMs,
|
|
399
|
+
logger,
|
|
400
|
+
});
|
|
401
|
+
localDeviceIdentifier = serialId;
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
logger.warn({ err }, `Failed to start Android Emulator ${localDeviceName} on attempt ${attempt}/${maxAttempts}.`);
|
|
405
|
+
try {
|
|
406
|
+
if (serialId) {
|
|
407
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
|
|
408
|
+
serialId,
|
|
409
|
+
deviceName: localDeviceName,
|
|
410
|
+
env,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
|
|
415
|
+
deviceName: localDeviceName,
|
|
416
|
+
env,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (cleanupErr) {
|
|
421
|
+
logger.warn({ err: cleanupErr }, `Failed to clean up Android Emulator ${localDeviceName}.`);
|
|
422
|
+
}
|
|
423
|
+
throw err;
|
|
424
|
+
}
|
|
425
|
+
}, {
|
|
377
426
|
logger,
|
|
427
|
+
retryOptions: {
|
|
428
|
+
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
|
429
|
+
retryIntervalMs: 1_000,
|
|
430
|
+
},
|
|
378
431
|
});
|
|
379
|
-
logger.info(`Starting Android Emulator ${localDeviceName}...`);
|
|
380
|
-
const { serialId } = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
381
|
-
deviceName: localDeviceName,
|
|
382
|
-
env,
|
|
383
|
-
});
|
|
384
|
-
logger.info(`Waiting for Android Emulator ${localDeviceName} to be ready...`);
|
|
385
|
-
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
386
|
-
serialId,
|
|
387
|
-
env,
|
|
388
|
-
});
|
|
389
|
-
localDeviceIdentifier = serialId;
|
|
390
432
|
break;
|
|
391
433
|
}
|
|
392
434
|
}
|
|
435
|
+
if (!localDeviceIdentifier) {
|
|
436
|
+
throw new Error('Device did not return an identifier after startup.');
|
|
437
|
+
}
|
|
393
438
|
// Run the function
|
|
394
439
|
const fnResult = await (0, results_1.asyncResult)(fn({ deviceIdentifier: localDeviceIdentifier }));
|
|
395
440
|
// Stop the device
|
|
@@ -89,10 +89,6 @@ function createRestoreCacheFunction() {
|
|
|
89
89
|
fn: async (stepsCtx, { env, inputs, outputs }) => {
|
|
90
90
|
const { logger } = stepsCtx;
|
|
91
91
|
try {
|
|
92
|
-
if (stepsCtx.global.staticContext.job.platform) {
|
|
93
|
-
logger.error('Caches are not supported in build jobs yet.');
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
92
|
const paths = zod_1.default
|
|
97
93
|
.array(zod_1.default.string())
|
|
98
94
|
.parse((inputs.path.value ?? '').split(/[\r\n]+/))
|
|
@@ -9,6 +9,8 @@ const steps_1 = require("@expo/steps");
|
|
|
9
9
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
10
10
|
const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
|
|
11
11
|
const retry_1 = require("../../utils/retry");
|
|
12
|
+
const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
|
|
13
|
+
const ANDROID_STARTUP_RETRIES_COUNT = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length - 1;
|
|
12
14
|
function createStartAndroidEmulatorBuildFunction() {
|
|
13
15
|
return new steps_1.BuildFunction({
|
|
14
16
|
namespace: 'eas',
|
|
@@ -68,24 +70,71 @@ function createStartAndroidEmulatorBuildFunction() {
|
|
|
68
70
|
retryIntervalMs: 1_000,
|
|
69
71
|
},
|
|
70
72
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
let emulatorPromise = null;
|
|
74
|
+
let serialId = null;
|
|
75
|
+
await (0, retry_1.retryAsync)(async (attemptCount) => {
|
|
76
|
+
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
|
77
|
+
const attempt = attemptCount + 1;
|
|
78
|
+
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
|
79
|
+
const attemptSuffix = attempt > 1 ? ` (attempt ${attempt}/${maxAttempts})` : '';
|
|
80
|
+
let attemptSerialId = null;
|
|
81
|
+
try {
|
|
82
|
+
logger.info(`Creating emulator device${attemptSuffix}.`);
|
|
83
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.createAsync({
|
|
84
|
+
deviceName,
|
|
85
|
+
systemImagePackage,
|
|
86
|
+
deviceIdentifier: deviceIdentifier ?? null,
|
|
87
|
+
env,
|
|
88
|
+
logger,
|
|
89
|
+
});
|
|
90
|
+
logger.info(`Starting emulator device${attemptSuffix}.`);
|
|
91
|
+
const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
92
|
+
deviceName,
|
|
93
|
+
env,
|
|
94
|
+
});
|
|
95
|
+
attemptSerialId = startResult.serialId;
|
|
96
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
97
|
+
env,
|
|
98
|
+
serialId: attemptSerialId,
|
|
99
|
+
timeoutMs,
|
|
100
|
+
logger,
|
|
101
|
+
});
|
|
102
|
+
logger.info(`${deviceName} is ready.`);
|
|
103
|
+
serialId = attemptSerialId;
|
|
104
|
+
emulatorPromise = startResult.emulatorPromise;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
logger.warn({ err }, `${deviceName} failed to start on attempt ${attempt}/${maxAttempts}.`);
|
|
108
|
+
try {
|
|
109
|
+
if (attemptSerialId) {
|
|
110
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
|
|
111
|
+
serialId: attemptSerialId,
|
|
112
|
+
deviceName,
|
|
113
|
+
env,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
|
|
118
|
+
deviceName,
|
|
119
|
+
env,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (cleanupErr) {
|
|
124
|
+
logger.warn({ err: cleanupErr }, `Failed to clean up ${deviceName}.`);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}, {
|
|
77
129
|
logger,
|
|
130
|
+
retryOptions: {
|
|
131
|
+
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
|
132
|
+
retryIntervalMs: 1_000,
|
|
133
|
+
},
|
|
78
134
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
env,
|
|
83
|
-
});
|
|
84
|
-
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
85
|
-
env,
|
|
86
|
-
serialId,
|
|
87
|
-
});
|
|
88
|
-
logger.info(`${deviceName} is ready.`);
|
|
135
|
+
if (!serialId || !emulatorPromise) {
|
|
136
|
+
throw new Error(`Failed to start emulator ${deviceName}.`);
|
|
137
|
+
}
|
|
89
138
|
const count = Number(inputs.count.value ?? 1);
|
|
90
139
|
if (count > 1) {
|
|
91
140
|
logger.info(`Requested ${count} emulators, shutting down ${deviceName} for cloning.`);
|
|
@@ -98,24 +147,64 @@ function createStartAndroidEmulatorBuildFunction() {
|
|
|
98
147
|
await (0, results_1.asyncResult)(emulatorPromise);
|
|
99
148
|
for (let i = 0; i < count; i++) {
|
|
100
149
|
const cloneIdentifier = `eas-simulator-${i + 1}`;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
await (0, retry_1.retryAsync)(async (attemptCount) => {
|
|
151
|
+
const timeoutMs = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS[attemptCount];
|
|
152
|
+
const attempt = attemptCount + 1;
|
|
153
|
+
const maxAttempts = ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS.length;
|
|
154
|
+
const attemptSuffix = attempt > 1 ? ` (attempt ${attempt}/${maxAttempts})` : '';
|
|
155
|
+
let cloneSerialId = null;
|
|
156
|
+
try {
|
|
157
|
+
logger.info(`Cloning ${deviceName} to ${cloneIdentifier}${attemptSuffix}.`);
|
|
158
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.cloneAsync({
|
|
159
|
+
sourceDeviceName: deviceName,
|
|
160
|
+
destinationDeviceName: cloneIdentifier,
|
|
161
|
+
env,
|
|
162
|
+
logger,
|
|
163
|
+
});
|
|
164
|
+
logger.info(`Starting emulator device${attemptSuffix}.`);
|
|
165
|
+
const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
166
|
+
deviceName: cloneIdentifier,
|
|
167
|
+
env,
|
|
168
|
+
});
|
|
169
|
+
cloneSerialId = startResult.serialId;
|
|
170
|
+
logger.info('Waiting for emulator to become ready');
|
|
171
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
172
|
+
serialId: cloneSerialId,
|
|
173
|
+
env,
|
|
174
|
+
timeoutMs,
|
|
175
|
+
logger,
|
|
176
|
+
});
|
|
177
|
+
logger.info(`${cloneIdentifier} is ready.`);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger.warn({ err }, `${cloneIdentifier} failed to start on attempt ${attempt}/${maxAttempts}.`);
|
|
181
|
+
try {
|
|
182
|
+
if (cloneSerialId) {
|
|
183
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
|
|
184
|
+
serialId: cloneSerialId,
|
|
185
|
+
deviceName: cloneIdentifier,
|
|
186
|
+
env,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.deleteAsync({
|
|
191
|
+
deviceName: cloneIdentifier,
|
|
192
|
+
env,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (cleanupErr) {
|
|
197
|
+
logger.warn({ err: cleanupErr }, `Failed to clean up ${cloneIdentifier}.`);
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
}, {
|
|
106
202
|
logger,
|
|
203
|
+
retryOptions: {
|
|
204
|
+
retries: ANDROID_STARTUP_RETRIES_COUNT,
|
|
205
|
+
retryIntervalMs: 1_000,
|
|
206
|
+
},
|
|
107
207
|
});
|
|
108
|
-
logger.info('Starting emulator device');
|
|
109
|
-
const { serialId } = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
110
|
-
deviceName: cloneIdentifier,
|
|
111
|
-
env,
|
|
112
|
-
});
|
|
113
|
-
logger.info('Waiting for emulator to become ready');
|
|
114
|
-
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
115
|
-
serialId,
|
|
116
|
-
env,
|
|
117
|
-
});
|
|
118
|
-
logger.info(`${cloneIdentifier} is ready.`);
|
|
119
208
|
}
|
|
120
209
|
}
|
|
121
210
|
},
|
|
@@ -1,2 +1,14 @@
|
|
|
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;
|
|
6
|
+
export declare function isInvalidBundleIdentifierError(messages: {
|
|
7
|
+
code: string;
|
|
8
|
+
}[]): boolean;
|
|
9
|
+
export declare function isMissingPurposeStringError(messages: {
|
|
10
|
+
code: string;
|
|
11
|
+
}[]): boolean;
|
|
12
|
+
export declare function parseMissingUsageDescriptionKeys(messages: {
|
|
13
|
+
description: string;
|
|
14
|
+
}[]): string[];
|
|
@@ -37,6 +37,12 @@ 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
|
+
exports.isInvalidBundleIdentifierError = isInvalidBundleIdentifierError;
|
|
42
|
+
exports.isMissingPurposeStringError = isMissingPurposeStringError;
|
|
43
|
+
exports.parseMissingUsageDescriptionKeys = parseMissingUsageDescriptionKeys;
|
|
44
|
+
const errors_1 = require("@expo/eas-build-job/dist/errors");
|
|
45
|
+
const results_1 = require("@expo/results");
|
|
40
46
|
const steps_1 = require("@expo/steps");
|
|
41
47
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
42
48
|
const jose = __importStar(require("jose"));
|
|
@@ -45,6 +51,8 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
45
51
|
const promises_1 = require("node:timers/promises");
|
|
46
52
|
const zod_1 = require("zod");
|
|
47
53
|
const AscApiClient_1 = require("../utils/ios/AscApiClient");
|
|
54
|
+
const AscApiUtils_1 = require("../utils/ios/AscApiUtils");
|
|
55
|
+
const readIpaInfo_1 = require("./readIpaInfo");
|
|
48
56
|
function createUploadToAscBuildFunction() {
|
|
49
57
|
return new steps_1.BuildFunction({
|
|
50
58
|
namespace: 'eas',
|
|
@@ -127,27 +135,16 @@ function createUploadToAscBuildFunction() {
|
|
|
127
135
|
.setExpirationTime('20m')
|
|
128
136
|
.sign(privateKey);
|
|
129
137
|
const client = new AscApiClient_1.AscApiClient({ token, logger: stepsCtx.logger });
|
|
130
|
-
stepsCtx.logger.info(
|
|
131
|
-
const appResponse = await
|
|
132
|
-
|
|
138
|
+
stepsCtx.logger.info(`Reading App information for Apple app identifier: ${appleAppIdentifier}...`);
|
|
139
|
+
const appResponse = await AscApiUtils_1.AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
|
|
140
|
+
const ascAppBundleIdentifier = appResponse.data.attributes.bundleId;
|
|
141
|
+
stepsCtx.logger.info(`Uploading Build to "${appResponse.data.attributes.name}" (${ascAppBundleIdentifier})...`);
|
|
133
142
|
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
|
-
},
|
|
143
|
+
const buildUploadResponse = await AscApiUtils_1.AscApiUtils.createBuildUploadAsync({
|
|
144
|
+
client,
|
|
145
|
+
appleAppIdentifier,
|
|
146
|
+
bundleShortVersion,
|
|
147
|
+
bundleVersion,
|
|
151
148
|
});
|
|
152
149
|
const buildUploadId = buildUploadResponse.data.id;
|
|
153
150
|
const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/ios/${buildUploadId}`;
|
|
@@ -223,10 +220,18 @@ function createUploadToAscBuildFunction() {
|
|
|
223
220
|
}
|
|
224
221
|
stepsCtx.logger.info('Checking build upload status...');
|
|
225
222
|
const waitingForBuildStartedAt = Date.now();
|
|
223
|
+
const waitingLogIntervalMs = 10 * 1000;
|
|
224
|
+
let lastWaitLogTime = 0;
|
|
225
|
+
let lastWaitLogState = null;
|
|
226
226
|
while (Date.now() - waitingForBuildStartedAt < 30 * 60 * 1000 /* 30 minutes */) {
|
|
227
227
|
const { data: { attributes: { state }, }, } = await client.getAsync(`/v1/buildUploads/:id`, { 'fields[buildUploads]': ['state', 'build'], include: ['build'] }, { id: buildUploadId });
|
|
228
228
|
if (state.state === 'AWAITING_UPLOAD' || state.state === 'PROCESSING') {
|
|
229
|
-
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
if (lastWaitLogState !== state.state || now - lastWaitLogTime >= waitingLogIntervalMs) {
|
|
231
|
+
stepsCtx.logger.info(`Waiting for build upload to complete... (status = ${state.state})`);
|
|
232
|
+
lastWaitLogTime = now;
|
|
233
|
+
lastWaitLogState = state.state;
|
|
234
|
+
}
|
|
230
235
|
await (0, promises_1.setTimeout)(2000);
|
|
231
236
|
continue;
|
|
232
237
|
}
|
|
@@ -242,6 +247,35 @@ function createUploadToAscBuildFunction() {
|
|
|
242
247
|
stepsCtx.logger.error(`Errors:\n${itemizeMessages(errors)}\n`);
|
|
243
248
|
}
|
|
244
249
|
if (state.state === 'FAILED') {
|
|
250
|
+
if (isInvalidBundleIdentifierError(errors)) {
|
|
251
|
+
const ipaInfoResult = await (0, results_1.asyncResult)((0, readIpaInfo_1.readIpaInfoAsync)(ipaPath));
|
|
252
|
+
const ipaBundleIdentifier = ipaInfoResult.ok
|
|
253
|
+
? ipaInfoResult.value.bundleIdentifier
|
|
254
|
+
: null;
|
|
255
|
+
throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID', `Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` +
|
|
256
|
+
`IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` +
|
|
257
|
+
`App Store Connect app bundle identifier: ${ascAppBundleIdentifier}\n\n` +
|
|
258
|
+
'Bundle identifier cannot be changed for an existing App Store Connect app. ' +
|
|
259
|
+
'If you selected the wrong app, change the Apple app identifier in the submit profile. ' +
|
|
260
|
+
'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).');
|
|
261
|
+
}
|
|
262
|
+
if (isMissingPurposeStringError(errors)) {
|
|
263
|
+
const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors);
|
|
264
|
+
throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_MISSING_PURPOSE_STRING', `Build upload was rejected by App Store Connect because Info.plist is missing one or more privacy purpose strings.\n\n` +
|
|
265
|
+
`${missingUsageDescriptionKeys.length > 0
|
|
266
|
+
? `Missing keys reported by App Store Connect:\n- ${missingUsageDescriptionKeys.join('\n- ')}\n\n`
|
|
267
|
+
: ''}` +
|
|
268
|
+
'Add the missing keys with clear user-facing explanations, then rebuild and submit again.\n' +
|
|
269
|
+
'If you use Continuous Native Generation (CNG), update `ios.infoPlist` in app.json/app.config.js.\n' +
|
|
270
|
+
'If you do not use CNG, update your app target Info.plist directly.', {
|
|
271
|
+
docsUrl: 'https://docs.expo.dev/guides/permissions/#ios',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (isClosedVersionTrainError(errors)) {
|
|
275
|
+
throw new errors_1.UserFacingError('EAS_UPLOAD_TO_ASC_CLOSED_VERSION_TRAIN', `Build upload was rejected by App Store Connect because the ${bundleShortVersion} app version is not accepted for new build submissions. ` +
|
|
276
|
+
'This usually means the version train is closed or lower than a previously approved version. ' +
|
|
277
|
+
'Bump the iOS app version (CFBundleShortVersionString, e.g. expo.version) to a higher version, then rebuild and submit again.');
|
|
278
|
+
}
|
|
245
279
|
throw new Error(`Build upload (ID: ${buildUploadId}) failed.`);
|
|
246
280
|
}
|
|
247
281
|
else if (state.state === 'COMPLETE') {
|
|
@@ -255,6 +289,27 @@ function createUploadToAscBuildFunction() {
|
|
|
255
289
|
function itemizeMessages(messages) {
|
|
256
290
|
return `- ${messages.map(m => `${m.description} (${m.code})`).join('\n- ')}`;
|
|
257
291
|
}
|
|
292
|
+
function isClosedVersionTrainError(messages) {
|
|
293
|
+
return (messages.length > 0 &&
|
|
294
|
+
messages.every(message => ['90062', '90186', '90478'].includes(message.code)));
|
|
295
|
+
}
|
|
296
|
+
function isInvalidBundleIdentifierError(messages) {
|
|
297
|
+
return (messages.length > 0 && messages.every(message => ['90054', '90055'].includes(message.code)));
|
|
298
|
+
}
|
|
299
|
+
function isMissingPurposeStringError(messages) {
|
|
300
|
+
return messages.length > 0 && messages.every(message => message.code === '90683');
|
|
301
|
+
}
|
|
302
|
+
function parseMissingUsageDescriptionKeys(messages) {
|
|
303
|
+
const usageDescriptionKeyRegex = /\b(\w+UsageDescription)\b/g;
|
|
304
|
+
const keys = new Set();
|
|
305
|
+
for (const message of messages) {
|
|
306
|
+
const matches = message.description.matchAll(usageDescriptionKeyRegex);
|
|
307
|
+
for (const match of matches) {
|
|
308
|
+
keys.add(match[1]);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return [...keys];
|
|
312
|
+
}
|
|
258
313
|
async function uploadChunksAsync({ uploadOperations, ipaPath, logger, }) {
|
|
259
314
|
const fd = await fs_extra_1.default.open(ipaPath, 'r');
|
|
260
315
|
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
|
+
docsUrl: 'https://expo.fyi/asc-app-id',
|
|
34
|
+
cause: error,
|
|
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
|
+
docsUrl: 'https://docs.expo.dev/build-reference/app-versions/',
|
|
71
|
+
cause: error,
|
|
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
|
+
}
|
|
@@ -41,9 +41,11 @@ export declare namespace AndroidEmulatorUtils {
|
|
|
41
41
|
emulatorPromise: SpawnPromise<SpawnResult>;
|
|
42
42
|
serialId: AndroidDeviceSerialId;
|
|
43
43
|
}>;
|
|
44
|
-
function waitForReadyAsync({ serialId, env, }: {
|
|
44
|
+
function waitForReadyAsync({ serialId, env, timeoutMs, logger, }: {
|
|
45
45
|
serialId: AndroidDeviceSerialId;
|
|
46
46
|
env: NodeJS.ProcessEnv;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
logger?: bunyan;
|
|
47
49
|
}): Promise<void>;
|
|
48
50
|
function collectLogsAsync({ serialId, env, }: {
|
|
49
51
|
serialId: AndroidDeviceSerialId;
|
|
@@ -51,10 +53,15 @@ export declare namespace AndroidEmulatorUtils {
|
|
|
51
53
|
}): Promise<{
|
|
52
54
|
outputPath: string;
|
|
53
55
|
}>;
|
|
54
|
-
function
|
|
56
|
+
function stopAsync({ serialId, env, }: {
|
|
55
57
|
serialId: AndroidDeviceSerialId;
|
|
56
58
|
env: NodeJS.ProcessEnv;
|
|
57
59
|
}): Promise<void>;
|
|
60
|
+
function deleteAsync({ serialId, deviceName, env, }: {
|
|
61
|
+
serialId?: AndroidDeviceSerialId;
|
|
62
|
+
deviceName?: AndroidVirtualDeviceName;
|
|
63
|
+
env: NodeJS.ProcessEnv;
|
|
64
|
+
}): Promise<void>;
|
|
58
65
|
function startScreenRecordingAsync({ serialId, env, }: {
|
|
59
66
|
serialId: AndroidDeviceSerialId;
|
|
60
67
|
env: NodeJS.ProcessEnv;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AndroidEmulatorUtils = void 0;
|
|
7
|
+
const results_1 = require("@expo/results");
|
|
7
8
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
8
9
|
const assert_1 = __importDefault(require("assert"));
|
|
9
10
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
@@ -14,6 +15,7 @@ const promises_1 = require("node:timers/promises");
|
|
|
14
15
|
const retry_1 = require("./retry");
|
|
15
16
|
var AndroidEmulatorUtils;
|
|
16
17
|
(function (AndroidEmulatorUtils) {
|
|
18
|
+
const RETRY_INTERVAL_MS = 1_000;
|
|
17
19
|
AndroidEmulatorUtils.defaultSystemImagePackage = `system-images;android-30;default;${process.arch === 'arm64' ? 'arm64-v8a' : 'x86_64'}`;
|
|
18
20
|
async function getAvailableDevicesAsync({ env, }) {
|
|
19
21
|
const result = await (0, turtle_spawn_1.default)('avdmanager', ['list', 'device', '--compact', '--null'], { env });
|
|
@@ -253,21 +255,37 @@ var AndroidEmulatorUtils;
|
|
|
253
255
|
return { emulatorPromise, serialId };
|
|
254
256
|
}
|
|
255
257
|
AndroidEmulatorUtils.startAsync = startAsync;
|
|
256
|
-
async function waitForReadyAsync({ serialId, env, }) {
|
|
258
|
+
async function waitForReadyAsync({ serialId, env, timeoutMs = 3 * 60 * 1_000, logger, }) {
|
|
259
|
+
const retries = Math.max(0, Math.ceil(timeoutMs / RETRY_INTERVAL_MS) - 1);
|
|
257
260
|
await (0, retry_1.retryAsync)(async () => {
|
|
258
261
|
const { stdout } = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'getprop', 'sys.boot_completed'], { env });
|
|
259
262
|
if (!stdout.startsWith('1')) {
|
|
260
263
|
throw new Error(`Emulator (${serialId}) boot has not completed.`);
|
|
261
264
|
}
|
|
265
|
+
const hasNetworkConnection = await hasNetworkConnectionAsync({ serialId, env });
|
|
266
|
+
if (!hasNetworkConnection) {
|
|
267
|
+
throw new Error(`Emulator (${serialId}) network is not ready.`);
|
|
268
|
+
}
|
|
262
269
|
}, {
|
|
263
|
-
// Retry every second for 3 minutes.
|
|
264
270
|
retryOptions: {
|
|
265
|
-
retries
|
|
266
|
-
retryIntervalMs:
|
|
271
|
+
retries,
|
|
272
|
+
retryIntervalMs: RETRY_INTERVAL_MS,
|
|
267
273
|
},
|
|
274
|
+
logger,
|
|
268
275
|
});
|
|
269
276
|
}
|
|
270
277
|
AndroidEmulatorUtils.waitForReadyAsync = waitForReadyAsync;
|
|
278
|
+
async function hasNetworkConnectionAsync({ serialId, env, }) {
|
|
279
|
+
const networkReadyCheckCommand = env.ANDROID_EMULATOR_NETWORK_READY_COMMAND?.trim();
|
|
280
|
+
if (networkReadyCheckCommand) {
|
|
281
|
+
const customNetworkCheckResult = await (0, results_1.asyncResult)((0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', networkReadyCheckCommand], { env }));
|
|
282
|
+
return customNetworkCheckResult.ok;
|
|
283
|
+
}
|
|
284
|
+
const netcatResult = await (0, results_1.asyncResult)((0, turtle_spawn_1.default)('adb', ['-s', serialId, 'shell', 'nc', '-w', '1', '1.1.1.1', '443'],
|
|
285
|
+
// Close stdin to make netcat exit cleanly on Android images that don't support `-z`.
|
|
286
|
+
{ env, stdio: ['ignore', 'pipe', 'pipe'] }));
|
|
287
|
+
return netcatResult.ok;
|
|
288
|
+
}
|
|
271
289
|
async function collectLogsAsync({ serialId, env, }) {
|
|
272
290
|
const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'android-emulator-logs-'));
|
|
273
291
|
const outputPath = node_path_1.default.join(outputDir, `${serialId}.log`);
|
|
@@ -298,11 +316,7 @@ var AndroidEmulatorUtils;
|
|
|
298
316
|
return { outputPath };
|
|
299
317
|
}
|
|
300
318
|
AndroidEmulatorUtils.collectLogsAsync = collectLogsAsync;
|
|
301
|
-
async function
|
|
302
|
-
const adbEmuAvdName = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
|
303
|
-
env,
|
|
304
|
-
});
|
|
305
|
-
const deviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0];
|
|
319
|
+
async function stopAsync({ serialId, env, }) {
|
|
306
320
|
await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'kill'], { env });
|
|
307
321
|
await (0, retry_1.retryAsync)(async () => {
|
|
308
322
|
const devices = await getAttachedDevicesAsync({ env });
|
|
@@ -312,10 +326,25 @@ var AndroidEmulatorUtils;
|
|
|
312
326
|
}, {
|
|
313
327
|
retryOptions: {
|
|
314
328
|
retries: 3 * 60,
|
|
315
|
-
retryIntervalMs:
|
|
329
|
+
retryIntervalMs: RETRY_INTERVAL_MS,
|
|
316
330
|
},
|
|
317
331
|
});
|
|
318
|
-
|
|
332
|
+
}
|
|
333
|
+
AndroidEmulatorUtils.stopAsync = stopAsync;
|
|
334
|
+
async function deleteAsync({ serialId, deviceName, env, }) {
|
|
335
|
+
let resolvedDeviceName = deviceName;
|
|
336
|
+
if (!resolvedDeviceName && serialId) {
|
|
337
|
+
const adbEmuAvdName = await (0, turtle_spawn_1.default)('adb', ['-s', serialId, 'emu', 'avd', 'name'], {
|
|
338
|
+
env,
|
|
339
|
+
});
|
|
340
|
+
resolvedDeviceName = adbEmuAvdName.stdout.replace(/\r\n/g, '\n').split('\n')[0];
|
|
341
|
+
}
|
|
342
|
+
(0, assert_1.default)(resolvedDeviceName, 'Either "serialId" or "deviceName" must resolve to a device name.');
|
|
343
|
+
const serialIdToStop = serialId ?? (await getSerialIdAsync({ deviceName: resolvedDeviceName, env })) ?? undefined;
|
|
344
|
+
if (serialIdToStop) {
|
|
345
|
+
await stopAsync({ serialId: serialIdToStop, env });
|
|
346
|
+
}
|
|
347
|
+
await (0, turtle_spawn_1.default)('avdmanager', ['delete', 'avd', '-n', resolvedDeviceName], { env });
|
|
319
348
|
}
|
|
320
349
|
AndroidEmulatorUtils.deleteAsync = deleteAsync;
|
|
321
350
|
async function startScreenRecordingAsync({ serialId, env, }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "18.0
|
|
3
|
+
"version": "18.2.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"@expo/config": "10.0.6",
|
|
39
39
|
"@expo/config-plugins": "9.0.12",
|
|
40
40
|
"@expo/downloader": "18.0.1",
|
|
41
|
-
"@expo/eas-build-job": "18.0
|
|
41
|
+
"@expo/eas-build-job": "18.2.0",
|
|
42
42
|
"@expo/env": "^0.4.0",
|
|
43
43
|
"@expo/logger": "18.0.1",
|
|
44
44
|
"@expo/package-manager": "1.9.10",
|
|
45
45
|
"@expo/plist": "^0.2.0",
|
|
46
46
|
"@expo/results": "^1.0.0",
|
|
47
47
|
"@expo/spawn-async": "1.7.2",
|
|
48
|
-
"@expo/steps": "18.0
|
|
48
|
+
"@expo/steps": "18.2.0",
|
|
49
49
|
"@expo/template-file": "18.0.1",
|
|
50
50
|
"@expo/turtle-spawn": "18.0.1",
|
|
51
51
|
"@expo/xcpretty": "^4.3.1",
|
|
@@ -97,5 +97,5 @@
|
|
|
97
97
|
"typescript": "^5.5.4",
|
|
98
98
|
"uuid": "^9.0.1"
|
|
99
99
|
},
|
|
100
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "5f400d77660990422c6976a65dfb41f99374c5cc"
|
|
101
101
|
}
|