Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

Commit

Permalink
[cli] better non interactive support for creds (#1881)
Browse files Browse the repository at this point in the history
* [cli] better non interactive support for creds

* [cli] credential tests

* pass in non interactive sites to constructors
  • Loading branch information
quinlanj authored Apr 16, 2020
1 parent 7fe8feb commit c94638e
Show file tree
Hide file tree
Showing 14 changed files with 683 additions and 79 deletions.
13 changes: 10 additions & 3 deletions packages/expo-cli/src/commands/build/ios/IOSBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import chalk from 'chalk';
import isEmpty from 'lodash/isEmpty';
import pickBy from 'lodash/pickBy';
import get from 'lodash/get';
import { XDLError } from '@expo/xdl';
Expand Down Expand Up @@ -136,6 +135,7 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/#
}

async produceCredentials(ctx: Context, experienceName: string, bundleIdentifier: string) {
const nonInteractive = this.options.parent && this.options.parent.nonInteractive;
const appCredentials = await ctx.ios.getAppCredentials(experienceName, bundleIdentifier);

if (ctx.hasAppleCtx()) {
Expand All @@ -150,7 +150,10 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/#
if (distCertFromParams) {
await useDistCertFromParams(ctx, appCredentials, distCertFromParams);
} else {
await runCredentialsManager(ctx, new SetupIosDist({ experienceName, bundleIdentifier }));
await runCredentialsManager(
ctx,
new SetupIosDist({ experienceName, bundleIdentifier, nonInteractive })
);
}

const distributionCert = await ctx.ios.getDistCert(experienceName, bundleIdentifier);
Expand All @@ -165,7 +168,10 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/#
if (pushKeyFromParams) {
await usePushKeyFromParams(ctx, appCredentials, pushKeyFromParams);
} else {
await runCredentialsManager(ctx, new SetupIosPush({ experienceName, bundleIdentifier }));
await runCredentialsManager(
ctx,
new SetupIosPush({ experienceName, bundleIdentifier, nonInteractive })
);
}

const provisioningProfileFromParams = await getProvisioningProfileFromParams(this.options);
Expand All @@ -184,6 +190,7 @@ See https://docs.expo.io/versions/latest/distribution/building-standalone-apps/#
experienceName,
bundleIdentifier,
distCert: distributionCert,
nonInteractive,
})
);
}
Expand Down
77 changes: 77 additions & 0 deletions packages/expo-cli/src/credentials/test-fixtures/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const today = new Date();
const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000);
export const testProvisioningProfile = {
provisioningProfileId: 'test-id',
};
export const testProvisioningProfiles = [testProvisioningProfile];
export const testProvisioningProfileFromApple = {
name: 'test-name',
status: 'Active',
expires: tomorrow,
distributionMethod: 'test',
certificates: [],
provisioningProfileId: testProvisioningProfile.provisioningProfileId,
};
export const testProvisioningProfilesFromApple = [testProvisioningProfileFromApple];

export const testDistCert = {
id: 1,
type: 'dist-cert',
certP12: 'test-p12',
certPassword: 'test-password',
distCertSerialNumber: 'test-serial',
teamId: 'test-team-id',
};
export const testDistCerts = [testDistCert];
export const testDistCertFromApple = {
id: 'test-id',
status: 'Active',
created: today.getTime(),
expires: tomorrow.getTime(),
serialNumber: testDistCert.distCertSerialNumber,
};
export const testDistCertsFromApple = [testDistCertFromApple];

export const testPushKey = {
id: 1,
type: 'push-key',
apnsKeyP8: 'test-p8',
apnsKeyId: 'test-key-id',
teamId: 'test-team-id',
};
export const testPushKeys = [testPushKey];
export const testPushKeyFromApple = {
id: testPushKey.apnsKeyId,
name: 'test-name',
};
export const testPushKeysFromApple = [testPushKeyFromApple];

