mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0b3b1237 |
No files matched your search
@@ -31,10 +31,10 @@ export enum SetupAscApiKeyChoice {
|
|||||||
export class SetUpAscApiKey {
|
export class SetUpAscApiKey {
|
||||||
public choices: { title: string; value: string }[] = [
|
public choices: { title: string; value: string }[] = [
|
||||||
{
|
{
|
||||||
title: '[Choose an existing ASC Api Key]',
|
title: '[Choose an existing key]',
|
||||||
value: SetupAscApiKeyChoice.USE_EXISTING,
|
value: SetupAscApiKeyChoice.USE_EXISTING,
|
||||||
},
|
},
|
||||||
{ title: '[Add a new ASC Api Key]', value: SetupAscApiKeyChoice.GENERATE },
|
{ title: '[Add a new key]', value: SetupAscApiKeyChoice.GENERATE },
|
||||||
];
|
];
|
||||||
|
|
||||||
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
|
constructor(private app: AppLookupParams, private purpose: AppStoreApiKeyPurpose) {}
|
||||||
@@ -123,7 +123,7 @@ export class SetUpAscApiKey {
|
|||||||
const result = await promptAsync({
|
const result = await promptAsync({
|
||||||
type: 'select',
|
type: 'select',
|
||||||
name: 'choice',
|
name: 'choice',
|
||||||
message: 'Select what to use for your project:',
|
message: 'Select the App Store Connect Api Key to use for your project:',
|
||||||
choices,
|
choices,
|
||||||
});
|
});
|
||||||
choice = result.choice;
|
choice = result.choice;
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import chalk from 'chalk';
|
|
||||||
import nullthrows from 'nullthrows';
|
|
||||||
import wrapAnsi from 'wrap-ansi';
|
|
||||||
|
|
||||||
import { CommonIosAppCredentialsFragment } from '../../../graphql/generated';
|
|
||||||
import Log, { learnMore } from '../../../log';
|
|
||||||
import { promptAsync } from '../../../prompts';
|
|
||||||
import { CredentialsContext } from '../../context';
|
|
||||||
import { UnsupportedCredentialsChoiceError } from '../../errors';
|
|
||||||
import { AppLookupParams } from '../api/GraphqlClient';
|
|
||||||
import { AppStoreApiKeyPurpose } from './AscApiKeyUtils';
|
|
||||||
import { SetUpAscApiKey } from './SetUpAscApiKey';
|
|
||||||
|
|
||||||
export const PROMPT_FOR_APP_SPECIFIC_PASSWORD = 'PROMPT_FOR_APP_SPECIFIC_PASSWORD';
|
|
||||||
|
|
||||||
export class SetUpSubmissionCredentials {
|
|
||||||
private setupAscApiKeyAction;
|
|
||||||
|
|
||||||
constructor(app: AppLookupParams) {
|
|
||||||
this.setupAscApiKeyAction = new SetUpAscApiKey(app, AppStoreApiKeyPurpose.SUBMISSION_SERVICE);
|
|
||||||
// Add this unrelated choice to ASC Api Key setup for legacy purposes -- we will deprecate it soon
|
|
||||||
this.setupAscApiKeyAction.choices = this.setupAscApiKeyAction.choices.concat({
|
|
||||||
title: '[Enter an App Specific Password]',
|
|
||||||
value: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runAsync(
|
|
||||||
ctx: CredentialsContext
|
|
||||||
): Promise<CommonIosAppCredentialsFragment | string> {
|
|
||||||
try {
|
|
||||||
const iosAppCredentials = await this.setupAscApiKeyAction.runAsync(ctx);
|
|
||||||
const { keyIdentifier, name } = nullthrows(
|
|
||||||
iosAppCredentials.appStoreConnectApiKeyForSubmissions,
|
|
||||||
'ASC Api Key must be defined for EAS Submit'
|
|
||||||
);
|
|
||||||
Log.log(`Using Api Key ID: ${keyIdentifier}${name ? ` (${name})` : ''}`);
|
|
||||||
return iosAppCredentials;
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof UnsupportedCredentialsChoiceError) {
|
|
||||||
if (e.choice === PROMPT_FOR_APP_SPECIFIC_PASSWORD) {
|
|
||||||
return await this.promptForAppSpecificPasswordAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async promptForAppSpecificPasswordAsync(): Promise<string> {
|
|
||||||
Log.addNewLineIfNone();
|
|
||||||
Log.log(`Please enter your Apple app-specific password.`);
|
|
||||||
Log.log(learnMore('https://expo.fyi/apple-app-specific-password'));
|
|
||||||
Log.warn(
|
|
||||||
wrapAnsi(
|
|
||||||
`This option will be deprecated soon. You will still be able to provide your password with the ${chalk.bold(
|
|
||||||
'EXPO_APPLE_APP_SPECIFIC_PASSWORD'
|
|
||||||
)} environment variable.`,
|
|
||||||
process.stdout.columns || 80
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
import { asMock } from '../../../../__tests__/utils';
|
|
||||||
import { findApplicationTarget } from '../../../../project/ios/target';
|
|
||||||
import { confirmAsync, promptAsync } from '../../../../prompts';
|
|
||||||
import { createCtxMock } from '../../../__tests__/fixtures-context';
|
|
||||||
import {
|
|
||||||
getNewIosApiMock,
|
|
||||||
testCommonIosAppCredentialsFragment,
|
|
||||||
testTargets,
|
|
||||||
} from '../../../__tests__/fixtures-ios';
|
|
||||||
import { getAppLookupParamsFromContext } from '../BuildCredentialsUtils';
|
|
||||||
import { SetUpAscApiKey } from '../SetUpAscApiKey';
|
|
||||||
import {
|
|
||||||
PROMPT_FOR_APP_SPECIFIC_PASSWORD,
|
|
||||||
SetUpSubmissionCredentials,
|
|
||||||
} from '../SetUpSubmissionCredentials';
|
|
||||||
|
|
||||||
jest.mock('../../../../prompts');
|
|
||||||
asMock(confirmAsync).mockImplementation(() => true);
|
|
||||||
|
|
||||||
describe(SetUpSubmissionCredentials, () => {
|
|
||||||
it('allows user to enter an App Specific Password', async () => {
|
|
||||||
asMock(promptAsync)
|
|
||||||
.mockImplementationOnce(() => ({
|
|
||||||
choice: PROMPT_FOR_APP_SPECIFIC_PASSWORD,
|
|
||||||
}))
|
|
||||||
.mockImplementationOnce(() => ({
|
|
||||||
appSpecificPassword: 'super secret',
|
|
||||||
}));
|
|
||||||
const ctx = createCtxMock({
|
|
||||||
nonInteractive: false,
|
|
||||||
ios: {
|
|
||||||
...getNewIosApiMock(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const appLookupParams = getAppLookupParamsFromContext(ctx, findApplicationTarget(testTargets));
|
|
||||||
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
|
|
||||||
const asp = await setupAscApiKeyAction.runAsync(ctx);
|
|
||||||
|
|
||||||
// prompt to choose ASP, then prompt to input ASP
|
|
||||||
expect(asMock(promptAsync).mock.calls.length).toBe(2);
|
|
||||||
expect(asp).toBe('super secret');
|
|
||||||
});
|
|
||||||
it('returns an ASC API key', async () => {
|
|
||||||
try {
|
|
||||||
jest
|
|
||||||
.spyOn(SetUpAscApiKey.prototype, 'runAsync')
|
|
||||||
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
|
|
||||||
const ctx = createCtxMock();
|
|
||||||
const appLookupParams = getAppLookupParamsFromContext(
|
|
||||||
ctx,
|
|
||||||
findApplicationTarget(testTargets)
|
|
||||||
);
|
|
||||||
const setupAscApiKeyAction = new SetUpSubmissionCredentials(appLookupParams);
|
|
||||||
const asc = await setupAscApiKeyAction.runAsync(ctx);
|
|
||||||
|
|
||||||
expect(asc).toEqual(testCommonIosAppCredentialsFragment);
|
|
||||||
} finally {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,21 +1,33 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
import { Platform } from '@expo/eas-build-job';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import nullthrows from 'nullthrows';
|
||||||
|
|
||||||
import { promptForAscApiKeyPathAsync } 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 { AscApiKeyPath, MinimalAscApiKey } from '../../credentials/ios/credentials';
|
||||||
import Log from '../../log';
|
import Log from '../../log';
|
||||||
|
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
||||||
|
import { findAccountByName } from '../../user/Account';
|
||||||
import { SubmissionContext } from '../context';
|
import { SubmissionContext } from '../context';
|
||||||
import { isExistingFileAsync } from '../utils/files';
|
import { isExistingFileAsync } from '../utils/files';
|
||||||
|
|
||||||
export enum AscApiKeySourceType {
|
export enum AscApiKeySourceType {
|
||||||
path,
|
path,
|
||||||
prompt,
|
prompt,
|
||||||
|
credentialsService,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AscApiKeySourceBase {
|
interface AscApiKeySourceBase {
|
||||||
sourceType: AscApiKeySourceType;
|
sourceType: AscApiKeySourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AscApiKeyCredentialsServiceSource extends AscApiKeySourceBase {
|
||||||
|
sourceType: AscApiKeySourceType.credentialsService;
|
||||||
|
}
|
||||||
|
|
||||||
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
|
interface AscApiKeyPromptSource extends AscApiKeySourceBase {
|
||||||
sourceType: AscApiKeySourceType.prompt;
|
sourceType: AscApiKeySourceType.prompt;
|
||||||
}
|
}
|
||||||
@@ -25,7 +37,10 @@ interface AscApiKeyEnvVarSource extends AscApiKeySourceBase {
|
|||||||
path: AscApiKeyPath;
|
path: AscApiKeyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AscApiKeySource = AscApiKeyEnvVarSource | AscApiKeyPromptSource;
|
export type AscApiKeySource =
|
||||||
|
| AscApiKeyEnvVarSource
|
||||||
|
| AscApiKeyPromptSource
|
||||||
|
| AscApiKeyCredentialsServiceSource;
|
||||||
|
|
||||||
type AscApiKeySummary = {
|
type AscApiKeySummary = {
|
||||||
source: 'local' | 'EAS servers';
|
source: 'local' | 'EAS servers';
|
||||||
@@ -43,7 +58,55 @@ export type AscApiKeyResult = {
|
|||||||
summary: AscApiKeySummary;
|
summary: AscApiKeySummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAscApiKeyLocallyAsync(
|
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>,
|
ctx: SubmissionContext<Platform.IOS>,
|
||||||
source: AscApiKeySource
|
source: AscApiKeySource
|
||||||
): Promise<AscApiKeyResult> {
|
): Promise<AscApiKeyResult> {
|
||||||
@@ -70,6 +133,8 @@ export async function getAscApiKeyPathAsync(
|
|||||||
return await handlePathSourceAsync(ctx, source);
|
return await handlePathSourceAsync(ctx, source);
|
||||||
case AscApiKeySourceType.prompt:
|
case AscApiKeySourceType.prompt:
|
||||||
return await handlePromptSourceAsync(ctx, source);
|
return await handlePromptSourceAsync(ctx, source);
|
||||||
|
case AscApiKeySourceType.credentialsService:
|
||||||
|
throw new Error(`AscApiKeySourceType ${source} does not return a path.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
|
||||||
import nullthrows from 'nullthrows';
|
|
||||||
|
|
||||||
import { SetUpSubmissionCredentials } from '../../credentials/ios/actions/SetUpSubmissionCredentials';
|
|
||||||
import Log from '../../log';
|
|
||||||
import { getBundleIdentifierAsync } from '../../project/ios/bundleIdentifier';
|
|
||||||
import { findAccountByName } from '../../user/Account';
|
|
||||||
import { SubmissionContext } from '../context';
|
|
||||||
import { AscApiKeyResult } from './AscApiKeySource';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Credentials Service will either return an ASC Api Key or an App Specific Password
|
|
||||||
* When we no longer support the App Specific Password user prompt, refactor this into the AscApiKeySource
|
|
||||||
*/
|
|
||||||
export const CREDENTIALS_SERVICE_SOURCE = 'CREDENTIALS_SERVICE_SOURCE';
|
|
||||||
export type CredentialsServiceSource = typeof CREDENTIALS_SERVICE_SOURCE;
|
|
||||||
|
|
||||||
export async function getFromCredentialsServiceAsync(
|
|
||||||
ctx: SubmissionContext<Platform.IOS>
|
|
||||||
): Promise<{ appSpecificPassword: string } | { ascApiKeyResult: 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 setupSubmissionCredentialsAction = new SetUpSubmissionCredentials(appLookupParams);
|
|
||||||
const ascOrAsp = await setupSubmissionCredentialsAction.runAsync(ctx.credentialsCtx);
|
|
||||||
const isAppSpecificPassword = typeof ascOrAsp === 'string';
|
|
||||||
if (isAppSpecificPassword) {
|
|
||||||
return { appSpecificPassword: ascOrAsp };
|
|
||||||
} else {
|
|
||||||
const ascKeyForSubmissions = nullthrows(
|
|
||||||
ascOrAsp.appStoreConnectApiKeyForSubmissions,
|
|
||||||
`An EAS Submit ASC Api Key could not be found for ${ascOrAsp.appleAppIdentifier.bundleIdentifier}`
|
|
||||||
);
|
|
||||||
const { id, keyIdentifier, name } = ascKeyForSubmissions;
|
|
||||||
return {
|
|
||||||
ascApiKeyResult: {
|
|
||||||
result: {
|
|
||||||
ascApiKeyId: id,
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
source: 'EAS servers',
|
|
||||||
keyId: keyIdentifier,
|
|
||||||
name: name ?? undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
AppSpecificPasswordSourceType,
|
AppSpecificPasswordSourceType,
|
||||||
} from './AppSpecificPasswordSource';
|
} from './AppSpecificPasswordSource';
|
||||||
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
|
import { AscApiKeySource, AscApiKeySourceType } from './AscApiKeySource';
|
||||||
import { CREDENTIALS_SERVICE_SOURCE, CredentialsServiceSource } from './CredentialsServiceSource';
|
|
||||||
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
import IosSubmitter, { IosSubmissionOptions } from './IosSubmitter';
|
||||||
|
|
||||||
export default class IosSubmitCommand {
|
export default class IosSubmitCommand {
|
||||||
@@ -34,15 +33,8 @@ export default class IosSubmitCommand {
|
|||||||
private async resolveCredentialSubmissionOptionsAsync(): Promise<
|
private async resolveCredentialSubmissionOptionsAsync(): Promise<
|
||||||
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
|
| { appSpecificPasswordSource: Result<AppSpecificPasswordSource> }
|
||||||
| { ascApiKeySource: Result<AscApiKeySource> }
|
| { ascApiKeySource: Result<AscApiKeySource> }
|
||||||
| { credentialsServiceSource: Result<CredentialsServiceSource> }
|
|
||||||
> {
|
> {
|
||||||
const ascApiKeySource = this.resolveAscApiKeySource();
|
// if an App Specific Password env var is not specified, use ASC Api Key
|
||||||
const shouldSkipAscApiKeySource =
|
|
||||||
!ascApiKeySource.ok && ascApiKeySource.enforceError() instanceof MissingCredentialsError;
|
|
||||||
if (!shouldSkipAscApiKeySource) {
|
|
||||||
return { ascApiKeySource };
|
|
||||||
}
|
|
||||||
|
|
||||||
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
const appSpecificPasswordSource = this.resolveAppSpecificPasswordSource();
|
||||||
const shouldSkipAppSpecificPasswordSource =
|
const shouldSkipAppSpecificPasswordSource =
|
||||||
!appSpecificPasswordSource.ok &&
|
!appSpecificPasswordSource.ok &&
|
||||||
@@ -51,9 +43,8 @@ export default class IosSubmitCommand {
|
|||||||
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
return { appSpecificPasswordSource: this.resolveAppSpecificPasswordSource() };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const ascApiKeySource = this.resolveAscApiKeySource();
|
||||||
credentialsServiceSource: result(CREDENTIALS_SERVICE_SOURCE),
|
return { ascApiKeySource };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
|
private async resolveSubmissionOptionsAsync(): Promise<IosSubmissionOptions> {
|
||||||
@@ -65,10 +56,6 @@ export default class IosSubmitCommand {
|
|||||||
: null;
|
: null;
|
||||||
const maybeAscApiKeySource =
|
const maybeAscApiKeySource =
|
||||||
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
|
'ascApiKeySource' in credentialsSource ? credentialsSource.ascApiKeySource : null;
|
||||||
const maybeCredentialsServiceSource =
|
|
||||||
'credentialsServiceSource' in credentialsSource
|
|
||||||
? credentialsSource.credentialsServiceSource
|
|
||||||
: null;
|
|
||||||
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
|
const ascAppIdentifier = await this.resolveAscAppIdentifierAsync();
|
||||||
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
|
const appleIdUsername = await this.resolveAppleIdUsernameAsync();
|
||||||
|
|
||||||
@@ -76,7 +63,6 @@ export default class IosSubmitCommand {
|
|||||||
archiveSource,
|
archiveSource,
|
||||||
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
|
...(maybeAppSpecificPasswordSource ? [maybeAppSpecificPasswordSource] : []),
|
||||||
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
|
...(maybeAscApiKeySource ? [maybeAscApiKeySource] : []),
|
||||||
...(maybeCredentialsServiceSource ? [maybeCredentialsServiceSource] : []),
|
|
||||||
ascAppIdentifier,
|
ascAppIdentifier,
|
||||||
appleIdUsername,
|
appleIdUsername,
|
||||||
].filter(r => !r.ok);
|
].filter(r => !r.ok);
|
||||||
@@ -101,11 +87,6 @@ export default class IosSubmitCommand {
|
|||||||
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
|
ascApiKeySource: maybeAscApiKeySource.enforceValue(),
|
||||||
}
|
}
|
||||||
: null),
|
: null),
|
||||||
...(maybeCredentialsServiceSource
|
|
||||||
? {
|
|
||||||
credentialsServiceSource: maybeCredentialsServiceSource.enforceValue(),
|
|
||||||
}
|
|
||||||
: null),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,11 +129,7 @@ export default class IosSubmitCommand {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result(
|
return result({ sourceType: AscApiKeySourceType.credentialsService });
|
||||||
new MissingCredentialsError(
|
|
||||||
'Set the ascApiKeyPath, ascApiKeyIssuerId and ascApiKeyId fields in eas.json.'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveArchiveSource(): Result<ArchiveSource> {
|
private resolveArchiveSource(): Result<ArchiveSource> {
|
||||||
|
|||||||
@@ -20,12 +20,8 @@ import {
|
|||||||
AscApiKeyFromExpoServers,
|
AscApiKeyFromExpoServers,
|
||||||
AscApiKeyResult,
|
AscApiKeyResult,
|
||||||
AscApiKeySource,
|
AscApiKeySource,
|
||||||
getAscApiKeyLocallyAsync,
|
getAscApiKeyResultAsync,
|
||||||
} from './AscApiKeySource';
|
} from './AscApiKeySource';
|
||||||
import {
|
|
||||||
CredentialsServiceSource,
|
|
||||||
getFromCredentialsServiceAsync,
|
|
||||||
} from './CredentialsServiceSource';
|
|
||||||
|
|
||||||
export interface IosSubmissionOptions
|
export interface IosSubmissionOptions
|
||||||
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
|
extends Pick<IosSubmissionConfigInput, 'appleIdUsername' | 'ascAppIdentifier'> {
|
||||||
@@ -33,7 +29,6 @@ export interface IosSubmissionOptions
|
|||||||
archiveSource: ArchiveSource;
|
archiveSource: ArchiveSource;
|
||||||
appSpecificPasswordSource?: AppSpecificPasswordSource;
|
appSpecificPasswordSource?: AppSpecificPasswordSource;
|
||||||
ascApiKeySource?: AscApiKeySource;
|
ascApiKeySource?: AscApiKeySource;
|
||||||
credentialsServiceSource?: CredentialsServiceSource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolvedSourceOptions {
|
interface ResolvedSourceOptions {
|
||||||
@@ -80,16 +75,12 @@ export default class IosSubmitter extends BaseSubmitter<Platform.IOS, IosSubmiss
|
|||||||
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
|
? await getAppSpecificPasswordAsync(this.options.appSpecificPasswordSource)
|
||||||
: null;
|
: null;
|
||||||
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
|
const maybeAppStoreConnectApiKey = this.options.ascApiKeySource
|
||||||
? await getAscApiKeyLocallyAsync(this.ctx, this.options.ascApiKeySource)
|
? await getAscApiKeyResultAsync(this.ctx, this.options.ascApiKeySource)
|
||||||
: null;
|
|
||||||
const maybeAscOrAspFromCredentialsService = this.options.credentialsServiceSource
|
|
||||||
? await getFromCredentialsServiceAsync(this.ctx)
|
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
archive,
|
archive,
|
||||||
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
|
...(maybeAppSpecificPassword ? { appSpecificPassword: maybeAppSpecificPassword } : null),
|
||||||
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
|
...(maybeAppStoreConnectApiKey ? { ascApiKeyResult: maybeAppStoreConnectApiKey } : null),
|
||||||
...(maybeAscOrAspFromCredentialsService ? maybeAscOrAspFromCredentialsService : null),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
|
|
||||||
import { asMock } from '../../../__tests__/utils';
|
import { asMock } from '../../../__tests__/utils';
|
||||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
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 { getCredentialsFromUserAsync } from '../../../credentials/utils/promptForCredentials';
|
||||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
import { createTestProject } from '../../../project/__tests__/project-utils';
|
||||||
|
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
|
||||||
import { promptAsync } from '../../../prompts';
|
import { promptAsync } from '../../../prompts';
|
||||||
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
|
import { SubmissionContext, createSubmissionContextAsync } from '../../context';
|
||||||
import {
|
import {
|
||||||
AscApiKeySource,
|
AscApiKeySource,
|
||||||
AscApiKeySourceType,
|
AscApiKeySourceType,
|
||||||
getAscApiKeyLocallyAsync,
|
|
||||||
getAscApiKeyPathAsync,
|
getAscApiKeyPathAsync,
|
||||||
|
getAscApiKeyResultAsync,
|
||||||
} from '../AscApiKeySource';
|
} from '../AscApiKeySource';
|
||||||
|
|
||||||
jest.mock('fs');
|
jest.mock('fs');
|
||||||
@@ -24,6 +27,7 @@ jest.mock('../../../user/User', () => ({
|
|||||||
jest.mock('../../../user/Account', () => ({
|
jest.mock('../../../user/Account', () => ({
|
||||||
findAccountByName: jest.fn(() => mockJester.accounts[0]),
|
findAccountByName: jest.fn(() => mockJester.accounts[0]),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../../../project/ios/bundleIdentifier');
|
||||||
|
|
||||||
const testProject = createTestProject(mockJester, {
|
const testProject = createTestProject(mockJester, {
|
||||||
android: {
|
android: {
|
||||||
@@ -167,14 +171,14 @@ describe(getAscApiKeyPathAsync, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(getAscApiKeyLocallyAsync, () => {
|
describe(getAscApiKeyResultAsync, () => {
|
||||||
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
|
it('returns a local Asc Api Key file with a AscApiKeySourceType.path source', async () => {
|
||||||
const ctx = await getIosSubmissionContextAsync();
|
const ctx = await getIosSubmissionContextAsync();
|
||||||
const source: AscApiKeySource = {
|
const source: AscApiKeySource = {
|
||||||
sourceType: AscApiKeySourceType.path,
|
sourceType: AscApiKeySourceType.path,
|
||||||
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
|
path: { keyP8Path: '/asc-api-key.p8', keyId: 'test-key-id', issuerId: 'test-issuer-id' },
|
||||||
};
|
};
|
||||||
const ascApiKeyResult = await getAscApiKeyLocallyAsync(ctx, source);
|
const ascApiKeyResult = await getAscApiKeyResultAsync(ctx, source);
|
||||||
expect(ascApiKeyResult).toMatchObject({
|
expect(ascApiKeyResult).toMatchObject({
|
||||||
result: {
|
result: {
|
||||||
keyP8: 'super secret',
|
keyP8: 'super secret',
|
||||||
@@ -201,7 +205,7 @@ describe(getAscApiKeyLocallyAsync, () => {
|
|||||||
const source: AscApiKeySource = {
|
const source: AscApiKeySource = {
|
||||||
sourceType: AscApiKeySourceType.prompt,
|
sourceType: AscApiKeySourceType.prompt,
|
||||||
};
|
};
|
||||||
const serviceAccountResult = await getAscApiKeyLocallyAsync(ctx, source);
|
const serviceAccountResult = await getAscApiKeyResultAsync(ctx, source);
|
||||||
expect(serviceAccountResult).toMatchObject({
|
expect(serviceAccountResult).toMatchObject({
|
||||||
result: {
|
result: {
|
||||||
keyP8: 'super secret',
|
keyP8: 'super secret',
|
||||||
@@ -215,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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
|
||||||
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 { SetUpSubmissionCredentials } from '../../../credentials/ios/actions/SetUpSubmissionCredentials';
|
|
||||||
import { createTestProject } from '../../../project/__tests__/project-utils';
|
|
||||||
import { getBundleIdentifierAsync } from '../../../project/ios/bundleIdentifier';
|
|
||||||
import { createSubmissionContextAsync } from '../../context';
|
|
||||||
import { getFromCredentialsServiceAsync } from '../CredentialsServiceSource';
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(getFromCredentialsServiceAsync, () => {
|
|
||||||
it('returns an App Specific Password from the 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,
|
|
||||||
});
|
|
||||||
jest
|
|
||||||
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
|
|
||||||
.mockImplementation(async _ctx => 'super secret');
|
|
||||||
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
|
|
||||||
|
|
||||||
const result = await getFromCredentialsServiceAsync(ctx);
|
|
||||||
expect(result).toEqual({
|
|
||||||
appSpecificPassword: 'super secret',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('returns an ASC Api Key from the 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,
|
|
||||||
});
|
|
||||||
jest
|
|
||||||
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
|
|
||||||
.mockImplementation(async _ctx => testCommonIosAppCredentialsFragment);
|
|
||||||
asMock(getBundleIdentifierAsync).mockImplementation(() => 'com.hello.world');
|
|
||||||
|
|
||||||
const result = await getFromCredentialsServiceAsync(ctx);
|
|
||||||
expect(result).toEqual({
|
|
||||||
ascApiKeyResult: {
|
|
||||||
result: {
|
|
||||||
ascApiKeyId: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.id,
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
keyId:
|
|
||||||
testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.keyIdentifier,
|
|
||||||
name: testCommonIosAppCredentialsFragment.appStoreConnectApiKeyForSubmissions?.name,
|
|
||||||
source: 'EAS servers',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user