Compare commits

...
Author SHA1 Message Date
Quinlan Jung dc0b3b1237 [eas-cli] remove asp prompt 2021-11-05 16:15:33 -07:00
Quinlan Jung 0e6254453a [eas-cli] integrate ascApiKey with submissions workflow (#737)
* [eas-cli] integrate ascApiKey with submissions workflow

* fix failing stuff

* update CHANGELOG.md

* barthap pr feedback

* dominik pr feedback
2021-11-05 13:30:16 -07:00
Quinlan Jung 587a29892f fix changelog 2021-11-05 13:28:03 -07:00
Quinlan Jung db8245d954 [eas-cli] setup -> set up (#735)
* [eas-cli] setup -> set up

* fix stuff

* update CHANGELOG.md

* fix merge errors
2021-11-05 11:59:44 -07:00
Bartłomiej Klocek 0088306bca [eas-cli] Handle Apple servers maintenance in eas submit (#738)
* [eas-cli] Handle Apple servers maintenance in `eas submit`

* update CHANGELOG.md
2021-11-05 11:08:30 +01:00
Wojciech KozyraandDominik Sokal b6fc718713 [eas-cli] improve vcs migration experience (#732)
* [eas-cli] improve vcs migration experience

* update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-05 10:40:00 +01:00
Quinlan Jung 18e55dafd4 [eas-cli] setup asc api key (#733)
* [eas-cli] setup asc api key

* setup -> set up

* pr feedback

* update CHANGELOG.md
2021-11-04 14:52:25 -07:00
Bartłomiej Klocek 8e013ab793 [eas-cli] Use actor displayName when printing build info (#730)
* [eas-cli] Use actor `displayName` when printing build info

* update CHANGELOG.md
2021-11-04 07:50:03 +01:00
Quinlan JungandDominik Sokal b88e79f0f3 [eas-cli] cleanup up creds prompt method (#728)
* [eas-cli] cleanup up creds prompt method

* update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-03 13:19:10 -07:00
Ryan Blunden 35b505120d Replace eas:secrets with eas:secret (#727) 2021-11-03 09:44:19 +01:00
Brent VatneandDominik Sokal 6807d3080b [eas-cli] Bump @expo/apple-utils to better handle some error messages (#723)
* [eas-cli] Bump @expo/apple-utils to better handle some error messages

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-03 09:35:11 +01:00
Quinlan JungandDominik Sokal 0d1cfb3286 [eas-cli] support removal of asc api keys (#721)
* [eas-cli] support removal of asc api keys

* update CHANGELOG.md

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

Co-authored-by: Dominik Sokal <[email protected]>

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

Co-authored-by: Dominik Sokal <[email protected]>

Co-authored-by: Dominik Sokal <[email protected]>
2021-11-02 21:03:34 -07:00
Quinlan Jung 0b9a5621fe fix changelog 2021-11-02 20:57:46 -07:00
Quinlan Jung 9bd15af02a [eas-cli] assign asc api keys to project (#719)
* [eas-cli] assign asc api keys to project

assign asc test

* fix compilation errors
2021-11-02 20:51:15 -07:00
Quinlan Jung aab3bf1330 [eas-cli] create + validate asc api key, infer issuer ids (#718)
* [eas-cli] create + validate asc api key, infer issuer ids

create asc tests

create asc key:

ascapikeyutils test

* update CHANGELOG.md

* some pr feedback

* API -> Api

* more pr feedbakc

* fix tsc
2021-11-02 20:41:17 -07:00
Expo CI 9e95218523 update CHANGELOG.md 2021-11-02 17:27:32 +00:00
72 changed files with 1780 additions and 311 deletions

No files matched your search

+19
View File
@@ -8,6 +8,25 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add ASC Api Key generation workflow. ([#718](https://github.com/expo/eas-cli/pull/718) by [@quinlanj](https://github.com/quinlanj))
- Add support for removal of ASC Api Keys. ([#721](https://github.com/expo/eas-cli/pull/721) by [@quinlanj](https://github.com/quinlanj))
- Allow users to assign an ASC Api Key to their project. ([#719](https://github.com/expo/eas-cli/pull/719) by [@quinlanj](https://github.com/quinlanj))
- Add setup support for ASC Api Keys. ([#733](https://github.com/expo/eas-cli/pull/733) by [@quinlanj](https://github.com/quinlanj))
- Show initiating user display name when selecting a build to submit. ([#730](https://github.com/expo/eas-cli/pull/730) by [@barthap](https://github.com/barthap))
- Handle Apple servers maintenance error in `eas submit`. ([#738](https://github.com/expo/eas-cli/pull/738) by [@barthap](https://github.com/barthap))
- Integrate ASC Api Key with submissions workflow. ([#737](https://github.com/expo/eas-cli/pull/737) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
### 🧹 Chores
- Clean up credentials prompt method. ([#728](https://github.com/expo/eas-cli/pull/728) by [@quinlanj](https://github.com/quinlanj))
- Bump @expo/apple-utils to 0.0.0-alpha.26. ([#723](https://github.com/expo/eas-cli/pull/723] by [@brentvatne](https://github.com/brentvatne))
- Grammar: replace setup with set up. ([#735](https://github.com/expo/eas-cli/pull/735) by [@quinlanj](https://github.com/quinlanj))
- Improve VCS workflow migration experience. ([#732](https://github.com/expo/eas-cli/pull/732) by [@wkozyra95](https://github.com/wkozyra95))
## [0.34.1](https://github.com/expo/eas-cli/releases/tag/0.34.1) - 2021-11-02
### 🐛 Bug fixes
- Don't show commit prompt in no-commit workflow after installing `expo-dev-client`. ([#722](https://github.com/expo/eas-cli/pull/722) by [@wkozyra95](https://github.com/wkozyra95))
+1 -1
View File
@@ -364,7 +364,7 @@ OPTIONS
--id=id ID of the secret to delete
DESCRIPTION
Unsure where to find the secret's ID? Run eas secrets:list
Unsure where to find the secret's ID? Run eas secret:list
```
_See code: [src/commands/secret/delete.js](https://github.com/expo/eas-cli/blob/v0.34.1/packages/eas-cli/src/commands/secret/delete.js)_
+1 -1
View File
@@ -8,7 +8,7 @@
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.25",
"@expo/apple-utils": "0.0.0-alpha.26",
"@expo/config": "6.0.3",
"@expo/config-plugins": "4.0.3",
"@expo/eas-build-job": "0.2.57",
@@ -120,11 +120,5 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
const getActorName = (build: BuildFragment): string => {
if (!build.initiatingActor) {
return 'unknown';
} else if (build.initiatingActor.__typename === 'User') {
return build.initiatingActor.username;
} else {
return build.initiatingActor.firstName ?? 'unknown';
}
return build.initiatingActor?.displayName || 'unknown';
};
+14 -4
View File
@@ -14,7 +14,7 @@ import { BuildContext, createBuildContextAsync } from '../../build/context';
import { prepareIosBuildAsync } from '../../build/ios/build';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from '../../build/utils/devClient';
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context';
import {
@@ -392,7 +392,8 @@ export async function handleDeprecatedEasJsonAsync(
projectDir: string,
nonInteractive: boolean
): Promise<void> {
if (!(await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir)))) {
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
if (!(await fs.pathExists(easJsonPath))) {
return;
}
const easJsonReader = new EasJsonReader(projectDir);
@@ -432,12 +433,21 @@ export async function handleDeprecatedEasJsonAsync(
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
]);
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await ensureRepoIsCleanAsync(nonInteractive);
}
rawEasJson.cli =
mode === 'requireCommit'
? { version: `>= ${easCliVersion}`, requireCommit: true }
: { version: `>= ${easCliVersion}` };
await fs.writeJSON(EasJsonReader.formatEasJsonPath(projectDir), rawEasJson, { spaces: 2 });
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
Log.withTick('Updated eas.json');
if (mode === 'requireCommit') {
setVcsClient(new GitClient());
await getVcsClient().trackFileAsync(easJsonPath);
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
nonInteractive,
});
}
}
@@ -23,7 +23,7 @@ import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand {
static description = `Delete an environment secret by ID.
Unsure where to find the secret's ID? Run ${chalk.bold('eas secrets:list')}`;
Unsure where to find the secret's ID? Run ${chalk.bold('eas secret:list')}`;
static flags = {
id: flags.string({
@@ -4,7 +4,7 @@ import { AuthCtx } from '../ios/appstore/authenticate';
export const testAuthCtx: AuthCtx = {
appleId: 'test-apple-id',
appleIdPassword: 'test-apple-password',
team: { id: 'test-team-id', name: 'test-team-name', inHouse: false },
team: { id: 'test-apple-team-identifier', name: 'test-team-name', inHouse: false },
};
export function getAppstoreMock(): AppStoreApi {
@@ -22,5 +22,9 @@ export function getAppstoreMock(): AppStoreApi {
createProvisioningProfileAsync: jest.fn(),
revokeProvisioningProfileAsync: jest.fn(),
createOrReuseAdhocProvisioningProfileAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
getAscApiKeyAsync: jest.fn(),
revokeAscApiKeyAsync: jest.fn(),
listAscApiKeysAsync: jest.fn(),
} as any;
}
@@ -1,5 +1,9 @@
import { UserRole } from '@expo/apple-utils';
import {
AppFragment,
AppStoreConnectApiKeyFragment,
AppStoreConnectUserRole,
AppleAppIdentifierFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
@@ -10,12 +14,27 @@ import {
IosDistributionType,
} from '../../graphql/generated';
import * as IosGraphqlClient from '../ios/api/GraphqlClient';
import { DistributionCertificate, ProvisioningProfile } from '../ios/appstore/Credentials.types';
import {
AscApiKey,
DistributionCertificate,
ProvisioningProfile,
} from '../ios/appstore/Credentials.types';
import { Target } from '../ios/types';
import { testProvisioningProfileBase64 } from './fixtures-base64-data';
const now = new Date();
export const testAscApiKey: AscApiKey = {
keyId: 'test-keyIdentifier',
issuerId: 'test-issuer-id-from-apple',
teamId: 'test-team-id',
teamName: 'test-team-name',
name: 'test-name',
roles: [UserRole.ADMIN],
isRevoked: false,
keyP8: 'test-key-p8',
};
export const testProvisioningProfile: ProvisioningProfile = {
provisioningProfileId: 'test-id',
provisioningProfile: testProvisioningProfileBase64,
@@ -38,6 +57,17 @@ export const testAppFragment: AppFragment = {
slug: 'testapp',
};
export const testAscApiKeyFragment: AppStoreConnectApiKeyFragment = {
id: 'test-asc-api-key-id',
appleTeam: { ...testAppleTeamFragment },
issuerIdentifier: 'test-issuerIdentifier',
keyIdentifier: 'test-keyIdentifier',
name: 'test-name',
roles: [AppStoreConnectUserRole.Admin],
updatedAt: now,
createdAt: now,
};
export const testPushKey: ApplePushKeyFragment = {
id: 'test-push-key-id',
keyIdentifier: 'test-key-identifier',
@@ -112,6 +142,7 @@ export const testCommonIosAppCredentialsFragment: CommonIosAppCredentialsFragmen
appleAppIdentifier: testAppleAppIdentifierFragment,
pushKey: testPushKey,
iosAppBuildCredentialsList: [testIosAppBuildCredentialsFragment],
appStoreConnectApiKeyForSubmissions: testAscApiKeyFragment,
};
export const testAppleTeam = {
@@ -139,6 +170,9 @@ export function getNewIosApiMock(): { [key in keyof typeof IosGraphqlClient]?: a
createPushKeyAsync: jest.fn(),
getPushKeyForAppAsync: jest.fn(),
deletePushKeyAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(),
deleteAscApiKeyAsync: jest.fn(),
getAscApiKeysForAccountAsync: jest.fn(() => []),
};
}
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
import { AndroidAppBuildCredentialsFragment } from '../../graphql/generated';
import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { AppLookupParams } from './api/GraphqlClient';
import { Keystore } from './credentials';
@@ -34,7 +34,7 @@ export default class AndroidCredentialsProvider {
}
private async getRemoteAsync(): Promise<AndroidCredentials> {
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: this.options.app });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: this.options.app });
const buildCredentials = await setupBuildCredentialsAction.runAsync(this.ctx);
return this.toAndroidCredentials(buildCredentials);
}
@@ -25,7 +25,7 @@ interface Options {
* Sets up Build Credentials for Android
* @name: sets up build credentials for the specified configuration. If no name is specified, the default configuration is setup
*/
export class SetupBuildCredentials {
export class SetUpBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment> {
@@ -14,7 +14,7 @@ import { AppLookupParams } from '../api/GraphqlClient';
import { getKeystoreWithType } from '../utils/keystoreNew';
import { BackupKeystore } from './DownloadKeystore';
export class SetupBuildCredentialsFromCredentialsJson {
export class SetUpBuildCredentialsFromCredentialsJson {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<AndroidAppBuildCredentialsFragment | null> {
@@ -13,7 +13,7 @@ import { AssignGoogleServiceAccountKey } from './AssignGoogleServiceAccountKey';
import { CreateGoogleServiceAccountKey } from './CreateGoogleServiceAccountKey';
import { UseExistingGoogleServiceAccountKey } from './UseExistingGoogleServiceAccountKey';
export class SetupGoogleServiceAccountKey {
export class SetUpGoogleServiceAccountKey {
constructor(private app: AppLookupParams) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonAndroidAppCredentialsFragment> {
@@ -9,7 +9,7 @@ import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { generateRandomKeystoreAsync } from '../../utils/keystore';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentials } from '../SetupBuildCredentials';
import { SetUpBuildCredentials } from '../SetUpBuildCredentials';
jest.mock('../../../../ora');
jest.mock('../../../../project/ensureProjectExists');
@@ -17,7 +17,7 @@ jest.mock('../../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() =
jest.mock('../../../../prompts', () => ({ confirmAsync: jest.fn(() => true) }));
jest.mock('../../utils/keystore', () => ({ generateRandomKeystoreAsync: jest.fn() }));
describe('SetupBuildCredentials', () => {
describe('SetUpBuildCredentials', () => {
beforeEach(() => {
asMock(generateRandomKeystoreAsync).mockReset();
});
@@ -33,7 +33,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore not to be created
@@ -48,7 +48,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await setupBuildCredentialsAction.runAsync(ctx);
// expect keystore to be created
@@ -63,7 +63,7 @@ describe('SetupBuildCredentials', () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentials({ app: appLookupParams });
const setupBuildCredentialsAction = new SetUpBuildCredentials({ app: appLookupParams });
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -14,7 +14,7 @@ import {
SelectAndroidBuildCredentialsResultType,
} from '../../../manager/SelectAndroidBuildCredentials';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupBuildCredentialsFromCredentialsJson } from '../SetupBuildCredentialsFromCredentialsJson';
import { SetUpBuildCredentialsFromCredentialsJson } from '../SetUpBuildCredentialsFromCredentialsJson';
jest.mock('fs');
@@ -26,7 +26,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetupBuildCredentialsFromCredentialsJson, () => {
describe(SetUpBuildCredentialsFromCredentialsJson, () => {
it('sets up a new build configuration from credentials.json upon user request', async () => {
asMock(SelectAndroidBuildCredentials).mockImplementation(() => {
return {
@@ -59,7 +59,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -109,7 +109,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
'keystore.jks': 'some-binary-content',
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await setupBuildCredentialsAction.runAsync(ctx);
@@ -132,7 +132,7 @@ describe(SetupBuildCredentialsFromCredentialsJson, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupBuildCredentialsAction = new SetupBuildCredentialsFromCredentialsJson(
const setupBuildCredentialsAction = new SetUpBuildCredentialsFromCredentialsJson(
appLookupParams
);
await expect(setupBuildCredentialsAction.runAsync(ctx)).rejects.toThrowError();
@@ -10,7 +10,7 @@ import {
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { getAppLookupParamsFromContextAsync } from '../BuildCredentialsUtils';
import { SetupGoogleServiceAccountKey } from '../SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../SetUpGoogleServiceAccountKey';
jest.mock('../../../../prompts');
jest.mock('fs');
@@ -22,7 +22,7 @@ beforeEach(() => {
vol.reset();
});
describe(SetupGoogleServiceAccountKey, () => {
describe(SetUpGoogleServiceAccountKey, () => {
it('skips setup when there is a Google Service Account Key already assigned to the project', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -34,7 +34,7 @@ describe(SetupGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).not.toHaveBeenCalled();
@@ -57,7 +57,7 @@ describe(SetupGoogleServiceAccountKey, () => {
},
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await setupGoogleServiceAccountKeyAction.runAsync(ctx);
expect(ctx.android.createGoogleServiceAccountKeyAsync).toHaveBeenCalledTimes(1);
@@ -68,7 +68,7 @@ describe(SetupGoogleServiceAccountKey, () => {
nonInteractive: true,
});
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx);
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
await expect(setupGoogleServiceAccountKeyAction.runAsync(ctx)).rejects.toThrowError(
MissingCredentialsNonInteractiveError
);
@@ -11,3 +11,9 @@ export class MissingCredentialsError extends Error {
super(message ?? 'Credentials are not set up.');
}
}
export class UnsupportedCredentialsChoiceError extends Error {
constructor(message: string, public choice: string) {
super(message);
}
}
@@ -9,8 +9,8 @@ import { CredentialsContext } from '../context';
import * as credentialsJsonReader from '../credentialsJson/read';
import { ensureAllTargetsAreConfigured } from '../credentialsJson/utils';
import { getAppFromContext } from './actions/BuildCredentialsUtils';
import { SetupBuildCredentials } from './actions/SetupBuildCredentials';
import { SetupPushKey } from './actions/SetupPushKey';
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
import { SetUpPushKey } from './actions/SetUpPushKey';
import { IosCapabilitiesOptions } from './appstore/ensureAppExists';
import { App, IosCredentials, Target } from './types';
import { isAdHocProfile } from './utils/provisioningProfile';
@@ -58,7 +58,7 @@ export default class IosCredentialsProvider {
}
private async getRemoteAsync(): Promise<IosCredentials> {
return await new SetupBuildCredentials({
return await new SetUpBuildCredentials({
app: this.options.app,
targets: this.options.targets,
distribution: this.options.distribution,
@@ -83,7 +83,7 @@ export default class IosCredentialsProvider {
parentBundleIdentifier: applicationTarget.parentBundleIdentifier,
};
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.succeed(
@@ -1,15 +1,122 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { nanoid } from 'nanoid';
import path from 'path';
import * as uuid from 'uuid';
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log, { learnMore } from '../../../log';
import { promptAsync } from '../../../prompts';
import { getCredentialsFromUserAsync } from '../../utils/promptForCredentials';
import { confirmAsync, promptAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { fromNow } from '../../../utils/date';
import { CredentialsContext } from '../../context';
import {
getCredentialsFromUserAsync,
shouldAutoGenerateCredentialsAsync,
} from '../../utils/promptForCredentials';
import { AscApiKey } from '../appstore/Credentials.types';
import { AscApiKeyPath, MinimalAscApiKey, ascApiKeyMetadataSchema } from '../credentials';
import {
AscApiKeyPath,
MinimalAscApiKey,
ascApiKeyIdSchema,
ascApiKeyIssuerIdSchema,
} from '../credentials';
import { isAscApiKeyValidAndTrackedAsync } from '../validators/validateAscApiKey';
import { formatAppleTeam } from './AppleTeamUtils';
export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
export enum AppStoreApiKeyPurpose {
SUBMISSION_SERVICE = 'EAS Submit',
}
export async function promptForAscApiKeyPathAsync(ctx: CredentialsContext): Promise<AscApiKeyPath> {
const { keyId, keyP8Path } = await promptForKeyP8AndIdAsync();
const bestEffortIssuerId = await getBestEffortIssuerIdAsync(ctx, keyId);
if (bestEffortIssuerId) {
Log.log(`Detected Issuer ID: ${bestEffortIssuerId}`);
return { keyId, issuerId: bestEffortIssuerId, keyP8Path };
}
const issuerId = await promptForIssuerIdAsync();
return { keyId, issuerId, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
// Do not perform uuid validation - Apple's issuerIds are not RFC4122 compliant
const { issuerId } = await getCredentialsFromUserAsync(ascApiKeyIssuerIdSchema);
return issuerId;
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
}
export async function provideOrGenerateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
if (ctx.nonInteractive) {
return await generateAscApiKeyAsync(ctx, purpose);
}
const userProvided = await promptForAscApiKeyAsync(ctx);
if (!userProvided) {
return await generateAscApiKeyAsync(ctx, purpose);
}
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate App Store Connect Api Key, you are not authenticated with Apple.');
return userProvided;
}
const isValidAndTracked = await isAscApiKeyValidAndTrackedAsync(ctx, userProvided);
if (isValidAndTracked) {
return userProvided;
}
const useUserProvided = await confirmAsync({
message: `App Store Connect Api Key with ID ${userProvided.keyId} is not valid on Apple's servers. Proceed anyway?`,
});
if (useUserProvided) {
return userProvided;
}
return await provideOrGenerateAscApiKeyAsync(ctx, purpose);
}
async function generateAscApiKeyAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<MinimalAscApiKey> {
await ctx.appStore.ensureAuthenticatedAsync();
const ascApiKey = await ctx.appStore.createAscApiKeyAsync({
nickname: getAscApiKeyName(purpose),
});
return await getMinimalAscApiKeyAsync(ascApiKey);
}
export function getAscApiKeyName(purpose: AppStoreApiKeyPurpose): string {
const nameParts = ['[Expo]', purpose, nanoid(10)];
return nameParts.join(' ');
}
async function promptForAscApiKeyAsync(ctx: CredentialsContext): Promise<MinimalAscApiKey | null> {
const shouldAutoGenerateCredentials = await shouldAutoGenerateCredentialsAsync(ascApiKeyIdSchema);
if (shouldAutoGenerateCredentials) {
return null;
}
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
return { keyP8: await fs.readFile(keyP8Path, 'utf-8'), keyId, issuerId };
}
async function promptForKeyP8AndIdAsync(): Promise<Pick<AscApiKeyPath, 'keyP8Path' | 'keyId'>> {
Log.log(
chalk.bold('An App Store Connect Api key is required to upload your app to the Apple App Store')
);
@@ -39,33 +146,108 @@ export async function promptForAscApiKeyAsync(): Promise<AscApiKeyPath> {
});
const regex = /^AuthKey_(?<keyId>\w+)\.p8$/; // Common ASC Api file name downloaded from Apple
const bestEffortKeyId = path.basename(keyP8Path).match(regex)?.groups?.keyId;
const ascApiKeyMetadata = await getCredentialsFromUserAsync(ascApiKeyMetadataSchema, {
const { keyId } = await getCredentialsFromUserAsync(ascApiKeyIdSchema, {
keyId: bestEffortKeyId,
});
return { ...ascApiKeyMetadata, keyP8Path };
return { keyId, keyP8Path };
}
export async function promptForIssuerIdAsync(): Promise<string> {
Log.log(chalk.bold('An App Store Connect Issuer ID is required'));
Log.log(
`If you're not sure what this is or how to find yours, ${learnMore(
'https://expo.fyi/asc-issuer-id'
)}`
);
async function getBestEffortIssuerIdAsync(
ctx: CredentialsContext,
ascApiKeyId: string
): Promise<string | null> {
if (!ctx.appStore.authCtx) {
return null;
}
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKeyId);
return ascApiKeyInfo?.issuerId ?? null;
}
const { issuerId } = await promptAsync({
type: 'text',
name: 'issuerId',
message: 'App Store Connect Issuer ID:',
validate: (input: string) => uuid.validate(input),
export async function getAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment[]> {
const ascApiKeysForAccount = await ctx.ios.getAscApiKeysForAccountAsync(account);
if (!filterDifferentAppleTeam) {
return ascApiKeysForAccount;
} else {
return filterKeysFromDifferentAppleTeam(ctx, ascApiKeysForAccount);
}
}
export async function selectAscApiKeysFromAccountAsync(
ctx: CredentialsContext,
account: Account,
{ filterDifferentAppleTeam }: { filterDifferentAppleTeam?: boolean } = {}
): Promise<AppStoreConnectApiKeyFragment | null> {
const ascApiKeysForAccount = await getAscApiKeysFromAccountAsync(ctx, account, {
filterDifferentAppleTeam,
});
return issuerId;
if (ascApiKeysForAccount.length === 0) {
if (filterDifferentAppleTeam) {
const maybeAppleTeamId = ctx.appStore.authCtx?.team.id;
Log.warn(
`There are no App Store Connect Api Keys in your EAS account${
maybeAppleTeamId ? ` matching Apple Team ID: ${maybeAppleTeamId}.` : '.'
}`
);
} else {
Log.warn(`There are no App Store Connect Api Keys available in your EAS account.`);
}
return null;
}
return selectAscApiKeysAsync(ascApiKeysForAccount);
}
export async function getMinimalAscApiKeyAsync(ascApiKey: AscApiKey): Promise<MinimalAscApiKey> {
return {
...ascApiKey,
issuerId: ascApiKey.issuerId ?? (await promptForIssuerIdAsync()),
};
async function selectAscApiKeysAsync(
ascApiKeys: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
const sortedAscApiKeys = sortAscApiKeysByUpdatedAtDesc(ascApiKeys);
const { chosenAscApiKey } = await promptAsync({
type: 'select',
name: 'chosenAscApiKey',
message: 'Select an Api Key from the list:',
choices: sortedAscApiKeys.map(ascApiKey => ({
title: formatAscApiKey(ascApiKey),
value: ascApiKey,
})),
});
return chosenAscApiKey;
}
function filterKeysFromDifferentAppleTeam(
ctx: CredentialsContext,
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
if (!ctx.appStore.authCtx) {
return keys;
}
const teamId = ctx.appStore.authCtx.team.id;
return keys.filter(key => !key.appleTeam || key.appleTeam?.appleTeamIdentifier === teamId);
}
export function sortAscApiKeysByUpdatedAtDesc(
keys: AppStoreConnectApiKeyFragment[]
): AppStoreConnectApiKeyFragment[] {
return keys.sort(
(keyA, keyB) => new Date(keyB.updatedAt).getTime() - new Date(keyA.updatedAt).getTime()
);
}
export function formatAscApiKey(ascApiKey: AppStoreConnectApiKeyFragment): string {
const { keyIdentifier, appleTeam, name, updatedAt } = ascApiKey;
let line: string = '';
line += `Key ID: ${keyIdentifier}`;
if (name) {
line += chalk.gray(`\n Name: ${name}`);
}
if (appleTeam) {
line += chalk.gray(`\n ${formatAppleTeam(appleTeam)}`);
}
line += chalk.gray(`\n Updated: ${fromNow(new Date(updatedAt))} ago`);
return line;
}
@@ -0,0 +1,38 @@
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
export class AssignAscApiKey {
constructor(private app: AppLookupParams) {}
public async runAsync(
ctx: CredentialsContext,
ascApiKey: AppStoreConnectApiKeyFragment,
purpose: AppStoreApiKeyPurpose
): Promise<CommonIosAppCredentialsFragment> {
const appleTeam =
(await resolveAppleTeamIfAuthenticatedAsync(ctx, this.app)) ?? ascApiKey.appleTeam ?? null;
const appCredentials = await ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync(
this.app,
{ appleTeam: appleTeam ?? undefined }
);
let updatedAppCredentials;
if (purpose === AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
updatedAppCredentials = await ctx.ios.updateIosAppCredentialsAsync(appCredentials, {
ascApiKeyIdForSubmissions: ascApiKey.id,
});
} else {
throw new Error(`${purpose} is not yet supported.`);
}
Log.succeed(
`App Store Connect Api Key assigned to ${this.app.projectName}: ${this.app.bundleIdentifier} for ${purpose}.`
);
return updatedAppCredentials;
}
}
@@ -0,0 +1,23 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { AppStoreApiKeyPurpose, provideOrGenerateAscApiKeyAsync } from './AscApiKeyUtils';
export class CreateAscApiKey {
constructor(private account: Account) {}
public async runAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<AppStoreConnectApiKeyFragment> {
if (ctx.nonInteractive) {
throw new Error(`A new App Store Connect Api Key cannot be created in non-interactive mode.`);
}
const ascApiKey = await provideOrGenerateAscApiKeyAsync(ctx, purpose);
const result = await ctx.ios.createAscApiKeyAsync(this.account, ascApiKey);
Log.succeed('Created App Store Connect Api Key');
return result;
}
}
@@ -159,7 +159,7 @@ export async function provideOrGenerateDistributionCertificateAsync(
if (userProvided) {
if (!ctx.appStore.authCtx) {
Log.warn(
'Unable to validate distribution certificate due to insufficient Apple Credentials'
'Unable to validate distribution certificate, you are not authenticated with Apple.'
);
return userProvided;
} else {
@@ -19,7 +19,7 @@ export async function provideOrGeneratePushKeyAsync(ctx: CredentialsContext): Pr
const userProvided = await promptForPushKeyAsync(ctx);
if (userProvided) {
if (!ctx.appStore.authCtx) {
Log.warn('Unable to validate push key due to insufficient Apple Credentials');
Log.warn('Unable to validate push key, you are not authenticated with Apple.');
return userProvided;
} else {
const isValidAndTracked = await isPushKeyValidAndTrackedAsync(ctx, userProvided);
@@ -0,0 +1,53 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync } from '../../../prompts';
import { Account } from '../../../user/Account';
import { CredentialsContext } from '../../context';
import { selectAscApiKeysFromAccountAsync } from './AscApiKeyUtils';
export class SelectAndRemoveAscApiKey {
constructor(private account: Account) {}
async runAsync(ctx: CredentialsContext): Promise<void> {
const selected = await selectAscApiKeysFromAccountAsync(ctx, this.account);
if (selected) {
await new RemoveAscApiKey(this.account, selected).runAsync(ctx);
Log.succeed('Removed App Store Connect Api Key');
Log.newLine();
}
}
}
export class RemoveAscApiKey {
constructor(private account: Account, private ascApiKey: AppStoreConnectApiKeyFragment) {}
public async runAsync(ctx: CredentialsContext): Promise<void> {
if (ctx.nonInteractive) {
throw new Error(`Cannot remove App Store Connect Api Keys in non-interactive mode.`);
}
// TODO(quin): add an extra edge on AppStoreConnectApiKey to find the apps using it
const confirm = await confirmAsync({
message: `Deleting this Api Key may affect your projects that rely on it. Do you want to continue?`,
});
if (!confirm) {
Log.log('Aborting');
return;
}
Log.log('Removing Api Key');
await ctx.ios.deleteAscApiKeyAsync(this.ascApiKey.id);
let shouldRevoke = false;
if (!ctx.nonInteractive) {
shouldRevoke = await confirmAsync({
message: `Do you also want to revoke this Api Key on the Apple Developer Portal?`,
});
} else if (ctx.nonInteractive) {
Log.log('Skipping Api Key revocation on the Apple Developer Portal.');
}
if (shouldRevoke) {
await ctx.appStore.revokeAscApiKeyAsync(this.ascApiKey.keyIdentifier);
}
}
}
@@ -21,7 +21,7 @@ import { validateProvisioningProfileAsync } from '../validators/validateProvisio
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCredentialsUtils';
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
enum ReuseAction {
Yes,
@@ -29,11 +29,11 @@ enum ReuseAction {
No,
}
export class SetupAdhocProvisioningProfile {
export class SetUpAdhocProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetupDistributionCertificate(
const distCert = await new SetUpDistributionCertificate(
this.app,
IosDistributionType.AdHoc
).runAsync(ctx);
@@ -0,0 +1,148 @@
import assert from 'assert';
import nullthrows from 'nullthrows';
import {
AppStoreConnectApiKeyFragment,
CommonIosAppCredentialsFragment,
} from '../../../graphql/generated';
import Log from '../../../log';
import { confirmAsync, promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import {
MissingCredentialsNonInteractiveError,
UnsupportedCredentialsChoiceError,
} from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
import { getValidAndTrackedAscApiKeysAsync } from '../validators/validateAscApiKey';
import {
AppStoreApiKeyPurpose,
formatAscApiKey,
getAscApiKeysFromAccountAsync,
selectAscApiKeysFromAccountAsync,
sortAscApiKeysByUpdatedAtDesc,
} from './AscApiKeyUtils';
import { AssignAscApiKey } from './AssignAscApiKey';
import { CreateAscApiKey } from './CreateAscApiKey';
export enum SetupAscApiKeyChoice {
GENERATE = 'GENERATE',
USE_EXISTING = 'USE_EXISTING',
}
export class SetUpAscApiKey {
public choices: { title: string; value: string }[] = [
{
title: '[Choose an existing key]',
value: SetupAscApiKeyChoice.USE_EXISTING,
},
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
];
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
public async runAsync(ctx: CredentialsContext): Promise<CommonIosAppCredentialsFragment> {
const isKeySetup = await this.isAscApiKeySetupAsync(ctx, this.purpose);
if (isKeySetup) {
Log.succeed('App Store Connect Api Key already set up.');
return nullthrows(
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app),
'iosAppCredentials cannot be null if App Store Connect Api Key is already set up'
);
}
if (ctx.nonInteractive) {
throw new MissingCredentialsNonInteractiveError(
'App Store Connect Api Keys cannot be set up in --non-interactive mode.'
);
}
const keysForAccount = await getAscApiKeysFromAccountAsync(ctx, this.app.account, {
filterDifferentAppleTeam: true,
});
const maybeAutoselectedKey = await this.doBestEffortAutoselectAsync(ctx, keysForAccount);
if (maybeAutoselectedKey) {
return await new AssignAscApiKey(this.app).runAsync(ctx, maybeAutoselectedKey, this.purpose);
}
const availableChoices =
keysForAccount.length === 0
? this.choices.filter(choice => choice.value !== SetupAscApiKeyChoice.USE_EXISTING)
: this.choices;
const ascApiKey = await this.processChoicesAsync(ctx, this.purpose, availableChoices);
return await new AssignAscApiKey(this.app).runAsync(ctx, ascApiKey, this.purpose);
}
private async doBestEffortAutoselectAsync(
ctx: CredentialsContext,
keysForAccount: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment | null> {
if (!ctx.appStore.authCtx) {
return null;
}
if (keysForAccount.length === 0) {
return null;
}
// only provide autoselect if we can find a key that is certainly valid
const validKeys = await getValidAndTrackedAscApiKeysAsync(ctx, keysForAccount);
if (validKeys.length === 0) {
return null;
}
const [autoselectedKey] = sortAscApiKeysByUpdatedAtDesc(validKeys);
const useAutoselected = await confirmAsync({
message: `Reuse this App Store Connect Api Key?\n${formatAscApiKey(autoselectedKey)}`,
});
if (useAutoselected) {
Log.log(`Using App Store Connect Api Key with ID ${autoselectedKey.keyIdentifier}`);
return autoselectedKey;
} else {
return null;
}
}
private async isAscApiKeySetupAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose
): Promise<boolean> {
const appCredentials = await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(this.app);
if (purpose !== AppStoreApiKeyPurpose.SUBMISSION_SERVICE) {
throw new Error(`App Store Connect Api Key setup is not yet supported for ${purpose}.`);
}
return !!appCredentials?.appStoreConnectApiKeyForSubmissions;
}
private async processChoicesAsync(
ctx: CredentialsContext,
purpose: AppStoreApiKeyPurpose,
choices: { title: string; value: string }[]
): Promise<AppStoreConnectApiKeyFragment> {
assert(choices.length > 0, 'SetupAscApiKey: There must be at least one choice');
let choice: string;
if (choices.length === 1) {
choice = choices[0].value;
} else {
const result = await promptAsync({
type: 'select',
name: 'choice',
message: 'Select the App Store Connect Api Key to use for your project:',
choices,
});
choice = result.choice;
}
if (choice === SetupAscApiKeyChoice.GENERATE) {
return await new CreateAscApiKey(this.app.account).runAsync(ctx, purpose);
} else if (choice === SetupAscApiKeyChoice.USE_EXISTING) {
const selectedAscApiKey = await selectAscApiKeysFromAccountAsync(ctx, this.app.account, {
filterDifferentAppleTeam: true,
});
if (!selectedAscApiKey) {
return await this.processChoicesAsync(ctx, purpose, choices);
}
return selectedAscApiKey;
}
throw new UnsupportedCredentialsChoiceError(
`AscApiKey Setup does not support choice:${choice}`,
choice
);
}
}
@@ -7,7 +7,7 @@ import { CredentialsContext } from '../../context';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { App, IosAppBuildCredentialsMap, IosCredentials, Target } from '../types';
import { displayProjectCredentials } from '../utils/printCredentials';
import { SetupTargetBuildCredentials } from './SetupTargetBuildCredentials';
import { SetUpTargetBuildCredentials } from './SetUpTargetBuildCredentials';
interface Options {
app: App;
@@ -17,7 +17,7 @@ interface Options {
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetupBuildCredentials {
export class SetUpBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosCredentials> {
@@ -49,7 +49,7 @@ export class SetupBuildCredentials {
} else {
Log.newLine();
}
const action = new SetupTargetBuildCredentials({
const action = new SetUpTargetBuildCredentials({
...this.options,
app: {
...this.options.app,
@@ -8,9 +8,9 @@ import { readIosCredentialsAsync } from '../../credentialsJson/read';
import { IosCredentials } from '../../credentialsJson/types';
import { ensureAllTargetsAreConfigured } from '../../credentialsJson/utils';
import { App, Target } from '../types';
import { SetupTargetBuildCredentialsFromCredentialsJson } from './SetupTargetBuildCredentialsFromCredentialsJson';
import { SetUpTargetBuildCredentialsFromCredentialsJson } from './SetUpTargetBuildCredentialsFromCredentialsJson';
export class SetupBuildCredentialsFromCredentialsJson {
export class SetUpBuildCredentialsFromCredentialsJson {
constructor(
private app: App,
private targets: Target[],
@@ -32,7 +32,7 @@ export class SetupBuildCredentialsFromCredentialsJson {
);
Log.newLine();
}
await new SetupTargetBuildCredentialsFromCredentialsJson(
await new SetUpTargetBuildCredentialsFromCredentialsJson(
{
account: this.app.account,
projectName: this.app.projectName,
@@ -18,7 +18,7 @@ import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
import { CreateDistributionCertificate } from './CreateDistributionCertificate';
import { formatDistributionCertificate } from './DistributionCertificateUtils';
export class SetupDistributionCertificate {
export class SetUpDistributionCertificate {
private validDistCerts?: AppleDistributionCertificateFragment[];
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
@@ -4,8 +4,8 @@ import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/GraphqlClient';
import { getAllBuildCredentialsAsync } from './BuildCredentialsUtils';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
/**
* It's used when setting up credentials for internal distribution but `enterpriseProvisioning` is not set.
@@ -14,7 +14,7 @@ import { SetupProvisioningProfile } from './SetupProvisioningProfile';
* to choose if they want to set up an adhoc or universal distribution provisioning profile. Otherwise, always
* set up an adhoc provisioning profile.
*/
export class SetupInternalProvisioningProfile {
export class SetUpInternalProvisioningProfile {
constructor(private app: AppLookupParams) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
@@ -82,13 +82,13 @@ export class SetupInternalProvisioningProfile {
private async setupAdhocProvisioningProfileAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetupAdhocProvisioningProfile(this.app).runAsync(ctx);
return await new SetUpAdhocProvisioningProfile(this.app).runAsync(ctx);
}
private async setupUniversalProvisioningProfileAsync(
ctx: CredentialsContext
): Promise<IosAppBuildCredentialsFragment> {
return await new SetupProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
return await new SetUpProvisioningProfile(this.app, IosDistributionType.Enterprise).runAsync(
ctx
);
}
@@ -20,12 +20,12 @@ import {
import { ConfigureProvisioningProfile } from './ConfigureProvisioningProfile';
import { CreateProvisioningProfile } from './CreateProvisioningProfile';
import { formatProvisioningProfileFromApple } from './ProvisioningProfileUtils';
import { SetupDistributionCertificate } from './SetupDistributionCertificate';
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
/**
* Sets up either APP_STORE or ENTERPRISE provisioning profiles
*/
export class SetupProvisioningProfile {
export class SetUpProvisioningProfile {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
@@ -84,7 +84,7 @@ export class SetupProvisioningProfile {
}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
const distCert = await new SetupDistributionCertificate(
const distCert = await new SetUpDistributionCertificate(
this.app,
this.distributionType
).runAsync(ctx);
@@ -14,7 +14,7 @@ import {
selectPushKeyAsync,
} from './PushKeyUtils';
export class SetupPushKey {
export class SetUpPushKey {
constructor(private app: AppLookupParams) {}
async isPushKeySetupAsync(ctx: CredentialsContext): Promise<boolean> {
@@ -8,9 +8,9 @@ import Log from '../../../log';
import { CredentialsContext } from '../../context';
import { AppLookupParams as GraphQLAppLookupParams } from '../api/GraphqlClient';
import { IosCapabilitiesOptions } from '../appstore/ensureAppExists';
import { SetupAdhocProvisioningProfile } from './SetupAdhocProvisioningProfile';
import { SetupInternalProvisioningProfile } from './SetupInternalProvisioningProfile';
import { SetupProvisioningProfile } from './SetupProvisioningProfile';
import { SetUpAdhocProvisioningProfile } from './SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from './SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from './SetUpProvisioningProfile';
interface Options {
app: GraphQLAppLookupParams;
@@ -18,7 +18,7 @@ interface Options {
enterpriseProvisioning?: IosEnterpriseProvisioning;
iosCapabilitiesOptions?: IosCapabilitiesOptions;
}
export class SetupTargetBuildCredentials {
export class SetUpTargetBuildCredentials {
constructor(private options: Options) {}
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
@@ -50,17 +50,17 @@ export class SetupTargetBuildCredentials {
const { app, distribution, enterpriseProvisioning } = this.options;
if (distribution === 'internal') {
if (enterpriseProvisioning === 'adhoc') {
return await new SetupAdhocProvisioningProfile(app).runAsync(ctx);
return await new SetUpAdhocProvisioningProfile(app).runAsync(ctx);
} else if (enterpriseProvisioning === 'universal') {
return await new SetupProvisioningProfile(
return await new SetUpProvisioningProfile(
app,
GraphQLIosDistributionType.Enterprise
).runAsync(ctx);
} else {
return await new SetupInternalProvisioningProfile(app).runAsync(ctx);
return await new SetUpInternalProvisioningProfile(app).runAsync(ctx);
}
} else {
return await new SetupProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
return await new SetUpProvisioningProfile(app, GraphQLIosDistributionType.AppStore).runAsync(
ctx
);
}
@@ -22,7 +22,7 @@ import {
getProvisioningProfileAsync,
} from './BuildCredentialsUtils';
export class SetupTargetBuildCredentialsFromCredentialsJson {
export class SetUpTargetBuildCredentialsFromCredentialsJson {
constructor(
private app: AppLookupParams,
private distributionType: IosDistributionType,
@@ -5,7 +5,7 @@ import {
import { testDistCertFragmentOneDependency } from '../../../__tests__/fixtures-ios';
import { CredentialsContext } from '../../../context';
import { AppLookupParams } from '../../api/GraphqlClient';
export class SetupDistributionCertificate {
export class SetUpDistributionCertificate {
constructor(private app: AppLookupParams, private distributionType: IosDistributionType) {}
public async runAsync(_ctx: CredentialsContext): Promise<AppleDistributionCertificateFragment> {
@@ -0,0 +1,90 @@
import { asMock } from '../../../../__tests__/utils';
import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey } from '../../../__tests__/fixtures-ios';
import { getCredentialsFromUserAsync } from '../../../utils/promptForCredentials';
import {
AppStoreApiKeyPurpose,
getAscApiKeyName,
promptForAscApiKeyPathAsync,
} from '../AscApiKeyUtils';
jest.mock('../../../../prompts');
jest.mock('../../../utils/promptForCredentials');
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
return Object.keys(obj) as K[];
}
afterEach(() => {
asMock(promptAsync).mockClear();
asMock(getCredentialsFromUserAsync).mockClear();
});
describe(getAscApiKeyName, () => {
// Apple enforces a 30 char limit on this name
it('produces a name under 30 chars', async () => {
for (const value of enumKeys(AppStoreApiKeyPurpose)) {
const purpose = AppStoreApiKeyPurpose[value];
const name = getAscApiKeyName(purpose);
expect(name.length < 30).toBe(true);
}
});
});
describe(promptForAscApiKeyPathAsync, () => {
it('prompts for keyId, keyP8Path and issuerId when user is not authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => {
throw new Error('this should not be called');
}),
authCtx: null,
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
keyP8Path: '/asc-api-key.p8',
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(2); // keyId, issuerId
});
it('prompts for keyId, keyP8Path and detects issuerId when user is authenticated to Apple', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
}));
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
getAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx);
expect(ascApiKeyPath).toEqual({
keyId: 'test-key-id',
issuerId: 'test-issuer-id-from-apple',
keyP8Path: '/asc-api-key.p8',
});
expect(promptAsync).toHaveBeenCalledTimes(1); // keyP8Path
expect(getCredentialsFromUserAsync).toHaveBeenCalledTimes(1); // keyId
expect(asMock(ctx.appStore.getAscApiKeyAsync).mock.calls.length).toBe(1); // issuerId
});
});
@@ -0,0 +1,46 @@
import { findApplicationTarget } from '../../../../project/ios/target';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { AssignAscApiKey } from '../AssignAscApiKey';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
describe(AssignAscApiKey, () => {
it('assigns an App Store Connect Api Key in Interactive Mode for EAS Submit', async () => {
const ctx = createCtxMock({
nonInteractive: false,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
await assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
// expect app credentials to be fetched/created, then updated
expect(ctx.ios.createOrGetIosAppCredentialsWithCommonFieldsAsync).toHaveBeenCalledTimes(1);
expect(ctx.ios.updateIosAppCredentialsAsync).toHaveBeenCalledTimes(1);
});
it('works in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const assignAscApiKeyAction = new AssignAscApiKey(appLookupParams);
await assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
// dont fail if users are running in non-interactive mode
await expect(
assignAscApiKeyAction.runAsync(
ctx,
testAscApiKeyFragment,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
)
).resolves.not.toThrowError();
});
});
@@ -0,0 +1,44 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKey, testTargets } from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { CreateAscApiKey } from '../CreateAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(CreateAscApiKey, () => {
it('creates a App Store Api Key in Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
await createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
// expect api key to be created on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect api key to be created on apple portal
expect(asMock(ctx.appStore.createAscApiKeyAsync).mock.calls.length).toBe(1);
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new CreateAscApiKey(appLookupParams.account);
await expect(
createAscApiKeyAction.runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE)
).rejects.toThrowError();
});
});
@@ -0,0 +1,33 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync } from '../../../../prompts';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { testAscApiKeyFragment, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { RemoveAscApiKey } from '../RemoveAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(RemoveAscApiKey, () => {
it('removes an Asc Api Key', async () => {
const ctx = createCtxMock({ nonInteractive: false });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
await removeAscApiKeyAction.runAsync(ctx);
expect(ctx.ios.deleteAscApiKeyAsync).toHaveBeenCalledTimes(1);
expect(ctx.appStore.revokeAscApiKeyAsync).toHaveBeenCalledTimes(1);
});
it('errors in Non-Interactive Mode', async () => {
const ctx = createCtxMock({ nonInteractive: true });
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const removeAscApiKeyAction = new RemoveAscApiKey(
appLookupParams.account,
testAscApiKeyFragment
);
await expect(removeAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -1,4 +1,4 @@
import { doUDIDsMatch } from '../SetupAdhocProvisioningProfile';
import { doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
describe(doUDIDsMatch, () => {
it('return false if UDIDs do not match', () => {
@@ -0,0 +1,146 @@
import { asMock } from '../../../../__tests__/utils';
import { findApplicationTarget } from '../../../../project/ios/target';
import { confirmAsync, promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import {
getNewIosApiMock,
testAscApiKey,
testAscApiKeyFragment,
testCommonIosAppCredentialsFragment,
testTargets,
} from '../../../__tests__/fixtures-ios';
import { AppStoreApiKeyPurpose } from '../AscApiKeyUtils';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetUpAscApiKey, SetupAscApiKeyChoice } from '../SetUpAscApiKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetUpAscApiKey, () => {
it('skips setting up a ASC Api Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC Api Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration not to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(0);
});
it('sets up a ASC Api Key, creating a new one if there are no existing ASC Api Keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
createAscApiKeyAsync: jest.fn(() => testAscApiKey),
},
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(),
createAscApiKeyAsync: jest.fn(() => testAscApiKeyFragment),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect to create a new ASC Api Key on expo servers
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(1);
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC Api Key, use autoselected ASC Api Key when there are existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
appStore: {
...getAppstoreMock(),
ensureAuthenticatedAsync: jest.fn(() => testAuthCtx),
authCtx: testAuthCtx,
listAscApiKeysAsync: jest.fn(() => [testAscApiKey]),
},
ios: {
...getNewIosApiMock(),
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('sets up a ASC Api Key, allowing user to choose existing keys', async () => {
const ctx = createCtxMock({
nonInteractive: false,
ios: {
...getNewIosApiMock(),
getAscApiKeysForAccountAsync: jest.fn(() => [testAscApiKeyFragment]),
},
});
asMock(promptAsync).mockImplementation(() => ({
choice: SetupAscApiKeyChoice.USE_EXISTING,
chosenAscApiKey: testAscApiKeyFragment,
}));
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await setupAscApiKeyAction.runAsync(ctx);
// expect not to create a new ASC Api Key on expo servers, we are using the existing one
expect(asMock(ctx.ios.createAscApiKeyAsync).mock.calls.length).toBe(0);
// expect configuration to be updated with a new ASC Api Key
expect(asMock(ctx.ios.updateIosAppCredentialsAsync).mock.calls.length).toBe(1);
});
it('works in Non Interactive Mode if ASC Key is configured', async () => {
const ctx = createCtxMock({
nonInteractive: true,
ios: {
...getNewIosApiMock(),
getIosAppCredentialsWithCommonFieldsAsync: jest.fn(
() => testCommonIosAppCredentialsFragment
),
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await expect(setupAscApiKeyAction.runAsync(ctx)).resolves.not.toThrowError();
});
it('errors in Non Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
await expect(createAscApiKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -4,13 +4,13 @@ import { promptAsync } from '../../../../prompts';
import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appstore';
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getAllBuildCredentialsAsync } from '../BuildCredentialsUtils';
import { SetupAdhocProvisioningProfile } from '../SetupAdhocProvisioningProfile';
import { SetupInternalProvisioningProfile } from '../SetupInternalProvisioningProfile';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
import { SetUpAdhocProvisioningProfile } from '../SetUpAdhocProvisioningProfile';
import { SetUpInternalProvisioningProfile } from '../SetUpInternalProvisioningProfile';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
jest.mock('../SetupAdhocProvisioningProfile');
jest.mock('../SetupProvisioningProfile');
jest.mock('../SetUpAdhocProvisioningProfile');
jest.mock('../SetUpProvisioningProfile');
jest.mock('../BuildCredentialsUtils', () => ({ getAllBuildCredentialsAsync: jest.fn() }));
beforeEach(() => {
@@ -33,15 +33,15 @@ const testEnterpriseBuildCredentials: IosAppBuildCredentialsFragment = {
iosDistributionType: IosDistributionType.Enterprise,
};
describe(SetupInternalProvisioningProfile, () => {
describe(SetUpInternalProvisioningProfile, () => {
describe('interactive mode', () => {
describe('when authenticated with apple', () => {
it('runs the SetupAdhocProvisioningProfile action for non-enterprise team', async () => {
it('runs the SetUpAdhocProvisioningProfile action for non-enterprise team', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -56,7 +56,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -72,7 +72,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -90,7 +90,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -110,7 +110,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -125,14 +125,14 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
@@ -141,7 +141,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -156,14 +156,14 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(promptAsync).mockImplementationOnce(() => ({
distributionType: IosDistributionType.Enterprise,
}));
@@ -174,7 +174,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -189,7 +189,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -205,7 +205,7 @@ describe(SetupInternalProvisioningProfile, () => {
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -235,7 +235,7 @@ describe(SetupInternalProvisioningProfile, () => {
];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -253,7 +253,7 @@ describe(SetupInternalProvisioningProfile, () => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -264,12 +264,12 @@ describe(SetupInternalProvisioningProfile, () => {
await expect(action.runAsync(ctx)).rejects.toThrow(/couldn't find any credentials/);
});
it('runs the SetupAdhocProvisioningProfile action when adhoc credentials exist', async () => {
it('runs the SetUpAdhocProvisioningProfile action when adhoc credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testAdhocBuildCredentials];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -279,19 +279,19 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupAdhocProvisioningProfile.prototype.runAsync = runAsync;
SetUpAdhocProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
expect(runAsync).toHaveBeenCalled();
});
it('runs the SetupProvisioningProfile action when enterprise credentials exist', async () => {
it('runs the SetUpProvisioningProfile action when enterprise credentials exist', async () => {
asMock(getAllBuildCredentialsAsync).mockImplementationOnce(() => {
const buildCredentials: IosAppBuildCredentialsFragment[] = [testEnterpriseBuildCredentials];
return buildCredentials;
});
const action = new SetupInternalProvisioningProfile({
const action = new SetUpInternalProvisioningProfile({
account: { id: 'account-id', name: 'account-name' },
bundleIdentifier: 'com.expo.test',
projectName: 'testproject',
@@ -301,7 +301,7 @@ describe(SetupInternalProvisioningProfile, () => {
});
const runAsync = jest.fn();
SetupProvisioningProfile.prototype.runAsync = runAsync;
SetUpProvisioningProfile.prototype.runAsync = runAsync;
await action.runAsync(ctx);
@@ -16,16 +16,16 @@ import {
import { MissingCredentialsNonInteractiveError } from '../../../errors';
import { validateProvisioningProfileAsync } from '../../validators/validateProvisioningProfile';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupProvisioningProfile } from '../SetupProvisioningProfile';
import { SetUpProvisioningProfile } from '../SetUpProvisioningProfile';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
jest.mock('../SetupDistributionCertificate');
jest.mock('../SetUpDistributionCertificate');
jest.mock('../ConfigureProvisioningProfile');
jest.mock('../CreateProvisioningProfile');
jest.mock('../../validators/validateProvisioningProfile');
describe('SetupProvisioningProfile', () => {
describe('SetUpProvisioningProfile', () => {
it('repairs existing Provisioning Profile with bad build credentials in Interactive Mode', async () => {
asMock(validateProvisioningProfileAsync).mockImplementation(() => false);
const ctx = createCtxMock({
@@ -54,7 +54,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -84,7 +84,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -114,7 +114,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -140,7 +140,7 @@ describe('SetupProvisioningProfile', () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -157,7 +157,7 @@ describe('SetupProvisioningProfile', () => {
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupProvisioningProfileAction = new SetupProvisioningProfile(
const setupProvisioningProfileAction = new SetUpProvisioningProfile(
appLookupParams,
IosDistributionType.AppStore
);
@@ -5,12 +5,12 @@ import { getAppstoreMock, testAuthCtx } from '../../../__tests__/fixtures-appsto
import { createCtxMock } from '../../../__tests__/fixtures-context';
import { getNewIosApiMock, testPushKey, testTargets } from '../../../__tests__/fixtures-ios';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupPushKey } from '../SetupPushKey';
import { SetUpPushKey } from '../SetUpPushKey';
jest.mock('../../../../prompts');
asMock(confirmAsync).mockImplementation(() => true);
describe(SetupPushKey, () => {
describe(SetUpPushKey, () => {
it('skips setting up a Push Key if it is already configured', async () => {
const ctx = createCtxMock({
nonInteractive: false,
@@ -20,7 +20,7 @@ describe(SetupPushKey, () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers
@@ -38,7 +38,7 @@ describe(SetupPushKey, () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
await setupPushKeyAction.runAsync(ctx);
// expect to create a new push key on expo servers
@@ -62,7 +62,7 @@ describe(SetupPushKey, () => {
},
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupPushKeyAction = new SetupPushKey(appLookupParams);
const setupPushKeyAction = new SetUpPushKey(appLookupParams);
await setupPushKeyAction.runAsync(ctx);
// expect not to create a new push key on expo servers, we are using the existing one
@@ -75,7 +75,7 @@ describe(SetupPushKey, () => {
nonInteractive: true,
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const createPushKeyAction = new SetupPushKey(appLookupParams);
const createPushKeyAction = new SetUpPushKey(appLookupParams);
await expect(createPushKeyAction.runAsync(ctx)).rejects.toThrowError();
});
});
@@ -15,7 +15,7 @@ import {
} from '../../../__tests__/fixtures-ios';
import { IosTargetCredentials } from '../../../credentialsJson/types';
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
import { SetupTargetBuildCredentialsFromCredentialsJson } from '../SetupTargetBuildCredentialsFromCredentialsJson';
import { SetUpTargetBuildCredentialsFromCredentialsJson } from '../SetUpTargetBuildCredentialsFromCredentialsJson';
jest.mock('../../../../prompts');
jest.mock('../../../credentialsJson/read');
@@ -25,7 +25,7 @@ beforeEach(() => {
asMock(confirmAsync).mockImplementation(() => true);
});
describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
describe('SetUpTargetBuildCredentialsFromCredentialsJson', () => {
const targetCredentials: IosTargetCredentials = {
distributionCertificate: {
certificateP12: testDistCert.certP12,
@@ -53,7 +53,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -82,7 +82,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -115,7 +115,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -149,7 +149,7 @@ describe('SetupTargetBuildCredentialsFromCredentialsJson', () => {
});
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
const setupBuildCredentialsFromCredentialsJsonAction =
new SetupTargetBuildCredentialsFromCredentialsJson(
new SetUpTargetBuildCredentialsFromCredentialsJson(
appLookupParams,
IosDistributionType.AppStore,
targetCredentials
@@ -1,7 +1,10 @@
import { UserRole } from '@expo/apple-utils';
import nullthrows from 'nullthrows';
import {
AppFragment,
AppStoreConnectApiKeyFragment,
AppStoreConnectUserRole,
AppleAppIdentifierFragment,
AppleDeviceFragment,
AppleDistributionCertificateFragment,
@@ -14,7 +17,9 @@ import {
import { isWildcardBundleIdentifier } from '../../../project/ios/bundleIdentifier';
import { Account } from '../../../user/Account';
import { DistributionCertificate, PushKey } from '../appstore/Credentials.types';
import { MinimalAscApiKey } from '../credentials';
import { AppleTeamMissingError } from '../errors';
import { AppStoreConnectApiKeyMutation } from './graphql/mutations/AppStoreConnectApiKeyMutation';
import { AppleAppIdentifierMutation } from './graphql/mutations/AppleAppIdentifierMutation';
import {
AppleDistributionCertificateMutation,
@@ -29,6 +34,7 @@ import { AppleTeamMutation } from './graphql/mutations/AppleTeamMutation';
import { IosAppBuildCredentialsMutation } from './graphql/mutations/IosAppBuildCredentialsMutation';
import { IosAppCredentialsMutation } from './graphql/mutations/IosAppCredentialsMutation';
import { AppQuery } from './graphql/queries/AppQuery';
import { AppStoreConnectApiKeyQuery } from './graphql/queries/AppStoreConnectApiKeyQuery';
import { AppleAppIdentifierQuery } from './graphql/queries/AppleAppIdentifierQuery';
import { AppleDeviceQuery } from './graphql/queries/AppleDeviceQuery';
import { AppleDistributionCertificateQuery } from './graphql/queries/AppleDistributionCertificateQuery';
@@ -161,8 +167,10 @@ export async function updateIosAppCredentialsAsync(
appCredentials: CommonIosAppCredentialsFragment,
{
applePushKeyId,
ascApiKeyIdForSubmissions,
}: {
applePushKeyId?: string;
ascApiKeyIdForSubmissions?: string;
}
): Promise<CommonIosAppCredentialsFragment> {
let updatedAppCredentials = appCredentials;
@@ -172,6 +180,13 @@ export async function updateIosAppCredentialsAsync(
applePushKeyId
);
}
if (ascApiKeyIdForSubmissions) {
updatedAppCredentials =
await IosAppCredentialsMutation.setAppStoreConnectApiKeyForSubmissionsAsync(
updatedAppCredentials.id,
ascApiKeyIdForSubmissions
);
}
return updatedAppCredentials;
}
@@ -413,5 +428,78 @@ export async function deletePushKeyAsync(pushKeyId: string): Promise<void> {
return await ApplePushKeyMutation.deleteApplePushKeyAsync(pushKeyId);
}
export async function createAscApiKeyAsync(
account: Account,
ascApiKey: MinimalAscApiKey
): Promise<AppStoreConnectApiKeyFragment> {
const maybeAppleTeam = ascApiKey.teamId
? await createOrGetExistingAppleTeamAsync(account, {
appleTeamIdentifier: ascApiKey.teamId,
appleTeamName: ascApiKey.teamName,
})
: null;
return await AppStoreConnectApiKeyMutation.createAppStoreConnectApiKeyAsync(
{
issuerIdentifier: ascApiKey.issuerId,
keyIdentifier: ascApiKey.keyId,
keyP8: ascApiKey.keyP8,
name: ascApiKey.name ?? null,
roles: ascApiKey.roles?.map(role => convertUserRoleToGraphqlType(role)) ?? null,
appleTeamId: maybeAppleTeam ? maybeAppleTeam.id : null,
},
account.id
);
}
export async function getAscApiKeysForAccountAsync(
account: Account
): Promise<AppStoreConnectApiKeyFragment[]> {
return await AppStoreConnectApiKeyQuery.getAllForAccountAsync(account.name);
}
export async function getAscApiKeyForAppSubmissionsAsync(
appLookupParams: AppLookupParams
): Promise<AppStoreConnectApiKeyFragment | null> {
const maybeIosAppCredentials = await getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
return maybeIosAppCredentials?.appStoreConnectApiKeyForSubmissions ?? null;
}
export async function deleteAscApiKeyAsync(ascApiKeyId: string): Promise<void> {
return await AppStoreConnectApiKeyMutation.deleteAppStoreConnectApiKeyAsync(ascApiKeyId);
}
function convertUserRoleToGraphqlType(userRole: UserRole): AppStoreConnectUserRole {
switch (userRole) {
case UserRole.ADMIN:
return AppStoreConnectUserRole.Admin;
case UserRole.ACCESS_TO_REPORTS:
return AppStoreConnectUserRole.AccessToReports;
case UserRole.ACCOUNT_HOLDER:
return AppStoreConnectUserRole.AccountHolder;
case UserRole.APP_MANAGER:
return AppStoreConnectUserRole.AppManager;
case UserRole.CLOUD_MANAGED_APP_DISTRIBUTION:
return AppStoreConnectUserRole.CloudManagedAppDistribution;
case UserRole.CLOUD_MANAGED_DEVELOPER_ID:
return AppStoreConnectUserRole.CloudManagedDeveloperId;
case UserRole.CREATE_APPS:
return AppStoreConnectUserRole.CreateApps;
case UserRole.CUSTOMER_SUPPORT:
return AppStoreConnectUserRole.CustomerSupport;
case UserRole.DEVELOPER:
return AppStoreConnectUserRole.Developer;
case UserRole.FINANCE:
return AppStoreConnectUserRole.Finance;
case UserRole.MARKETING:
return AppStoreConnectUserRole.Marketing;
case UserRole.READ_ONLY:
return AppStoreConnectUserRole.ReadOnly;
case UserRole.SALES:
return AppStoreConnectUserRole.Sales;
case UserRole.TECHNICAL:
return AppStoreConnectUserRole.Technical;
}
}
const formatProjectFullName = ({ account, projectName }: AppLookupParams): string =>
`@${account.name}/${projectName}`;
@@ -0,0 +1,75 @@
import assert from 'assert';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyFragment,
AppStoreConnectApiKeyInput,
CreateAppStoreConnectApiKeyMutation,
DeleteAppStoreConnectApiKeyMutation,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
export const AppStoreConnectApiKeyMutation = {
async createAppStoreConnectApiKeyAsync(
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput,
accountId: string
): Promise<AppStoreConnectApiKeyFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateAppStoreConnectApiKeyMutation>(
gql`
mutation CreateAppStoreConnectApiKeyMutation(
$appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput!
$accountId: ID!
) {
appStoreConnectApiKey {
createAppStoreConnectApiKey(
appStoreConnectApiKeyInput: $appStoreConnectApiKeyInput
accountId: $accountId
) {
id
...AppStoreConnectApiKeyFragment
}
}
}
${print(AppStoreConnectApiKeyFragmentNode)}
`,
{
appStoreConnectApiKeyInput,
accountId,
}
)
.toPromise()
);
assert(
data.appStoreConnectApiKey.createAppStoreConnectApiKey,
'GraphQL: `createAppStoreConnectApiKey` not defined in server response'
);
return data.appStoreConnectApiKey.createAppStoreConnectApiKey;
},
async deleteAppStoreConnectApiKeyAsync(appStoreConnectApiKeyId: string): Promise<void> {
await withErrorHandlingAsync(
graphqlClient
.mutation<DeleteAppStoreConnectApiKeyMutation>(
gql`
mutation DeleteAppStoreConnectApiKeyMutation($appStoreConnectApiKeyId: ID!) {
appStoreConnectApiKey {
deleteAppStoreConnectApiKey(id: $appStoreConnectApiKeyId) {
id
}
}
}
`,
{
appStoreConnectApiKeyId,
},
{
additionalTypenames: ['AppStoreConnectApiKey', 'IosAppCredentials'],
}
)
.toPromise()
);
},
};
@@ -7,6 +7,7 @@ import {
CommonIosAppCredentialsFragment,
CreateIosAppCredentialsMutation,
IosAppCredentialsInput,
SetAppStoreConnectApiKeyForSubmissionsMutation,
SetPushKeyMutation,
} from '../../../../../graphql/generated';
import { CommonIosAppCredentialsFragmentNode } from '../../../../../graphql/types/credentials/IosAppCredentials';
@@ -80,4 +81,37 @@ export const IosAppCredentialsMutation = {
);
return data.iosAppCredentials.setPushKey;
},
async setAppStoreConnectApiKeyForSubmissionsAsync(
iosAppCredentialsId: string,
ascApiKeyId: string
): Promise<CommonIosAppCredentialsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<SetAppStoreConnectApiKeyForSubmissionsMutation>(
gql`
mutation SetAppStoreConnectApiKeyForSubmissionsMutation(
$iosAppCredentialsId: ID!
$ascApiKeyId: ID!
) {
iosAppCredentials {
setAppStoreConnectApiKeyForSubmissions(
id: $iosAppCredentialsId
ascApiKeyId: $ascApiKeyId
) {
id
...CommonIosAppCredentialsFragment
}
}
}
${print(CommonIosAppCredentialsFragmentNode)}
`,
{
iosAppCredentialsId,
ascApiKeyId,
}
)
.toPromise()
);
return data.iosAppCredentials.setAppStoreConnectApiKeyForSubmissions;
},
};
@@ -0,0 +1,41 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AppStoreConnectApiKeyByAccountQuery,
AppStoreConnectApiKeyFragment,
} from '../../../../../graphql/generated';
import { AppStoreConnectApiKeyFragmentNode } from '../../../../../graphql/types/credentials/AppStoreConnectApiKey';
export const AppStoreConnectApiKeyQuery = {
async getAllForAccountAsync(accountName: string): Promise<AppStoreConnectApiKeyFragment[]> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<AppStoreConnectApiKeyByAccountQuery>(
gql`
query AppStoreConnectApiKeyByAccountQuery($accountName: String!) {
account {
byName(accountName: $accountName) {
id
appStoreConnectApiKeys {
id
...AppStoreConnectApiKeyFragment
}
}
}
}
${print(AppStoreConnectApiKeyFragmentNode)}
`,
{
accountName,
},
{
additionalTypenames: ['AppStoreConnectApiKey'],
}
)
.toPromise()
);
return data.account.byName.appStoreConnectApiKeys;
},
};
@@ -150,7 +150,7 @@ export default class AppStoreApi {
return await listAscApiKeysAsync(ctx);
}
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo> {
public async getAscApiKeyAsync(keyId: string): Promise<AscApiKeyInfo | null> {
const ctx = await this.ensureAuthenticatedAsync();
return await getAscApiKeyAsync(ctx, keyId);
}
@@ -76,6 +76,7 @@ export type AscApiKeyInfo = {
name: string;
teamName?: string;
roles: UserRole[];
isRevoked: boolean;
};
export type AscApiKey = AscApiKeyInfo & {
@@ -47,6 +47,7 @@ const mockAscApiKeyInfo = {
roles: ['ADMIN'],
teamId: 'test-id',
teamName: 'test-name',
isRevoked: false,
};
beforeEach(() => {
@@ -6,28 +6,36 @@ import { AscApiKey, AscApiKeyInfo } from './Credentials.types';
import { AuthCtx, getRequestContext } from './authenticate';
export async function listAscApiKeysAsync(authCtx: AuthCtx): Promise<AscApiKeyInfo[]> {
const spinner = ora(`Fetching App Store Connect API Keys.`).start();
const spinner = ora(`Fetching App Store Connect Api Keys.`).start();
try {
const context = getRequestContext(authCtx);
const keys = await ApiKey.getAsync(context);
spinner.succeed(`Fetched App Store Connect API Keys.`);
spinner.succeed(`Fetched App Store Connect Api Keys.`);
return keys.map(key => getAscApiKeyInfo(key, authCtx));
} catch (error) {
spinner.fail(`Failed to fetch App Store Connect API Keys.`);
spinner.fail(`Failed to fetch App Store Connect Api Keys.`);
throw error;
}
}
export async function getAscApiKeyAsync(authCtx: AuthCtx, keyId: string): Promise<AscApiKeyInfo> {
const spinner = ora(`Fetching App Store Connect API Key.`).start();
export async function getAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo | null> {
const spinner = ora(`Fetching App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
spinner.succeed(`Fetched App Store Connect API Key (ID: ${keyId}).`);
spinner.succeed(`Fetched App Store Connect Api Key (ID: ${keyId}).`);
return getAscApiKeyInfo(apiKey, authCtx);
} catch (error) {
} catch (error: any) {
const message = error?.message ?? '';
if (message.includes("There is no resource of type 'apiKeys' with id")) {
spinner.stop();
return null;
}
Log.error(error);
spinner.fail(`Failed to fetch App Store Connect API Key.`);
spinner.fail(`Failed to fetch App Store Connect Api Key.`);
throw error;
}
}
@@ -41,7 +49,7 @@ export async function createAscApiKeyAsync(
keyType,
}: Partial<Pick<ApiKeyProps, 'nickname' | 'roles' | 'allAppsVisible' | 'keyType'>>
): Promise<AscApiKey> {
const spinner = ora(`Creating App Store Connect API Key.`).start();
const spinner = ora(`Creating App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const key = await ApiKey.createAsync(context, {
@@ -64,12 +72,15 @@ export async function createAscApiKeyAsync(
}
throw new Error(`Failed to download .p8 file of ${humanReadableKey}.`);
}
// this object has more optional parameters populated
const fullKey = await ApiKey.infoAsync(context, { id: key.id });
spinner.succeed(`Created App Store Connect Api Key.`);
return {
...getAscApiKeyInfo(key, authCtx),
...getAscApiKeyInfo(fullKey, authCtx),
keyP8,
};
} catch (err: any) {
spinner.fail('Failed to create App Store Connect API Key.');
spinner.fail('Failed to create App Store Connect Api Key.');
throw err;
}
}
@@ -78,16 +89,16 @@ export async function revokeAscApiKeyAsync(
authCtx: AuthCtx,
keyId: string
): Promise<AscApiKeyInfo> {
const spinner = ora(`Revoking App Store Connect API Key.`).start();
const spinner = ora(`Revoking App Store Connect Api Key.`).start();
try {
const context = getRequestContext(authCtx);
const apiKey = await ApiKey.infoAsync(context, { id: keyId });
const revokedKey = await apiKey.revokeAsync();
spinner.succeed(`Revoked App Store Connect API Key.`);
spinner.succeed(`Revoked App Store Connect Api Key.`);
return getAscApiKeyInfo(revokedKey, authCtx);
} catch (error) {
Log.error(error);
spinner.fail(`Failed to revoke App Store Connect API Key.`);
spinner.fail(`Failed to revoke App Store Connect Api Key.`);
throw error;
}
}
@@ -100,5 +111,6 @@ export function getAscApiKeyInfo(apiKey: ApiKey, authCtx: AuthCtx): AscApiKeyInf
teamId: authCtx.team.id,
teamName: authCtx.team.name,
roles: apiKey.attributes.roles,
isRevoked: !!apiKey.attributes.revokingDate,
};
}
@@ -118,14 +118,20 @@ export interface AscApiKeyPath {
issuerId: string;
}
export const ascApiKeyMetadataSchema: CredentialSchema<Omit<MinimalAscApiKey, 'keyP8'>> = {
name: 'App Store Connect API Key',
export const ascApiKeyIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'keyId'>> = {
name: 'App Store Connect Api Key',
questions: [
{
field: 'keyId',
type: 'string',
question: 'Key ID:',
},
],
};
export const ascApiKeyIssuerIdSchema: CredentialSchema<Pick<MinimalAscApiKey, 'issuerId'>> = {
name: 'App Store Connect Api Key',
questions: [
{
field: 'issuerId',
type: 'string',
@@ -0,0 +1,29 @@
import { AppStoreConnectApiKeyFragment } from '../../../graphql/generated';
import { CredentialsContext } from '../../context';
import { AscApiKeyInfo } from '../appstore/Credentials.types';
import { MinimalAscApiKey } from '../credentials';
export async function isAscApiKeyValidAndTrackedAsync(
ctx: CredentialsContext,
ascApiKey: MinimalAscApiKey
): Promise<boolean> {
const ascApiKeyInfo = await ctx.appStore.getAscApiKeyAsync(ascApiKey.keyId);
return isKeyValid(ascApiKeyInfo);
}
export async function getValidAndTrackedAscApiKeysAsync(
ctx: CredentialsContext,
ascApiKeys: AppStoreConnectApiKeyFragment[]
): Promise<AppStoreConnectApiKeyFragment[]> {
const ascApiKeysInfo = await ctx.appStore.listAscApiKeysAsync();
const validAscApiKeysInfo = ascApiKeysInfo.filter(keyInfo => isKeyValid(keyInfo));
const validKeyIdentifiers = new Set(validAscApiKeysInfo.map(keyInfo => keyInfo.keyId));
return ascApiKeys.filter(key => validKeyIdentifiers.has(key.keyIdentifier));
}
function isKeyValid(ascApiKeyInfo: AscApiKeyInfo | null): boolean {
if (!ascApiKeyInfo) {
return false;
}
return !ascApiKeyInfo.isRevoked;
}
@@ -25,26 +25,31 @@ export enum AndroidActionType {
CreateGsaKey,
UseExistingGsaKey,
RemoveGsaKey,
SetupGsaKey,
SetUpGsaKey,
UpdateCredentialsJson,
SetupBuildCredentialsFromCredentialsJson,
SetUpBuildCredentialsFromCredentialsJson,
}
export enum IosActionType {
ManageCredentialsJson,
ManageBuildCredentials,
ManagePushKey,
ManageAscApiKey,
GoBackToCaller,
GoBackToHighLevelActions,
SetupBuildCredentials,
SetupBuildCredentialsFromCredentialsJson,
SetUpBuildCredentials,
SetUpBuildCredentialsFromCredentialsJson,
UpdateCredentialsJson,
UseExistingDistributionCertificate,
RemoveProvisioningProfile,
CreateDistributionCertificate,
RemoveDistributionCertificate,
SetupPushKey,
SetUpPushKey,
CreatePushKey,
UseExistingPushKey,
RemovePushKey,
SetUpAscApiKeyForSubmissions,
UseExistingAscApiKeyForSubmissions,
CreateAscApiKeyForSubmissions,
RemoveAscApiKey,
}
@@ -35,7 +35,7 @@ export const credentialsJsonActions: ActionInfo[] = [
scope: Scope.Project,
},
{
value: AndroidActionType.SetupBuildCredentialsFromCredentialsJson,
value: AndroidActionType.SetUpBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
@@ -89,8 +89,8 @@ export const fcmActions: ActionInfo[] = [
export const gsaKeyActions: ActionInfo[] = [
{
value: AndroidActionType.SetupGsaKey,
title: 'Setup a Google Service Account Key',
value: AndroidActionType.SetUpGsaKey,
title: 'Set up a Google Service Account Key',
scope: Scope.Project,
},
{
@@ -12,6 +12,11 @@ export const highLevelActions: ActionInfo[] = [
title: 'Push Notifications: Manage your Apple Push Notifications Key',
scope: Scope.Manager,
},
{
value: IosActionType.ManageAscApiKey,
title: 'App Store Connect: Manage your Api Key',
scope: Scope.Manager,
},
{
value: IosActionType.ManageCredentialsJson,
title: 'credentials.json: Upload/Download credentials between EAS servers and your local json ',
@@ -31,7 +36,7 @@ export const credentialsJsonActions: ActionInfo[] = [
scope: Scope.Project,
},
{
value: IosActionType.SetupBuildCredentialsFromCredentialsJson,
value: IosActionType.SetUpBuildCredentialsFromCredentialsJson,
title: 'Upload credentials from credentials.json to EAS',
scope: Scope.Project,
},
@@ -45,8 +50,8 @@ export const credentialsJsonActions: ActionInfo[] = [
export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: IosActionType.SetupPushKey,
title: 'Setup your project to use Push Notifications',
value: IosActionType.SetUpPushKey,
title: 'Set up your project to use Push Notifications',
scope: Scope.Project,
},
{
@@ -72,12 +77,42 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
];
}
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
value: IosActionType.SetUpAscApiKeyForSubmissions,
title: 'Set up your project to use an Api Key for EAS Submit',
scope: Scope.Project,
},
{
value: IosActionType.UseExistingAscApiKeyForSubmissions,
title: 'Use an existing Api Key for EAS Submit',
scope: Scope.Project,
},
{
value: IosActionType.CreateAscApiKeyForSubmissions,
title: 'Add a new Api Key For EAS Submit',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account,
},
{
value: IosActionType.RemoveAscApiKey,
title: 'Delete an Api Key',
scope: Scope.Account,
},
{
value: IosActionType.GoBackToHighLevelActions,
title: 'Go back',
scope: Scope.Manager,
},
];
}
export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] {
return [
{
// This command will be triggered during build to ensure all credentials are ready
// I'm leaving it here for now to simplify testing
value: IosActionType.SetupBuildCredentials,
value: IosActionType.SetUpBuildCredentials,
title: 'All: Set up all the required credentials to build your project',
scope: Scope.Project,
},
@@ -25,8 +25,8 @@ 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 { SetupBuildCredentialsFromCredentialsJson } from '../android/actions/SetupBuildCredentialsFromCredentialsJson';
import { SetupGoogleServiceAccountKey } from '../android/actions/SetupGoogleServiceAccountKey';
import { SetUpBuildCredentialsFromCredentialsJson } from '../android/actions/SetUpBuildCredentialsFromCredentialsJson';
import { SetUpGoogleServiceAccountKey } from '../android/actions/SetUpGoogleServiceAccountKey';
import { UpdateCredentialsJson } from '../android/actions/UpdateCredentialsJson';
import { UseExistingGoogleServiceAccountKey } from '../android/actions/UseExistingGoogleServiceAccountKey';
import {
@@ -228,8 +228,8 @@ export class ManageAndroid {
}
} else if (action === AndroidActionType.RemoveGsaKey) {
await new SelectAndRemoveGoogleServiceAccountKey(appLookupParams.account).runAsync(ctx);
} else if (action === AndroidActionType.SetupGsaKey) {
await new SetupGoogleServiceAccountKey(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.SetUpGsaKey) {
await new SetUpGoogleServiceAccountKey(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.UpdateCredentialsJson) {
const buildCredentials = await new SelectExistingAndroidBuildCredentials(
appLookupParams
@@ -237,8 +237,8 @@ export class ManageAndroid {
if (buildCredentials) {
await new UpdateCredentialsJson().runAsync(ctx, buildCredentials);
}
} else if (action === AndroidActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
} else if (action === AndroidActionType.SetUpBuildCredentialsFromCredentialsJson) {
await new SetUpBuildCredentialsFromCredentialsJson(appLookupParams).runAsync(ctx);
}
}
}
@@ -19,20 +19,28 @@ import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { Account, findAccountByName } from '../../user/Account';
import { ensureActorHasUsername } from '../../user/actions';
import { CredentialsContext } from '../context';
import {
AppStoreApiKeyPurpose,
selectAscApiKeysFromAccountAsync,
} from '../ios/actions/AscApiKeyUtils';
import { AssignAscApiKey } from '../ios/actions/AssignAscApiKey';
import { AssignPushKey } from '../ios/actions/AssignPushKey';
import { getAppLookupParamsFromContext } from '../ios/actions/BuildCredentialsUtils';
import { CreateAscApiKey } from '../ios/actions/CreateAscApiKey';
import { CreateDistributionCertificate } from '../ios/actions/CreateDistributionCertificate';
import { CreatePushKey } from '../ios/actions/CreatePushKey';
import { selectValidDistributionCertificateAsync } from '../ios/actions/DistributionCertificateUtils';
import { selectPushKeyAsync } from '../ios/actions/PushKeyUtils';
import { SelectAndRemoveAscApiKey } from '../ios/actions/RemoveAscApiKey';
import { SelectAndRemoveDistributionCertificate } from '../ios/actions/RemoveDistributionCertificate';
import { RemoveProvisioningProfiles } from '../ios/actions/RemoveProvisioningProfile';
import { SelectAndRemovePushKey } from '../ios/actions/RemovePushKey';
import { SetupAdhocProvisioningProfile } from '../ios/actions/SetupAdhocProvisioningProfile';
import { SetupBuildCredentials } from '../ios/actions/SetupBuildCredentials';
import { SetupBuildCredentialsFromCredentialsJson } from '../ios/actions/SetupBuildCredentialsFromCredentialsJson';
import { SetupProvisioningProfile } from '../ios/actions/SetupProvisioningProfile';
import { SetupPushKey } from '../ios/actions/SetupPushKey';
import { SetUpAdhocProvisioningProfile } from '../ios/actions/SetUpAdhocProvisioningProfile';
import { SetUpAscApiKey } from '../ios/actions/SetUpAscApiKey';
import { SetUpBuildCredentials } from '../ios/actions/SetUpBuildCredentials';
import { SetUpBuildCredentialsFromCredentialsJson } from '../ios/actions/SetUpBuildCredentialsFromCredentialsJson';
import { SetUpProvisioningProfile } from '../ios/actions/SetUpProvisioningProfile';
import { SetUpPushKey } from '../ios/actions/SetUpPushKey';
import { UpdateCredentialsJson } from '../ios/actions/UpdateCredentialsJson';
import { AppLookupParams } from '../ios/api/GraphqlClient';
import { getManagedEntitlementsJsonAsync } from '../ios/appstore/entitlements';
@@ -42,6 +50,7 @@ import { ActionInfo, IosActionType, Scope } from './Actions';
import { Action, PressAnyKeyToContinue } from './HelperActions';
import {
credentialsJsonActions,
getAscApiKeyActions,
getBuildCredentialsActions,
getPushKeyActions,
highLevelActions,
@@ -58,9 +67,9 @@ export class ManageIos {
): Promise<void> {
const buildCredentialsActions = getBuildCredentialsActions(ctx);
const pushKeyActions = getPushKeyActions(ctx);
const ascApiKeyActions = getAscApiKeyActions(ctx);
await ctx.bestEffortAppStoreAuthenticateAsync();
const accountName = ctx.hasProjectContext
? getProjectAccountName(ctx.exp, ctx.user)
: ensureActorHasUsername(ctx.user);
@@ -70,7 +79,6 @@ export class ManageIos {
throw new Error(`You do not have access to account: ${accountName}`);
}
const { app, targets, buildProfile } = await this.createProjectContextAsync(ctx, account);
while (true) {
try {
if (ctx.hasProjectContext) {
@@ -107,6 +115,9 @@ export class ManageIos {
} else if (chosenAction === IosActionType.ManagePushKey) {
currentActions = pushKeyActions;
continue;
} else if (chosenAction === IosActionType.ManageAscApiKey) {
currentActions = ascApiKeyActions;
continue;
} else if (chosenAction === IosActionType.GoBackToHighLevelActions) {
currentActions = highLevelActions;
continue;
@@ -195,8 +206,12 @@ export class ManageIos {
await new CreateDistributionCertificate(account).runAsync(ctx);
} else if (action === IosActionType.CreatePushKey) {
await new CreatePushKey(account).runAsync(ctx);
} else if (action === IosActionType.CreateAscApiKeyForSubmissions) {
await new CreateAscApiKey(account).runAsync(ctx, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
} else if (action === IosActionType.RemovePushKey) {
await new SelectAndRemovePushKey(account).runAsync(ctx);
} else if (action === IosActionType.RemoveAscApiKey) {
await new SelectAndRemoveAscApiKey(account).runAsync(ctx);
}
}
@@ -207,8 +222,8 @@ export class ManageIos {
buildProfile: IosBuildProfile,
action: IosActionType
): Promise<void> {
if (action === IosActionType.SetupBuildCredentials) {
await new SetupBuildCredentials({
if (action === IosActionType.SetUpBuildCredentials) {
await new SetUpBuildCredentials({
app,
targets,
distribution: buildProfile.distribution,
@@ -224,8 +239,8 @@ export class ManageIos {
buildProfile
).runAsync(ctx);
if (action === IosActionType.SetupBuildCredentialsFromCredentialsJson) {
await new SetupBuildCredentialsFromCredentialsJson(app, targets, distributionType).runAsync(
if (action === IosActionType.SetUpBuildCredentialsFromCredentialsJson) {
await new SetUpBuildCredentialsFromCredentialsJson(app, targets, distributionType).runAsync(
ctx
);
return;
@@ -290,15 +305,15 @@ export class ManageIos {
}
return;
}
case IosActionType.SetupPushKey: {
const setupPushKeyAction = await new SetupPushKey(appLookupParams);
case IosActionType.SetUpPushKey: {
const setupPushKeyAction = await new SetUpPushKey(appLookupParams);
const isPushKeySetup = await setupPushKeyAction.isPushKeySetupAsync(ctx);
if (isPushKeySetup) {
Log.log(
`Push Key is already set up for ${appLookupParams.projectName} ${appLookupParams.bundleIdentifier}`
);
} else {
await new SetupPushKey(appLookupParams).runAsync(ctx);
await new SetUpPushKey(appLookupParams).runAsync(ctx);
}
return;
}
@@ -319,6 +334,43 @@ export class ManageIos {
}
return;
}
case IosActionType.SetUpAscApiKeyForSubmissions: {
await new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
).runAsync(ctx);
return;
}
case IosActionType.UseExistingAscApiKeyForSubmissions: {
const ascApiKey = await selectAscApiKeysFromAccountAsync(ctx, appLookupParams.account, {
filterDifferentAppleTeam: true,
});
if (ascApiKey) {
await new AssignAscApiKey(appLookupParams).runAsync(
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
}
return;
}
case IosActionType.CreateAscApiKeyForSubmissions: {
const ascApiKey = await new CreateAscApiKey(appLookupParams.account).runAsync(
ctx,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const confirm = await confirmAsync({
message: `Do you want ${appLookupParams.projectName} to use the new Api Key?`,
});
if (confirm) {
await new AssignAscApiKey(appLookupParams).runAsync(
ctx,
ascApiKey,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
}
return;
}
default:
throw new Error('Unknown action selected');
}
@@ -333,11 +385,11 @@ export class ManageIos {
Log.log(`Setting up ${appLookupParams.projectName} to use Distribution Certificate`);
Log.log(`Creating provisioning profile...`);
if (distributionType === IosDistributionTypeGraphql.AdHoc) {
return await new SetupAdhocProvisioningProfile(
return await new SetUpAdhocProvisioningProfile(
appLookupParams
).runWithDistributionCertificateAsync(ctx, distCert);
} else {
return await new SetupProvisioningProfile(
return await new SetUpProvisioningProfile(
appLookupParams,
distributionType
).createAndAssignProfileAsync(ctx, distCert);
@@ -49,7 +49,7 @@ export async function askForUserProvidedAsync<T>(
export async function getCredentialsFromUserAsync<T>(
credentialsSchema: CredentialSchema<T>,
initialValues: Partial<T>
initialValues: Partial<T> = {}
): Promise<T> {
const results: any = {};
for (const question of credentialsSchema.questions) {
@@ -63,7 +63,7 @@ export async function getCredentialsFromUserAsync<T>(
: (results as T);
}
async function shouldAutoGenerateCredentialsAsync<T>(
export async function shouldAutoGenerateCredentialsAsync<T>(
schema: CredentialSchema<T>
): Promise<boolean> {
const answer = await confirmAsync({
+75 -2
View File
@@ -4091,6 +4091,40 @@ export type GoogleServiceAccountKeyByAccountQuery = (
) }
);
export type CreateAppStoreConnectApiKeyMutationVariables = Exact<{
appStoreConnectApiKeyInput: AppStoreConnectApiKeyInput;
accountId: Scalars['ID'];
}>;
export type CreateAppStoreConnectApiKeyMutation = (
{ __typename?: 'RootMutation' }
& { appStoreConnectApiKey: (
{ __typename?: 'AppStoreConnectApiKeyMutation' }
& { createAppStoreConnectApiKey: (
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id'>
& AppStoreConnectApiKeyFragment
) }
) }
);
export type DeleteAppStoreConnectApiKeyMutationVariables = Exact<{
appStoreConnectApiKeyId: Scalars['ID'];
}>;
export type DeleteAppStoreConnectApiKeyMutation = (
{ __typename?: 'RootMutation' }
& { appStoreConnectApiKey: (
{ __typename?: 'AppStoreConnectApiKeyMutation' }
& { deleteAppStoreConnectApiKey: (
{ __typename?: 'deleteAppStoreConnectApiKeyResult' }
& Pick<DeleteAppStoreConnectApiKeyResult, 'id'>
) }
) }
);
export type CreateAppleAppIdentifierMutationVariables = Exact<{
appleAppIdentifierInput: AppleAppIdentifierInput;
accountId: Scalars['ID'];
@@ -4394,6 +4428,24 @@ export type SetPushKeyMutation = (
) }
);
export type SetAppStoreConnectApiKeyForSubmissionsMutationVariables = Exact<{
iosAppCredentialsId: Scalars['ID'];
ascApiKeyId: Scalars['ID'];
}>;
export type SetAppStoreConnectApiKeyForSubmissionsMutation = (
{ __typename?: 'RootMutation' }
& { iosAppCredentials: (
{ __typename?: 'IosAppCredentialsMutation' }
& { setAppStoreConnectApiKeyForSubmissions: (
{ __typename?: 'IosAppCredentials' }
& Pick<IosAppCredentials, 'id'>
& CommonIosAppCredentialsFragment
) }
) }
);
export type AppByFullNameQueryVariables = Exact<{
fullName: Scalars['String'];
}>;
@@ -4411,6 +4463,27 @@ export type AppByFullNameQuery = (
)> }
);
export type AppStoreConnectApiKeyByAccountQueryVariables = Exact<{
accountName: Scalars['String'];
}>;
export type AppStoreConnectApiKeyByAccountQuery = (
{ __typename?: 'RootQuery' }
& { account: (
{ __typename?: 'AccountQuery' }
& { byName: (
{ __typename?: 'Account' }
& Pick<Account, 'id'>
& { appStoreConnectApiKeys: Array<(
{ __typename?: 'AppStoreConnectApiKey' }
& Pick<AppStoreConnectApiKey, 'id'>
& AppStoreConnectApiKeyFragment
)> }
) }
) }
);
export type AppleAppIdentifierByBundleIdQueryVariables = Exact<{
accountName: Scalars['String'];
bundleIdentifier: Scalars['String'];
@@ -5245,10 +5318,10 @@ export type BuildFragment = (
& Pick<BuildArtifacts, 'buildUrl' | 'xcodeBuildLogsUrl'>
)>, initiatingActor?: Maybe<(
{ __typename: 'User' }
& Pick<User, 'username' | 'id'>
& Pick<User, 'id' | 'displayName'>
) | (
{ __typename: 'Robot' }
& Pick<Robot, 'firstName' | 'id'>
& Pick<Robot, 'id' | 'displayName'>
)>, project: (
{ __typename: 'Snack' }
& Pick<Snack, 'id' | 'name'>
+1 -6
View File
@@ -17,12 +17,7 @@ export const BuildFragmentNode = gql`
initiatingActor {
__typename
id
... on User {
username
}
... on Robot {
firstName
}
displayName
}
project {
__typename
@@ -288,6 +288,7 @@ function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choi
buildProfile,
appBuildVersion,
releaseChannel,
initiatingActor,
} = build;
const formatValue = (field?: string | null): string =>
@@ -310,6 +311,7 @@ function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choi
.filter(it => it != null)
.join(', '),
`\tProfile: ${formatValue(buildProfile)}, Release channel: ${formatValue(releaseChannel)}`,
`\tAuthored by: ${formatValue(initiatingActor?.displayName)}`,
].join('\n');
return {
@@ -3,7 +3,7 @@ import chalk from 'chalk';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { SetupGoogleServiceAccountKey } from '../../credentials/android/actions/SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { readAndValidateServiceAccountKey } from '../../credentials/android/utils/googleServiceAccountKey';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
@@ -109,7 +109,7 @@ export async function getServiceAccountFromCredentialsServiceAsync(
projectName: ctx.projectName,
androidApplicationIdentifier,
};
const setupGoogleServiceAccountKeyAction = new SetupGoogleServiceAccountKey(appLookupParams);
const setupGoogleServiceAccountKeyAction = new SetUpGoogleServiceAccountKey(appLookupParams);
const androidAppCredentials = await setupGoogleServiceAccountKeyAction.runAsync(
ctx.credentialsCtx
);
@@ -6,7 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { testAndroidAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-android';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { SetupGoogleServiceAccountKey } from '../../../credentials/android/actions/SetupGoogleServiceAccountKey';
import { SetUpGoogleServiceAccountKey } from '../../../credentials/android/actions/SetUpGoogleServiceAccountKey';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { promptAsync } from '../../../prompts';
import { createSubmissionContextAsync } from '../../context';
@@ -20,7 +20,7 @@ import {
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../project/projectUtils');
jest.mock('../../../credentials/android/actions/SetupGoogleServiceAccountKey');
jest.mock('../../../credentials/android/actions/SetUpGoogleServiceAccountKey');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
@@ -52,7 +52,7 @@ beforeAll(() => {
'/other_dir/invalid_file.txt': 'this is not even a JSON',
});
jest
.spyOn(SetupGoogleServiceAccountKey.prototype, 'runAsync')
.spyOn(SetUpGoogleServiceAccountKey.prototype, 'runAsync')
.mockImplementation(async () => testAndroidAppCredentialsFragment);
});
afterAll(() => {
@@ -1,12 +1,5 @@
import chalk from 'chalk';
import wrapAnsi from 'wrap-ansi';
import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts';
export enum AppSpecificPasswordSourceType {
userDefined,
prompt,
}
interface AppSpecificPasswordSourceBase {
@@ -18,38 +11,13 @@ interface AppSpecificPasswordUserDefinedSource extends AppSpecificPasswordSource
appSpecificPassword: string;
}
interface AppSpecificPasswordPromptSource extends AppSpecificPasswordSourceBase {
sourceType: AppSpecificPasswordSourceType.prompt;
}
export type AppSpecificPasswordSource =
| AppSpecificPasswordUserDefinedSource
| AppSpecificPasswordPromptSource;
export type AppSpecificPasswordSource = AppSpecificPasswordUserDefinedSource;
export async function getAppSpecificPasswordAsync(
source: AppSpecificPasswordSource
): Promise<string> {
if (source.sourceType === AppSpecificPasswordSourceType.userDefined) {
return source.appSpecificPassword;
} else if (source.sourceType === AppSpecificPasswordSourceType.prompt) {
Log.addNewLineIfNone();
Log.log(
wrapAnsi(
`Please enter your Apple app-specific password. You can also provide it by using the ${chalk.bold(
'EXPO_APPLE_APP_SPECIFIC_PASSWORD'
)} environment variable.`,
process.stdout.columns || 80
)
);
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
const { appSpecificPassword } = await promptAsync({
name: 'appSpecificPassword',
message: 'Your Apple app-specific password:',
type: 'password',
validate: (val: string) => val !== '' || 'Apple app-specific password cannot be empty!',
});
return appSpecificPassword;
} else {
// exhaustive -- should never happen
throw new Error(`Unknown app specific password source type "${(source as any)?.sourceType}"`);
@@ -1,19 +1,33 @@
import { Platform } from '@expo/eas-build-job';
import fs from 'fs-extra';
import nullthrows from 'nullthrows';
import { promptForAscApiKeyAsync } from '../../credentials/ios/actions/AscApiKeyUtils';
import {
AppStoreApiKeyPurpose,
promptForAscApiKeyPathAsync,
} from '../../credentials/ios/actions/AscApiKeyUtils';
import { SetUpAscApiKey } from '../../credentials/ios/actions/SetUpAscApiKey';
import { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
import Log from '../../log';
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
import { findAccountByName } from '../../user/Account';
import { SubmissionContext } from '../context';
import { isExistingFileAsync } from '../utils/files';
export enum AscApiKeySourceType {
path,
prompt,
credentialsService,
}
interface AscApiKeySourceBase {
sourceType: AscApiKeySourceType;
}
interface AscApiKeyCredentialsServiceSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.credentialsService;
}
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
sourceType: AscApiKeySourceType.prompt;
}
@@ -23,21 +37,80 @@ interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
path: AscApiKeyPath;
}
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
export type AscApiKeySource =
| AscApiKeyEnvVarSource
| AscApiKeyPromptSource
| AscApiKeyCredentialsServiceSource;
type AscApiKeySummary = {
source: 'local' | 'EAS servers';
path?: string;
keyId: string;
name?: string;
};
export type AscApiKeyFromExpoServers = {
ascApiKeyId: string;
};
export type AscApiKeyResult = {
result: MinimalAscApiKey;
result: MinimalAscApiKey | AscApiKeyFromExpoServers;
summary: AscApiKeySummary;
};
export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
export async function getAscApiKeyResultAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
if (source.sourceType === AscApiKeySourceType.credentialsService) {
return await getAscApiKeyFromCredentialsServiceAsync(ctx);
} else {
return await getAscApiKeyLocallyAsync(ctx, source);
}
}
async function getAscApiKeyFromCredentialsServiceAsync(
ctx: SubmissionContext<Platform.IOS>
): Promise<AscApiKeyResult> {
const bundleIdentifier = await getBundleIdentifierAsync(ctx.projectDir, ctx.exp);
Log.log(`Looking up credentials configuration for ${bundleIdentifier}...`);
const appLookupParams = {
account: nullthrows(
findAccountByName(ctx.user.accounts, ctx.accountName),
`You do not have access to account: ${ctx.accountName}`
),
projectName: ctx.projectName,
bundleIdentifier,
};
const setupAscApiKeyAction = new SetUpAscApiKey(
appLookupParams,
AppStoreApiKeyPurpose.SUBMISSION_SERVICE
);
const iosAppCredentials = await setupAscApiKeyAction.runAsync(ctx.credentialsCtx);
const ascKeyForSubmissions = nullthrows(
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
`An EAS Submit ASC Api Key could not be found for ${iosAppCredentials.appleAppIdentifier.bundleIdentifier}`
);
const { id, keyIdentifier, name } = ascKeyForSubmissions;
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
return {
result: {
ascApiKeyId: id,
},
summary: {
source: 'EAS servers',
keyId: keyIdentifier,
name: name ?? undefined,
},
};
}
async function getAscApiKeyLocallyAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyResult> {
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
const { keyP8Path, keyId, issuerId } = ascApiKeyPath;
const keyP8 = await fs.readFile(keyP8Path, 'utf-8');
@@ -51,27 +124,38 @@ export async function getAscApiKeyLocallyAsync(source: AscApiKeySource): Promise
};
}
export async function getAscApiKeyPathAsync(source: AscApiKeySource): Promise<AscApiKeyPath> {
export async function getAscApiKeyPathAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeySource
): Promise<AscApiKeyPath> {
switch (source.sourceType) {
case AscApiKeySourceType.path:
return await handlePathSourceAsync(source);
return await handlePathSourceAsync(ctx, source);
case AscApiKeySourceType.prompt:
return await handlePromptSourceAsync(source);
return await handlePromptSourceAsync(ctx, source);
case AscApiKeySourceType.credentialsService:
throw new Error(`AscApiKeySourceType ${source} does not return a path.`);
}
}
async function handlePathSourceAsync(source: AscApiKeyEnvVarSource): Promise<AscApiKeyPath> {
async function handlePathSourceAsync(
ctx: SubmissionContext<Platform.IOS>,
source: AscApiKeyEnvVarSource
): Promise<AscApiKeyPath> {
const { keyP8Path } = source.path;
if (!(await isExistingFileAsync(keyP8Path))) {
Log.warn(`File ${keyP8Path} doesn't exist.`);
return await getAscApiKeyPathAsync({ sourceType: AscApiKeySourceType.prompt });
return await getAscApiKeyPathAsync(ctx, { sourceType: AscApiKeySourceType.prompt });
}
return source.path;
}
async function handlePromptSourceAsync(_source: AscApiKeyPromptSource): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyAsync();
return await getAscApiKeyPathAsync({
async function handlePromptSourceAsync(
ctx: SubmissionContext<Platform.IOS>,
_source: AscApiKeyPromptSource
): Promise<AscApiKeyPath> {
const ascApiKeyPath = await promptForAscApiKeyPathAsync(ctx.credentialsCtx);
return await getAscApiKeyPathAsync(ctx, {
sourceType: AscApiKeySourceType.path,
path: ascApiKeyPath,
});
@@ -34,15 +34,17 @@ export default class IosSubmitCommand {
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
| { ascApiKeySource: Result<AscApiKeySource> }
> {
// Fall back to app specific password if no ascApiKey defined
const ascApiKeySource = this.resolveAscApiKeySource();
const shouldUseAppSpecificPassword =
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
if (shouldUseAppSpecificPassword) {
// if an App Specific Password env var is not specified, use ASC Api Key
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
const shouldSkipAppSpecificPasswordSource =
!appSpecificPasswordSource.ok &&
appSpecificPasswordSource.enforceError() instanceof MissingCredentialsError;
if (!shouldSkipAppSpecificPasswordSource) {
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
} else {
return { ascApiKeySource };
}
const ascApiKeySource = this.resolveAscApiKeySource();
return { ascApiKeySource };
}
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
@@ -96,13 +98,13 @@ export default class IosSubmitCommand {
sourceType: AppSpecificPasswordSourceType.userDefined,
appSpecificPassword: envAppSpecificPassword,
});
} else if (this.ctx.nonInteractive) {
return result(new Error('Set the EXPO_APPLE_APP_SPECIFIC_PASSWORD environment variable.'));
} else {
return result({
sourceType: AppSpecificPasswordSourceType.prompt,
});
}
return result(
new MissingCredentialsError(
'The EXPO_APPLE_APP_SPECIFIC_PASSWORD environment variable must be set.'
)
);
}
private resolveAscApiKeySource(): Result<AscApiKeySource> {
@@ -127,11 +129,7 @@ export default class IosSubmitCommand {
});
}
return result(
new MissingCredentialsError(
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
)
);
return result({ sourceType: AscApiKeySourceType.credentialsService });
}
private resolveArchiveSource(): Result<ArchiveSource> {
+34 -20
View File
@@ -1,6 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import chalk from 'chalk';
import { MinimalAscApiKey } from '../../credentials/ios/credentials';
import { IosSubmissionConfigInput, SubmissionFragment } from '../../graphql/generated';
import { SubmissionMutation } from '../../graphql/mutations/SubmissionMutation';
import formatFields from '../../utils/formatFields';
@@ -15,7 +16,12 @@ import {
AppSpecificPasswordSource,
getAppSpecificPasswordAsync,
} from './AppSpecificPasswordSource';
import { AscApiKeyResult, AscApiKeySource, getAscApiKeyLocallyAsync } from './AscApiKeySource';
import {
AscApiKeyFromExpoServers,
AscApiKeyResult,
AscApiKeySource,
getAscApiKeyResultAsync,
} from './AscApiKeySource';
export interface IosSubmissionOptions
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
@@ -69,7 +75,7 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
: null;
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
? await getAscApiKeyLocallyAsync(this.options.ascApiKeySource)
? await getAscApiKeyResultAsync(this.ctx, this.options.ascApiKeySource)
: null;
return {
archive,
@@ -88,18 +94,24 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
appleIdUsername,
archiveUrl: archive.url,
appleAppSpecificPassword: appSpecificPassword,
...(ascApiKeyResult?.result
? {
ascApiKey: {
keyP8: ascApiKeyResult?.result.keyP8,
keyIdentifier: ascApiKeyResult?.result.keyId,
issuerIdentifier: ascApiKeyResult?.result.issuerId,
},
}
: null),
...(ascApiKeyResult?.result ? this.formatAscApiKeyResult(ascApiKeyResult.result) : null),
};
}
private formatAscApiKeyResult(
result: MinimalAscApiKey | AscApiKeyFromExpoServers
): Pick<IosSubmissionConfigInput, 'ascApiKey'> | Pick<IosSubmissionConfigInput, 'ascApiKeyId'> {
return 'ascApiKeyId' in result
? { ascApiKeyId: result.ascApiKeyId }
: {
ascApiKey: {
keyP8: result.keyP8,
keyIdentifier: result.keyId,
issuerIdentifier: result.issuerId,
},
};
}
private prepareSummaryData(
options: IosSubmissionOptions,
{ archive, ascApiKeyResult }: ResolvedSourceOptions
@@ -111,9 +123,7 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
ascAppIdentifier,
appleIdUsername: appleIdUsername ?? undefined,
projectId,
...(ascApiKeyResult
? { formattedAscApiKey: formatServiceAccountSummary(ascApiKeyResult) }
: null),
...(ascApiKeyResult ? { formattedAscApiKey: formatAscApiKeySummary(ascApiKeyResult) } : null),
...formatArchiveSourceSummary(archive),
};
}
@@ -136,10 +146,18 @@ const SummaryHumanReadableKeys: Record<keyof SummaryData, string> = {
formattedAscApiKey: 'App Store Connect Api Key',
};
function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId } = summary;
function formatAscApiKeySummary({ summary }: AscApiKeyResult): string {
const { source, path, keyId, name } = summary;
const fields = [
{
label: 'Key Name',
value: name,
},
{
label: 'Key ID',
value: keyId,
},
{
label: 'Key Source',
value: source,
@@ -148,10 +166,6 @@ function formatServiceAccountSummary({ summary }: AscApiKeyResult): string {
label: 'Key Path',
value: path,
},
{
label: 'Key ID',
value: keyId,
},
];
const filteredFields = fields.filter(({ value }) => value !== undefined && value !== null) as {
@@ -1,18 +1,59 @@
import { Platform } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { v4 as uuidv4 } from 'uuid';
import { asMock } from '../../../__tests__/utils';
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
import { testCommonIosAppCredentialsFragment } from '../../../credentials/__tests__/fixtures-ios';
import { SetUpAscApiKey } from '../../../credentials/ios/actions/SetUpAscApiKey';
import { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
import { createTestProject } from '../../../project/__tests__/project-utils';
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
import { promptAsync } from '../../../prompts';
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
import {
AscApiKeySource,
AscApiKeySourceType,
getAscApiKeyLocallyAsync,
getAscApiKeyPathAsync,
getAscApiKeyResultAsync,
} from '../AscApiKeySource';
jest.mock('fs');
jest.mock('../../../prompts');
jest.mock('../../../credentials/utils/promptForCredentials');
jest.mock('../../../user/User', () => ({
getUserAsync: jest.fn(() => mockJester),
}));
jest.mock('../../../user/Account', () => ({
findAccountByName: jest.fn(() => mockJester.accounts[0]),
}));
jest.mock('../../../project/ios/bundleIdentifier');
const testProject = createTestProject(mockJester, {
android: {
package: 'com.expo.test.project',
},
});
const mockManifest = { exp: testProject.appJSON.expo };
jest.mock('@expo/config', () => ({
getConfig: jest.fn(() => mockManifest),
}));
const projectId = uuidv4();
async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platform.IOS>> {
return await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
}
beforeAll(() => {
vol.fromJSON({
@@ -35,15 +76,16 @@ describe(getAscApiKeyPathAsync, () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/doesnt-exist.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
@@ -53,20 +95,22 @@ describe(getAscApiKeyPathAsync, () => {
});
it("doesn't prompt for path if the provided file exists", async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
await getAscApiKeyPathAsync(source);
await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).not.toHaveBeenCalled();
});
it('returns the provided file path if the file exists', async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(ascApiKeyPath).toEqual({
issuerId: 'test-issuer-id',
keyId: 'test-key-id',
@@ -80,14 +124,15 @@ describe(getAscApiKeyPathAsync, () => {
asMock(promptAsync).mockImplementationOnce(() => ({
keyP8Path: '/asc-api-key.p8',
}));
asMock(getCredentialsFromUserAsync).mockImplementationOnce(() => ({
asMock(getCredentialsFromUserAsync).mockImplementation(() => ({
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).toHaveBeenCalled();
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
@@ -111,10 +156,11 @@ describe(getAscApiKeyPathAsync, () => {
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const ascApiKeyPath = await getAscApiKeyPathAsync(source);
const ascApiKeyPath = await getAscApiKeyPathAsync(ctx, source);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(ascApiKeyPath).toEqual({
keyP8Path: '/asc-api-key.p8',
@@ -125,13 +171,14 @@ describe(getAscApiKeyPathAsync, () => {
});
});
describe(getAscApiKeyLocallyAsync, () => {
describe(getAscApiKeyResultAsync, () => {
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.path,
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
};
const ascApiKeyResult = await getAscApiKeyLocallyAsync(source);
const ascApiKeyResult = await getAscApiKeyResultAsync(ctx, source);
expect(ascApiKeyResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -154,10 +201,11 @@ describe(getAscApiKeyLocallyAsync, () => {
keyId: 'test-key-id',
issuerId: 'test-issuer-id',
}));
const ctx = await getIosSubmissionContextAsync();
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.prompt,
};
const serviceAccountResult = await getAscApiKeyLocallyAsync(source);
const serviceAccountResult = await getAscApiKeyResultAsync(ctx, source);
expect(serviceAccountResult).toMatchObject({
result: {
keyP8: 'super secret',
@@ -171,4 +219,39 @@ describe(getAscApiKeyLocallyAsync, () => {
},
});
});
it('returns an Asc Api Key from server with a AscApiKeySourceType.credentialService source', async () => {
const ctx = await createSubmissionContextAsync({
platform: Platform.IOS,
projectDir: testProject.projectRoot,
projectId,
archiveFlags: {
url: 'http://expo.dev/fake.apk',
},
profile: {
language: 'en-US',
},
nonInteractive: true,
});
const source: AscApiKeySource = {
sourceType: AscApiKeySourceType.credentialsService,
};
jest
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
const result = await getAscApiKeyResultAsync(ctx, source);
expect(result).toEqual({
result: {
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
},
summary: {
keyId:
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
source: 'EAS servers',
},
});
});
});
@@ -16,6 +16,7 @@ enum SubmissionErrorCode {
IOS_INVALID_SIGNATURE = 'SUBMISSION_SERVICE_IOS_INVALID_SIGNATURE',
IOS_INCORRECT_CREDENTIALS = 'SUBMISSION_SERVICE_IOS_INVALID_CREDENTIALS',
IOS_IPAD_INVALID_ORIENTATION = 'SUBMISSION_SERVICE_IOS_IPAD_INVALID_ORIENTATION',
IOS_APPLE_MAINTENANCE = 'SUBMISSION_SERVICE_IOS_APPLE_MAINTENANCE',
}
const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
@@ -62,6 +63,8 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
"Your app doesn't support iPad multitasking and has to require full screen.\n" +
"If you're submitting a managed Expo project, set the `expo.ios.requireFullScreen` to true in app.json and build the project again.\n" +
`${learnMore('https://expo.fyi/ipad-requires-fullscreen')}`,
[SubmissionErrorCode.IOS_APPLE_MAINTENANCE]:
'It looks like Apple servers are undergoing an unscheduled maintenance. Please try again later.',
};
export function printSubmissionError(error: SubmissionError): boolean {
Generated
+4 -4
View File
@@ -833,10 +833,10 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@expo/[email protected]5":
version "0.0.0-alpha.25"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.25.tgz#fea4f5a409cc1bf79a0d2acec3eca52dbd8443c1"
integrity sha512-meyCJ/5jHJsgZNBMwlAugqD5Z0bcazCLgAyYQCw8O1/sXh4gWf0IiWJGnkevdxnA3g1R9nmJFmjkFuLoGOt+Bw==
"@expo/[email protected]6":
version "0.0.0-alpha.26"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.26.tgz#cf5a893b7c9cc779af5b54a482196325dcde1426"
integrity sha512-t+xOhn4bYSAXkXamhDPUiI2Ol+QIwHRHLn/2QiCmNAGHolaVan/hMaVveSzvCYitpaJ16b4nthvcWFoJipxGlA==
"@expo/bunyan@^4.0.0":
version "4.0.0"