eas-cli 18.0.4 → 18.0.6

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.
@@ -57,7 +57,7 @@ exports.EasJsonOnlyFlag = {
57
57
  };
58
58
  exports.EasUpdateEnvironmentFlag = {
59
59
  environment: core_1.Flags.string({
60
- description: 'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
60
+ description: 'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development". Required for projects using Expo SDK 55 or greater.',
61
61
  required: false,
62
62
  default: undefined,
63
63
  }),
@@ -0,0 +1,23 @@
1
+ import EasCommand from '../commandUtils/EasCommand';
2
+ export default class Go extends EasCommand {
3
+ static description: string;
4
+ static hidden: boolean;
5
+ static flags: {
6
+ 'bundle-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined>;
7
+ name: import("@oclif/core/lib/interfaces").OptionFlag<string>;
8
+ credentials: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ static contextDefinition: {
11
+ analytics: import("../commandUtils/context/AnalyticsContextField").default;
12
+ loggedIn: import("../commandUtils/context/LoggedInContextField").default;
13
+ };
14
+ runAsync(): Promise<void>;
15
+ private generateBundleId;
16
+ private createProjectFilesAsync;
17
+ private initGitRepoAsync;
18
+ private ensureEasProjectAsync;
19
+ private setupCredentialsAsync;
20
+ private runWorkflowAsync;
21
+ private monitorWorkflowJobsAsync;
22
+ private formatSpinnerText;
23
+ }
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const apple_utils_1 = require("@expo/apple-utils");
5
+ const spawn_async_1 = tslib_1.__importDefault(require("@expo/spawn-async"));
6
+ const core_1 = require("@oclif/core");
7
+ const chalk_1 = tslib_1.__importDefault(require("chalk"));
8
+ const fs = tslib_1.__importStar(require("fs-extra"));
9
+ const os = tslib_1.__importStar(require("os"));
10
+ const path = tslib_1.__importStar(require("path"));
11
+ const EasCommand_1 = tslib_1.__importDefault(require("../commandUtils/EasCommand"));
12
+ const getProjectIdAsync_1 = require("../commandUtils/context/contextUtils/getProjectIdAsync");
13
+ const context_1 = require("../credentials/context");
14
+ const AscApiKeyUtils_1 = require("../credentials/ios/actions/AscApiKeyUtils");
15
+ const BuildCredentialsUtils_1 = require("../credentials/ios/actions/BuildCredentialsUtils");
16
+ const SetUpAscApiKey_1 = require("../credentials/ios/actions/SetUpAscApiKey");
17
+ const SetUpBuildCredentials_1 = require("../credentials/ios/actions/SetUpBuildCredentials");
18
+ const SetUpPushKey_1 = require("../credentials/ios/actions/SetUpPushKey");
19
+ const ensureAppExists_1 = require("../credentials/ios/appstore/ensureAppExists");
20
+ const generated_1 = require("../graphql/generated");
21
+ const AppMutation_1 = require("../graphql/mutations/AppMutation");
22
+ const WorkflowRunMutation_1 = require("../graphql/mutations/WorkflowRunMutation");
23
+ const AppQuery_1 = require("../graphql/queries/AppQuery");
24
+ const WorkflowRunQuery_1 = require("../graphql/queries/WorkflowRunQuery");
25
+ const log_1 = tslib_1.__importStar(require("../log"));
26
+ const prompts_1 = require("../prompts");
27
+ const ora_1 = require("../ora");
28
+ const expoConfig_1 = require("../project/expoConfig");
29
+ const fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync_1 = require("../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync");
30
+ const uploadAccountScopedFileAsync_1 = require("../project/uploadAccountScopedFileAsync");
31
+ const uploadAccountScopedProjectSourceAsync_1 = require("../project/uploadAccountScopedProjectSourceAsync");
32
+ const User_1 = require("../user/User");
33
+ const promise_1 = require("../utils/promise");
34
+ const vcs_1 = require("../vcs");
35
+ // Expo Go release info - update when releasing a new version
36
+ const EXPO_GO_SDK_VERSION = '55';
37
+ const EXPO_GO_APP_VERSION = '55.0.11';
38
+ const EXPO_GO_BUILD_NUMBER = '1017799';
39
+ const TESTFLIGHT_GROUP_NAME = 'Team (Expo)';
40
+ async function setupTestFlightAsync(ascApp) {
41
+ // Create or get TestFlight group
42
+ let group;
43
+ for (let attempt = 0; attempt < 10; attempt++) {
44
+ try {
45
+ const groups = await ascApp.getBetaGroupsAsync({
46
+ query: { includes: ['betaTesters'] },
47
+ });
48
+ group = groups.find(g => g.attributes.isInternalGroup && g.attributes.name === TESTFLIGHT_GROUP_NAME);
49
+ if (!group) {
50
+ group = await ascApp.createBetaGroupAsync({
51
+ name: TESTFLIGHT_GROUP_NAME,
52
+ isInternalGroup: true,
53
+ hasAccessToAllBuilds: true,
54
+ });
55
+ }
56
+ break;
57
+ }
58
+ catch (error) {
59
+ // Apple returns this error when the app isn't ready yet
60
+ if (error?.data?.errors?.some((e) => e.code === 'ENTITY_ERROR.RELATIONSHIP.INVALID')) {
61
+ if (attempt < 9) {
62
+ await new Promise(resolve => setTimeout(resolve, 10000));
63
+ continue;
64
+ }
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ if (!group) {
70
+ throw new Error('Failed to create TestFlight group');
71
+ }
72
+ const users = await apple_utils_1.User.getAsync(ascApp.context);
73
+ const admins = users.filter(u => u.attributes.roles?.includes(apple_utils_1.UserRole.ADMIN));
74
+ const existingEmails = new Set(group.attributes.betaTesters?.map((t) => t.attributes.email?.toLowerCase()) ?? []);
75
+ const newTesters = admins
76
+ .filter(u => u.attributes.email && !existingEmails.has(u.attributes.email.toLowerCase()))
77
+ .map(u => ({
78
+ email: u.attributes.email,
79
+ firstName: u.attributes.firstName ?? '',
80
+ lastName: u.attributes.lastName ?? '',
81
+ }));
82
+ if (newTesters.length > 0) {
83
+ await group.createBulkBetaTesterAssignmentsAsync(newTesters);
84
+ }
85
+ }
86
+ /* eslint-disable no-console */
87
+ async function withSuppressedOutputAsync(fn) {
88
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
89
+ const originalConsoleLog = console.log;
90
+ const originalConsoleError = console.error;
91
+ const originalConsoleWarn = console.warn;
92
+ let capturedOutput = '';
93
+ const capture = (chunk) => {
94
+ if (typeof chunk === 'string') {
95
+ capturedOutput += chunk;
96
+ }
97
+ return true;
98
+ };
99
+ // Only suppress stdout, not stderr — ora writes spinner frames to stderr and
100
+ // patching it would freeze the spinner animation during suppressed async work.
101
+ process.stdout.write = capture;
102
+ console.log = () => { };
103
+ console.error = () => { };
104
+ console.warn = () => { };
105
+ try {
106
+ return await fn();
107
+ }
108
+ catch (error) {
109
+ process.stdout.write = originalStdoutWrite;
110
+ console.log = originalConsoleLog;
111
+ console.error = originalConsoleError;
112
+ console.warn = originalConsoleWarn;
113
+ if (capturedOutput) {
114
+ originalConsoleLog(capturedOutput);
115
+ }
116
+ throw error;
117
+ }
118
+ finally {
119
+ process.stdout.write = originalStdoutWrite;
120
+ console.log = originalConsoleLog;
121
+ console.error = originalConsoleError;
122
+ console.warn = originalConsoleWarn;
123
+ }
124
+ }
125
+ /* eslint-enable no-console */
126
+ class Go extends EasCommand_1.default {
127
+ static description = 'Create a custom Expo Go and submit to TestFlight';
128
+ static hidden = true;
129
+ static flags = {
130
+ 'bundle-id': core_1.Flags.string({
131
+ description: 'iOS bundle identifier (auto-generated if not provided)',
132
+ required: false,
133
+ }),
134
+ name: core_1.Flags.string({
135
+ description: 'App name',
136
+ default: 'My Expo Go',
137
+ }),
138
+ credentials: core_1.Flags.boolean({
139
+ description: 'Interactively select credentials (default: auto-select)',
140
+ default: false,
141
+ }),
142
+ };
143
+ static contextDefinition = {
144
+ ...this.ContextOptions.LoggedIn,
145
+ ...this.ContextOptions.Analytics,
146
+ };
147
+ async runAsync() {
148
+ log_1.default.log(chalk_1.default.bold(`Creating your personal Expo Go and deploying to TestFlight. ${(0, log_1.learnMore)('https://expo.fyi/deploy-expo-go-testflight')}`));
149
+ const { flags } = await this.parse(Go);
150
+ const spinner = (0, ora_1.ora)('Logging in to Expo...').start();
151
+ const { loggedIn: { actor, graphqlClient }, analytics, } = await this.getContextAsync(Go, {
152
+ nonInteractive: false,
153
+ });
154
+ spinner.succeed(`Logged in as ${chalk_1.default.cyan((0, User_1.getActorDisplayName)(actor))}`);
155
+ const bundleId = flags['bundle-id'] ?? this.generateBundleId(actor);
156
+ const appName = flags.name ?? 'My Expo Go';
157
+ const slug = bundleId.split('.').pop() || 'my-expo-go';
158
+ const projectDir = path.join(os.tmpdir(), `eas-go-${slug}`);
159
+ await fs.emptyDir(projectDir);
160
+ const originalCwd = process.cwd();
161
+ process.chdir(projectDir);
162
+ const setupSpinner = (0, ora_1.ora)('Creating project...').start();
163
+ // Step 1: Create project files and initialize git (silently)
164
+ try {
165
+ await withSuppressedOutputAsync(async () => {
166
+ await this.createProjectFilesAsync(projectDir, bundleId, appName);
167
+ await this.initGitRepoAsync(projectDir);
168
+ });
169
+ const vcsClient = (0, vcs_1.resolveVcsClient)();
170
+ // Step 2: Create/link EAS project (silently)
171
+ const projectId = await withSuppressedOutputAsync(() => this.ensureEasProjectAsync(graphqlClient, actor, projectDir, bundleId));
172
+ // Step 3: Set up iOS credentials and create App Store Connect app
173
+ const ascApp = await this.setupCredentialsAsync(projectDir, projectId, bundleId, appName, graphqlClient, actor, analytics, vcsClient, flags.credentials, () => {
174
+ setupSpinner.stop();
175
+ log_1.default.markFreshLine();
176
+ });
177
+ // Step 4: Start workflow and monitor progress
178
+ const { workflowUrl, workflowRunId } = await this.runWorkflowAsync(graphqlClient, projectDir, projectId, actor, vcsClient);
179
+ log_1.default.withTick(`Workflow started: ${chalk_1.default.cyan(workflowUrl)}`);
180
+ const status = await this.monitorWorkflowJobsAsync(graphqlClient, workflowRunId);
181
+ if (status === generated_1.WorkflowRunStatus.Failure) {
182
+ throw new Error('Workflow failed');
183
+ }
184
+ else if (status === generated_1.WorkflowRunStatus.Canceled) {
185
+ throw new Error('Workflow was canceled');
186
+ }
187
+ // Step 5: Set up TestFlight group (silently)
188
+ try {
189
+ await setupTestFlightAsync(ascApp);
190
+ }
191
+ catch {
192
+ // Non-fatal: TestFlight group setup failure shouldn't block the user
193
+ }
194
+ log_1.default.newLine();
195
+ log_1.default.succeed(`Done! Your custom Expo Go has been submitted to TestFlight. ${(0, log_1.learnMore)(`https://appstoreconnect.apple.com/apps/${ascApp.id}/testflight`, { learnMoreMessage: 'Open it on App Store Connect' })}`);
196
+ log_1.default.log(`App Store processing may take several minutes to complete. ${(0, log_1.learnMore)('https://expo.fyi/personal-expo-go', { learnMoreMessage: 'Learn more about Expo Go on TestFlight' })}`);
197
+ await fs.remove(projectDir);
198
+ }
199
+ catch (error) {
200
+ log_1.default.gray(`Project files preserved for debugging: ${projectDir}`);
201
+ throw error;
202
+ }
203
+ finally {
204
+ process.chdir(originalCwd);
205
+ }
206
+ }
207
+ generateBundleId(actor) {
208
+ const username = actor.accounts[0].name;
209
+ // Sanitize username for bundle ID: only alphanumeric and hyphens allowed
210
+ const sanitizedUsername = username
211
+ .toLowerCase()
212
+ .replace(/[^a-z0-9-]/g, '')
213
+ .replace(/^-+|-+$/g, ''); // trim leading/trailing hyphens
214
+ // Deterministic bundle ID per user + SDK version (reuses same ASC app)
215
+ return `com.${sanitizedUsername || 'app'}.expogo${EXPO_GO_SDK_VERSION}`;
216
+ }
217
+ async createProjectFilesAsync(projectDir, bundleId, appName) {
218
+ const slug = bundleId.split('.').pop() || 'custom-expo-go';
219
+ const extensionBundleId = `${bundleId}.ExpoNotificationServiceExtension`;
220
+ const appJson = {
221
+ expo: {
222
+ name: appName,
223
+ slug,
224
+ version: EXPO_GO_APP_VERSION,
225
+ ios: {
226
+ bundleIdentifier: bundleId,
227
+ buildNumber: EXPO_GO_BUILD_NUMBER,
228
+ config: {
229
+ usesNonExemptEncryption: false,
230
+ },
231
+ },
232
+ extra: {
233
+ eas: {
234
+ build: {
235
+ experimental: {
236
+ ios: {
237
+ appExtensions: [
238
+ {
239
+ targetName: 'ExpoNotificationServiceExtension',
240
+ bundleIdentifier: extensionBundleId,
241
+ },
242
+ ],
243
+ },
244
+ },
245
+ },
246
+ },
247
+ },
248
+ },
249
+ };
250
+ const easJson = {
251
+ cli: {
252
+ version: '>= 5.0.0',
253
+ },
254
+ build: {
255
+ production: {
256
+ distribution: 'store',
257
+ credentialsSource: 'remote',
258
+ },
259
+ },
260
+ submit: {
261
+ production: {
262
+ ios: {},
263
+ },
264
+ },
265
+ };
266
+ const packageJson = {
267
+ name: slug,
268
+ version: '1.0.0',
269
+ dependencies: {
270
+ expo: '~54.0.0',
271
+ },
272
+ };
273
+ await fs.writeJson(path.join(projectDir, 'app.json'), appJson, { spaces: 2 });
274
+ await fs.writeJson(path.join(projectDir, 'eas.json'), easJson, { spaces: 2 });
275
+ await fs.writeJson(path.join(projectDir, 'package.json'), packageJson, { spaces: 2 });
276
+ await (0, spawn_async_1.default)('npm', ['install'], { cwd: projectDir });
277
+ }
278
+ async initGitRepoAsync(projectDir) {
279
+ await (0, spawn_async_1.default)('git', ['init'], { cwd: projectDir });
280
+ await (0, spawn_async_1.default)('git', ['add', '.'], { cwd: projectDir });
281
+ await (0, spawn_async_1.default)('git', ['commit', '-m', 'Initial commit'], { cwd: projectDir });
282
+ }
283
+ async ensureEasProjectAsync(graphqlClient, actor, projectDir, bundleId) {
284
+ const slug = bundleId.split('.').pop() || 'custom-expo-go';
285
+ const account = actor.accounts[0];
286
+ const existingProjectId = await (0, fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync_1.findProjectIdByAccountNameAndSlugNullableAsync)(graphqlClient, account.name, slug);
287
+ if (existingProjectId) {
288
+ await (0, getProjectIdAsync_1.saveProjectIdToAppConfigAsync)(projectDir, existingProjectId);
289
+ return existingProjectId;
290
+ }
291
+ const projectId = await AppMutation_1.AppMutation.createAppAsync(graphqlClient, {
292
+ accountId: account.id,
293
+ projectName: slug,
294
+ });
295
+ await (0, getProjectIdAsync_1.saveProjectIdToAppConfigAsync)(projectDir, projectId);
296
+ return projectId;
297
+ }
298
+ async setupCredentialsAsync(projectDir, projectId, bundleId, appName, graphqlClient, actor, analytics, vcsClient, customizeCreds, onBeforeAppleAuth) {
299
+ const exp = await (0, expoConfig_1.getPrivateExpoConfigAsync)(projectDir);
300
+ const extensionBundleId = `${bundleId}.ExpoNotificationServiceExtension`;
301
+ const credentialsCtx = new context_1.CredentialsContext({
302
+ projectInfo: { exp, projectId },
303
+ nonInteractive: false,
304
+ autoAcceptCredentialReuse: !customizeCreds,
305
+ projectDir,
306
+ user: actor,
307
+ graphqlClient,
308
+ analytics,
309
+ vcsClient,
310
+ });
311
+ onBeforeAppleAuth?.();
312
+ const userAuthCtx = await credentialsCtx.appStore.ensureUserAuthenticatedAsync();
313
+ const app = await (0, BuildCredentialsUtils_1.getAppFromContextAsync)(credentialsCtx);
314
+ const targets = [
315
+ {
316
+ targetName: exp.slug,
317
+ bundleIdentifier: bundleId,
318
+ entitlements: {},
319
+ },
320
+ {
321
+ targetName: 'ExpoNotificationServiceExtension',
322
+ bundleIdentifier: extensionBundleId,
323
+ parentBundleIdentifier: bundleId,
324
+ entitlements: {},
325
+ },
326
+ ];
327
+ const ascApp = await withSuppressedOutputAsync(async () => {
328
+ await new SetUpBuildCredentials_1.SetUpBuildCredentials({
329
+ app,
330
+ targets,
331
+ distribution: 'store',
332
+ }).runAsync(credentialsCtx);
333
+ const appLookupParams = {
334
+ ...app,
335
+ bundleIdentifier: bundleId,
336
+ };
337
+ await new SetUpAscApiKey_1.SetUpAscApiKey(appLookupParams, AscApiKeyUtils_1.AppStoreApiKeyPurpose.SUBMISSION_SERVICE).runAsync(credentialsCtx);
338
+ const ascAppResult = await (0, ensureAppExists_1.ensureAppExistsAsync)(userAuthCtx, {
339
+ name: appName,
340
+ bundleIdentifier: bundleId,
341
+ });
342
+ const easJsonPath = path.join(projectDir, 'eas.json');
343
+ const easJson = await fs.readJson(easJsonPath);
344
+ easJson.submit = easJson.submit || {};
345
+ easJson.submit.production = easJson.submit.production || {};
346
+ easJson.submit.production.ios = easJson.submit.production.ios || {};
347
+ easJson.submit.production.ios.ascAppId = ascAppResult.id;
348
+ await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
349
+ await (0, spawn_async_1.default)('git', ['add', 'eas.json'], { cwd: projectDir });
350
+ await (0, spawn_async_1.default)('git', ['commit', '-m', 'Add ascAppId to eas.json'], { cwd: projectDir });
351
+ return ascAppResult;
352
+ });
353
+ // Set up push notifications (outside suppressed block so prompts are visible)
354
+ const appLookupParamsForPushKey = { ...app, bundleIdentifier: bundleId };
355
+ const setupPushKeyAction = new SetUpPushKey_1.SetUpPushKey(appLookupParamsForPushKey);
356
+ const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(credentialsCtx);
357
+ if (!isPushKeySetup) {
358
+ if (customizeCreds) {
359
+ const wantsPushNotifications = await (0, prompts_1.confirmAsync)({
360
+ message: 'Would you like to set up Push Notifications for your app?',
361
+ initial: true,
362
+ });
363
+ if (wantsPushNotifications) {
364
+ await setupPushKeyAction.runAsync(credentialsCtx);
365
+ }
366
+ }
367
+ else {
368
+ await setupPushKeyAction.runAsync(credentialsCtx);
369
+ }
370
+ }
371
+ return ascApp;
372
+ }
373
+ async runWorkflowAsync(graphqlClient, projectDir, projectId, actor, vcsClient) {
374
+ const account = actor.accounts[0];
375
+ const { projectArchiveBucketKey, easJsonBucketKey, packageJsonBucketKey } = await withSuppressedOutputAsync(async () => {
376
+ const { projectArchiveBucketKey } = await (0, uploadAccountScopedProjectSourceAsync_1.uploadAccountScopedProjectSourceAsync)({
377
+ graphqlClient,
378
+ vcsClient,
379
+ accountId: account.id,
380
+ });
381
+ const { fileBucketKey: easJsonBucketKey } = await (0, uploadAccountScopedFileAsync_1.uploadAccountScopedFileAsync)({
382
+ graphqlClient,
383
+ accountId: account.id,
384
+ filePath: path.join(projectDir, 'eas.json'),
385
+ maxSizeBytes: 1024 * 1024,
386
+ });
387
+ const { fileBucketKey: packageJsonBucketKey } = await (0, uploadAccountScopedFileAsync_1.uploadAccountScopedFileAsync)({
388
+ graphqlClient,
389
+ accountId: account.id,
390
+ filePath: path.join(projectDir, 'package.json'),
391
+ maxSizeBytes: 1024 * 1024,
392
+ });
393
+ return { projectArchiveBucketKey, easJsonBucketKey, packageJsonBucketKey };
394
+ });
395
+ const { id: workflowRunId } = await WorkflowRunMutation_1.WorkflowRunMutation.createExpoGoRepackWorkflowRunAsync(graphqlClient, {
396
+ appId: projectId,
397
+ projectSource: {
398
+ type: generated_1.WorkflowProjectSourceType.Gcs,
399
+ projectArchiveBucketKey,
400
+ easJsonBucketKey,
401
+ packageJsonBucketKey,
402
+ projectRootDirectory: '.',
403
+ },
404
+ });
405
+ const app = await AppQuery_1.AppQuery.byIdAsync(graphqlClient, projectId);
406
+ const workflowUrl = `https://expo.dev/accounts/${account.name}/projects/${app.slug}/workflows/${workflowRunId}`;
407
+ return { workflowUrl, workflowRunId };
408
+ }
409
+ async monitorWorkflowJobsAsync(graphqlClient, workflowRunId) {
410
+ const EXPECTED_BUILD_DURATION_SECONDS = 5 * 60;
411
+ const EXPECTED_SUBMIT_DURATION_SECONDS = 2 * 60;
412
+ const buildStartTime = Date.now();
413
+ let submitStartTime = null;
414
+ const buildSpinner = (0, ora_1.ora)(this.formatSpinnerText('Building Expo Go', EXPECTED_BUILD_DURATION_SECONDS, buildStartTime)).start();
415
+ let submitSpinner = null;
416
+ let buildCompleted = false;
417
+ let failedFetchesCount = 0;
418
+ while (true) {
419
+ if (!buildCompleted) {
420
+ buildSpinner.text = this.formatSpinnerText('Building Expo Go', EXPECTED_BUILD_DURATION_SECONDS, buildStartTime);
421
+ }
422
+ if (submitSpinner && submitStartTime) {
423
+ submitSpinner.text = this.formatSpinnerText('Submitting to TestFlight', EXPECTED_SUBMIT_DURATION_SECONDS, submitStartTime);
424
+ }
425
+ try {
426
+ const workflowRun = await WorkflowRunQuery_1.WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
427
+ useCache: false,
428
+ });
429
+ failedFetchesCount = 0;
430
+ const repackJob = workflowRun.jobs.find(j => j.name === 'Repack Expo Go');
431
+ const submitJob = workflowRun.jobs.find(j => j.name === 'Submit to TestFlight');
432
+ if (!buildCompleted) {
433
+ if (repackJob?.status === generated_1.WorkflowJobStatus.Success) {
434
+ buildSpinner.succeed('Built Expo Go');
435
+ buildCompleted = true;
436
+ }
437
+ else if (repackJob?.status === generated_1.WorkflowJobStatus.Failure ||
438
+ repackJob?.status === generated_1.WorkflowJobStatus.Canceled) {
439
+ buildSpinner.fail('Build failed');
440
+ return generated_1.WorkflowRunStatus.Failure;
441
+ }
442
+ }
443
+ if (buildCompleted && submitSpinner === null && submitJob) {
444
+ submitStartTime = Date.now();
445
+ submitSpinner = (0, ora_1.ora)(this.formatSpinnerText('Submitting to TestFlight', EXPECTED_SUBMIT_DURATION_SECONDS, submitStartTime)).start();
446
+ }
447
+ if (workflowRun.status === generated_1.WorkflowRunStatus.Success) {
448
+ submitSpinner?.stop();
449
+ return generated_1.WorkflowRunStatus.Success;
450
+ }
451
+ else if (workflowRun.status === generated_1.WorkflowRunStatus.Failure) {
452
+ buildSpinner.stop();
453
+ submitSpinner?.fail('Submission failed');
454
+ return generated_1.WorkflowRunStatus.Failure;
455
+ }
456
+ else if (workflowRun.status === generated_1.WorkflowRunStatus.Canceled) {
457
+ buildSpinner.stop();
458
+ submitSpinner?.stop();
459
+ return generated_1.WorkflowRunStatus.Canceled;
460
+ }
461
+ }
462
+ catch {
463
+ failedFetchesCount++;
464
+ if (failedFetchesCount > 6) {
465
+ buildSpinner.fail();
466
+ submitSpinner?.fail();
467
+ throw new Error('Failed to fetch the workflow run status 6 times in a row');
468
+ }
469
+ }
470
+ await (0, promise_1.sleepAsync)(10 * 1000);
471
+ }
472
+ }
473
+ formatSpinnerText(label, expectedDurationSeconds, startTime) {
474
+ const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
475
+ const remainingSeconds = Math.max(0, expectedDurationSeconds - elapsedSeconds);
476
+ if (remainingSeconds === 0) {
477
+ return `${label} (almost done...)`;
478
+ }
479
+ const minutes = Math.ceil(remainingSeconds / 60);
480
+ const unit = minutes === 1 ? 'minute' : 'minutes';
481
+ return `${label} (~${minutes} ${unit} remaining)`;
482
+ }
483
+ }
484
+ exports.default = Go;
@@ -10,6 +10,7 @@ const flags_1 = require("../../commandUtils/flags");
10
10
  const log_1 = tslib_1.__importStar(require("../../log"));
11
11
  const platform_1 = require("../../platform");
12
12
  const configure_2 = require("../../update/configure");
13
+ const utils_1 = require("../../update/utils");
13
14
  class UpdateConfigure extends EasCommand_1.default {
14
15
  static description = 'configure the project to support EAS Update';
15
16
  static flags = {
@@ -33,6 +34,10 @@ class UpdateConfigure extends EasCommand_1.default {
33
34
  nonInteractive: flags['non-interactive'],
34
35
  withServerSideEnvironment: flags['environment'] ?? null,
35
36
  });
37
+ (0, utils_1.assertEnvironmentFlagForSdk55OrGreater)({
38
+ sdkVersion: exp.sdkVersion,
39
+ environment: flags['environment'],
40
+ });
36
41
  log_1.default.log('💡 The following process will configure your project to use EAS Update. These changes only apply to your local project files and you can safely revert them at any time.');
37
42
  await vcsClient.ensureRepoExistsAsync();
38
43
  const easJsonAccessor = eas_json_1.EasJsonAccessor.fromProjectPath(projectDir);
@@ -12,6 +12,7 @@ const repository_1 = require("../../build/utils/repository");
12
12
  const url_1 = require("../../build/utils/url");
13
13
  const EasCommand_1 = tslib_1.__importDefault(require("../../commandUtils/EasCommand"));
14
14
  const flags_1 = require("../../commandUtils/flags");
15
+ const utils_1 = require("../../update/utils");
15
16
  const pagination_1 = require("../../commandUtils/pagination");
16
17
  const fetch_1 = tslib_1.__importDefault(require("../../fetch"));
17
18
  const generated_1 = require("../../graphql/generated");
@@ -25,7 +26,7 @@ const projectUtils_1 = require("../../project/projectUtils");
25
26
  const publish_1 = require("../../project/publish");
26
27
  const workflow_1 = require("../../project/workflow");
27
28
  const configure_1 = require("../../update/configure");
28
- const utils_1 = require("../../update/utils");
29
+ const utils_2 = require("../../update/utils");
29
30
  const code_signing_1 = require("../../utils/code-signing");
30
31
  const areSetsEqual_1 = tslib_1.__importDefault(require("../../utils/expodash/areSetsEqual"));
31
32
  const uniqBy_1 = tslib_1.__importDefault(require("../../utils/expodash/uniqBy"));
@@ -140,6 +141,10 @@ class UpdatePublish extends EasCommand_1.default {
140
141
  await vcsClient.ensureRepoExistsAsync();
141
142
  await (0, repository_1.ensureRepoIsCleanAsync)(vcsClient, nonInteractive);
142
143
  const { exp: expPossiblyWithoutEasUpdateConfigured, projectId, projectDir, } = await getDynamicPublicProjectConfigAsync();
144
+ (0, utils_1.assertEnvironmentFlagForSdk55OrGreater)({
145
+ sdkVersion: expPossiblyWithoutEasUpdateConfigured.sdkVersion,
146
+ environment,
147
+ });
143
148
  await (0, statuspageService_1.maybeWarnAboutEasOutagesAsync)(graphqlClient, [generated_1.StatuspageServiceName.EasUpdate]);
144
149
  const easJsonAccessor = eas_json_1.EasJsonAccessor.fromProjectPath(projectDir);
145
150
  const easJsonCliConfig = (await eas_json_1.EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
@@ -415,15 +420,15 @@ class UpdatePublish extends EasCommand_1.default {
415
420
  publishSpinner.fail('Failed to publish updates');
416
421
  throw e;
417
422
  }
418
- if ((0, utils_1.isBundleDiffingEnabled)(exp)) {
419
- await (0, utils_1.prewarmDiffingAsync)(graphqlClient, projectId, newUpdates);
423
+ if ((0, utils_2.isBundleDiffingEnabled)(exp)) {
424
+ await (0, utils_2.prewarmDiffingAsync)(graphqlClient, projectId, newUpdates);
420
425
  }
421
426
  if (!skipBundler && emitMetadata) {
422
427
  log_1.default.log('Generating eas-update-metadata.json');
423
- await (0, publish_1.generateEasMetadataAsync)(distRoot, (0, utils_1.getUpdateJsonInfosForUpdates)(newUpdates));
428
+ await (0, publish_1.generateEasMetadataAsync)(distRoot, (0, utils_2.getUpdateJsonInfosForUpdates)(newUpdates));
424
429
  }
425
430
  if (jsonFlag) {
426
- (0, json_1.printJsonOnlyOutput)((0, utils_1.getUpdateJsonInfosForUpdates)(newUpdates));
431
+ (0, json_1.printJsonOnlyOutput)((0, utils_2.getUpdateJsonInfosForUpdates)(newUpdates));
427
432
  return;
428
433
  }
429
434
  if (new Set(newUpdates.map(update => update.group)).size > 1) {
@@ -18,6 +18,7 @@ export declare class CredentialsContext {
18
18
  readonly appStore: AppStoreApi;
19
19
  readonly ios: typeof IosGraphqlClient;
20
20
  readonly nonInteractive: boolean;
21
+ readonly autoAcceptCredentialReuse: boolean;
21
22
  readonly freezeCredentials: boolean;
22
23
  readonly projectDir: string;
23
24
  readonly user: Actor;
@@ -38,6 +39,7 @@ export declare class CredentialsContext {
38
39
  analytics: Analytics;
39
40
  vcsClient: Client;
40
41
  freezeCredentials?: boolean;
42
+ autoAcceptCredentialReuse?: boolean;
41
43
  env?: Env;
42
44
  });
43
45
  get hasProjectContext(): boolean;
@@ -16,6 +16,7 @@ class CredentialsContext {
16
16
  appStore = new AppStoreApi_1.default();
17
17
  ios = IosGraphqlClient;
18
18
  nonInteractive;
19
+ autoAcceptCredentialReuse;
19
20
  freezeCredentials = false;
20
21
  projectDir;
21
22
  user;
@@ -35,6 +36,7 @@ class CredentialsContext {
35
36
  this.analytics = options.analytics;
36
37
  this.vcsClient = options.vcsClient;
37
38
  this.nonInteractive = options.nonInteractive ?? false;
39
+ this.autoAcceptCredentialReuse = options.autoAcceptCredentialReuse ?? false;
38
40
  this.projectInfo = options.projectInfo;
39
41
  this.freezeCredentials = options.freezeCredentials ?? false;
40
42
  this.usesBroadcastPushNotifications =
@@ -53,6 +53,10 @@ async function provideOrGenerateAscApiKeyAsync(ctx, purpose) {
53
53
  if (ctx.nonInteractive) {
54
54
  throw new Error(`A new App Store Connect API Key cannot be created in non-interactive mode.`);
55
55
  }
56
+ // When auto-accepting credentials, always auto-generate without asking for user input
57
+ if (ctx.autoAcceptCredentialReuse) {
58
+ return await generateAscApiKeyAsync(ctx, purpose);
59
+ }
56
60
  const userProvided = await promptForAscApiKeyAsync(ctx);
57
61
  if (!userProvided) {
58
62
  return await generateAscApiKeyAsync(ctx, purpose);
@@ -40,7 +40,7 @@ class CreateProvisioningProfile {
40
40
  return provisioningProfileMutationResult;
41
41
  }
42
42
  async maybeGetUserProvidedAsync(ctx) {
43
- if (ctx.nonInteractive) {
43
+ if (ctx.nonInteractive || ctx.autoAcceptCredentialReuse) {
44
44
  return null;
45
45
  }
46
46
  const userProvided = await (0, promptForCredentials_1.askForUserProvidedAsync)(credentials_1.provisioningProfileSchema);
@@ -104,7 +104,7 @@ Revoke the old ones or reuse existing from your other apps.
104
104
  Remember that Apple Distribution Certificates are not application specific!
105
105
  `;
106
106
  async function provideOrGenerateDistributionCertificateAsync(ctx) {
107
- if (!ctx.nonInteractive) {
107
+ if (!ctx.nonInteractive && !ctx.autoAcceptCredentialReuse) {
108
108
  const userProvided = await promptForDistCertAsync(ctx);
109
109
  if (userProvided) {
110
110
  if (!ctx.appStore.authCtx) {
@@ -17,7 +17,7 @@ const pushKey_1 = require("../appstore/pushKey");
17
17
  const credentials_1 = require("../credentials");
18
18
  const validatePushKey_1 = require("../validators/validatePushKey");
19
19
  async function provideOrGeneratePushKeyAsync(ctx) {
20
- if (!ctx.nonInteractive) {
20
+ if (!ctx.nonInteractive && !ctx.autoAcceptCredentialReuse) {
21
21
  const userProvided = await promptForPushKeyAsync(ctx);
22
22
  if (userProvided) {
23
23
  if (!ctx.appStore.authCtx) {