export const testAppCredentials = [{ experienceName: 'testApp', bundleIdentifier: 'test.com.app' }];
export function getCtxMock() {
return {
ios: {
getDistCert: jest.fn(),
createDistCert: jest.fn(() => testDistCert),
useDistCert: jest.fn(),
getPushKey: jest.fn(),
createPushKey: jest.fn(() => testPushKey),
usePushKey: jest.fn(),
updateProvisioningProfile: jest.fn(),
getAppCredentials: jest.fn(() => testAppCredentials),
getProvisioningProfile: jest.fn(),
credentials: {
userCredentials: [...testDistCerts, ...testPushKeys],
appCredentials: testAppCredentials,
},
},
appleCtx: {
appleId: 'test-id',
appleIdPassword: 'test-password',
team: { id: 'test-team-id' },
fastlaneSession: 'test-fastlane-session',
},
ensureAppleCtx: jest.fn(),
user: jest.fn(),
hasAppleCtx: jest.fn(() => true),
};
}
49 changes: 37 additions & 12 deletions packages/expo-cli/src/credentials/views/IosDistCert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,22 @@ Please revoke the old ones or reuse existing from your other apps.
Please remember that Apple Distribution Certificates are not application specific!
`;

type CliOptions = {
nonInteractive?: boolean;
};

export type DistCertOptions = {
experienceName: string;
bundleIdentifier: string;
};
} & CliOptions;

export class CreateIosDist implements IView {
_nonInteractive: boolean;

constructor(options: CliOptions = {}) {
this._nonInteractive = options.nonInteractive ?? false;
}

async create(ctx: Context): Promise<IosDistCredentials> {
const newDistCert = await this.provideOrGenerate(ctx);
return await ctx.ios.createDistCert(newDistCert);
Expand All @@ -51,10 +61,12 @@ export class CreateIosDist implements IView {
}

async provideOrGenerate(ctx: Context): Promise<DistCert> {
const userProvided = await promptForDistCert(ctx);
if (userProvided) {
const isValid = await validateDistributionCertificate(ctx, userProvided);
return isValid ? userProvided : await this.provideOrGenerate(ctx);
if (!this._nonInteractive) {
const userProvided = await promptForDistCert(ctx);
if (userProvided) {
const isValid = await validateDistributionCertificate(ctx, userProvided);
return isValid ? userProvided : await this.provideOrGenerate(ctx);
}
}
return await generateDistCert(ctx);
}
Expand Down Expand Up @@ -232,11 +244,13 @@ export class UseExistingDistributionCert implements IView {
export class CreateOrReuseDistributionCert implements IView {
_experienceName: string;
_bundleIdentifier: string;
_nonInteractive: boolean;

constructor(options: DistCertOptions) {
const { experienceName, bundleIdentifier } = options;
this._experienceName = experienceName;
this._bundleIdentifier = bundleIdentifier;
this._nonInteractive = options.nonInteractive ?? false;
}

async assignDistCert(ctx: Context, userCredentialsId: number) {
Expand All @@ -256,7 +270,9 @@ export class CreateOrReuseDistributionCert implements IView {
const existingCertificates = await getValidDistCerts(ctx.ios.credentials, ctx);

if (existingCertificates.length === 0) {
const distCert = await new CreateIosDist().create(ctx);
const distCert = await new CreateIosDist({ nonInteractive: this._nonInteractive }).create(
ctx
);
await this.assignDistCert(ctx, distCert.id);
return null;
}
Expand All @@ -274,13 +290,20 @@ export class CreateOrReuseDistributionCert implements IView {
pageSize: Infinity,
};

const { confirm } = await prompt(confirmQuestion);
if (confirm) {
log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`);
await this.assignDistCert(ctx, autoselectedCertificate.id);
return null;
if (!this._nonInteractive) {
const { confirm } = await prompt(confirmQuestion);
if (!confirm) {
return await this._createOrReuse(ctx);
}
}

// Use autosuggested push key
log(`Using Distribution Certificate: ${autoselectedCertificate.certId || '-----'}`);
await this.assignDistCert(ctx, autoselectedCertificate.id);
return null;
}

