diff --git a/CHANGELOG.md b/CHANGELOG.md index b388ac46d6..71ee2cc6b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This is the log of notable changes to EAS CLI and related packages. ### 🛠 Breaking changes +- Prompt the users to set `appVersionSource`, while mentioning that `remote` is the default. ([#2411](https://github.com/expo/eas-cli/pull/2411) by [@radoslawkrzemien](https://github.com/radoslawkrzemien)) + ### 🎉 New features - Add support for syncing Journaling Suggestions, Managed App Installation UI, and 5G Network Slicing capabilities. ([#2525](https://github.com/expo/eas-cli/pull/2525) by [@szdziedzic](https://github.com/szdziedzic)) 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 1541ca6c8d..97ab224807 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 aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL 1`] = `"Aborting..."`; + 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 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 e462a4850e..938b2ce8f6 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 @@ -1,10 +1,18 @@ -import { AppVersionSource, EasJson } from '@expo/eas-json'; import chalk from 'chalk'; -import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils'; +import { + getMockEasJson, + mockCommandContext, + mockProjectId, + mockTestCommand, + withLocalVersionSource, + withRemoteVersionSource, +} from './utils'; 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'); @@ -12,16 +20,7 @@ jest.mock('fs'); jest.mock('../../log'); jest.mock('../../utils/json'); jest.mock('../../graphql/queries/AppVersionQuery'); - -function withRemoteVersionSource(easJson: EasJson): EasJson { - return { - ...easJson, - cli: { - ...easJson.cli, - appVersionSource: AppVersionSource.REMOTE, - }, - }; -} +jest.mock('../../prompts'); describe(BuildVersionGetView, () => { afterEach(() => { @@ -49,6 +48,29 @@ describe(BuildVersionGetView, () => { expect(printJsonOnlyOutput).not.toHaveBeenCalled(); }); + 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, + mockProjectId, + 'ANDROID', + 'eas.test.com' + ); + expect(Log.log).toHaveBeenCalledWith(`Android versionCode - ${chalk.bold('100')}`); + expect(enableJsonOutput).not.toHaveBeenCalled(); + expect(printJsonOnlyOutput).not.toHaveBeenCalled(); + }); + test('reading version for platform android when no remote version is set', async () => { const ctx = mockCommandContext(BuildVersionGetView, { easJson: withRemoteVersionSource(getMockEasJson()), @@ -145,7 +167,9 @@ describe(BuildVersionGetView, () => { }); test('reading version when appVersionSource is set to local ', async () => { - const ctx = mockCommandContext(BuildVersionGetView, {}); + const ctx = mockCommandContext(BuildVersionGetView, { + easJson: withLocalVersionSource(getMockEasJson()), + }); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ buildVersion: '100', storeVersion: '1.0.0', @@ -158,4 +182,51 @@ describe(BuildVersionGetView, () => { ); await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot(); }); + + test('reading version aborts 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, ['--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, ['--platform=android'], ctx); + await expect(cmd.run()).rejects.toThrowError('Aborted.'); + }); + + test('reading version sets appVersionSource to LOCAL and aborts when the appVersionSource is not specified and is run in non-interactive mode', async () => { + const ctx = mockCommandContext(BuildVersionGetView, {}); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ + buildVersion: '100', + storeVersion: '1.0.0', + })); + + const cmd = mockTestCommand( + BuildVersionGetView, + ['--non-interactive', '--json', '--platform=android'], + ctx + ); + await expect(cmd.run()).rejects.toThrowError( + `This project is not configured for using remote version source. Add ${chalk.bold( + '{"cli": { "appVersionSource": "remote" }}' + )} in eas.json or re-run this command without "--non-interactive" flag.` + ); + }); }); 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 d03585c17c..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 @@ -1,4 +1,3 @@ -import { AppVersionSource, EasJson } from '@expo/eas-json'; import fs from 'fs-extra'; import path from 'path'; @@ -8,12 +7,15 @@ import { mockCommandContext, mockProjectId, mockTestCommand, + withLocalVersionSource, + withRemoteVersionSource, } from './utils'; import BuildVersionSetView from '../../commands/build/version/set'; 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'); @@ -25,16 +27,6 @@ jest.mock('../../log'); jest.mock('../../prompts'); jest.mock('../../utils/json'); -function withRemoteVersionSource(easJson: EasJson): EasJson { - return { - ...easJson, - cli: { - ...easJson.cli, - appVersionSource: AppVersionSource.REMOTE, - }, - }; -} - describe(BuildVersionSetView, () => { afterEach(() => { jest.clearAllMocks(); @@ -67,6 +59,34 @@ describe(BuildVersionSetView, () => { ); }); + 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).mockImplementationOnce(async () => ({ + version: '1000', + })); + jest + .mocked(prompts.selectAsync) + .mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE); + + const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx); + await cmd.run(); + expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith( + ctx.loggedIn.graphqlClient, + mockProjectId, + 'ANDROID', + 'eas.test.com' + ); + expect(AppVersionMutation.createAppVersionAsync).toHaveBeenCalledWith( + ctx.loggedIn.graphqlClient, + expect.objectContaining({ + buildVersion: '1000', + storeVersion: '1.0.0', + }) + ); + }); + test('printing current remote version before prompting for a new one', async () => { const ctx = mockCommandContext(BuildVersionSetView, { easJson: withRemoteVersionSource(getMockEasJson()), @@ -115,7 +135,9 @@ describe(BuildVersionSetView, () => { }); test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => { - const ctx = mockCommandContext(BuildVersionSetView, {}); + const ctx = mockCommandContext(BuildVersionSetView, { + easJson: withLocalVersionSource(getMockEasJson()), + }); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(prompts.confirmAsync).mockImplementation(async () => false); @@ -125,10 +147,26 @@ describe(BuildVersionSetView, () => { expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); }); - test('setting version when appVersionSource is set to local and user allows auto configuration', async () => { + 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()), + }); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(prompts.confirmAsync).mockImplementation(async () => true); const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx); @@ -137,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 bb43399379..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 @@ -1,5 +1,4 @@ import { Workflow } from '@expo/eas-build-job'; -import { AppVersionSource, EasJson } from '@expo/eas-json'; import fs from 'fs-extra'; import path from 'path'; @@ -9,6 +8,8 @@ import { mockCommandContext, mockProjectId, mockTestCommand, + withLocalVersionSource, + withRemoteVersionSource, } from './utils'; import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version'; import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version'; @@ -19,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'; @@ -37,16 +39,6 @@ jest.mock('../../log'); jest.mock('../../prompts'); jest.mock('../../utils/json'); -function withRemoteVersionSource(easJson: EasJson): EasJson { - return { - ...easJson, - cli: { - ...easJson.cli, - appVersionSource: AppVersionSource.REMOTE, - }, - }; -} - describe(BuildVersionSyncView, () => { afterEach(() => { jest.clearAllMocks(); @@ -79,6 +71,34 @@ describe(BuildVersionSyncView, () => { expect(syncAndroidAsync).not.toHaveBeenCalled(); }); + 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).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); + const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any); + + await cmd.run(); + expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith( + ctx.loggedIn.graphqlClient, + mockProjectId, + 'ANDROID', + 'eas.test.com' + ); + expect(syncAndroidAsync).not.toHaveBeenCalled(); + }); + test('syncing version for bare project on platform android', async () => { const ctx = mockCommandContext(BuildVersionSyncView, { easJson: withRemoteVersionSource(getMockEasJson()), @@ -179,9 +199,25 @@ describe(BuildVersionSyncView, () => { }); test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => { + const ctx = mockCommandContext(BuildVersionSyncView, { + easJson: withLocalVersionSource(getMockEasJson()), + }); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + 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 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); @@ -190,9 +226,27 @@ describe(BuildVersionSyncView, () => { }); test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => { + const ctx = mockCommandContext(BuildVersionSyncView, { + easJson: withLocalVersionSource(getMockEasJson()), + }); + jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); + jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); + 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 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); @@ -201,4 +255,17 @@ describe(BuildVersionSyncView, () => { 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/__tests__/commands/utils.ts b/packages/eas-cli/src/__tests__/commands/utils.ts index 74d5096f2a..a2ada10a4e 100644 --- a/packages/eas-cli/src/__tests__/commands/utils.ts +++ b/packages/eas-cli/src/__tests__/commands/utils.ts @@ -1,5 +1,5 @@ import { ExpoConfig } from '@expo/config-types'; -import { EasJson } from '@expo/eas-json'; +import { AppVersionSource, EasJson } from '@expo/eas-json'; import { Command, Config } from '@oclif/core'; import { vol } from 'memfs'; import path from 'path'; @@ -148,3 +148,23 @@ export const getError = (call: () => unknown): TError | NoErrorThr return error as TError; } }; + +export function withRemoteVersionSource(easJson: EasJson): EasJson { + return { + ...easJson, + cli: { + ...easJson.cli, + appVersionSource: AppVersionSource.REMOTE, + }, + }; +} + +export function withLocalVersionSource(easJson: EasJson): EasJson { + return { + ...easJson, + cli: { + ...easJson.cli, + appVersionSource: AppVersionSource.LOCAL, + }, + }; +} diff --git a/packages/eas-cli/src/build/__tests__/configure-test.ts b/packages/eas-cli/src/build/__tests__/configure-test.ts new file mode 100644 index 0000000000..04050a4099 --- /dev/null +++ b/packages/eas-cli/src/build/__tests__/configure-test.ts @@ -0,0 +1,77 @@ +import { AppVersionSource, EasJsonAccessor } from '@expo/eas-json'; +import fs from 'fs-extra'; +import { vol } from 'memfs'; + +import { easCliVersion } from '../../utils/easCli'; +import GitClient from '../../vcs/clients/git'; +import { Client } from '../../vcs/vcs'; +import { ensureProjectConfiguredAsync } from '../configure'; + +jest.mock('fs'); +jest.mock('../../vcs/vcs'); +jest.mock('../../vcs/clients/git'); + +beforeEach(async () => { + vol.reset(); +}); + +describe(ensureProjectConfiguredAsync, () => { + it('returns false and does not configure if eas.json exists', async () => { + vol.fromJSON({ + './eas.json': JSON.stringify({ + cli: { + version: `>= ${easCliVersion}`, + }, + build: { + development: { + developmentClient: true, + distribution: 'internal', + }, + preview: { + distribution: 'internal', + }, + production: {}, + }, + submit: { + production: {}, + }, + }), + }); + await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy(); + const vcsClientMock = jest.mocked(new GitClient()); + vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {}); + vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false); + vcsClientMock.trackFileAsync.mockImplementation(async () => {}); + const result = await ensureProjectConfiguredAsync({ + projectDir: '.', + nonInteractive: false, + vcsClient: vcsClientMock as unknown as Client, + }); + expect(result).toBeFalsy(); + await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy(); + }); + it('returns true and configures if eas.json does not exist', async () => { + const writeFileMock = jest.spyOn(fs, 'writeFile'); + writeFileMock.mockImplementation(async (...args) => { + const easJsonPath = args[0] as string; + const easJsonData = args[1] as string; + vol.fromJSON({ + [easJsonPath]: easJsonData, + }); + }); + await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy(); + const vcsClientMock = jest.mocked(new GitClient()); + vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {}); + vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false); + vcsClientMock.trackFileAsync.mockImplementation(async () => {}); + const result = await ensureProjectConfiguredAsync({ + projectDir: '.', + nonInteractive: false, + vcsClient: vcsClientMock as unknown as Client, + }); + expect(result).toBeTruthy(); + await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy(); + const easJson = await EasJsonAccessor.fromProjectPath('.').readAsync(); + expect(easJson.cli?.appVersionSource).toEqual(AppVersionSource.REMOTE); + }); +}); diff --git a/packages/eas-cli/src/build/configure.ts b/packages/eas-cli/src/build/configure.ts index 10682ea829..eacde12149 100644 --- a/packages/eas-cli/src/build/configure.ts +++ b/packages/eas-cli/src/build/configure.ts @@ -1,4 +1,4 @@ -import { EasJson, EasJsonAccessor } from '@expo/eas-json'; +import { AppVersionSource, EasJson, EasJsonAccessor } from '@expo/eas-json'; import chalk from 'chalk'; import fs from 'fs-extra'; @@ -55,6 +55,7 @@ async function configureAsync({ const EAS_JSON_DEFAULT: EasJson = { cli: { version: `>= ${easCliVersion}`, + appVersionSource: AppVersionSource.REMOTE, }, build: { development: { @@ -64,7 +65,9 @@ const EAS_JSON_DEFAULT: EasJson = { preview: { distribution: 'internal', }, - production: {}, + production: { + autoIncrement: true, + }, }, submit: { production: {}, diff --git a/packages/eas-cli/src/build/runBuildAndSubmit.ts b/packages/eas-cli/src/build/runBuildAndSubmit.ts index c3f8546fae..4589448a63 100644 --- a/packages/eas-cli/src/build/runBuildAndSubmit.ts +++ b/packages/eas-cli/src/build/runBuildAndSubmit.ts @@ -58,8 +58,9 @@ import { validateAppVersionRuntimePolicySupportAsync, } from '../project/projectUtils'; import { + ensureAppVersionSourceIsSetAsync, validateAppConfigForRemoteVersionSource, - validateBuildProfileVersionSettings, + validateBuildProfileVersionSettingsAsync, } from '../project/remoteVersionSource'; import { confirmAsync } from '../prompts'; import { runAsync } from '../run/run'; @@ -168,7 +169,12 @@ export async function runBuildAndSubmitAsync( const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } = {}; for (const buildProfile of buildProfiles) { - validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig); + await validateBuildProfileVersionSettingsAsync( + buildProfile, + easJsonCliConfig, + projectDir, + flags + ); const maybeMetadata = await validateCustomBuildConfigAsync({ projectDir, profile: buildProfile.profile, @@ -425,6 +431,25 @@ async function prepareAndStartBuildAsync({ } await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp); + if ( + easJsonCliConfig?.appVersionSource === undefined && + buildProfile.profile.autoIncrement !== 'version' + ) { + if (buildProfile.profile.autoIncrement !== true) { + Log.warn( + `The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore( + 'https://docs.expo.dev/build-reference/app-versions/' + )}` + ); + } else { + const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir); + easJsonCliConfig = await ensureAppVersionSourceIsSetAsync( + easJsonAccessor, + easJsonCliConfig, + flags.nonInteractive + ); + } + } if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) { validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform); } diff --git a/packages/eas-cli/src/build/types.ts b/packages/eas-cli/src/build/types.ts index 4af6ccfe64..a8a8b526cd 100644 --- a/packages/eas-cli/src/build/types.ts +++ b/packages/eas-cli/src/build/types.ts @@ -1,3 +1,9 @@ +import { ResourceClass } from '@expo/eas-json'; +import { LoggerLevel } from '@expo/logger'; + +import { LocalBuildOptions } from './local'; +import { RequestedPlatform } from '../platform'; + export enum BuildStatus { NEW = 'new', IN_QUEUE = 'in-queue', @@ -14,3 +20,20 @@ export enum BuildDistributionType { /** @deprecated Use simulator flag instead */ SIMULATOR = 'simulator', } + +export interface BuildFlags { + requestedPlatform: RequestedPlatform; + profile?: string; + nonInteractive: boolean; + wait: boolean; + clearCache: boolean; + json: boolean; + autoSubmit: boolean; + submitProfile?: string; + localBuildOptions: LocalBuildOptions; + resourceClass?: ResourceClass; + message?: string; + buildLoggerLevel?: LoggerLevel; + freezeCredentials: boolean; + repack: boolean; +} diff --git a/packages/eas-cli/src/commands/project/onboarding.ts b/packages/eas-cli/src/commands/project/onboarding.ts index cf6a1fd4bb..ae03d0e2ca 100644 --- a/packages/eas-cli/src/commands/project/onboarding.ts +++ b/packages/eas-cli/src/commands/project/onboarding.ts @@ -1,6 +1,6 @@ import { ExpoConfig } from '@expo/config-types'; import { Platform } from '@expo/eas-build-job'; -import { EasJson } from '@expo/eas-json'; +import { AppVersionSource, EasJson } from '@expo/eas-json'; import chalk from 'chalk'; import fs from 'fs-extra'; import path from 'path'; @@ -411,6 +411,7 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({ const easJson: EasJson = { cli: { version: `>= ${easCliVersion}`, + appVersionSource: AppVersionSource.REMOTE, }, build: { development: { @@ -431,6 +432,7 @@ async function configureProjectFromBareDefaultExpoTemplateAsync({ }, production: { channel: 'production', + autoIncrement: true, ...easBuildGitHubConfig, }, }, diff --git a/packages/eas-cli/src/project/remoteVersionSource.ts b/packages/eas-cli/src/project/remoteVersionSource.ts index fe6bb31b9e..bb24e84d21 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 { BuildFlags } from '../build/types'; +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); + let easJsonCliConfig = await EasJsonUtils.getCliConfigAsync(easJsonAccessor); + if (easJsonCliConfig?.appVersionSource === undefined) { + easJsonCliConfig = await ensureAppVersionSourceIsSetAsync( + easJsonAccessor, + easJsonCliConfig ?? undefined, + nonInteractive + ); + } if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) { return; } @@ -45,10 +60,31 @@ 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, + flags: BuildFlags +): Promise { + if ( + cliConfig?.appVersionSource === undefined && + profileInfo.profile.autoIncrement !== 'version' + ) { + if (profileInfo.profile.autoIncrement !== true) { + Log.warn( + `The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore( + 'https://docs.expo.dev/build-reference/app-versions/' + )}` + ); + } else { + const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir); + cliConfig = await ensureAppVersionSourceIsSetAsync( + easJsonAccessor, + cliConfig, + flags.nonInteractive + ); + } + } if (cliConfig?.appVersionSource !== AppVersionSource.REMOTE) { return; } @@ -98,3 +134,96 @@ export function getBuildVersionName(platform: Platform): string { return 'buildNumber'; } } + +export async function ensureAppVersionSourceIsSetAsync( + easJsonAccessor: EasJsonAccessor, + easJsonCliConfig: EasJson['cli'] | undefined, + nonInteractive: boolean +): Promise { + let selectOption, updateEasJson; + if (nonInteractive) { + Log.warn( + `The field "cli.appVersionSource" is not set, but it will be required in the future Proceeding with the default "local" value. ${learnMore( + 'https://docs.expo.dev/build-reference/app-versions/' + )}` + ); + selectOption = AppVersionSourceUpdateOption.SET_TO_LOCAL; + updateEasJson = false; + } else { + Log.log( + 'Since EAS CLI version `12.0.0` explicitly specifying app version source is required. Please select your app version source:' + ); + Log.log( + `\t1) With the "local" app version source and "autoIncrement" option enabled, the build number/version code is sourced from local project files and incremented automatically if possible, by editing local project files. ${learnMore( + 'https://docs.expo.dev/build-reference/app-versions/#local-version-source' + )}` + ); + Log.log( + `\t2) With the "remote" app version source and "autoIncrement" option enabled, the build number/version code is stored on EAS servers and updated every time you create a new build. Remote auto-incrementation won't edit the version in the local project files, but instead, the new version will be injected automatically during the build process. ${learnMore( + 'https://docs.expo.dev/build-reference/app-versions/#remote-version-source' + )}` + ); + Log.log( + `Until now, this project has been using the "local" version source (which was the previous default). App version source can now be set for you automatically, or you can configure it manually by setting the "appVersionSource" value in your eas.json.` + ); + + selectOption = await selectAsync(`What would you like to do?`, [ + { + title: 'Update eas.json to use the default "remote" version source (recommended)', + value: AppVersionSourceUpdateOption.SET_TO_REMOTE, + }, + { + title: 'Update eas.json to use "local" version source (old behavior)', + value: AppVersionSourceUpdateOption.SET_TO_LOCAL, + }, + { + title: "Don't update eas.json, abort command and configure manually", + value: AppVersionSourceUpdateOption.ABORT, + }, + ]); + updateEasJson = true; + } + + if (selectOption === AppVersionSourceUpdateOption.SET_TO_LOCAL) { + if (updateEasJson) { + await easJsonAccessor.readRawJsonAsync(); + easJsonAccessor.patch(easJsonRawObject => { + easJsonRawObject.cli = { + ...easJsonRawObject?.cli, + appVersionSource: AppVersionSource.LOCAL, + }; + return easJsonRawObject; + }); + await easJsonAccessor.writeAsync(); + } + if (easJsonCliConfig) { + easJsonCliConfig.appVersionSource = AppVersionSource.LOCAL; + } + Log.withTick('Updated eas.json'); + } else if (selectOption === AppVersionSourceUpdateOption.SET_TO_REMOTE) { + if (updateEasJson) { + await easJsonAccessor.readRawJsonAsync(); + easJsonAccessor.patch(easJsonRawObject => { + easJsonRawObject.cli = { + ...easJsonRawObject?.cli, + appVersionSource: AppVersionSource.REMOTE, + }; + return easJsonRawObject; + }); + await easJsonAccessor.writeAsync(); + } + if (easJsonCliConfig) { + easJsonCliConfig.appVersionSource = AppVersionSource.REMOTE; + } + Log.withTick('Updated eas.json'); + } else { + Log.warn( + `You'll need to configure ${chalk.bold('appVersionSource')} manually. ${learnMore( + 'https://docs.expo.dev/build-reference/app-versions/' + )}` + ); + Errors.error('Aborted.', { exit: 1 }); + } + + return easJsonCliConfig; +}