Skip to content

Commit

Permalink
[ENG-444] support internal distribution in non-interactive builds (#269)
Browse files Browse the repository at this point in the history
* support internal distribution in non-interactive builds

* update CHANGELOG.md

* address PR feedback

* Update packages/eas-cli/src/credentials/ios/actions/new/SetupAdhocProvisioningProfile.ts

Co-authored-by: Wojciech Kozyra <[email protected]>

* address more PR feedback

* update error message

Co-authored-by: Wojciech Kozyra <[email protected]>
  • Loading branch information
dsokal and wkozyra95 authored Mar 9, 2021
1 parent c330003 commit fa6e9c1
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 75 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
59 changes: 33 additions & 26 deletions packages/eas-cli/src/credentials/ios/IosCredentialsProvider.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 };
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions packages/eas-cli/src/credentials/ios/actions/new/AppleTeamUtils.ts
Original file line number Diff line number Diff line change
@@ -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<AppleTeamFragment | null> {
if (!ctx.appStore.authCtx) {
return null;
}
return await ctx.newIos.createOrGetExistingAppleTeamAsync(app, {
appleTeamIdentifier: ctx.appStore.authCtx.team.id,
appleTeamName: ctx.appStore.authCtx.team.name,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -33,15 +36,55 @@ export class SetupAdhocProvisioningProfile implements Action {
}

async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
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<void> {
// 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<void> {
// 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
Expand All @@ -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;
Expand Down Expand Up @@ -110,18 +153,18 @@ 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,
}
);
}
} else {
currentProfileFromExpoServers = await ctx.newIos.createProvisioningProfileAsync(
profileFromExpoServersToUse = await ctx.newIos.createProvisioningProfileAsync(
this.app,
appleAppIdentifier,
{
Expand All @@ -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,
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -31,21 +33,48 @@ export class SetupDistributionCertificate implements Action {
}

public async runAsync(manager: CredentialsManager, ctx: Context): Promise<void> {
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<void> {
// 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<void> {
if (await this.isCurrentCertificateValidAsync(ctx, currentCertificate)) {
assert(currentCertificate, 'currentCertificate is defined here');
this._distributionCertificate = currentCertificate;
Expand Down
Loading

0 comments on commit fa6e9c1

Please sign in to comment.