diff --git a/packages/eas-cli/src/commands/update/configure.ts b/packages/eas-cli/src/commands/update/configure.ts index 3af4478a17..f070293d4f 100644 --- a/packages/eas-cli/src/commands/update/configure.ts +++ b/packages/eas-cli/src/commands/update/configure.ts @@ -6,6 +6,7 @@ import chalk from 'chalk'; import { getEASUpdateURL } from '../../api'; import EasCommand from '../../commandUtils/EasCommand'; +import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { AppPlatform } from '../../graphql/generated'; import Log, { learnMore } from '../../log'; import { RequestedPlatform, appPlatformDisplayNames } from '../../platform'; @@ -13,7 +14,7 @@ import { installExpoUpdatesAsync, isExpoUpdatesInstalledOrAvailable, } from '../../project/projectUtils'; -import { resolveWorkflowAsync } from '../../project/workflow'; +import { resolveWorkflowPerPlatformAsync } from '../../project/workflow'; import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule'; import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule'; @@ -54,38 +55,24 @@ export default class UpdateConfigure extends EasCommand { await installExpoUpdatesAsync(projectDir); } - const [androidWorkflow, iosWorkflow] = await Promise.all([ - resolveWorkflowAsync(projectDir, Platform.ANDROID), - resolveWorkflowAsync(projectDir, Platform.IOS), - ]); + const workflows = await resolveWorkflowPerPlatformAsync(projectDir); const updatedExp = await configureAppJSONForEASUpdateAsync({ projectDir, + projectId, exp, platform, - workflows: { - android: androidWorkflow, - ios: iosWorkflow, - }, - projectId, + workflows, }); - Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`); - // configure native files for EAS Update - if ( - [RequestedPlatform.Android, RequestedPlatform.All].includes(platform) && - androidWorkflow === Workflow.GENERIC - ) { - await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId); - Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`); - } - if ( - [RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) && - iosWorkflow === Workflow.GENERIC - ) { - await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, updatedExp, projectId); - Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`); - } + await configureNativeFilesForEASUpdateAsync({ + projectDir, + projectId, + exp: updatedExp, + platform, + workflows, + graphqlClient, + }); Log.addNewLineIfNone(); Log.warn( @@ -97,31 +84,31 @@ export default class UpdateConfigure extends EasCommand { } } -async function configureAppJSONForEASUpdateAsync({ +type ConfigureForEASUpdateArgs = { + projectDir: string; + exp: ExpoConfig; + platform: RequestedPlatform; + workflows: Record; + projectId: string; +}; + +export async function configureAppJSONForEASUpdateAsync({ projectDir, exp, platform, workflows, projectId, -}: { - projectDir: string; - exp: ExpoConfig; - platform: RequestedPlatform; - workflows: { - [key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow; - }; - projectId: string; -}): Promise { +}: ConfigureForEASUpdateArgs): Promise { // this command is non-interactive in the way it was designed const easUpdateURL = getEASUpdateURL(projectId); const updates = { ...exp.updates, url: easUpdateURL }; const androidDefaultRuntimeVersion = - workflows['android'] === Workflow.GENERIC + workflows.android === Workflow.GENERIC ? DEFAULT_BARE_RUNTIME_VERSION : DEFAULT_MANAGED_RUNTIME_VERSION; const iosDefaultRuntimeVersion = - workflows['ios'] === Workflow.GENERIC + workflows.ios === Workflow.GENERIC ? DEFAULT_BARE_RUNTIME_VERSION : DEFAULT_MANAGED_RUNTIME_VERSION; @@ -282,9 +269,9 @@ async function configureAppJSONForEASUpdateAsync({ )}` ); Log.warn( - `In order to finish configuring your project for EAS Update, you are going to need manually add the following to your app.config.js:\n${learnMore( - 'https://expo.fyi/eas-update-config.md' - )}\n` + `In order to finish configuring your project for EAS Update, you are going to need manually add the following to your ${chalk.bold( + 'app.config.js' + )}:\n${learnMore('https://expo.fyi/eas-update-config.md')}\n` ); Log.log(chalk.bold(JSON.stringify(newConfigOnlyAddedValues, null, 2))); Log.addNewLineIfNone(); @@ -305,9 +292,35 @@ async function configureAppJSONForEASUpdateAsync({ } assert(result.config, 'A successful result should have a config'); + Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`); + return result.config.expo; } +export async function configureNativeFilesForEASUpdateAsync({ + projectDir, + exp, + platform, + workflows, + projectId, + graphqlClient, +}: ConfigureForEASUpdateArgs & { graphqlClient: ExpoGraphqlClient }): Promise { + if ( + [RequestedPlatform.Android, RequestedPlatform.All].includes(platform) && + workflows.android === Workflow.GENERIC + ) { + await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId); + Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`); + } + if ( + [RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) && + workflows.ios === Workflow.GENERIC + ) { + await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId); + Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`); + } +} + function isRuntimeEqual( runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' }, runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' } diff --git a/packages/eas-cli/src/commands/update/index.ts b/packages/eas-cli/src/commands/update/index.ts index abdde68999..ec1eb9fd6a 100644 --- a/packages/eas-cli/src/commands/update/index.ts +++ b/packages/eas-cli/src/commands/update/index.ts @@ -11,6 +11,7 @@ import { selectBranchOnAppAsync } from '../../branch/queries'; import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils'; import { getUpdateGroupUrl } from '../../build/utils/url'; import EasCommand from '../../commandUtils/EasCommand'; +import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField'; import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { getPaginatedQueryOptions } from '../../commandUtils/pagination'; @@ -28,6 +29,7 @@ import { BranchQuery } from '../../graphql/queries/BranchQuery'; import { UpdateQuery } from '../../graphql/queries/UpdateQuery'; import Log, { learnMore, link } from '../../log'; import { ora } from '../../ora'; +import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform'; import { getOwnerAccountForProjectIdAsync, installExpoUpdatesAsync, @@ -41,8 +43,8 @@ import { isUploadedAssetCountAboveWarningThreshold, uploadAssetsAsync, } from '../../project/publish'; -import { resolveWorkflowAsync } from '../../project/workflow'; -import { confirmAsync, promptAsync } from '../../prompts'; +import { resolveWorkflowAsync, resolveWorkflowPerPlatformAsync } from '../../project/workflow'; +import { confirmAsync, promptAsync, selectAsync } from '../../prompts'; import { selectUpdateGroupOnBranchAsync } from '../../update/queries'; import { formatUpdateMessage } from '../../update/utils'; import { @@ -57,6 +59,10 @@ import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService'; import { getVcsClient } from '../../vcs'; import { createUpdateBranchOnAppAsync } from '../branch/create'; import { createUpdateChannelOnAppAsync } from '../channel/create'; +import { + configureAppJSONForEASUpdateAsync, + configureNativeFilesForEASUpdateAsync, +} from './configure'; export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios']; export type PublishPlatformFlag = PublishPlatform | 'all'; @@ -209,7 +215,11 @@ export default class UpdatePublish extends EasCommand { // If a group was specified, that means we are republishing it. republish = republish || !!group; - const { exp, projectId, projectDir } = await getDynamicProjectConfigAsync({ + const { + exp: expBeforeRuntimeVersionUpdate, + projectId, + projectDir, + } = await getDynamicProjectConfigAsync({ isPublicConfig: true, }); @@ -221,7 +231,10 @@ export default class UpdatePublish extends EasCommand { const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath); - const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable(projectDir, exp.sdkVersion); + const hasExpoUpdates = isExpoUpdatesInstalledOrAvailable( + projectDir, + expBeforeRuntimeVersionUpdate.sdkVersion + ); if (!hasExpoUpdates && nonInteractive) { Errors.error( `${chalk.bold( @@ -245,7 +258,16 @@ export default class UpdatePublish extends EasCommand { } } - const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir); + const [runtimeVersions, exp] = await getRuntimeVersionObjectAsync( + expBeforeRuntimeVersionUpdate, + platformFlag, + projectDir, + projectId, + nonInteractive, + graphqlClient, + getDynamicProjectConfigAsync + ); + await checkEASUpdateURLIsSetAsync(exp, projectId); if (!branchName) { @@ -327,7 +349,7 @@ export default class UpdatePublish extends EasCommand { if (updatesToRepublishFilteredByPlatform.length !== defaultPublishPlatforms.length) { Log.warn(`You are republishing an update that wasn't published for all platforms.`); } - publicationPlatformMessage = `The republished update will appear on the same plaforms it was originally published on: ${updatesToRepublishFilteredByPlatform + publicationPlatformMessage = `The republished update will appear on the same platforms it was originally published on: ${updatesToRepublishFilteredByPlatform .map(update => update.platform) .join(', ')}`; } else { @@ -579,11 +601,29 @@ export default class UpdatePublish extends EasCommand { } } +function transformRuntimeVersions(exp: ExpoConfig, platforms: Platform[]): Record { + return Object.fromEntries( + platforms.map(platform => [ + platform, + nullthrows( + Updates.getRuntimeVersion(exp, platform), + `Unable to determine runtime version for ${ + requestedPlatformDisplayNames[platform] + }. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}` + ), + ]) + ); +} + async function getRuntimeVersionObjectAsync( exp: ExpoConfig, platformFlag: PublishPlatformFlag, - projectDir: string -): Promise> { + projectDir: string, + projectId: string, + nonInteractive: boolean, + graphqlClient: ExpoGraphqlClient, + getDynamicProjectConfigAsync: DynamicConfigContextFn +): Promise<[Record, ExpoConfig]> { const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[]; for (const platform of platforms) { @@ -598,15 +638,68 @@ async function getRuntimeVersionObjectAsync( } } - return Object.fromEntries( - platforms.map(platform => [ - platform, - nullthrows( - Updates.getRuntimeVersion(exp, platform), - `Unable to determine runtime version for ${platform}` - ), - ]) - ); + try { + return [transformRuntimeVersions(exp, platforms), exp]; + } catch (error: any) { + if (nonInteractive) { + throw error; + } + + Log.fail(error.message); + + const runConfig = await selectAsync( + `Configure runtime version in ${chalk.bold('app.json')} automatically for EAS Update?`, + [ + { title: 'Yes', value: true }, + { + title: 'No, I will set the runtime version manually (EAS CLI exits)', + value: false, + }, + ] + ); + + if (!runConfig) { + Errors.exit(1); + } + + const workflows = await resolveWorkflowPerPlatformAsync(projectDir); + await configureAppJSONForEASUpdateAsync({ + exp, + projectDir, + projectId, + platform: platformFlag as RequestedPlatform, + workflows, + }); + + const newConfig: ExpoConfig = (await getDynamicProjectConfigAsync({ isPublicConfig: true })) + .exp; + + await configureNativeFilesForEASUpdateAsync({ + exp: newConfig, + projectDir, + projectId, + platform: platformFlag as RequestedPlatform, + workflows, + graphqlClient, + }); + + const continueWithChanges = await selectAsync( + `Continue update process with uncommitted changes in repository?`, + [ + { title: 'Yes', value: true }, + { + title: 'No, I will commit the modified files first (EAS CLI exits)', + value: false, + }, + ] + ); + + if (!continueWithChanges) { + Errors.exit(1); + } + + return [transformRuntimeVersions(newConfig, platforms), newConfig]; + } } async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig, projectId: string): Promise { diff --git a/packages/eas-cli/src/log.ts b/packages/eas-cli/src/log.ts index bd7f8cd449..48d29ea822 100644 --- a/packages/eas-cli/src/log.ts +++ b/packages/eas-cli/src/log.ts @@ -45,6 +45,10 @@ export default class Log { Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`); } + public static fail(message: string): void { + Log.log(`${chalk.red(logSymbols.error)} ${message}`); + } + public static succeed(message: string): void { Log.log(`${chalk.green(logSymbols.success)} ${message}`); } diff --git a/packages/eas-cli/src/project/__tests__/workflow-test.ts b/packages/eas-cli/src/project/__tests__/workflow-test.ts index cad0fa1b36..ec70813a60 100644 --- a/packages/eas-cli/src/project/__tests__/workflow-test.ts +++ b/packages/eas-cli/src/project/__tests__/workflow-test.ts @@ -1,17 +1,17 @@ import { Platform, Workflow } from '@expo/eas-build-job'; import { vol } from 'memfs'; -import { resolveWorkflowAsync } from '../workflow'; +import { resolveWorkflowAsync, resolveWorkflowPerPlatformAsync } from '../workflow'; jest.mock('fs'); +const projectDir = '/app'; + describe(resolveWorkflowAsync, () => { beforeEach(() => { vol.reset(); }); - const projectDir = '/app'; - test('bare workflow for both platforms', async () => { vol.fromJSON( { @@ -57,3 +57,24 @@ describe(resolveWorkflowAsync, () => { await expect(resolveWorkflowAsync(projectDir, Platform.IOS)).resolves.toBe(Workflow.MANAGED); }); }); + +describe(resolveWorkflowPerPlatformAsync, () => { + beforeEach(() => { + vol.reset(); + }); + + test('returns the correct combined value', async () => { + vol.fromJSON( + { + './ios/helloworld.xcodeproj/project.pbxproj': 'fake', + './android/app/src/main/AndroidManifest.xml': 'fake', + }, + projectDir + ); + + await expect(resolveWorkflowPerPlatformAsync(projectDir)).resolves.toEqual({ + android: Workflow.GENERIC, + ios: Workflow.GENERIC, + }); + }); +}); diff --git a/packages/eas-cli/src/project/workflow.ts b/packages/eas-cli/src/project/workflow.ts index aed70cd644..b964f4aaae 100644 --- a/packages/eas-cli/src/project/workflow.ts +++ b/packages/eas-cli/src/project/workflow.ts @@ -34,3 +34,13 @@ export async function resolveWorkflowAsync( } return Workflow.MANAGED; } + +export async function resolveWorkflowPerPlatformAsync( + projectDir: string +): Promise> { + const [android, ios] = await Promise.all([ + resolveWorkflowAsync(projectDir, Platform.ANDROID), + resolveWorkflowAsync(projectDir, Platform.IOS), + ]); + return { android, ios }; +}