From faec5e67a3f3af0ae4b43133dae761f4b481e743 Mon Sep 17 00:00:00 2001 From: Radoslaw Krzemien Date: Wed, 12 Jun 2024 11:09:45 +0200 Subject: [PATCH] [eas-cli] Prompt the user The user is now prompted when they don't specify `appVersionSource` to either set it automatically to LOCAL/REMOTE or abort the command and configure it manually. This removes the default behaviour alltogether and avoids a situation where some users have been relying on the old default and would now be surprised See: https://linear.app/expo/issue/ENG-11843/change-default-appversionsource-to-remote --- .../build-version-get-test.ts.snap | 2 + .../commands/build-version-get-test.ts | 46 ++++++++- .../commands/build-version-set-test.ts | 51 +++++++++- .../commands/build-version-sync-test.ts | 51 +++++++++- .../eas-cli/src/build/runBuildAndSubmit.ts | 15 ++- .../src/project/remoteVersionSource.ts | 94 ++++++++++++++++++- 6 files changed, 246 insertions(+), 13 deletions(-) diff --git a/packages/eas-cli/src/__tests__/commands/__snapshots__/build-version-get-test.ts.snap b/packages/eas-cli/src/__tests__/commands/__snapshots__/build-version-get-test.ts.snap index e580de02bd..9bfd4b779f 100644 --- a/packages/eas-cli/src/__tests__/commands/__snapshots__/build-version-get-test.ts.snap +++ b/packages/eas-cli/src/__tests__/commands/__snapshots__/build-version-get-test.ts.snap @@ -1,3 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add {"cli": { "appVersionSource": "remote" }} in eas.json (or remove {"cli": { "appVersionSource": "local" }} to use "remote" as a default) or re-run this command without "--non-interactive" flag."`; + +exports[`BuildVersionGetView reading version when the appVersionSource is not specified and the user chooses to set it to LOCAL 1`] = `"This project is not configured for using remote version source. Add {"cli": { "appVersionSource": "remote" }} in eas.json (or remove {"cli": { "appVersionSource": "local" }} to use "remote" as a default) or re-run this command without "--non-interactive" flag."`; diff --git a/packages/eas-cli/src/__tests__/commands/build-version-get-test.ts b/packages/eas-cli/src/__tests__/commands/build-version-get-test.ts index 8666156dfc..7fbdbb2d7c 100644 --- a/packages/eas-cli/src/__tests__/commands/build-version-get-test.ts +++ b/packages/eas-cli/src/__tests__/commands/build-version-get-test.ts @@ -11,6 +11,8 @@ import { import BuildVersionGetView from '../../commands/build/version/get'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import Log from '../../log'; +import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource'; +import * as prompts from '../../prompts'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; jest.mock('../../project/applicationIdentifier'); @@ -18,6 +20,7 @@ jest.mock('fs'); jest.mock('../../log'); jest.mock('../../utils/json'); jest.mock('../../graphql/queries/AppVersionQuery'); +jest.mock('../../prompts'); describe(BuildVersionGetView, () => { afterEach(() => { @@ -45,14 +48,17 @@ describe(BuildVersionGetView, () => { expect(printJsonOnlyOutput).not.toHaveBeenCalled(); }); - test('reading version for platform android when appVersionSource is not set and defaults to REMOTE', async () => { + test('reading version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => { const ctx = mockCommandContext(BuildVersionGetView, {}); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ buildVersion: '100', storeVersion: '1.0.0', })); - const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE); + await cmd.run(); expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith( ctx.loggedIn.graphqlClient, @@ -176,4 +182,40 @@ describe(BuildVersionGetView, () => { ); await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot(); }); + + test('reading version when the appVersionSource is not specified and the user chooses to set it to LOCAL', async () => { + const ctx = mockCommandContext(BuildVersionGetView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ + buildVersion: '100', + storeVersion: '1.0.0', + })); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL); + + const cmd = mockTestCommand( + BuildVersionGetView, + ['--non-interactive', '--json', '--platform=android'], + ctx + ); + await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('reading version aborts when the appVersionSource is not specified and the user chooses to configure it manually', async () => { + const ctx = mockCommandContext(BuildVersionGetView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ + buildVersion: '100', + storeVersion: '1.0.0', + })); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT); + + const cmd = mockTestCommand( + BuildVersionGetView, + ['--non-interactive', '--json', '--platform=android'], + ctx + ); + await expect(cmd.run()).rejects.toThrowError('Aborted.'); + }); }); diff --git a/packages/eas-cli/src/__tests__/commands/build-version-set-test.ts b/packages/eas-cli/src/__tests__/commands/build-version-set-test.ts index c14e11ef1c..0c1fd92fe1 100644 --- a/packages/eas-cli/src/__tests__/commands/build-version-set-test.ts +++ b/packages/eas-cli/src/__tests__/commands/build-version-set-test.ts @@ -15,6 +15,7 @@ import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation'; import { AppQuery } from '../../graphql/queries/AppQuery'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import Log from '../../log'; +import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource'; import * as prompts from '../../prompts'; jest.mock('../../project/applicationIdentifier'); @@ -58,13 +59,16 @@ describe(BuildVersionSetView, () => { ); }); - test('setting version for platform android when the appVersionSource is not specified and defaults to REMOTE', async () => { + test('setting version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => { const ctx = mockCommandContext(BuildVersionSetView, {}); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); - jest.mocked(prompts.promptAsync).mockImplementation(async () => ({ + jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({ version: '1000', })); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE); const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx); await cmd.run(); @@ -143,6 +147,20 @@ describe(BuildVersionSetView, () => { expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); }); + test('setting version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => { + const ctx = mockCommandContext(BuildVersionSetView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL); + jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => false); + + const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx); + await expect(cmd.run()).rejects.toThrowError('Aborting...'); + expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); + }); + test('setting version when appVersionSource is set to local and user allows auto configuration', async () => { const ctx = mockCommandContext(BuildVersionSetView, { easJson: withLocalVersionSource(getMockEasJson()), @@ -157,4 +175,33 @@ describe(BuildVersionSetView, () => { const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json')); expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote'); }); + + test('setting version when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they allow auto configuration', async () => { + const ctx = mockCommandContext(BuildVersionSetView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL); + jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => true); + + const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx); + await cmd.run(); + + const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json')); + expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote'); + }); + + test('setting version aborts when the appVersionSource is not specified and the user chooses to configure manually', async () => { + const ctx = mockCommandContext(BuildVersionSetView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT); + + const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx); + await expect(cmd.run()).rejects.toThrowError('Aborted.'); + expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); + }); }); diff --git a/packages/eas-cli/src/__tests__/commands/build-version-sync-test.ts b/packages/eas-cli/src/__tests__/commands/build-version-sync-test.ts index 559966b15c..e0f5eed5d7 100644 --- a/packages/eas-cli/src/__tests__/commands/build-version-sync-test.ts +++ b/packages/eas-cli/src/__tests__/commands/build-version-sync-test.ts @@ -20,6 +20,7 @@ import { AppQuery } from '../../graphql/queries/AppQuery'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import { getAppBuildGradleAsync } from '../../project/android/gradleUtils'; import { resolveTargetsAsync } from '../../project/ios/target'; +import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource'; import { resolveWorkflowAsync } from '../../project/workflow'; import * as prompts from '../../prompts'; @@ -70,16 +71,19 @@ describe(BuildVersionSyncView, () => { expect(syncAndroidAsync).not.toHaveBeenCalled(); }); - test('syncing version for managed project on platform android when appVersionSource is not set and defaults to REMOTE', async () => { + test('syncing version for managed project on platform android when appVersionSource is not set and the user chooses to set it to REMOTE', async () => { const ctx = mockCommandContext(BuildVersionSyncView, {}); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ buildVersion: '1000', storeVersion: '0.0.1', })); - jest.mocked(prompts.promptAsync).mockImplementation(async () => ({ + jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({ version: '1000', })); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE); jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED); const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx); @@ -207,6 +211,20 @@ describe(BuildVersionSyncView, () => { expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); }); + test('syncing version aborts when appVersionSource is not set and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => { + const ctx = mockCommandContext(BuildVersionSyncView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL); + jest.mocked(prompts.confirmAsync).mockImplementation(async () => false); + + const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx); + await expect(cmd.run()).rejects.toThrowError('Aborting...'); + expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); + }); + test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => { const ctx = mockCommandContext(BuildVersionSyncView, { easJson: withLocalVersionSource(getMockEasJson()), @@ -221,4 +239,33 @@ describe(BuildVersionSyncView, () => { const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json')); expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote'); }); + + test('syncing version when appVersionSource is not set and the user chooses to set it to LOCAL and they allow auto configuration', async () => { + const ctx = mockCommandContext(BuildVersionSyncView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL); + jest.mocked(prompts.confirmAsync).mockImplementation(async () => true); + + const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx); + await cmd.run(); + + const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json')); + expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote'); + }); + + test('syncing version aborts when appVersionSource is not set and the user chooses to configure manually', async () => { + const ctx = mockCommandContext(BuildVersionSyncView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT); + + const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx); + await expect(cmd.run()).rejects.toThrowError('Aborted.'); + expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); + }); }); diff --git a/packages/eas-cli/src/build/runBuildAndSubmit.ts b/packages/eas-cli/src/build/runBuildAndSubmit.ts index d3246153cb..123ec31237 100644 --- a/packages/eas-cli/src/build/runBuildAndSubmit.ts +++ b/packages/eas-cli/src/build/runBuildAndSubmit.ts @@ -57,8 +57,10 @@ import { validateAppVersionRuntimePolicySupportAsync, } from '../project/projectUtils'; import { + AppVersionSourceUpdateOption, + ensureAppVersionSourceIsSetAsync, validateAppConfigForRemoteVersionSource, - validateBuildProfileVersionSettings, + validateBuildProfileVersionSettingsAsync, } from '../project/remoteVersionSource'; import { confirmAsync } from '../prompts'; import { runAsync } from '../run/run'; @@ -177,7 +179,7 @@ export async function runBuildAndSubmitAsync( const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } = {}; for (const buildProfile of buildProfiles) { - validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig); + await validateBuildProfileVersionSettingsAsync(buildProfile, easJsonCliConfig, projectDir); const maybeMetadata = await validateCustomBuildConfigAsync({ projectDir, profile: buildProfile.profile, @@ -415,6 +417,15 @@ async function prepareAndStartBuildAsync({ } await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp); + if (easJsonCliConfig?.appVersionSource === undefined) { + const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir); + const selection = await ensureAppVersionSourceIsSetAsync(easJsonAccessor); + if (selection === AppVersionSourceUpdateOption.SET_TO_LOCAL && easJsonCliConfig) { + easJsonCliConfig.appVersionSource = AppVersionSource.LOCAL; + } else if (selection === AppVersionSourceUpdateOption.SET_TO_REMOTE && easJsonCliConfig) { + easJsonCliConfig.appVersionSource = AppVersionSource.REMOTE; + } + } if (easJsonCliConfig?.appVersionSource !== AppVersionSource.LOCAL) { validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform); } diff --git a/packages/eas-cli/src/project/remoteVersionSource.ts b/packages/eas-cli/src/project/remoteVersionSource.ts index ac8d7c1369..d199cf6f53 100644 --- a/packages/eas-cli/src/project/remoteVersionSource.ts +++ b/packages/eas-cli/src/project/remoteVersionSource.ts @@ -1,17 +1,32 @@ import { ExpoConfig } from '@expo/config'; import { Platform } from '@expo/eas-build-job'; import { AppVersionSource, EasJson, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json'; +import { Errors } from '@oclif/core'; import chalk from 'chalk'; -import Log from '../log'; -import { confirmAsync } from '../prompts'; +import Log, { learnMore } from '../log'; +import { confirmAsync, selectAsync } from '../prompts'; import { ProfileData } from '../utils/profiles'; +export enum AppVersionSourceUpdateOption { + SET_TO_REMOTE, + SET_TO_LOCAL, + ABORT, +} + export async function ensureVersionSourceIsRemoteAsync( easJsonAccessor: EasJsonAccessor, { nonInteractive }: { nonInteractive: boolean } ): Promise { const easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor); + if (easJsonCliConfig?.appVersionSource === undefined) { + const selection = await ensureAppVersionSourceIsSetAsync(easJsonAccessor); + if (selection === AppVersionSourceUpdateOption.SET_TO_LOCAL && easJsonCliConfig) { + easJsonCliConfig.appVersionSource = AppVersionSource.LOCAL; + } else if (selection === AppVersionSourceUpdateOption.SET_TO_REMOTE && easJsonCliConfig) { + easJsonCliConfig.appVersionSource = AppVersionSource.REMOTE; + } + } if (easJsonCliConfig?.appVersionSource !== AppVersionSource.LOCAL) { return; } @@ -49,10 +64,20 @@ export async function ensureVersionSourceIsRemoteAsync( Log.withTick('Updated eas.json'); } -export function validateBuildProfileVersionSettings( +export async function validateBuildProfileVersionSettingsAsync( profileInfo: ProfileData<'build'>, - cliConfig: EasJson['cli'] -): void { + cliConfig: EasJson['cli'], + projectDir: string +): Promise { + if (cliConfig?.appVersionSource === undefined) { + const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir); + const selection = await ensureAppVersionSourceIsSetAsync(easJsonAccessor); + if (selection === AppVersionSourceUpdateOption.SET_TO_LOCAL && cliConfig) { + cliConfig.appVersionSource = AppVersionSource.LOCAL; + } else if (selection === AppVersionSourceUpdateOption.SET_TO_REMOTE && cliConfig) { + cliConfig.appVersionSource = AppVersionSource.REMOTE; + } + } if (cliConfig?.appVersionSource === AppVersionSource.LOCAL) { return; } @@ -102,3 +127,62 @@ export function getBuildVersionName(platform: Platform): string { return 'buildNumber'; } } + +export async function ensureAppVersionSourceIsSetAsync( + easJsonAccessor: EasJsonAccessor +): Promise { + Log.log( + `This project is using the default app version source, which currently is the "remote" version source. Prior to version 10.0.0 of CLI the default used to be the "local" version source. App version source can be set for you automatically, or you can configure it yourself - if you wish to use the default "remote" version source add ${chalk.bold( + '{"cli": { "appVersionSource": "remote" }}' + )} to your eas.json or if you want to use the "local" version source add ${chalk.bold( + '{"cli": { "appVersionSource": "local" }}' + )} to your eas.json.` + ); + + const selectOption = await selectAsync(`What would you like to do?`, [ + { + title: 'Update eas.json to use the default "remote" version source', + value: AppVersionSourceUpdateOption.SET_TO_REMOTE, + }, + { + title: 'Update eas.json to use "local" version source', + value: AppVersionSourceUpdateOption.SET_TO_LOCAL, + }, + { + title: "Don't update eas.json, abort command and configure manually", + value: AppVersionSourceUpdateOption.ABORT, + }, + ]); + + if (selectOption === AppVersionSourceUpdateOption.SET_TO_LOCAL) { + await easJsonAccessor.readRawJsonAsync(); + easJsonAccessor.patch(easJsonRawObject => { + easJsonRawObject.cli = { ...easJsonRawObject?.cli, appVersionSource: AppVersionSource.LOCAL }; + return easJsonRawObject; + }); + await easJsonAccessor.writeAsync(); + Log.withTick('Updated eas.json'); + } else if (selectOption === AppVersionSourceUpdateOption.SET_TO_REMOTE) { + await easJsonAccessor.readRawJsonAsync(); + easJsonAccessor.patch(easJsonRawObject => { + easJsonRawObject.cli = { + ...easJsonRawObject?.cli, + appVersionSource: AppVersionSource.REMOTE, + }; + return easJsonRawObject; + }); + await easJsonAccessor.writeAsync(); + Log.withTick('Updated eas.json'); + } else { + Log.warn(`You'll need to configure ${chalk.bold('appVersionSource')} manually.`); + Log.warn( + learnMore('https://docs.expo.dev/build-reference/app-versions/', { + learnMoreMessage: 'See the docs on what are the options and how to do it.', + dim: false, + }) + ); + Errors.error('Aborted.', { exit: 1 }); + } + + return selectOption; +}