diff --git a/CHANGELOG.md b/CHANGELOG.md index 553cb79f04..5c87a682a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐ŸŽ‰ New features +- Add `eas credentials:configure-build` subcommand. + ([#2282](https://github.com/expo/eas-cli/pull/2282) by [@fiberjw](https://github.com/fiberjw)) + ### ๐Ÿ› Bug fixes ### ๐Ÿงน Chores @@ -61,7 +64,7 @@ This is the log of notable changes to EAS CLI and related packages. - Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2237](https://github.com/expo/eas-cli/pull/2237) by [@expo-bot](https://github.com/expo-bot)) - Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2240](https://github.com/expo/eas-cli/pull/2240) by [@expo-bot](https://github.com/expo-bot)) - Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2253](https://github.com/expo/eas-cli/pull/2253) by [@expo-bot](https://github.com/expo-bot)) -- Include src/**/build directories in vscode search and replace. ([#2250](https://github.com/expo/eas-cli/pull/2250) by [@wschurman](https://github.com/wschurman)) +- Include src/\*\*/build directories in vscode search and replace. ([#2250](https://github.com/expo/eas-cli/pull/2250) by [@wschurman](https://github.com/wschurman)) - Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies. ([#2259](https://github.com/expo/eas-cli/pull/2259) by [@expo-bot](https://github.com/expo-bot)) ## [7.3.0](https://github.com/expo/eas-cli/releases/tag/v7.3.0) - 2024-02-19 diff --git a/packages/eas-cli/src/commands/credentials/configure-build.ts b/packages/eas-cli/src/commands/credentials/configure-build.ts new file mode 100644 index 0000000000..6d26da79f2 --- /dev/null +++ b/packages/eas-cli/src/commands/credentials/configure-build.ts @@ -0,0 +1,64 @@ +import { Platform } from '@expo/eas-build-job'; +import { Flags } from '@oclif/core'; + +import EasCommand from '../../commandUtils/EasCommand'; +import { SelectBuildProfileFromEasJson } from '../../credentials/manager/SelectBuildProfileFromEasJson'; +import { SetUpBuildCredentialsCommandAction } from '../../credentials/manager/SetUpBuildCredentialsCommandAction'; +import { selectPlatformAsync } from '../../platform'; + +export default class InitializeBuildCredentials extends EasCommand { + static override description = 'Set up credentials for building your project.'; + + static override flags = { + platform: Flags.enum({ + char: 'p', + options: [Platform.ANDROID, Platform.IOS], + }), + profile: Flags.string({ + char: 'e', + description: 'The name of the build profile in eas.json.', + helpValue: 'PROFILE_NAME', + }), + }; + + static override contextDefinition = { + ...this.ContextOptions.LoggedIn, + ...this.ContextOptions.ProjectConfig, + ...this.ContextOptions.DynamicProjectConfig, + ...this.ContextOptions.Analytics, + ...this.ContextOptions.Vcs, + }; + + async runAsync(): Promise { + const { flags } = await this.parse(InitializeBuildCredentials); + const { + loggedIn: { actor, graphqlClient }, + privateProjectConfig, + getDynamicPrivateProjectConfigAsync, + analytics, + vcsClient, + } = await this.getContextAsync(InitializeBuildCredentials, { + nonInteractive: false, + }); + + const platform = await selectPlatformAsync(flags.platform); + + const buildProfile = + flags.profile ?? + (await new SelectBuildProfileFromEasJson( + privateProjectConfig.projectDir, + Platform.IOS + ).getProfileNameFromEasConfigAsync()); + + await new SetUpBuildCredentialsCommandAction( + actor, + graphqlClient, + vcsClient, + analytics, + privateProjectConfig ?? null, + getDynamicPrivateProjectConfigAsync, + platform, + buildProfile + ).runAsync(); + } +} diff --git a/packages/eas-cli/src/commands/credentials.ts b/packages/eas-cli/src/commands/credentials/index.ts similarity index 89% rename from packages/eas-cli/src/commands/credentials.ts rename to packages/eas-cli/src/commands/credentials/index.ts index 56ac736e5f..2acfacb2c6 100644 --- a/packages/eas-cli/src/commands/credentials.ts +++ b/packages/eas-cli/src/commands/credentials/index.ts @@ -1,7 +1,7 @@ import { Flags } from '@oclif/core'; -import EasCommand from '../commandUtils/EasCommand'; -import { SelectPlatform } from '../credentials/manager/SelectPlatform'; +import EasCommand from '../../commandUtils/EasCommand'; +import { SelectPlatform } from '../../credentials/manager/SelectPlatform'; export default class Credentials extends EasCommand { static override description = 'manage credentials'; diff --git a/packages/eas-cli/src/credentials/manager/Actions.ts b/packages/eas-cli/src/credentials/manager/Actions.ts index dfea7a0ffa..6c40fc88e6 100644 --- a/packages/eas-cli/src/credentials/manager/Actions.ts +++ b/packages/eas-cli/src/credentials/manager/Actions.ts @@ -35,6 +35,7 @@ export enum AndroidActionType { SetUpGsaKeyForFcmV1, UpdateCredentialsJson, SetUpBuildCredentialsFromCredentialsJson, + SetUpBuildCredentials, } export enum IosActionType { diff --git a/packages/eas-cli/src/credentials/manager/CheckBuildProfileFlagAgainstEasJson.ts b/packages/eas-cli/src/credentials/manager/CheckBuildProfileFlagAgainstEasJson.ts new file mode 100644 index 0000000000..11e31a8ffa --- /dev/null +++ b/packages/eas-cli/src/credentials/manager/CheckBuildProfileFlagAgainstEasJson.ts @@ -0,0 +1,46 @@ +import { Platform } from '@expo/eas-build-job'; +import { BuildProfile, EasJsonAccessor, EasJsonUtils } from '@expo/eas-json'; + +import Log from '../../log'; + +export class CheckBuildProfileFlagAgainstEasJson { + private easJsonAccessor: EasJsonAccessor; + + constructor( + projectDir: string, + private platform: T, + private profileNameFromFlag: string + ) { + this.easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir); + } + + async runAsync(): Promise> { + const profileName = await this.getProfileNameFromEasConfigAsync(); + const easConfig = await EasJsonUtils.getBuildProfileAsync( + this.easJsonAccessor, + this.platform, + profileName + ); + Log.succeed(`Using build profile: ${profileName}`); + return easConfig; + } + + async getProfileNameFromEasConfigAsync(): Promise { + const buildProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(this.easJsonAccessor); + if (buildProfileNames.length === 0) { + throw new Error( + 'You need at least one iOS build profile declared in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details' + ); + } else if (buildProfileNames.length === 1) { + return buildProfileNames[0]; + } + + if (buildProfileNames.includes(this.profileNameFromFlag)) { + return this.profileNameFromFlag; + } else { + throw new Error( + `Build profile ${this.profileNameFromFlag} does not exist in eas.json. Go to https://docs.expo.dev/build/eas-json/ for more details` + ); + } + } +} diff --git a/packages/eas-cli/src/credentials/manager/ManageAndroid.ts b/packages/eas-cli/src/credentials/manager/ManageAndroid.ts index a0c997ac87..5592861692 100644 --- a/packages/eas-cli/src/credentials/manager/ManageAndroid.ts +++ b/packages/eas-cli/src/credentials/manager/ManageAndroid.ts @@ -34,6 +34,7 @@ import { DownloadKeystore } from '../android/actions/DownloadKeystore'; import { RemoveFcm } from '../android/actions/RemoveFcm'; import { SelectAndRemoveGoogleServiceAccountKey } from '../android/actions/RemoveGoogleServiceAccountKey'; import { RemoveKeystore } from '../android/actions/RemoveKeystore'; +import { SetUpBuildCredentials } from '../android/actions/SetUpBuildCredentials'; import { SetUpBuildCredentialsFromCredentialsJson } from '../android/actions/SetUpBuildCredentialsFromCredentialsJson'; import { SetUpGoogleServiceAccountKeyForFcmV1 } from '../android/actions/SetUpGoogleServiceAccountKeyForFcmV1'; import { SetUpGoogleServiceAccountKeyForSubmissions } from '../android/actions/SetUpGoogleServiceAccountKeyForSubmissions'; @@ -48,8 +49,8 @@ import { AndroidPackageNotDefinedError } from '../errors'; export class ManageAndroid { constructor( - private callingAction: Action, - private projectDir: string + protected callingAction: Action, + protected projectDir: string ) {} async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise { @@ -164,7 +165,7 @@ export class ManageAndroid { } } - private async createProjectContextAsync( + protected async createProjectContextAsync( ctx: CredentialsContext, buildProfile: BuildProfile ): Promise { @@ -172,7 +173,7 @@ export class ManageAndroid { return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile, ctx.vcsClient); } - private async runProjectSpecificActionAsync( + protected async runProjectSpecificActionAsync( ctx: CredentialsContext, action: AndroidActionType, gradleContext?: GradleBuildContext @@ -239,6 +240,8 @@ export class ManageAndroid { } } else if (action === AndroidActionType.SetUpBuildCredentialsFromCredentialsJson) { await new SetUpBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx); + } else if (action === AndroidActionType.SetUpBuildCredentials) { + await new SetUpBuildCredentials({ app: appLookupParams }).runAsync(ctx); } } } diff --git a/packages/eas-cli/src/credentials/manager/ManageIos.ts b/packages/eas-cli/src/credentials/manager/ManageIos.ts index c70e642350..818685b276 100644 --- a/packages/eas-cli/src/credentials/manager/ManageIos.ts +++ b/packages/eas-cli/src/credentials/manager/ManageIos.ts @@ -56,8 +56,8 @@ import { displayIosCredentials } from '../ios/utils/printCredentials'; export class ManageIos { constructor( - private callingAction: Action, - private projectDir: string + protected callingAction: Action, + protected projectDir: string ) {} async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise { @@ -182,7 +182,7 @@ export class ManageIos { } } - private async createProjectContextAsync( + protected async createProjectContextAsync( ctx: CredentialsContext, account: AccountFragment, buildProfile: BuildProfile @@ -215,7 +215,7 @@ export class ManageIos { }; } - private async runAccountSpecificActionAsync( + protected async runAccountSpecificActionAsync( ctx: CredentialsContext, account: AccountFragment, action: IosActionType @@ -235,7 +235,7 @@ export class ManageIos { } } - private async runProjectSpecificActionAsync( + protected async runProjectSpecificActionAsync( ctx: CredentialsContext, app: App, targets: Target[], @@ -396,7 +396,7 @@ export class ManageIos { } } - private async setupProvisioningProfileWithSpecificDistCertAsync( + protected async setupProvisioningProfileWithSpecificDistCertAsync( ctx: CredentialsContext, target: Target, appLookupParams: AppLookupParams, @@ -419,7 +419,7 @@ export class ManageIos { } } - private async selectTargetAsync(targets: Target[]): Promise { + protected async selectTargetAsync(targets: Target[]): Promise { if (targets.length === 1) { return targets[0]; } diff --git a/packages/eas-cli/src/credentials/manager/SetUpAndroidBuildCredentials.ts b/packages/eas-cli/src/credentials/manager/SetUpAndroidBuildCredentials.ts new file mode 100644 index 0000000000..613025d336 --- /dev/null +++ b/packages/eas-cli/src/credentials/manager/SetUpAndroidBuildCredentials.ts @@ -0,0 +1,60 @@ +import { Platform } from '@expo/eas-build-job'; +import assert from 'assert'; + +import { AndroidActionType } from './Actions'; +import { CheckBuildProfileFlagAgainstEasJson } from './CheckBuildProfileFlagAgainstEasJson'; +import { Action } from './HelperActions'; +import { ManageAndroid } from './ManageAndroid'; +import { CredentialsContext, CredentialsContextProjectInfo } from '../context'; + +export class SetUpAndroidBuildCredentials extends ManageAndroid { + constructor( + callingAction: Action, + projectDir: string, + private setUpBuildCredentialsWithProfileNameFromFlag: string + ) { + super(callingAction, projectDir); + } + + override async runAsync(): Promise { + const hasProjectContext = !!this.callingAction.projectInfo; + const buildProfile = hasProjectContext + ? await new CheckBuildProfileFlagAgainstEasJson( + this.projectDir, + Platform.ANDROID, + this.setUpBuildCredentialsWithProfileNameFromFlag + ).runAsync() + : null; + let projectInfo: CredentialsContextProjectInfo | null = null; + if (hasProjectContext) { + const { exp, projectId } = await this.callingAction.getDynamicPrivateProjectConfigAsync({ + env: buildProfile?.env, + }); + projectInfo = { exp, projectId }; + } + const ctx = new CredentialsContext({ + projectDir: process.cwd(), + projectInfo, + user: this.callingAction.actor, + graphqlClient: this.callingAction.graphqlClient, + analytics: this.callingAction.analytics, + env: buildProfile?.env, + nonInteractive: false, + vcsClient: this.callingAction.vcsClient, + }); + + let gradleContext; + if (ctx.hasProjectContext) { + assert(buildProfile, 'buildProfile must be defined in a project context'); + gradleContext = await this.createProjectContextAsync(ctx, buildProfile); + } + + if (this.setUpBuildCredentialsWithProfileNameFromFlag) { + await this.runProjectSpecificActionAsync( + ctx, + AndroidActionType.SetUpBuildCredentials, + gradleContext + ); + } + } +} diff --git a/packages/eas-cli/src/credentials/manager/SetUpBuildCredentialsCommandAction.ts b/packages/eas-cli/src/credentials/manager/SetUpBuildCredentialsCommandAction.ts new file mode 100644 index 0000000000..53e8357488 --- /dev/null +++ b/packages/eas-cli/src/credentials/manager/SetUpBuildCredentialsCommandAction.ts @@ -0,0 +1,30 @@ +import { Platform } from '@expo/eas-build-job'; + +import { Analytics } from '../../analytics/AnalyticsManager'; +import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField'; +import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; +import { Actor } from '../../user/User'; +import { Client } from '../../vcs/vcs'; +import { CredentialsContextProjectInfo } from '../context'; +import { SetUpAndroidBuildCredentials } from '../manager/SetUpAndroidBuildCredentials'; +import { SetUpIosBuildCredentials } from '../manager/SetUpIosBuildCredentials'; + +export class SetUpBuildCredentialsCommandAction { + constructor( + public readonly actor: Actor, + public readonly graphqlClient: ExpoGraphqlClient, + public readonly vcsClient: Client, + public readonly analytics: Analytics, + public readonly projectInfo: CredentialsContextProjectInfo | null, + public readonly getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn, + private readonly platform: Platform, + private readonly profileName: string + ) {} + + async runAsync(): Promise { + if (this.platform === Platform.IOS) { + return await new SetUpIosBuildCredentials(this, process.cwd(), this.profileName).runAsync(); + } + return await new SetUpAndroidBuildCredentials(this, process.cwd(), this.profileName).runAsync(); + } +} diff --git a/packages/eas-cli/src/credentials/manager/SetUpIosBuildCredentials.ts b/packages/eas-cli/src/credentials/manager/SetUpIosBuildCredentials.ts new file mode 100644 index 0000000000..12e9c0142f --- /dev/null +++ b/packages/eas-cli/src/credentials/manager/SetUpIosBuildCredentials.ts @@ -0,0 +1,78 @@ +import { Platform } from '@expo/eas-build-job'; +import assert from 'assert'; +import nullthrows from 'nullthrows'; + +import { IosActionType } from './Actions'; +import { CheckBuildProfileFlagAgainstEasJson } from './CheckBuildProfileFlagAgainstEasJson'; +import { Action } from './HelperActions'; +import { ManageIos } from './ManageIos'; +import { AccountFragment } from '../../graphql/generated'; +import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils'; +import { ensureActorHasPrimaryAccount } from '../../user/actions'; +import { CredentialsContext, CredentialsContextProjectInfo } from '../context'; + +export class SetUpIosBuildCredentials extends ManageIos { + constructor( + callingAction: Action, + projectDir: string, + private setUpBuildCredentialsWithProfileNameFromFlag: string + ) { + super(callingAction, projectDir); + } + + override async runAsync(): Promise { + const buildProfile = this.callingAction.projectInfo + ? await new CheckBuildProfileFlagAgainstEasJson( + this.projectDir, + Platform.IOS, + this.setUpBuildCredentialsWithProfileNameFromFlag + ).runAsync() + : null; + + let projectInfo: CredentialsContextProjectInfo | null = null; + if (this.callingAction.projectInfo) { + const { exp, projectId } = await this.callingAction.getDynamicPrivateProjectConfigAsync({ + env: buildProfile?.env, + }); + projectInfo = { exp, projectId }; + } + + const ctx = new CredentialsContext({ + projectDir: process.cwd(), + projectInfo, + user: this.callingAction.actor, + graphqlClient: this.callingAction.graphqlClient, + analytics: this.callingAction.analytics, + env: buildProfile?.env, + nonInteractive: false, + vcsClient: this.callingAction.vcsClient, + }); + + await ctx.bestEffortAppStoreAuthenticateAsync(); + + const getAccountForProjectAsync = async (projectId: string): Promise => { + return await getOwnerAccountForProjectIdAsync(ctx.graphqlClient, projectId); + }; + + const account = ctx.hasProjectContext + ? await getAccountForProjectAsync(ctx.projectId) + : ensureActorHasPrimaryAccount(ctx.user); + + let app = null; + let targets = null; + if (ctx.hasProjectContext) { + assert(buildProfile, 'buildProfile must be defined in project context'); + const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile); + app = projectContext.app; + targets = projectContext.targets; + } + + await this.runProjectSpecificActionAsync( + ctx, + nullthrows(app, 'app must be defined in project context'), + nullthrows(targets, 'targets must be defined in project context'), + nullthrows(buildProfile, 'buildProfile must be defined in project context'), + IosActionType.SetUpBuildCredentials + ); + } +}