Skip to content

Commit

Permalink
[ENG-8801][eas-cli] allow modification of provisioning profile in CI,…
Browse files Browse the repository at this point in the history
… add `--freeze-credentials` flag (#2347)

* freeze creds flag

* allow profile to be modified in non interactive mode

* Temporary Commit at 4/24/2024, 4:15:30 PM

* Temporary Commit at 4/25/2024, 4:33:46 PM

* Temporary Commit at 4/25/2024, 5:24:12 PM

* update CHANGELOG.md

* Temporary Commit at 4/25/2024, 6:30:26 PM

* Temporary Commit at 5/2/2024, 1:10:17 PM
  • Loading branch information
quinlanj authored May 2, 2024
1 parent cdd046b commit d54abee
Show file tree
Hide file tree
Showing 15 changed files with 382 additions and 165 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🛠 Breaking changes

- Allow modification of provisioning profile in CI, add --freeze-credentials flag. ([#2347](https://github.com/expo/eas-cli/pull/2347) by [@quinlanj](https://github.com/quinlanj))

### 🎉 New features

### 🐛 Bug fixes
Expand Down
3 changes: 3 additions & 0 deletions packages/eas-cli/src/build/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export async function createBuildContextAsync<T extends Platform>({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
Expand All @@ -60,6 +61,7 @@ export async function createBuildContextAsync<T extends Platform>({
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
Expand All @@ -86,6 +88,7 @@ export async function createBuildContextAsync<T extends Platform>({
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
});

const devClientProperties = getDevClientEventProperties({
Expand Down
2 changes: 2 additions & 0 deletions packages/eas-cli/src/build/runBuildAndSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface BuildFlags {
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
}

export async function runBuildAndSubmitAsync(
Expand Down Expand Up @@ -362,6 +363,7 @@ async function prepareAndStartBuildAsync({
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel,
freezeCredentials: flags.freezeCredentials,
});

if (moreBuilds) {
Expand Down
6 changes: 6 additions & 0 deletions packages/eas-cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ interface RawBuildFlags {
'resource-class'?: ResourceClass;
message?: string;
'build-logger-level'?: LoggerLevel;
'freeze-credentials': boolean;
}

export default class Build extends EasCommand {
Expand Down Expand Up @@ -104,6 +105,10 @@ export default class Build extends EasCommand {
description: 'The level of logs to output during the build process. Defaults to "info".',
options: Object.values(LoggerLevel),
}),
'freeze-credentials': Flags.boolean({
default: false,
description: 'Prevent the build from updating credentials in non-interactive mode',
}),
...EasNonInteractiveAndJsonFlags,
};

Expand Down Expand Up @@ -217,6 +222,7 @@ export default class Build extends EasCommand {
resourceClass: flags['resource-class'],
message,
buildLoggerLevel: flags['build-logger-level'],
freezeCredentials: flags['freeze-credentials'],
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export default class BuildInspect extends EasCommand {
projectDir,
{
nonInteractive: false,
freezeCredentials: false,
wait: true,
clearCache: false,
json: false,
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/commands/build/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default class BuildInternal extends EasCommand {
requestedPlatform: flags.platform as RequestedPlatform,
profile: flags.profile,
nonInteractive: true,
freezeCredentials: false,
wait: false,
clearCache: false,
json: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AppStoreApi from '../ios/appstore/AppStoreApi';
import { AuthCtx } from '../ios/appstore/authenticateTypes';
import { AuthCtx, AuthenticationMode } from '../ios/appstore/authenticateTypes';

export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
Expand All @@ -9,6 +9,7 @@ export const testAuthCtx: AuthCtx = {

export function getAppstoreMock(): AppStoreApi {
return {
defaultAuthenticationMode: AuthenticationMode.USER,
ensureAuthenticatedAsync: jest.fn(),
ensureBundleIdExistsAsync: jest.fn(),
listDistributionCertificatesAsync: jest.fn(),
Expand Down
3 changes: 3 additions & 0 deletions packages/eas-cli/src/credentials/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class CredentialsContext {
public readonly appStore = new AppStoreApi();
public readonly ios = IosGraphqlClient;
public readonly nonInteractive: boolean;
public readonly freezeCredentials: boolean = false;
public readonly projectDir: string;
public readonly user: Actor;
public readonly graphqlClient: ExpoGraphqlClient;
Expand All @@ -47,6 +48,7 @@ export class CredentialsContext {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
freezeCredentials?: boolean;
env?: Env;
}
) {
Expand All @@ -58,6 +60,7 @@ export class CredentialsContext {
this.vcsClient = options.vcsClient;
this.nonInteractive = options.nonInteractive ?? false;
this.projectInfo = options.projectInfo;
this.freezeCredentials = options.freezeCredentials ?? false;
}

get hasProjectContext(): boolean {
Expand Down
22 changes: 22 additions & 0 deletions packages/eas-cli/src/credentials/errors.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import { learnMore } from '../log';

export class MissingCredentialsNonInteractiveError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up. Run this command again in interactive mode.');
}
}

export class InsufficientAuthenticationNonInteractiveError extends Error {
constructor(message?: string) {
super(
message ??
`Authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
}

export class ForbidCredentialModificationError extends Error {
constructor(message?: string) {
super(
message ??
'Credentials cannot be modified. Run this command again without the --freeze-credentials flag.'
);
}
}

export class MissingCredentialsError extends Error {
constructor(message?: string) {
super(message ?? 'Credentials are not set up.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import {
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticateTypes';
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
import { Target } from '../types';

export class ConfigureProvisioningProfile {
Expand All @@ -27,9 +30,18 @@ export class ConfigureProvisioningProfile {
public async runAsync(
ctx: CredentialsContext
): Promise<AppleProvisioningProfileMutationResult | null> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Configuring Provisioning Profiles is only supported in interactive mode.'
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Remove the --freeze-credentials flag to configure a Provisioning Profile.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
const { developerPortalIdentifier, provisioningProfile } = this.originalProvisioningProfile;
Expand All @@ -46,7 +58,7 @@ export class ConfigureProvisioningProfile {
return null;
}

const applePlatform = await getApplePlatformFromTarget(this.target);
const applePlatform = getApplePlatformFromTarget(this.target);
const profilesFromApple = await ctx.appStore.listProvisioningProfilesAsync(
this.app.bundleIdentifier,
applePlatform
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import nullthrows from 'nullthrows';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { generateProvisioningProfileAsync } from './ProvisioningProfileUtils';
import { AppleDistributionCertificateFragment } from '../../../graphql/generated';
import Log from '../../../log';
import Log, { learnMore } from '../../../log';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { AppleProvisioningProfileMutationResult } from '../api/graphql/mutations/AppleProvisioningProfileMutation';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfile } from '../appstore/Credentials.types';
import { AuthCtx } from '../appstore/authenticateTypes';
import { AuthCtx, AuthenticationMode } from '../appstore/authenticateTypes';
import { provisioningProfileSchema } from '../credentials';
import { Target } from '../types';

Expand All @@ -23,9 +26,18 @@ export class CreateProvisioningProfile {
) {}

async runAsync(ctx: CredentialsContext): Promise<AppleProvisioningProfileMutationResult> {
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Creating Provisioning Profiles is only supported in interactive mode.'
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Run this command again without the --freeze-credentials flag in order to generate a new Provisioning Profile.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to generate a new Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}
const appleAuthCtx = await ctx.appStore.ensureAuthenticatedAsync();
Expand All @@ -49,15 +61,30 @@ export class CreateProvisioningProfile {
return provisioningProfileMutationResult;
}

private async maybeGetUserProvidedAsync(
ctx: CredentialsContext
): Promise<ProvisioningProfile | null> {
if (ctx.nonInteractive) {
return null;
}
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
if (userProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
Log.warn('Provisioning profile: Unable to validate specified profile.');
return userProvided;
}
return null;
}

private async provideOrGenerateAsync(
ctx: CredentialsContext,
appleAuthCtx: AuthCtx
): Promise<ProvisioningProfile> {
const userProvided = await askForUserProvidedAsync(provisioningProfileSchema);
if (userProvided) {
const maybeUserProvided = await this.maybeGetUserProvidedAsync(ctx);
if (maybeUserProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
Log.warn('Provisioning profile: Unable to validate specified profile.');
return userProvided;
return maybeUserProvided;
}
assert(
this.distributionCertificate.certificateP12,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ import {
IosAppBuildCredentialsFragment,
IosDistributionType,
} from '../../../graphql/generated';
import { learnMore } from '../../../log';
import { getApplePlatformFromTarget } from '../../../project/ios/target';
import { confirmAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import {
ForbidCredentialModificationError,
InsufficientAuthenticationNonInteractiveError,
} from '../../errors';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfileStoreInfo } from '../appstore/Credentials.types';
import { AuthenticationMode } from '../appstore/authenticateTypes';
import { Target } from '../types';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';

Expand Down Expand Up @@ -102,9 +107,18 @@ export class SetUpProvisioningProfile {
if (areBuildCredentialsSetup) {
return nullthrows(await getBuildCredentialsAsync(ctx, this.app, this.distributionType));
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'Provisioning profile is not configured correctly. Run this command again in interactive mode.'
if (ctx.freezeCredentials) {
throw new ForbidCredentialModificationError(
'Provisioning profile is not configured correctly. Remove the --freeze-credentials flag to configure it.'
);
} else if (
ctx.nonInteractive &&
ctx.appStore.defaultAuthenticationMode !== AuthenticationMode.API_KEY
) {
throw new InsufficientAuthenticationNonInteractiveError(
`In order to configure your Provisioning Profile, authentication with an ASC API key is required in non-interactive mode. ${learnMore(
'https://docs.expo.dev/build/building-on-ci/#optional-provide-an-asc-api-token-for-your-apple-team'
)}`
);
}

Expand All @@ -127,11 +141,18 @@ export class SetUpProvisioningProfile {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}

const confirm = await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
const isNonInteractiveOrUserDidConfirmAsync = async (): Promise<boolean> => {
if (ctx.nonInteractive) {
return true;
}
return await confirmAsync({
message: `${formatProvisioningProfileFromApple(
currentProfileFromServer
)} \n Would you like to reuse the original profile?`,
});
};

const confirm = await isNonInteractiveOrUserDidConfirmAsync();
if (!confirm) {
return await this.assignNewAndDeleteOldProfileAsync(ctx, distCert, currentProfile);
}
Expand Down
Loading

0 comments on commit d54abee

Please sign in to comment.