async _createOrReuse(ctx: Context): Promise<IView | null> {
const choices = [
{
name: '[Choose existing certificate] (Recommended)',
Expand All @@ -300,7 +323,9 @@ export class CreateOrReuseDistributionCert implements IView {
const { action } = await prompt(question);

if (action === 'GENERATE') {
const distCert = await new CreateIosDist().create(ctx);
const distCert = await new CreateIosDist({ nonInteractive: this._nonInteractive }).create(
ctx
);
await this.assignDistCert(ctx, distCert.id);
return null;
} else if (action === 'CHOOSE_EXISTING') {
Expand Down
54 changes: 36 additions & 18 deletions packages/expo-cli/src/credentials/views/IosProvisioningProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Context, IView } from '../context';
import {
IosAppCredentials,
IosCredentials,
IosDistCredentials,
appleTeamSchema,
provisioningProfileSchema,
} from '../credentials';
Expand All @@ -25,11 +24,15 @@ import {
ProvisioningProfileManager,
} from '../../appleApi';

type CliOptions = {
nonInteractive?: boolean;
};

export type ProvisioningProfileOptions = {
experienceName: string;
bundleIdentifier: string;
distCert: DistCert;
};
} & CliOptions;

export class RemoveProvisioningProfile implements IView {
shouldRevoke: boolean;
Expand Down Expand Up @@ -81,12 +84,14 @@ export class CreateProvisioningProfile implements IView {
_experienceName: string;
_bundleIdentifier: string;
_distCert: DistCert;
_nonInteractive: boolean;

constructor(options: ProvisioningProfileOptions) {
const { experienceName, bundleIdentifier, distCert } = options;
this._experienceName = experienceName;
this._bundleIdentifier = bundleIdentifier;
this._distCert = distCert;
this._nonInteractive = options.nonInteractive ?? false;
}

async create(ctx: Context): Promise<ProvisioningProfile> {
Expand Down Expand Up @@ -121,11 +126,13 @@ export class CreateProvisioningProfile implements IView {
}

async provideOrGenerate(ctx: Context): Promise<ProvisioningProfile> {
const userProvided = await askForUserProvided(provisioningProfileSchema);
if (userProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
log(chalk.yellow('Provisioning profile: Unable to validate uploaded profile.'));
return userProvided;
if (!this._nonInteractive) {
const userProvided = await askForUserProvided(provisioningProfileSchema);
if (userProvided) {
// userProvided profiles don't come with ProvisioningProfileId's (only accessible from Apple Portal API)
log(chalk.yellow('Provisioning profile: Unable to validate uploaded profile.'));
return userProvided;
}
}
return await generateProvisioningProfile(ctx, this._bundleIdentifier, this._distCert);
}
Expand Down Expand Up @@ -163,12 +170,14 @@ export class CreateOrReuseProvisioningProfile implements IView {
_experienceName: string;
_bundleIdentifier: string;
_distCert: DistCert;
_nonInteractive: boolean;

constructor(options: ProvisioningProfileOptions) {
const { experienceName, bundleIdentifier, distCert } = options;
this._experienceName = experienceName;
this._bundleIdentifier = bundleIdentifier;
this._distCert = distCert;
this._nonInteractive = options.nonInteractive ?? false;
}

choosePreferred(profiles: ProvisioningProfileInfo[]): ProvisioningProfileInfo {
Expand All @@ -191,6 +200,7 @@ export class CreateOrReuseProvisioningProfile implements IView {
experienceName: this._experienceName,
bundleIdentifier: this._bundleIdentifier,
distCert: this._distCert,
nonInteractive: this._nonInteractive,
});
}

Expand All @@ -202,6 +212,7 @@ export class CreateOrReuseProvisioningProfile implements IView {
experienceName: this._experienceName,
bundleIdentifier: this._bundleIdentifier,
distCert: this._distCert,
nonInteractive: this._nonInteractive,
});
}

Expand All @@ -216,19 +227,25 @@ export class CreateOrReuseProvisioningProfile implements IView {
pageSize: Infinity,
};

const { confirm } = await prompt(confirmQuestion);
if (confirm) {
log(`Using Provisioning Profile: ${autoselectedProfile.provisioningProfileId}`);
await configureAndUpdateProvisioningProfile(
ctx,
this._experienceName,
this._bundleIdentifier,
this._distCert,
autoselectedProfile
);
return null;
if (!this._nonInteractive) {
const { confirm } = await prompt(confirmQuestion);
if (!confirm) {
return await this._createOrReuse(ctx);
}
}

log(`Using Provisioning Profile: ${autoselectedProfile.provisioningProfileId}`);
await configureAndUpdateProvisioningProfile(
ctx,
this._experienceName,
this._bundleIdentifier,
this._distCert,
autoselectedProfile
);
return null;
}

async _createOrReuse(ctx: Context): Promise<IView | null> {
const choices = [
{
name: '[Choose existing provisioning profile] (Recommended)',
Expand All @@ -252,6 +269,7 @@ export class CreateOrReuseProvisioningProfile implements IView {
experienceName: this._experienceName,
bundleIdentifier: this._bundleIdentifier,
distCert: this._distCert,
nonInteractive: this._nonInteractive,
});
} else if (action === 'CHOOSE_EXISTING') {
return new UseExistingProvisioningProfile({
Expand Down
Loading

0 comments on commit c94638e

Please sign in to comment.