diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a784f6c3e..a5790d913e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ This is the log of notable changes to EAS CLI and related packages. - Add more build metadata (release channel, build profile name, git commit hash). ([#265](https://github.com/expo/eas-cli/pull/265) by [@dsokal](https://github.com/dsokal)) - Display App Store link after successful submission. ([#144](https://github.com/expo/eas-cli/pull/144) by [@barthap](https://github.com/barthap)) - Add `experimental.disableIosBundleIdentifierValidation` flag to eas.json. ([#263](https://github.com/expo/eas-cli/pull/263) by [@wkozyra95](https://github.com/wkozyra95)) +- Support internal distribution in non-interactive builds. ([#269](https://github.com/expo/eas-cli/pull/269) by [@dsokal](https://github.com/dsokal)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts b/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts index a20739ff24..ea3493d1f9 100644 --- a/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts +++ b/packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts @@ -1,7 +1,7 @@ import { Platform } from '@expo/eas-build-job'; import { CredentialsSource, iOSDistributionType } from '@expo/eas-json'; -import { IosDistributionType } from '../../graphql/generated'; +import { AppleTeamFragment, IosDistributionType } from '../../graphql/generated'; import Log from '../../log'; import { findAccountByName } from '../../user/Account'; import { CredentialsManager } from '../CredentialsManager'; @@ -10,6 +10,8 @@ import { Context } from '../context'; import * as credentialsJsonReader from '../credentialsJson/read'; import type { IosCredentials } from '../credentialsJson/read'; import { SetupBuildCredentials } from './actions/SetupBuildCredentials'; +import { resolveAppleTeamIfAuthenticatedAsync } from './actions/new/AppleTeamUtils'; +import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from './errors'; import { isAdHocProfile } from './utils/provisioningProfile'; export { IosCredentials }; @@ -183,31 +185,36 @@ export default class IosCredentialsProvider implements CredentialsProvider { projectName: this.options.app.projectName, }; - // for now, let's require the user to authenticate with Apple - const { team } = await this.ctx.appStore.ensureAuthenticatedAsync(); - const appleTeam = await this.ctx.newIos.createOrGetExistingAppleTeamAsync(appLookupParams, { - appleTeamIdentifier: team.id, - appleTeamName: team.name, - }); - const [distCert, provisioningProfile] = await Promise.all([ - this.ctx.newIos.getDistributionCertificateForAppAsync( - appLookupParams, - appleTeam, - IosDistributionType.AdHoc - ), - this.ctx.newIos.getProvisioningProfileAsync( - appLookupParams, - appleTeam, - IosDistributionType.AdHoc - ), - ]); - return { - provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined, - distributionCertificate: { - certP12: distCert?.certificateP12 ?? undefined, - certPassword: distCert?.certificatePassword ?? undefined, - }, - }; + let appleTeam: AppleTeamFragment | null = null; + if (!this.ctx.nonInteractive) { + await this.ctx.appStore.ensureAuthenticatedAsync(); + appleTeam = await resolveAppleTeamIfAuthenticatedAsync(this.ctx, appLookupParams); + } + + try { + const [distCert, provisioningProfile] = await Promise.all([ + this.ctx.newIos.getDistributionCertificateForAppAsync( + appLookupParams, + IosDistributionType.AdHoc, + { appleTeam } + ), + this.ctx.newIos.getProvisioningProfileAsync(appLookupParams, IosDistributionType.AdHoc, { + appleTeam, + }), + ]); + return { + provisioningProfile: provisioningProfile?.provisioningProfile ?? undefined, + distributionCertificate: { + certP12: distCert?.certificateP12 ?? undefined, + certPassword: distCert?.certificatePassword ?? undefined, + }, + }; + } catch (err) { + if (err instanceof AppleTeamMissingError && this.ctx.nonInteractive) { + throw new MissingCredentialsNonInteractiveError(); + } + throw err; + } } else { const [distCert, provisioningProfile] = await Promise.all([ this.ctx.ios.getDistributionCertificateAsync(this.options.app), diff --git a/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts b/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts index 33f44f2751..0788596526 100644 --- a/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts +++ b/packages/eas-cli/src/credentials/ios/actions/SetupBuildCredentials.ts @@ -37,11 +37,6 @@ export class SetupBuildCredentials implements Action { if (!account) { throw new Error(`You do not have access to the ${this.app.accountName} account`); } - if (ctx.nonInteractive) { - throw new Error('Internal distribution builds are not supported in non-interactive mode'); - } - // for now, let's require the user to authenticate with Apple - await ctx.appStore.ensureAuthenticatedAsync(); const action = new SetupAdhocProvisioningProfile({ account, projectName: this.app.projectName, diff --git a/packages/eas-cli/src/credentials/ios/actions/new/AppleTeamUtils.ts b/packages/eas-cli/src/credentials/ios/actions/new/AppleTeamUtils.ts new file mode 100644 index 0000000000..70562bf1d9 --- /dev/null +++ b/packages/eas-cli/src/credentials/ios/actions/new/AppleTeamUtils.ts @@ -0,0 +1,16 @@ +import { AppleTeamFragment } from '../../../../graphql/generated'; +import { Context } from '../../../context'; +import { AppLookupParams } from '../../api/GraphqlClient'; + +export async function resolveAppleTeamIfAuthenticatedAsync( + ctx: Context, + app: AppLookupParams +): Promise { + if (!ctx.appStore.authCtx) { + return null; + } + return await ctx.newIos.createOrGetExistingAppleTeamAsync(app, { + appleTeamIdentifier: ctx.appStore.authCtx.team.id, + appleTeamName: ctx.appStore.authCtx.team.name, + }); +} diff --git a/packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts b/packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts index 161a177b42..e295b08150 100644 --- a/packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts +++ b/packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts @@ -6,6 +6,7 @@ import { IosAppBuildCredentialsFragment, IosDistributionType, } from '../../../../graphql/generated'; +import Log from '../../../../log'; import { confirmAsync } from '../../../../prompts'; import { Action, CredentialsManager } from '../../../CredentialsManager'; import { Context } from '../../../context'; @@ -15,6 +16,8 @@ import { AppleDeviceFragmentWithAppleTeam } from '../../api/graphql/queries/Appl import { AppleProvisioningProfileQueryResult } from '../../api/graphql/queries/AppleProvisioningProfileQuery'; import { ProvisioningProfileStoreInfo } from '../../appstore/Credentials.types'; import { ProfileClass } from '../../appstore/provisioningProfile'; +import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors'; +import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils'; import { chooseDevices } from './DeviceUtils'; import { doUDIDsMatch, isDevPortalAdhocProfileValid } from './ProvisioningProfileUtils'; import { SetupDistributionCertificate } from './SetupDistributionCertificate'; @@ -33,15 +36,55 @@ export class SetupAdhocProvisioningProfile implements Action { } async runAsync(manager: CredentialsManager, ctx: Context): Promise { - const authCtx = await ctx.appStore.ensureAuthenticatedAsync(); + if (ctx.nonInteractive) { + try { + await this.runNonInteractiveAsync(manager, ctx); + } catch (err) { + if (err instanceof AppleTeamMissingError) { + throw new MissingCredentialsNonInteractiveError(); + } + throw err; + } + } else { + await this.runInteractiveAsync(manager, ctx); + } + } - // 0. Fetch apple team object - const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, { - appleTeamIdentifier: authCtx.team.id, - appleTeamName: authCtx.team.name, - }); + private async runNonInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise { + // 1. Setup Distribution Certificate + const distCertAction = new SetupDistributionCertificate(this.app); + await manager.runActionAsync(distCertAction); + + // 2. Fetch profile from EAS servers + const currentProfile = await ctx.newIos.getProvisioningProfileAsync( + this.app, + IosDistributionType.AdHoc + ); - // 1. Fetch devices registered on EAS servers. + if (!currentProfile) { + throw new MissingCredentialsNonInteractiveError(); + } + + // TODO: implement validation + Log.warn( + 'Provisioning Profile is not validated for non-interactive internal distribution builds.' + ); + + // app credentials should exist here because the profile exists + const appCredentials = nullthrows( + await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync(this.app, { + iosDistributionType: IosDistributionType.AdHoc, + }) + ); + this._iosAppBuildCredentials = appCredentials.iosAppBuildCredentialsArray[0]; + } + + private async runInteractiveAsync(manager: CredentialsManager, ctx: Context): Promise { + // 0. Ensure the user is authenticated with Apple and resolve the Apple team object + await ctx.appStore.ensureAuthenticatedAsync(); + const appleTeam = nullthrows(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)); + + // 1. Fetch devices registered on EAS servers const registeredAppleDevices = await ctx.newIos.getDevicesForAppleTeamAsync( this.app, appleTeam @@ -58,17 +101,17 @@ export class SetupAdhocProvisioningProfile implements Action { await manager.runActionAsync(distCertAction); const distCert = distCertAction.distributionCertificate; - // 3. Fetch profile from EAS servers - let currentProfileFromExpoServers = (await ctx.newIos.getProvisioningProfileAsync( - this.app, + let profileFromExpoServersToUse: + | AppleProvisioningProfileQueryResult + | AppleProvisioningProfileMutationResult + | null = await ctx.newIos.getProvisioningProfileAsync(this.app, IosDistributionType.AdHoc, { appleTeam, - IosDistributionType.AdHoc - )) as AppleProvisioningProfileQueryResult | AppleProvisioningProfileMutationResult | null; + }); // 4. Choose devices for internal distribution let chosenDevices: AppleDeviceFragmentWithAppleTeam[]; - if (currentProfileFromExpoServers) { - const appleDeviceIdentifiersFromProfile = ((currentProfileFromExpoServers.appleDevices ?? + if (profileFromExpoServersToUse) { + const appleDeviceIdentifiersFromProfile = ((profileFromExpoServersToUse.appleDevices ?? []) as AppleDevice[]).map(({ identifier }) => identifier); let shouldAskToChooseDevices = true; @@ -110,10 +153,10 @@ export class SetupAdhocProvisioningProfile implements Action { this.app, appleTeam ); - if (currentProfileFromExpoServers) { + if (profileFromExpoServersToUse) { if (profileWasRecreated) { - currentProfileFromExpoServers = await ctx.newIos.updateProvisioningProfileAsync( - currentProfileFromExpoServers.id, + profileFromExpoServersToUse = await ctx.newIos.updateProvisioningProfileAsync( + profileFromExpoServersToUse.id, { appleProvisioningProfile: profileToUse.provisioningProfile, developerPortalIdentifier: profileToUse.provisioningProfileId, @@ -121,7 +164,7 @@ export class SetupAdhocProvisioningProfile implements Action { ); } } else { - currentProfileFromExpoServers = await ctx.newIos.createProvisioningProfileAsync( + profileFromExpoServersToUse = await ctx.newIos.createProvisioningProfileAsync( this.app, appleAppIdentifier, { @@ -138,7 +181,7 @@ export class SetupAdhocProvisioningProfile implements Action { appleTeam, appleAppIdentifierId: appleAppIdentifier.id, appleDistributionCertificateId: distCert.id, - appleProvisioningProfileId: currentProfileFromExpoServers.id, + appleProvisioningProfileId: profileFromExpoServersToUse.id, iosDistributionType: IosDistributionType.AdHoc, } ); diff --git a/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts b/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts index 4d393a54e2..280da6f2a6 100644 --- a/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts +++ b/packages/eas-cli/src/credentials/ios/actions/new/SetupDistributionCertificate.ts @@ -13,6 +13,8 @@ import { Context } from '../../../context'; import { AppLookupParams } from '../../api/GraphqlClient'; import { AppleDistributionCertificateMutationResult } from '../../api/graphql/mutations/AppleDistributionCertificateMutation'; import { getValidCertSerialNumbers } from '../../appstore/CredentialsUtils'; +import { AppleTeamMissingError, MissingCredentialsNonInteractiveError } from '../../errors'; +import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils'; import { CreateDistributionCertificate } from './CreateDistributionCertificate'; import { formatDistributionCertificate } from './DistributionCertificateUtils'; @@ -31,21 +33,48 @@ export class SetupDistributionCertificate implements Action { } public async runAsync(manager: CredentialsManager, ctx: Context): Promise { - assert( - ctx.appStore.authCtx, - 'authCtx is defined in this context - enforced by ensureAuthenticatedAsync call in SetupBuildCredentials' - ); - const appleTeam = await ctx.newIos.createOrGetExistingAppleTeamAsync(this.app, { - appleTeamIdentifier: ctx.appStore.authCtx.team.id, - appleTeamName: ctx.appStore.authCtx.team.name, - }); + const appleTeam = await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app); + + try { + const currentCertificate = await ctx.newIos.getDistributionCertificateForAppAsync( + this.app, + IosDistributionType.AdHoc, + { appleTeam } + ); + + if (ctx.nonInteractive) { + await this.runNonInteractiveAsync(ctx, currentCertificate); + } else { + await this.runInteractiveAsync(ctx, manager, currentCertificate); + } + } catch (err) { + if (err instanceof AppleTeamMissingError && ctx.nonInteractive) { + throw new MissingCredentialsNonInteractiveError(); + } + throw err; + } + } - const currentCertificate = await ctx.newIos.getDistributionCertificateForAppAsync( - this.app, - appleTeam, - IosDistributionType.AdHoc + private async runNonInteractiveAsync( + ctx: Context, + currentCertificate: AppleDistributionCertificateFragment | null + ): Promise { + // TODO: implement validation + Log.addNewLineIfNone(); + Log.warn( + 'Distribution Certificate is not validated for non-interactive internal distribution builds.' ); + if (!currentCertificate) { + throw new MissingCredentialsNonInteractiveError(); + } + this._distributionCertificate = currentCertificate; + } + private async runInteractiveAsync( + ctx: Context, + manager: CredentialsManager, + currentCertificate: AppleDistributionCertificateFragment | null + ): Promise { if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) { assert(currentCertificate, 'currentCertificate is defined here'); this._distributionCertificate = currentCertificate; diff --git a/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts b/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts index 8930b18a29..e91fceb4c9 100644 --- a/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts +++ b/packages/eas-cli/src/credentials/ios/api/GraphqlClient.ts @@ -11,6 +11,7 @@ import { } from '../../../graphql/generated'; import { Account } from '../../../user/Account'; import { DistributionCertificate } from '../appstore/Credentials.types'; +import { AppleTeamMissingError } from '../errors'; import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation'; import { AppleDistributionCertificateMutation, @@ -98,6 +99,25 @@ export async function createOrUpdateIosAppBuildCredentialsAsync( } export async function getIosAppCredentialsWithBuildCredentialsAsync( + appLookupParams: AppLookupParams, + { iosDistributionType }: { iosDistributionType: IosDistributionType } +): Promise { + const { account, bundleIdentifier } = appLookupParams; + const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync( + account.name, + bundleIdentifier + ); + if (!appleAppIdentifier) { + return null; + } + const projectFullName = formatProjectFullName(appLookupParams); + return await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(projectFullName, { + appleAppIdentifierId: appleAppIdentifier.id, + iosDistributionType, + }); +} + +export async function getIosAppCredentialsWithCommonFieldsAsync( appLookupParams: AppLookupParams ): Promise { const { account, bundleIdentifier } = appLookupParams; @@ -126,12 +146,12 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs iosDistributionType: IosDistributionType; } ): Promise { - const projectFullName = formatProjectFullName(appLookupParams); - const maybeIosAppCredentials = await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync( - projectFullName, - { appleAppIdentifierId, iosDistributionType } + const maybeIosAppCredentials = await getIosAppCredentialsWithBuildCredentialsAsync( + appLookupParams, + { + iosDistributionType, + } ); - if (maybeIosAppCredentials) { return maybeIosAppCredentials; } else { @@ -144,6 +164,7 @@ export async function createOrGetExistingIosAppCredentialsWithBuildCredentialsAs app.id, appleAppIdentifier.id ); + const projectFullName = formatProjectFullName(appLookupParams); return nullthrows( await IosAppCredentialsQuery.withBuildCredentialsByAppIdentifierIdAsync(projectFullName, { appleAppIdentifierId, @@ -173,7 +194,7 @@ export async function createOrGetExistingAppleTeamAsync( export async function createOrGetExistingAppleAppIdentifierAsync( { account, bundleIdentifier }: AppLookupParams, - appleTeam: AppleTeamFragment + appleTeam: AppleTeamFragment | null ): Promise { const appleAppIdentifier = await AppleAppIdentifierQuery.byBundleIdentifierAsync( account.name, @@ -182,6 +203,9 @@ export async function createOrGetExistingAppleAppIdentifierAsync( if (appleAppIdentifier) { return appleAppIdentifier; } else { + if (!appleTeam) { + throw new AppleTeamMissingError(); + } return await AppleAppIdentifierMutation.createAppleAppIdentifierAsync( { bundleIdentifier, appleTeamId: appleTeam.id }, account.id @@ -213,8 +237,8 @@ export async function createProvisioningProfileAsync( export async function getProvisioningProfileAsync( appLookupParams: AppLookupParams, - appleTeam: AppleTeamFragment, - iosDistributionType: IosDistributionType + iosDistributionType: IosDistributionType, + { appleTeam }: { appleTeam: AppleTeamFragment | null } = { appleTeam: null } ): Promise { const projectFullName = formatProjectFullName(appLookupParams); const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync( @@ -222,7 +246,7 @@ export async function getProvisioningProfileAsync( appleTeam ); return await AppleProvisioningProfileQuery.getForAppAsync(projectFullName, { - appleAppIdentifierId: appleAppIdentifier?.id, + appleAppIdentifierId: appleAppIdentifier.id, iosDistributionType, }); } @@ -250,8 +274,8 @@ export async function deleteProvisioningProfilesAsync( export async function getDistributionCertificateForAppAsync( appLookupParams: AppLookupParams, - appleTeam: AppleTeamFragment, - iosDistributionType: IosDistributionType + iosDistributionType: IosDistributionType, + { appleTeam }: { appleTeam: AppleTeamFragment | null } = { appleTeam: null } ): Promise { const projectFullName = formatProjectFullName(appLookupParams); const appleAppIdentifier = await createOrGetExistingAppleAppIdentifierAsync( @@ -259,7 +283,7 @@ export async function getDistributionCertificateForAppAsync( appleTeam ); return await AppleDistributionCertificateQuery.getForAppAsync(projectFullName, { - appleAppIdentifierId: appleAppIdentifier?.id, + appleAppIdentifierId: appleAppIdentifier.id, iosDistributionType, }); } diff --git a/packages/eas-cli/src/credentials/ios/errors.ts b/packages/eas-cli/src/credentials/ios/errors.ts new file mode 100644 index 0000000000..ece8bc60e9 --- /dev/null +++ b/packages/eas-cli/src/credentials/ios/errors.ts @@ -0,0 +1,13 @@ +export class AppleTeamMissingError extends Error { + constructor(message?: string) { + super(message ?? 'Apple Team is necessary to create Apple App Identifier'); + } +} + +export class MissingCredentialsNonInteractiveError extends Error { + constructor(message?: string) { + super( + message ?? 'Credentials are not set up. Please run this command again in interactive mode.' + ); + } +} diff --git a/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts b/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts index 862ffceec8..5ebde2fe56 100644 --- a/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts +++ b/packages/eas-cli/src/credentials/manager/ManageIosBeta.ts @@ -40,7 +40,7 @@ export class ManageIosBeta implements Action { } if (ctx.hasProjectContext) { const appLookupParams = ManageIosBeta.getAppLookupParamsFromContext(ctx); - const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithBuildCredentialsAsync( + const iosAppCredentials = await ctx.newIos.getIosAppCredentialsWithCommonFieldsAsync( appLookupParams ); if (!iosAppCredentials) {