Compare commits

...
85 changed files with 712 additions and 576 deletions

No files matched your search

+2 -2
View File
@@ -57,7 +57,7 @@ This means that it will most likely produce an AAB and you will not be able to i
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile); const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) { if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp); await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
} }
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext); const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
@@ -91,7 +91,7 @@ export async function prepareAndroidBuildAsync(
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false ? false
: ctx.buildProfile.autoIncrement, : ctx.buildProfile.autoIncrement,
nonInteractive: ctx.nonInteractive, projectId: ctx.projectId,
}); });
}, },
prepareJobAsync: async ( prepareJobAsync: async (
@@ -5,7 +5,6 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider'; import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getUsername } from '../../project/projectUtils'; import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs'; import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -24,10 +23,7 @@ export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>, ctx: BuildContext<Platform.ANDROID>,
jobData: JobData jobData: JobData
): Promise<Job> { ): Promise<Job> {
const username = getUsername( const username = getUsername(ctx.exp, ctx.user);
ctx.exp,
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile; const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory = const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.'; slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
@@ -16,12 +16,12 @@ export async function syncProjectConfigurationAsync({
projectDir, projectDir,
exp, exp,
localAutoIncrement, localAutoIncrement,
nonInteractive, projectId,
}: { }: {
projectDir: string; projectDir: string;
exp: ExpoConfig; exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement; localAutoIncrement?: AndroidVersionAutoIncrement;
nonInteractive: boolean; projectId: string;
}): Promise<void> { }): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
@@ -29,7 +29,7 @@ export async function syncProjectConfigurationAsync({
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir); await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) { if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp, { nonInteractive }); await syncUpdatesConfigurationAsync(projectDir, exp, projectId);
} }
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy }); await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else { } else {
+9 -10
View File
@@ -10,9 +10,9 @@ import { Analytics, BuildEvent } from '../analytics/events';
import { CredentialsContext } from '../credentials/context'; import { CredentialsContext } from '../credentials/context';
import { BuildResourceClass } from '../graphql/generated'; import { BuildResourceClass } from '../graphql/generated';
import { getExpoConfig } from '../project/expoConfig'; import { getExpoConfig } from '../project/expoConfig';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow'; import { resolveWorkflowAsync } from '../project/workflow';
import { ensureLoggedInAsync } from '../user/actions'; import { Actor } from '../user/User';
import { createAndroidContextAsync } from './android/build'; import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context'; import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build'; import { createIosContextAsync } from './ios/build';
@@ -30,6 +30,8 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir, projectDir,
resourceClass, resourceClass,
message, message,
actor,
projectId,
}: { }: {
buildProfileName: string; buildProfileName: string;
buildProfile: BuildProfile<T>; buildProfile: BuildProfile<T>;
@@ -42,25 +44,22 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir: string; projectDir: string;
resourceClass: BuildResourceClass; resourceClass: BuildResourceClass;
message?: string; message?: string;
actor: Actor;
projectId: string;
}): Promise<BuildContext<T>> { }): Promise<BuildContext<T>> {
const exp = getExpoConfig(projectDir, { env: buildProfile.env }); const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const user = await ensureLoggedInAsync({ nonInteractive });
const projectName = exp.slug; const projectName = exp.slug;
const projectId = await getProjectIdAsync(exp, {
env: buildProfile.env,
nonInteractive,
});
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform); const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = account.id; const accountId = account.id;
const runFromCI = getenv.boolish('CI', false); const runFromCI = getenv.boolish('CI', false);
const credentialsCtx = new CredentialsContext({ const credentialsCtx = new CredentialsContext({
exp, projectInfo: { exp, projectId },
nonInteractive, nonInteractive,
projectDir, projectDir,
user, user: actor,
env: buildProfile.env, env: buildProfile.env,
easJsonCliConfig, easJsonCliConfig,
}); });
@@ -99,7 +98,7 @@ export async function createBuildContextAsync<T extends Platform>({
projectId, projectId,
projectName, projectName,
trackingCtx, trackingCtx,
user, user: actor,
workflow, workflow,
message, message,
runFromCI, runFromCI,
+2 -2
View File
@@ -23,7 +23,7 @@ export async function createIosContextAsync(
const { buildProfile } = ctx; const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) { if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp); await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
} }
checkNodeEnvVariable(ctx); checkNodeEnvVariable(ctx);
@@ -80,7 +80,7 @@ export async function prepareIosBuildAsync(
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false ? false
: ctx.buildProfile.autoIncrement, : ctx.buildProfile.autoIncrement,
nonInteractive: ctx.nonInteractive, projectId: ctx.projectId,
}); });
}, },
prepareJobAsync: async ( prepareJobAsync: async (
+1 -5
View File
@@ -4,7 +4,6 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types'; import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { getUsername } from '../../project/projectUtils'; import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs'; import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -26,10 +25,7 @@ export async function prepareJobAsync(
): Promise<Job> { ): Promise<Job> {
const projectRootDirectory = const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.'; slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const username = getUsername( const username = getUsername(ctx.exp, ctx.user);
ctx.exp,
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
);
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {}; const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) { if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials); const targetNames = Object.keys(jobData.credentials);
@@ -13,20 +13,20 @@ export async function syncProjectConfigurationAsync({
exp, exp,
targets, targets,
localAutoIncrement, localAutoIncrement,
nonInteractive, projectId,
}: { }: {
projectDir: string; projectDir: string;
exp: ExpoConfig; exp: ExpoConfig;
targets: Target[]; targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement; localAutoIncrement?: IosVersionAutoIncrement;
nonInteractive: boolean; projectId: string;
}): Promise<void> { }): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) { if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(projectDir, exp, { nonInteractive }); await syncUpdatesConfigurationAsync(projectDir, exp, projectId);
} }
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets }); await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else { } else {
+1 -5
View File
@@ -14,7 +14,6 @@ import {
readChannelSafelyAsync as readIosChannelSafelyAsync, readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync, readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from '../update/ios/UpdatesModule'; } from '../update/ios/UpdatesModule';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli'; import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs'; import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version'; import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
@@ -49,10 +48,7 @@ export async function collectMetadataAsync<T extends Platform>(
(await vcsClient.getLastCommitMessageAsync()) ?? undefined (await vcsClient.getLastCommitMessageAsync()) ?? undefined
), ),
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(), isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername( username: getUsername(ctx.exp, ctx.user),
ctx.exp,
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
),
message: ctx.message, message: ctx.message,
...(ctx.platform === Platform.IOS && { ...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning( iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
@@ -41,6 +41,7 @@ import {
waitToCompleteAsync as waitForSubmissionsToCompleteAsync, waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit'; } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls'; import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { Actor } from '../user/User';
import { printJsonOnlyOutput } from '../utils/json'; import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles'; import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs'; import { getVcsClient } from '../vcs';
@@ -87,7 +88,12 @@ const platformToGraphQLResourceClassMapping: Record<
}, },
}; };
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> { export async function runBuildAndSubmitAsync(
projectDir: string,
flags: BuildFlags,
actor: Actor,
projectId: string
): Promise<void> {
await getVcsClient().ensureRepoExistsAsync(); await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive); await ensureRepoIsCleanAsync(flags.nonInteractive);
@@ -134,6 +140,8 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
], ],
easJsonCliConfig, easJsonCliConfig,
actor,
projectId,
}); });
if (maybeBuild) { if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile }); startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -227,6 +235,8 @@ async function prepareAndStartBuildAsync({
buildProfile, buildProfile,
resourceClass, resourceClass,
easJsonCliConfig, easJsonCliConfig,
actor,
projectId,
}: { }: {
projectDir: string; projectDir: string;
flags: BuildFlags; flags: BuildFlags;
@@ -234,6 +244,8 @@ async function prepareAndStartBuildAsync({
buildProfile: ProfileData<'build'>; buildProfile: ProfileData<'build'>;
resourceClass: BuildResourceClass; resourceClass: BuildResourceClass;
easJsonCliConfig: EasJson['cli']; easJsonCliConfig: EasJson['cli'];
actor: Actor;
projectId: string;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> { }): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({ const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName, buildProfileName: buildProfile.profileName,
@@ -247,6 +259,8 @@ async function prepareAndStartBuildAsync({
localBuildOptions: flags.localBuildOptions, localBuildOptions: flags.localBuildOptions,
easJsonCliConfig, easJsonCliConfig,
message: flags.message, message: flags.message,
actor,
projectId,
}); });
if (moreBuilds) { if (moreBuilds) {
@@ -318,6 +332,7 @@ async function prepareAndStartSubmissionAsync({
env: buildProfile.env, env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx, credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier, applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
}); });
if (moreBuilds) { if (moreBuilds) {
+103 -8
View File
@@ -13,20 +13,113 @@ import {
logEvent, logEvent,
} from '../analytics/rudderstackClient'; } from '../analytics/rudderstackClient';
import { learnMore } from '../log'; import { learnMore } from '../log';
import { findProjectRootAsync } from '../project/projectUtils'; import { getExpoConfig } from '../project/expoConfig';
import { getUserAsync } from '../user/User'; import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { Actor, getUserAsync } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions'; import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli'; import { easCliVersion } from '../utils/easCli';
import { setVcsClient } from '../vcs'; import { setVcsClient } from '../vcs';
import GitClient from '../vcs/clients/git'; import GitClient from '../vcs/clients/git';
export default abstract class EasCommand extends Command { export interface CommandConfiguration {
/** /**
* When user data is unavailable locally, determines if the command will * When user data is unavailable locally, determines if the command will
* force the user to log in * force the user to log in. By default, all commands require authentication.
*/ */
protected requiresAuthentication = true; allowUnauthenticated?: boolean;
protected mustBeRunInsideProject = true;
/**
* Whether a command can be run outside of an Expo project directory.
* By default, all commands must be run inside an Expo project directory.
*/
canRunOutsideProject?: boolean;
}
export interface ContextOptions {
nonInteractive: boolean;
}
export abstract class ContextField<T> {
abstract getValueAsync(options: ContextOptions): Promise<T>;
}
export class ProjectIdContextField extends ContextField<string> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string> {
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
return await getProjectIdAsync(exp, { nonInteractive });
}
}
export class OptionalProjectIdContextField extends ContextField<string | undefined> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<string | undefined> {
const projectDir = await findProjectRootAsync();
if (!projectDir) {
return undefined;
}
const exp = getExpoConfig(projectDir);
return await getProjectIdAsync(exp, { nonInteractive });
}
}
export class ActorContextField extends ContextField<Actor> {
async getValueAsync({ nonInteractive }: ContextOptions): Promise<Actor> {
return await ensureLoggedInAsync({ nonInteractive });
}
}
export const EASCommandProjectIdContext = {
projectId: new ProjectIdContextField(),
};
export const EASCommandProjectIdIfProjectDirContext = {
projectId: new OptionalProjectIdContextField(),
};
export const EASCommandLoggedInContext = {
actor: new ActorContextField(),
};
type ContextInput<
T extends {
[name: string]: any;
} = object
> = {
[P in keyof T]: ContextField<T[P]>;
};
type ContextOutput<
T extends {
[name: string]: any;
} = object
> = {
[P in keyof T]: T[P];
};
export default abstract class EasCommand extends Command {
static contextDefinition: ContextInput = {};
protected commandConfiguration: CommandConfiguration = {};
protected async getContextAsync<
C extends {
[name: string]: any;
} = object
>(
commandClass: { contextDefinition: ContextInput<C> },
{ nonInteractive }: { nonInteractive: boolean }
): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition;
const contextValuePairs = await Promise.all(
Object.keys(contextDefinition).map(async contextKey => {
return [contextKey, await contextDefinition[contextKey].getValueAsync({ nonInteractive })];
})
);
return Object.fromEntries(contextValuePairs);
}
protected abstract runAsync(): Promise<any>; protected abstract runAsync(): Promise<any>;
@@ -34,24 +127,26 @@ export default abstract class EasCommand extends Command {
async run(): Promise<any> { async run(): Promise<any> {
await initAnalyticsAsync(); await initAnalyticsAsync();
if (this.mustBeRunInsideProject) { if (!this.commandConfiguration.canRunOutsideProject) {
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
await this.applyCliConfigAsync(projectDir); await this.applyCliConfigAsync(projectDir);
await this.ensureEasCliIsNotInDependenciesAsync(projectDir); await this.ensureEasCliIsNotInDependenciesAsync(projectDir);
} }
if (this.requiresAuthentication) { if (!this.commandConfiguration.allowUnauthenticated) {
const { flags } = await this.parse(); const { flags } = await this.parse();
const nonInteractive = (flags as any)['non-interactive'] ?? false; const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive }); await ensureLoggedInAsync({ nonInteractive });
} else { } else {
await getUserAsync(); await getUserAsync();
} }
logEvent(AnalyticsEvent.ACTION, { logEvent(AnalyticsEvent.ACTION, {
// id is assigned by oclif in constructor based on the filepath: // id is assigned by oclif in constructor based on the filepath:
// commands/submit === submit, commands/build/list === build:list // commands/submit === submit, commands/build/list === build:list
action: `eas ${this.id}`, action: `eas ${this.id}`,
}); });
return this.runAsync(); return this.runAsync();
} }
@@ -2,7 +2,7 @@ import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClie
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants'; import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { getUserAsync } from '../../user/User'; import { getUserAsync } from '../../user/User';
import { ensureLoggedInAsync } from '../../user/actions'; import { ensureLoggedInAsync } from '../../user/actions';
import EasCommand from '../EasCommand'; import EasCommand, { CommandConfiguration } from '../EasCommand';
jest.mock('../../user/User'); jest.mock('../../user/User');
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() })); jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
@@ -43,7 +43,9 @@ beforeEach(() => {
const createTestEasCommand = (authValue: boolean): typeof EasCommand => { const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand { class TestEasCommand extends EasCommand {
override requiresAuthentication = authValue; protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: !authValue,
};
async runAsync(): Promise<void> {} async runAsync(): Promise<void> {}
} }
@@ -1,4 +1,4 @@
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
import Log from '../../log'; import Log from '../../log';
import { showLoginPromptAsync } from '../../user/actions'; import { showLoginPromptAsync } from '../../user/actions';
@@ -6,8 +6,10 @@ export default class AccountLogin extends EasCommand {
static override description = 'log in with your Expo account'; static override description = 'log in with your Expo account';
static override aliases = ['login']; static override aliases = ['login'];
protected override mustBeRunInsideProject = false; protected override commandConfiguration: CommandConfiguration = {
protected override requiresAuthentication = false; allowUnauthenticated: true,
canRunOutsideProject: true,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
await showLoginPromptAsync(); await showLoginPromptAsync();
@@ -1,4 +1,4 @@
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
import Log from '../../log'; import Log from '../../log';
import { logoutAsync } from '../../user/User'; import { logoutAsync } from '../../user/User';
@@ -6,8 +6,10 @@ export default class AccountLogout extends EasCommand {
static override description = 'log out'; static override description = 'log out';
static override aliases = ['logout']; static override aliases = ['logout'];
protected override mustBeRunInsideProject = false; protected override commandConfiguration: CommandConfiguration = {
protected override requiresAuthentication = false; allowUnauthenticated: true,
canRunOutsideProject: true,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
await logoutAsync(); await logoutAsync();
@@ -1,6 +1,6 @@
import chalk from 'chalk'; import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { CommandConfiguration } from '../../commandUtils/EasCommand';
import Log from '../../log'; import Log from '../../log';
import { getActorDisplayName, getUserAsync } from '../../user/User'; import { getActorDisplayName, getUserAsync } from '../../user/User';
@@ -8,8 +8,10 @@ export default class AccountView extends EasCommand {
static override description = 'show the username you are logged in as'; static override description = 'show the username you are logged in as';
static override aliases = ['whoami']; static override aliases = ['whoami'];
protected override mustBeRunInsideProject = false; protected override commandConfiguration: CommandConfiguration = {
protected override requiresAuthentication = false; allowUnauthenticated: true,
canRunOutsideProject: true,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const user = await getUserAsync(); const user = await getUserAsync();
+4 -2
View File
@@ -1,4 +1,4 @@
import EasCommand from '../commandUtils/EasCommand'; import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
import Log from '../log'; import Log from '../log';
import UserSettings from '../user/UserSettings'; import UserSettings from '../user/UserSettings';
@@ -7,7 +7,9 @@ export default class AnalyticsView extends EasCommand {
static override args = [{ name: 'STATUS', options: ['on', 'off'] }]; static override args = [{ name: 'STATUS', options: ['on', 'off'] }];
protected override requiresAuthentication = false; protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { STATUS: status } = (await this.parse(AnalyticsView)).args; const { STATUS: status } = (await this.parse(AnalyticsView)).args;
+10 -10
View File
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { getDefaultBranchNameAsync } from '../../branch/utils'; import { getDefaultBranchNameAsync } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { import {
@@ -11,12 +11,7 @@ import {
UpdateBranch, UpdateBranch,
} from '../../graphql/generated'; } from '../../graphql/generated';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -66,18 +61,23 @@ export default class BranchCreate extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
args: { name }, args: { name },
flags: { json: jsonFlag, 'non-interactive': nonInteractive }, flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(BranchCreate); } = await this.parse(BranchCreate);
const { projectId } = await this.getContextAsync(BranchCreate, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId); const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!name) { if (!name) {
+9 -11
View File
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries'; import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination'; import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
@@ -14,12 +14,7 @@ import {
GetBranchInfoQueryVariables, GetBranchInfoQueryVariables,
} from '../../graphql/generated'; } from '../../graphql/generated';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { toggleConfirmAsync } from '../../prompts'; import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -81,6 +76,10 @@ async function deleteBranchOnAppAsync({
export default class BranchDelete extends EasCommand { export default class BranchDelete extends EasCommand {
static override description = 'delete a branch'; static override description = 'delete a branch';
static override contextDefinition = {
...EASCommandProjectIdContext,
};
static override args = [ static override args = [
{ {
name: 'name', name: 'name',
@@ -98,15 +97,14 @@ export default class BranchDelete extends EasCommand {
args: { name: branchName }, args: { name: branchName },
flags, flags,
} = await this.parse(BranchDelete); } = await this.parse(BranchDelete);
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags; const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync(); const { projectId } = await this.getContextAsync(BranchDelete, { nonInteractive });
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId); const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!branchName) { if (!branchName) {
+8 -8
View File
@@ -1,9 +1,7 @@
import { listAndRenderBranchesOnAppAsync } from '../../branch/queries'; import { listAndRenderBranchesOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination'; import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput } from '../../utils/json'; import { enableJsonOutput } from '../../utils/json';
export default class BranchList extends EasCommand { export default class BranchList extends EasCommand {
@@ -14,19 +12,21 @@ export default class BranchList extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(BranchList); const { flags } = await this.parse(BranchList);
const { projectId } = await this.getContextAsync(BranchList, {
nonInteractive: flags['non-interactive'],
});
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (paginatedQueryOptions.json) { if (paginatedQueryOptions.json) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, {
nonInteractive: flags['non-interactive'],
});
await listAndRenderBranchesOnAppAsync({ projectId, paginatedQueryOptions }); await listAndRenderBranchesOnAppAsync({ projectId, paginatedQueryOptions });
} }
} }
+9 -10
View File
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk'; import chalk from 'chalk';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { import {
@@ -12,12 +12,7 @@ import {
UpdateBranch, UpdateBranch,
} from '../../graphql/generated'; } from '../../graphql/generated';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -67,17 +62,21 @@ export default class BranchRename extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive }, flags: { json: jsonFlag, from: currentName, to: newName, 'non-interactive': nonInteractive },
} = await this.parse(BranchRename); } = await this.parse(BranchRename);
const { projectId } = await this.getContextAsync(BranchRename, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId); const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
if (!currentName) { if (!currentName) {
+8 -7
View File
@@ -1,13 +1,11 @@
import { selectBranchOnAppAsync } from '../../branch/queries'; import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { import {
EasPaginatedQueryFlags, EasPaginatedQueryFlags,
getLimitFlagWithCustomValues, getLimitFlagWithCustomValues,
getPaginatedQueryOptions, getPaginatedQueryOptions,
} from '../../commandUtils/pagination'; } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { listAndRenderUpdateGroupsOnBranchAsync } from '../../update/queries'; import { listAndRenderUpdateGroupsOnBranchAsync } from '../../update/queries';
import { enableJsonOutput } from '../../utils/json'; import { enableJsonOutput } from '../../utils/json';
@@ -28,22 +26,25 @@ export default class BranchView extends EasCommand {
limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }), limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
args: { name: branchName }, args: { name: branchName },
flags, flags,
} = await this.parse(BranchView); } = await this.parse(BranchView);
const { 'non-interactive': nonInteractive } = flags; const { 'non-interactive': nonInteractive } = flags;
const { projectId } = await this.getContextAsync(BranchView, {
nonInteractive,
});
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (paginatedQueryOptions.json) { if (paginatedQueryOptions.json) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!branchName) { if (!branchName) {
if (nonInteractive) { if (nonInteractive) {
throw new Error('Branch name may not be empty.'); throw new Error('Branch name may not be empty.');
+9 -10
View File
@@ -1,7 +1,7 @@
import chalk from 'chalk'; import chalk from 'chalk';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags'; import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { import {
@@ -14,12 +14,7 @@ import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { appPlatformEmojis } from '../../platform'; import { appPlatformEmojis } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { confirmAsync, selectAsync } from '../../prompts'; import { confirmAsync, selectAsync } from '../../prompts';
async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> { async function cancelBuildAsync(buildId: string): Promise<Pick<Build, 'id' | 'status'>> {
@@ -133,15 +128,19 @@ export default class BuildCancel extends EasCommand {
...EASNonInteractiveFlag, ...EASNonInteractiveFlag,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
args: { BUILD_ID: buildIdFromArg }, args: { BUILD_ID: buildIdFromArg },
flags: { 'non-interactive': nonInteractive }, flags: { 'non-interactive': nonInteractive },
} = await this.parse(BuildCancel); } = await this.parse(BuildCancel);
const { projectId } = await this.getContextAsync(BuildCancel, {
nonInteractive,
});
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const displayName = await getDisplayNameForProjectIdAsync(projectId); const displayName = await getDisplayNameForProjectIdAsync(projectId);
if (buildIdFromArg) { if (buildIdFromArg) {
@@ -4,7 +4,7 @@ import chalk from 'chalk';
import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration'; import { cleanUpOldEasBuildGradleScriptAsync } from '../../build/android/syncProjectConfiguration';
import { ensureProjectConfiguredAsync } from '../../build/configure'; import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { RequestedPlatform } from '../../platform'; import { RequestedPlatform } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig'; import { getExpoConfig } from '../../project/expoConfig';
@@ -26,8 +26,15 @@ export default class BuildConfigure extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildConfigure); const { flags } = await this.parse(BuildConfigure);
const { projectId } = await this.getContextAsync(BuildConfigure, {
nonInteractive: false,
});
Log.log( Log.log(
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.' '💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
@@ -60,14 +67,14 @@ export default class BuildConfigure extends EasCommand {
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) { if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(projectDir, exp, { nonInteractive: false }); await syncAndroidUpdatesConfigurationAsync(projectDir, exp, projectId);
} }
} }
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) { if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(projectDir, exp, { nonInteractive: false }); await syncIosUpdatesConfigurationAsync(projectDir, exp, projectId);
} }
} }
} }
+14 -2
View File
@@ -7,7 +7,10 @@ import path from 'path';
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit'; import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import { UserInputResourceClass } from '../../build/types'; import { UserInputResourceClass } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { StatuspageServiceName } from '../../graphql/generated'; import { StatuspageServiceName } from '../../graphql/generated';
import Log, { link } from '../../log'; import Log, { link } from '../../log';
@@ -96,6 +99,11 @@ export default class Build extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Build); const { flags: rawFlags } = await this.parse(Build);
@@ -104,6 +112,10 @@ export default class Build extends EasCommand {
} }
const flags = this.sanitizeFlags(rawFlags); const flags = this.sanitizeFlags(rawFlags);
const { actor, projectId } = await this.getContextAsync(Build, {
nonInteractive: flags.nonInteractive,
});
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive); await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
@@ -118,7 +130,7 @@ export default class Build extends EasCommand {
const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags); const flagsWithPlatform = await this.ensurePlatformSelectedAsync(flags);
await runBuildAndSubmitAsync(projectDir, flagsWithPlatform); await runBuildAndSubmitAsync(projectDir, flagsWithPlatform, actor, projectId);
} }
private sanitizeFlags( private sanitizeFlags(
+20 -3
View File
@@ -5,7 +5,10 @@ import path from 'path';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit'; import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform'; import { RequestedPlatform } from '../../platform';
@@ -62,8 +65,17 @@ export default class BuildInspect extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildInspect); const { flags } = await this.parse(BuildInspect);
const { actor, projectId } = await this.getContextAsync(BuildInspect, {
nonInteractive: false,
});
const outputDirectory = path.resolve(process.cwd(), flags.output); const outputDirectory = path.resolve(process.cwd(), flags.output);
const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4()); const tmpWorkingdir = path.join(getTmpDirectory(), uuidv4());
@@ -83,7 +95,9 @@ export default class BuildInspect extends EasCommand {
} else { } else {
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
try { try {
await runBuildAndSubmitAsync(projectDir, { await runBuildAndSubmitAsync(
projectDir,
{
nonInteractive: false, nonInteractive: false,
wait: true, wait: true,
clearCache: false, clearCache: false,
@@ -99,7 +113,10 @@ export default class BuildInspect extends EasCommand {
workingdir: tmpWorkingdir, workingdir: tmpWorkingdir,
artifactsDir: path.join(tmpWorkingdir, 'artifacts'), artifactsDir: path.join(tmpWorkingdir, 'artifacts'),
}, },
}); },
actor,
projectId
);
if (!flags.verbose) { if (!flags.verbose) {
Log.log(chalk.green('Build successful')); Log.log(chalk.green('Build successful'));
} }
+9 -11
View File
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries'; import { BUILDS_LIMIT, listAndRenderBuildsOnAppAsync } from '../../build/queries';
import { BuildDistributionType, BuildStatus } from '../../build/types'; import { BuildDistributionType, BuildStatus } from '../../build/types';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { import {
EasPaginatedQueryFlags, EasPaginatedQueryFlags,
@@ -15,12 +15,7 @@ import {
BuildStatus as GraphQLBuildStatus, BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated'; } from '../../graphql/generated';
import { RequestedPlatform } from '../../platform'; import { RequestedPlatform } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput } from '../../utils/json'; import { enableJsonOutput } from '../../utils/json';
export default class BuildList extends EasCommand { export default class BuildList extends EasCommand {
@@ -60,6 +55,10 @@ export default class BuildList extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildList); const { flags } = await this.parse(BuildList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
@@ -70,6 +69,9 @@ export default class BuildList extends EasCommand {
distribution: buildDistribution, distribution: buildDistribution,
'non-interactive': nonInteractive, 'non-interactive': nonInteractive,
} = flags; } = flags;
const { projectId } = await this.getContextAsync(BuildList, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
@@ -77,10 +79,6 @@ export default class BuildList extends EasCommand {
const platform = toAppPlatform(requestedPlatform); const platform = toAppPlatform(requestedPlatform);
const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus); const graphqlBuildStatus = toGraphQLBuildStatus(buildStatus);
const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution); const graphqlBuildDistribution = toGraphQLBuildDistribution(buildDistribution);
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const displayName = await getDisplayNameForProjectIdAsync(projectId); const displayName = await getDisplayNameForProjectIdAsync(projectId);
await listAndRenderBuildsOnAppAsync({ await listAndRenderBuildsOnAppAsync({
@@ -4,7 +4,10 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import chalk from 'chalk'; import chalk from 'chalk';
import EasCommand from '../../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../../commandUtils/EasCommand';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation'; import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery'; import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { toAppPlatform } from '../../../graphql/types/AppPlatform'; import { toAppPlatform } from '../../../graphql/types/AppPlatform';
@@ -17,7 +20,6 @@ import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/
import { import {
findProjectRootAsync, findProjectRootAsync,
getDisplayNameForProjectIdAsync, getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../../project/projectUtils'; } from '../../../project/projectUtils';
import { import {
ensureVersionSourceIsRemoteAsync, ensureVersionSourceIsRemoteAsync,
@@ -42,8 +44,16 @@ export default class BuildVersionSetView extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
public async runAsync(): Promise<void> { public async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildVersionSetView); const { flags } = await this.parse(BuildVersionSetView);
const { actor, projectId } = await this.getContextAsync(BuildVersionSetView, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
@@ -57,9 +67,6 @@ export default class BuildVersionSetView extends EasCommand {
); );
const exp = getExpoConfig(projectDir, { env: profile.env }); const exp = getExpoConfig(projectDir, { env: profile.env });
// this command is always interactive (see prompt below)
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const displayName = await getDisplayNameForProjectIdAsync(projectId); const displayName = await getDisplayNameForProjectIdAsync(projectId);
validateAppConfigForRemoteVersionSource(exp, platform); validateAppConfigForRemoteVersionSource(exp, platform);
@@ -68,7 +75,8 @@ export default class BuildVersionSetView extends EasCommand {
projectDir, projectDir,
exp, exp,
profile, profile,
platform platform,
actor
); );
const remoteVersions = await AppVersionQuery.latestVersionAsync( const remoteVersions = await AppVersionQuery.latestVersionAsync(
projectId, projectId,
@@ -6,7 +6,10 @@ import chalk from 'chalk';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../../build/android/version'; import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../../build/android/version';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../../build/ios/version'; import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../../build/ios/version';
import EasCommand from '../../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../../commandUtils/EasCommand';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery'; import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { toAppPlatform } from '../../../graphql/types/AppPlatform'; import { toAppPlatform } from '../../../graphql/types/AppPlatform';
import Log from '../../../log'; import Log from '../../../log';
@@ -22,7 +25,7 @@ import { getExpoConfig } from '../../../project/expoConfig';
import { resolveXcodeBuildContextAsync } from '../../../project/ios/scheme'; import { resolveXcodeBuildContextAsync } from '../../../project/ios/scheme';
import { resolveTargetsAsync } from '../../../project/ios/target'; import { resolveTargetsAsync } from '../../../project/ios/target';
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions'; import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
import { findProjectRootAsync, getProjectIdAsync } from '../../../project/projectUtils'; import { findProjectRootAsync } from '../../../project/projectUtils';
import { import {
ensureVersionSourceIsRemoteAsync, ensureVersionSourceIsRemoteAsync,
getBuildVersionName, getBuildVersionName,
@@ -56,8 +59,16 @@ export default class BuildVersionSyncView extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
public async runAsync(): Promise<void> { public async runAsync(): Promise<void> {
const { flags } = await this.parse(BuildVersionSyncView); const { flags } = await this.parse(BuildVersionSyncView);
const { actor, projectId } = await this.getContextAsync(BuildVersionSyncView, {
nonInteractive: true,
});
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform); const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
@@ -74,9 +85,6 @@ export default class BuildVersionSyncView extends EasCommand {
for (const profileInfo of buildProfiles) { for (const profileInfo of buildProfiles) {
const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env }); const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env });
// this command is always non-interactive
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform); validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)]; const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
@@ -84,7 +92,8 @@ export default class BuildVersionSyncView extends EasCommand {
projectDir, projectDir,
exp, exp,
profileInfo.profile, profileInfo.profile,
profileInfo.platform profileInfo.platform,
actor
); );
const remoteVersions = await AppVersionQuery.latestVersionAsync( const remoteVersions = await AppVersionQuery.latestVersionAsync(
projectId, projectId,
+9 -12
View File
@@ -1,16 +1,11 @@
import { formatGraphQLBuild } from '../../build/utils/formatBuild'; import { formatGraphQLBuild } from '../../build/utils/formatBuild';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasJsonOnlyFlag } from '../../commandUtils/flags'; import { EasJsonOnlyFlag } from '../../commandUtils/flags';
import { BuildFragment } from '../../graphql/generated'; import { BuildFragment } from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery'; import { BuildQuery } from '../../graphql/queries/BuildQuery';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class BuildView extends EasCommand { export default class BuildView extends EasCommand {
@@ -22,20 +17,22 @@ export default class BuildView extends EasCommand {
...EasJsonOnlyFlag, ...EasJsonOnlyFlag,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
args: { BUILD_ID: buildId }, args: { BUILD_ID: buildId },
flags, flags,
} = await this.parse(BuildView); } = await this.parse(BuildView);
const { projectId } = await this.getContextAsync(BuildView, {
nonInteractive: true,
});
if (flags.json) { if (flags.json) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
// this command is always non-interactive
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const displayName = await getDisplayNameForProjectIdAsync(projectId); const displayName = await getDisplayNameForProjectIdAsync(projectId);
const spinner = ora().start('Fetching the build…'); const spinner = ora().start('Fetching the build…');
@@ -2,7 +2,7 @@ import chalk from 'chalk';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { BranchNotFoundError } from '../../branch/utils'; import { BranchNotFoundError } from '../../branch/utils';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { import {
@@ -11,12 +11,7 @@ import {
} from '../../graphql/generated'; } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery'; import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import formatFields from '../../utils/formatFields'; import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -75,19 +70,22 @@ export default class ChannelCreate extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
args: { name: channelName }, args: { name: channelName },
flags: { json: jsonFlag, 'non-interactive': nonInteractive }, flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelCreate); } = await this.parse(ChannelCreate);
const { projectId } = await this.getContextAsync(ChannelCreate, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!channelName) { if (!channelName) {
const validationMessage = 'Channel name may not be empty.'; const validationMessage = 'Channel name may not be empty.';
if (nonInteractive) { if (nonInteractive) {
@@ -1,7 +1,7 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { selectChannelOnAppAsync } from '../../channel/queries'; import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { import {
@@ -11,8 +11,6 @@ import {
} from '../../graphql/generated'; } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery'; import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { toggleConfirmAsync } from '../../prompts'; import { toggleConfirmAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
@@ -31,19 +29,22 @@ export default class ChannelDelete extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
args: { name: nameArg }, args: { name: nameArg },
flags: { json: jsonFlag, 'non-interactive': nonInteractive }, flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(ChannelDelete); } = await this.parse(ChannelDelete);
const { projectId } = await this.getContextAsync(ChannelDelete, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
let channelId, channelName; let channelId, channelName;
if (nameArg) { if (nameArg) {
const { id, name } = await ChannelQuery.viewUpdateChannelAsync({ const { id, name } = await ChannelQuery.viewUpdateChannelAsync({
@@ -4,7 +4,7 @@ import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries'; import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries'; import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { import {
@@ -14,8 +14,6 @@ import {
import { BranchQuery } from '../../graphql/queries/BranchQuery'; import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery'; import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function updateChannelBranchMappingAsync({ export async function updateChannelBranchMappingAsync({
@@ -67,19 +65,22 @@ export default class ChannelEdit extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
args, args,
flags: { branch: branchFlag, json, 'non-interactive': nonInteractive }, flags: { branch: branchFlag, json, 'non-interactive': nonInteractive },
} = await this.parse(ChannelEdit); } = await this.parse(ChannelEdit);
const { projectId } = await this.getContextAsync(ChannelEdit, {
nonInteractive,
});
if (json) { if (json) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const existingChannel = args.name const existingChannel = args.name
? await ChannelQuery.viewUpdateChannelAsync({ appId: projectId, channelName: args.name }) ? await ChannelQuery.viewUpdateChannelAsync({ appId: projectId, channelName: args.name })
: await selectChannelOnAppAsync({ : await selectChannelOnAppAsync({
@@ -1,13 +1,11 @@
import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries'; import { CHANNELS_LIMIT, listAndRenderChannelsOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { import {
EasPaginatedQueryFlags, EasPaginatedQueryFlags,
getLimitFlagWithCustomValues, getLimitFlagWithCustomValues,
getPaginatedQueryOptions, getPaginatedQueryOptions,
} from '../../commandUtils/pagination'; } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput } from '../../utils/json'; import { enableJsonOutput } from '../../utils/json';
export default class ChannelList extends EasCommand { export default class ChannelList extends EasCommand {
@@ -19,18 +17,21 @@ export default class ChannelList extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(ChannelList); const { flags } = await this.parse(ChannelList);
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags; const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const { projectId } = await this.getContextAsync(ChannelList, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
await listAndRenderChannelsOnAppAsync({ await listAndRenderChannelsOnAppAsync({
projectId, projectId,
paginatedQueryOptions, paginatedQueryOptions,
@@ -2,18 +2,13 @@ import { Flags } from '@oclif/core';
import chalk from 'chalk'; import chalk from 'chalk';
import { BranchMapping, getBranchMapping } from '../../channel/utils'; import { BranchMapping, getBranchMapping } from '../../channel/utils';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { UpdateBranch } from '../../graphql/generated'; import { UpdateBranch } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery'; import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelByNameObject } from '../../graphql/queries/ChannelQuery'; import { ChannelQuery, UpdateChannelByNameObject } from '../../graphql/queries/ChannelQuery';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts'; import { promptAsync, selectAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { updateChannelBranchMappingAsync } from './edit'; import { updateChannelBranchMappingAsync } from './edit';
@@ -316,6 +311,10 @@ export default class ChannelRollout extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
args: { channel: channelName }, args: { channel: channelName },
@@ -327,13 +326,13 @@ export default class ChannelRollout extends EasCommand {
'non-interactive': nonInteractive, 'non-interactive': nonInteractive,
}, },
} = await this.parse(ChannelRollout); } = await this.parse(ChannelRollout);
const { projectId } = await this.getContextAsync(ChannelRollout, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId); const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
const channel = await ChannelQuery.viewUpdateChannelAsync({ const channel = await ChannelQuery.viewUpdateChannelAsync({
@@ -4,11 +4,9 @@ import {
listAndRenderBranchesAndUpdatesOnChannelAsync, listAndRenderBranchesAndUpdatesOnChannelAsync,
selectChannelOnAppAsync, selectChannelOnAppAsync,
} from '../../channel/queries'; } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination'; import { EasPaginatedQueryFlags, getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { enableJsonOutput } from '../../utils/json'; import { enableJsonOutput } from '../../utils/json';
export default class ChannelView extends EasCommand { export default class ChannelView extends EasCommand {
@@ -27,6 +25,10 @@ export default class ChannelView extends EasCommand {
...EasPaginatedQueryFlags, ...EasPaginatedQueryFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
args: { name: channelName }, args: { name: channelName },
@@ -34,14 +36,13 @@ export default class ChannelView extends EasCommand {
} = await this.parse(ChannelView); } = await this.parse(ChannelView);
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
const { json: jsonFlag, 'non-interactive': nonInteractive } = flags; const { json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const { projectId } = await this.getContextAsync(ChannelView, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!channelName) { if (!channelName) {
const validationMessage = 'A channel name is required to view a specific channel.'; const validationMessage = 'A channel name is required to view a specific channel.';
if (nonInteractive) { if (nonInteractive) {
+4 -2
View File
@@ -4,7 +4,7 @@ import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import chalk from 'chalk'; import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand'; import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
import { toAppPlatform } from '../graphql/types/AppPlatform'; import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log'; import Log from '../log';
import { appPlatformEmojis } from '../platform'; import { appPlatformEmojis } from '../platform';
@@ -25,7 +25,9 @@ export default class Config extends EasCommand {
}), }),
}; };
protected override requiresAuthentication = false; protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(Config); const { flags } = await this.parse(Config);
+17 -2
View File
@@ -1,7 +1,12 @@
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import EasCommand from '../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../commandUtils/EasCommand';
import { SelectPlatform } from '../credentials/manager/SelectPlatform'; import { SelectPlatform } from '../credentials/manager/SelectPlatform';
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync } from '../project/projectUtils';
export default class Credentials extends EasCommand { export default class Credentials extends EasCommand {
static override description = 'manage credentials'; static override description = 'manage credentials';
@@ -10,8 +15,18 @@ export default class Credentials extends EasCommand {
platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }), platform: Flags.enum({ char: 'p', options: ['android', 'ios'] }),
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(Credentials); const { flags } = await this.parse(Credentials);
await new SelectPlatform(flags.platform).runAsync(); const { actor, projectId } = await this.getContextAsync(Credentials, { nonInteractive: false });
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
await new SelectPlatform(actor, { projectId, exp }, flags.platform).runAsync();
} }
} }
+13 -4
View File
@@ -1,17 +1,26 @@
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import AppStoreApi from '../../credentials/ios/appstore/AppStoreApi'; import AppStoreApi from '../../credentials/ios/appstore/AppStoreApi';
import { createContextAsync } from '../../devices/context'; import { createContextAsync } from '../../devices/context';
import DeviceManager from '../../devices/manager'; import DeviceManager from '../../devices/manager';
import { ensureLoggedInAsync } from '../../user/actions';
export default class DeviceCreate extends EasCommand { export default class DeviceCreate extends EasCommand {
static override description = 'register new Apple Devices to use for internal distribution'; static override description = 'register new Apple Devices to use for internal distribution';
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
// this command is interactive by design // this command is interactive by design
const user = await ensureLoggedInAsync({ nonInteractive: false }); const { actor, projectId } = await this.getContextAsync(DeviceCreate, {
nonInteractive: false,
});
const ctx = await createContextAsync({ appStore: new AppStoreApi(), user }); const ctx = await createContextAsync({ appStore: new AppStoreApi(), user: actor, projectId });
const manager = new DeviceManager(ctx); const manager = new DeviceManager(ctx);
await manager.createAsync(); await manager.createAsync();
} }
+9 -11
View File
@@ -2,7 +2,7 @@ import { Device, DeviceStatus } from '@expo/apple-utils';
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import assert from 'assert'; import assert from 'assert';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils'; import { chooseDevicesToDeleteAsync } from '../../credentials/ios/actions/DeviceUtils';
import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation'; import { AppleDeviceMutation } from '../../credentials/ios/api/graphql/mutations/AppleDeviceMutation';
import { import {
@@ -16,12 +16,7 @@ import formatDevice from '../../devices/utils/formatDevice';
import { AppleDevice, Maybe } from '../../graphql/generated'; import { AppleDevice, Maybe } from '../../graphql/generated';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts'; import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class DeviceDelete extends EasCommand { export default class DeviceDelete extends EasCommand {
@@ -32,15 +27,18 @@ export default class DeviceDelete extends EasCommand {
udid: Flags.string({ multiple: true }), udid: Flags.string({ multiple: true }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
flags: { 'apple-team-id': appleTeamIdentifier, udid: udids }, flags: { 'apple-team-id': appleTeamIdentifier, udid: udids },
} = await this.parse(DeviceDelete); } = await this.parse(DeviceDelete);
const { projectId } = await this.getContextAsync(DeviceDelete, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
// this command is interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
if (!appleTeamIdentifier) { if (!appleTeamIdentifier) {
+9 -12
View File
@@ -2,18 +2,13 @@ import { Flags } from '@oclif/core';
import assert from 'assert'; import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery'; import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery'; import { AppleTeamQuery } from '../../credentials/ios/api/graphql/queries/AppleTeamQuery';
import formatDevice from '../../devices/utils/formatDevice'; import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log'; import Log from '../../log';
import { Ora, ora } from '../../ora'; import { Ora, ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
export default class BuildList extends EasCommand { export default class BuildList extends EasCommand {
@@ -23,14 +18,16 @@ export default class BuildList extends EasCommand {
'apple-team-id': Flags.string(), 'apple-team-id': Flags.string(),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id']; let appleTeamIdentifier = (await this.parse(BuildList)).flags['apple-team-id'];
const { projectId } = await this.getContextAsync(BuildList, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
// this command is interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
let spinner: Ora; let spinner: Ora;
+9 -13
View File
@@ -1,20 +1,19 @@
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery'; import { AppleDeviceQuery } from '../../credentials/ios/api/graphql/queries/AppleDeviceQuery';
import formatDevice from '../../devices/utils/formatDevice'; import formatDevice from '../../devices/utils/formatDevice';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
export default class DeviceView extends EasCommand { export default class DeviceView extends EasCommand {
static override description = 'view a device for your project'; static override description = 'view a device for your project';
static override args = [{ name: 'UDID' }]; static override args = [{ name: 'UDID' }];
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { UDID } = (await this.parse(DeviceView)).args; const { UDID } = (await this.parse(DeviceView)).args;
@@ -31,12 +30,9 @@ If you are not sure what is the UDID of the device you are looking for, run:
); );
throw new Error('Device UDID is missing'); throw new Error('Device UDID is missing');
} }
const { projectId } = await this.getContextAsync(DeviceView, {
const projectDir = await findProjectRootAsync(); nonInteractive: true,
const exp = getExpoConfig(projectDir); });
// this command is non-interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
const spinner = ora().start(`Fetching device details for ${UDID}…`); const spinner = ora().start(`Fetching device details for ${UDID}…`);
+4 -2
View File
@@ -1,7 +1,7 @@
import { Platform } from '@expo/eas-build-job'; import { Platform } from '@expo/eas-build-job';
import envinfo from 'envinfo'; import envinfo from 'envinfo';
import EasCommand from '../commandUtils/EasCommand'; import EasCommand, { CommandConfiguration } from '../commandUtils/EasCommand';
import Log from '../log'; import Log from '../log';
import { findProjectRootAsync } from '../project/projectUtils'; import { findProjectRootAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow'; import { resolveWorkflowAsync } from '../project/workflow';
@@ -10,7 +10,9 @@ import { easCliVersion } from '../utils/easCli';
export default class Diagnostics extends EasCommand { export default class Diagnostics extends EasCommand {
static override description = 'display environment info'; static override description = 'display environment info';
protected override requiresAuthentication = false; protected override commandConfiguration: CommandConfiguration = {
allowUnauthenticated: true,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const info = await envinfo.run( const info = await envinfo.run(
+16 -6
View File
@@ -4,14 +4,16 @@ import chalk from 'chalk';
import path from 'path'; import path from 'path';
import { ensureProjectConfiguredAsync } from '../../build/configure'; import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context'; import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context'; import { createMetadataContextAsync } from '../../metadata/context';
import { downloadMetadataAsync } from '../../metadata/download'; import { downloadMetadataAsync } from '../../metadata/download';
import { handleMetadataError } from '../../metadata/errors'; import { handleMetadataError } from '../../metadata/errors';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils'; import { findProjectRootAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
export default class MetadataPull extends EasCommand { export default class MetadataPull extends EasCommand {
static override description = 'generate the local store configuration from the app stores'; static override description = 'generate the local store configuration from the app stores';
@@ -24,21 +26,29 @@ export default class MetadataPull extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
...EASCommandLoggedInContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.'); Log.warn('EAS Metadata is in beta and subject to breaking changes.');
const { flags } = await this.parse(MetadataPull); const { flags } = await this.parse(MetadataPull);
const { actor, projectId } = await this.getContextAsync(MetadataPull, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true }); const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
// this command is interactive (all nonInteractive flags passed to utility functions are false) // this command is interactive (all nonInteractive flags passed to utility functions are false)
await getProjectIdAsync(exp, { nonInteractive: false });
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false }); await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const credentialsCtx = new CredentialsContext({ const credentialsCtx = new CredentialsContext({
exp, projectInfo: { exp, projectId },
projectDir, projectDir,
user: await ensureLoggedInAsync({ nonInteractive: false }), user: actor,
nonInteractive: false, nonInteractive: false,
}); });
+16 -6
View File
@@ -2,14 +2,16 @@ import { getConfig } from '@expo/config';
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import { ensureProjectConfiguredAsync } from '../../build/configure'; import { ensureProjectConfiguredAsync } from '../../build/configure';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../../commandUtils/EasCommand';
import { CredentialsContext } from '../../credentials/context'; import { CredentialsContext } from '../../credentials/context';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { createMetadataContextAsync } from '../../metadata/context'; import { createMetadataContextAsync } from '../../metadata/context';
import { handleMetadataError } from '../../metadata/errors'; import { handleMetadataError } from '../../metadata/errors';
import { uploadMetadataAsync } from '../../metadata/upload'; import { uploadMetadataAsync } from '../../metadata/upload';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils'; import { findProjectRootAsync } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
export default class MetadataPush extends EasCommand { export default class MetadataPush extends EasCommand {
static override description = 'sync the local store configuration to the app stores'; static override description = 'sync the local store configuration to the app stores';
@@ -22,21 +24,29 @@ export default class MetadataPush extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
...EASCommandLoggedInContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
Log.warn('EAS Metadata is in beta and subject to breaking changes.'); Log.warn('EAS Metadata is in beta and subject to breaking changes.');
const { flags } = await this.parse(MetadataPush); const { flags } = await this.parse(MetadataPush);
const { actor, projectId } = await this.getContextAsync(MetadataPush, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true }); const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
// this command is interactive (all nonInteractive flags passed to utility functions are false) // this command is interactive (all nonInteractive flags passed to utility functions are false)
await getProjectIdAsync(exp, { nonInteractive: false });
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false }); await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const credentialsCtx = new CredentialsContext({ const credentialsCtx = new CredentialsContext({
exp, projectInfo: { exp, projectId },
projectDir, projectDir,
user: await ensureLoggedInAsync({ nonInteractive: false }), user: actor,
nonInteractive: false, nonInteractive: false,
}); });
+10 -9
View File
@@ -1,25 +1,26 @@
import openBrowserAsync from 'better-opn'; import openBrowserAsync from 'better-opn';
import { getProjectDashboardUrl } from '../build/utils/url'; import { getProjectDashboardUrl } from '../build/utils/url';
import EasCommand from '../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../commandUtils/EasCommand';
import { ora } from '../ora'; import { ora } from '../ora';
import { getExpoConfig } from '../project/expoConfig'; import { getExpoConfig } from '../project/expoConfig';
import { import { findProjectRootAsync, getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
findProjectRootAsync,
getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../project/projectUtils';
export default class Open extends EasCommand { export default class Open extends EasCommand {
static override description = 'open the project page in a web browser'; static override description = 'open the project page in a web browser';
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
// this command is interactive by nature (only really run by humans in a terminal)
const { projectId } = await this.getContextAsync(Open, {
nonInteractive: false,
});
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir); const exp = getExpoConfig(projectDir);
// this command is interactive by nature (only really run by humans in a terminal)
// this ensures the project exists before opening the browser
const projectId = await getProjectIdAsync(exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
const projectName = exp.slug; const projectName = exp.slug;
@@ -1,11 +1,9 @@
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client'; import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated'; import { AppInfoQuery, AppInfoQueryVariables } from '../../graphql/generated';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import formatFields from '../../utils/formatFields'; import formatFields from '../../utils/formatFields';
async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> { async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
@@ -34,12 +32,14 @@ async function projectInfoByIdAsync(appId: string): Promise<AppInfoQuery> {
export default class ProjectInfo extends EasCommand { export default class ProjectInfo extends EasCommand {
static override description = 'information about the current project'; static override description = 'information about the current project';
async runAsync(): Promise<void> { static override contextDefinition = {
const projectDir = await findProjectRootAsync(); ...EASCommandProjectIdContext,
const exp = getExpoConfig(projectDir); };
// this command is non-interactive by design async runAsync(): Promise<void> {
const projectId = await getProjectIdAsync(exp, { nonInteractive: true }); const { projectId } = await this.getContextAsync(ProjectInfo, {
nonInteractive: true,
});
const { app } = await projectInfoByIdAsync(projectId); const { app } = await projectInfoByIdAsync(projectId);
if (!app) { if (!app) {
@@ -4,7 +4,7 @@ import nullthrows from 'nullthrows';
import terminalLink from 'terminal-link'; import terminalLink from 'terminal-link';
import { getProjectDashboardUrl } from '../../build/utils/url'; import { getProjectDashboardUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandLoggedInContext } from '../../commandUtils/EasCommand';
import { AppPrivacy, Role } from '../../graphql/generated'; import { AppPrivacy, Role } from '../../graphql/generated';
import { AppMutation } from '../../graphql/mutations/AppMutation'; import { AppMutation } from '../../graphql/mutations/AppMutation';
import Log from '../../log'; import Log from '../../log';
@@ -17,7 +17,7 @@ import {
toAppPrivacy, toAppPrivacy,
} from '../../project/projectUtils'; } from '../../project/projectUtils';
import { confirmAsync, promptAsync } from '../../prompts'; import { confirmAsync, promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions'; import { Actor } from '../../user/User';
type InitializeMethodOptions = { type InitializeMethodOptions = {
force: boolean; force: boolean;
@@ -43,6 +43,10 @@ export default class ProjectInit extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
};
private static async saveProjectIdAndLogSuccessAsync( private static async saveProjectIdAndLogSuccessAsync(
projectDir: string, projectDir: string,
projectId: string projectId: string
@@ -97,7 +101,7 @@ export default class ProjectInit extends EasCommand {
} }
} }
private static async initializeWithInteractiveSelectionAsync(): Promise<void> { private static async initializeWithInteractiveSelectionAsync(actor: Actor): Promise<void> {
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir); const exp = getExpoConfig(projectDir);
const existingProjectId = exp.extra?.eas?.projectId; const existingProjectId = exp.extra?.eas?.projectId;
@@ -111,7 +115,6 @@ export default class ProjectInit extends EasCommand {
return; return;
} }
const actor = await ensureLoggedInAsync({ nonInteractive: false });
const allAccounts = actor.accounts; const allAccounts = actor.accounts;
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set( const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(
allAccounts allAccounts
@@ -215,11 +218,12 @@ export default class ProjectInit extends EasCommand {
const { const {
flags: { id, force, 'non-interactive': nonInteractive }, flags: { id, force, 'non-interactive': nonInteractive },
} = await this.parse(ProjectInit); } = await this.parse(ProjectInit);
const { actor } = await this.getContextAsync(ProjectInit, { nonInteractive });
if (id) { if (id) {
await ProjectInit.initializeWithExplicitIDAsync(id, { force, nonInteractive }); await ProjectInit.initializeWithExplicitIDAsync(id, { force, nonInteractive });
} else { } else {
await ProjectInit.initializeWithInteractiveSelectionAsync(); await ProjectInit.initializeWithInteractiveSelectionAsync(actor);
} }
} }
} }
@@ -4,7 +4,7 @@ import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags'; import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation'; import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import { import {
@@ -16,12 +16,9 @@ import {
SecretTypeToEnvironmentSecretType, SecretTypeToEnvironmentSecretType,
} from '../../graphql/types/EnvironmentSecret'; } from '../../graphql/types/EnvironmentSecret';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync, getDisplayNameForProjectIdAsync,
getOwnerAccountForProjectIdAsync, getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils'; } from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts'; import { promptAsync, selectAsync } from '../../prompts';
@@ -52,6 +49,10 @@ export default class EnvironmentSecretCreate extends EasCommand {
...EASNonInteractiveFlag, ...EASNonInteractiveFlag,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
flags: { flags: {
@@ -63,10 +64,10 @@ export default class EnvironmentSecretCreate extends EasCommand {
'non-interactive': nonInteractive, 'non-interactive': nonInteractive,
}, },
} = await this.parse(EnvironmentSecretCreate); } = await this.parse(EnvironmentSecretCreate);
const { projectId } = await this.getContextAsync(EnvironmentSecretCreate, {
nonInteractive,
});
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId); const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
const ownerAccount = await getOwnerAccountForProjectIdAsync(projectId); const ownerAccount = await getOwnerAccountForProjectIdAsync(projectId);
@@ -1,6 +1,6 @@
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags'; import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation'; import { EnvironmentSecretMutation } from '../../graphql/mutations/EnvironmentSecretMutation';
import { import {
@@ -9,8 +9,6 @@ import {
EnvironmentSecretsQuery, EnvironmentSecretsQuery,
} from '../../graphql/queries/EnvironmentSecretsQuery'; } from '../../graphql/queries/EnvironmentSecretsQuery';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts'; import { promptAsync, toggleConfirmAsync } from '../../prompts';
export default class EnvironmentSecretDelete extends EasCommand { export default class EnvironmentSecretDelete extends EasCommand {
@@ -23,16 +21,19 @@ export default class EnvironmentSecretDelete extends EasCommand {
...EASNonInteractiveFlag, ...EASNonInteractiveFlag,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
flags: { id, 'non-interactive': nonInteractive }, flags: { id, 'non-interactive': nonInteractive },
} = await this.parse(EnvironmentSecretDelete); } = await this.parse(EnvironmentSecretDelete);
const { projectId } = await this.getContextAsync(EnvironmentSecretDelete, {
nonInteractive,
});
let secret: EnvironmentSecretWithScope | undefined; let secret: EnvironmentSecretWithScope | undefined;
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (!id) { if (!id) {
const validationMessage = 'You must select which secret to delete.'; const validationMessage = 'You must select which secret to delete.';
if (nonInteractive) { if (nonInteractive) {
+8 -11
View File
@@ -2,26 +2,23 @@ import chalk from 'chalk';
import Table from 'cli-table3'; import Table from 'cli-table3';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery'; import { EnvironmentSecretsQuery } from '../../graphql/queries/EnvironmentSecretsQuery';
import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret'; import { EnvironmentSecretTypeToSecretType } from '../../graphql/types/EnvironmentSecret';
import Log from '../../log'; import Log from '../../log';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
export default class EnvironmentSecretList extends EasCommand { export default class EnvironmentSecretList extends EasCommand {
static override description = 'list environment secrets available for your current app'; static override description = 'list environment secrets available for your current app';
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const projectDir = await findProjectRootAsync(); const { projectId } = await this.getContextAsync(EnvironmentSecretList, {
if (!projectDir) { nonInteractive: true,
throw new Error("Run this command inside your project's directory"); });
}
const exp = getExpoConfig(projectDir);
// this command is non-interacive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const secrets = await EnvironmentSecretsQuery.allAsync(projectId); const secrets = await EnvironmentSecretsQuery.allAsync(projectId);
const table = new Table({ const table = new Table({
+12 -5
View File
@@ -2,7 +2,10 @@ import { EasJsonAccessor } from '@expo/eas-json';
import { Errors, Flags } from '@oclif/core'; import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk'; import chalk from 'chalk';
import EasCommand from '../commandUtils/EasCommand'; import EasCommand, {
EASCommandLoggedInContext,
EASCommandProjectIdContext,
} from '../commandUtils/EasCommand';
import { StatuspageServiceName, SubmissionFragment } from '../graphql/generated'; import { StatuspageServiceName, SubmissionFragment } from '../graphql/generated';
import { toAppPlatform } from '../graphql/types/AppPlatform'; import { toAppPlatform } from '../graphql/types/AppPlatform';
import Log from '../log'; import Log from '../log';
@@ -13,8 +16,7 @@ import {
selectRequestedPlatformAsync, selectRequestedPlatformAsync,
toPlatforms, toPlatforms,
} from '../platform'; } from '../platform';
import { getExpoConfig } from '../project/expoConfig'; import { findProjectRootAsync } from '../project/projectUtils';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context'; import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish, exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
@@ -91,14 +93,18 @@ export default class Submit extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandLoggedInContext,
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(Submit); const { flags: rawFlags } = await this.parse(Submit);
const { actor, projectId } = await this.getContextAsync(Submit, { nonInteractive: false });
const flags = this.sanitizeFlags(rawFlags); const flags = this.sanitizeFlags(rawFlags);
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive: flags.nonInteractive });
await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasSubmit]); await maybeWarnAboutEasOutagesAsync([StatuspageServiceName.EasSubmit]);
@@ -121,6 +127,7 @@ export default class Submit extends EasCommand {
profile: submissionProfile.profile, profile: submissionProfile.profile,
archiveFlags: flagsWithPlatform.archiveFlags, archiveFlags: flagsWithPlatform.archiveFlags,
nonInteractive: flagsWithPlatform.nonInteractive, nonInteractive: flagsWithPlatform.nonInteractive,
actor,
}); });
if (submissionProfiles.length > 1) { if (submissionProfiles.length > 1) {
@@ -5,14 +5,13 @@ import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
import { getEASUpdateURL } from '../../api'; import { getEASUpdateURL } from '../../api';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { AppPlatform } from '../../graphql/generated'; import { AppPlatform } from '../../graphql/generated';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { RequestedPlatform, appPlatformDisplayNames } from '../../platform'; import { RequestedPlatform, appPlatformDisplayNames } from '../../platform';
import { getExpoConfig } from '../../project/expoConfig'; import { getExpoConfig } from '../../project/expoConfig';
import { import {
findProjectRootAsync, findProjectRootAsync,
getProjectIdAsync,
installExpoUpdatesAsync, installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable, isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils'; } from '../../project/projectUtils';
@@ -35,11 +34,18 @@ export default class UpdateConfigure extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
Log.log( Log.log(
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.' '💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
); );
const { flags } = await this.parse(UpdateConfigure); const { flags } = await this.parse(UpdateConfigure);
const { projectId } = await this.getContextAsync(UpdateConfigure, {
nonInteractive: true,
});
const platform = flags.platform as RequestedPlatform; const platform = flags.platform as RequestedPlatform;
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir); const exp = getExpoConfig(projectDir);
@@ -61,6 +67,7 @@ export default class UpdateConfigure extends EasCommand {
android: androidWorkflow, android: androidWorkflow,
ios: iosWorkflow, ios: iosWorkflow,
}, },
projectId,
}); });
Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`); Log.withTick(`Configured ${chalk.bold('app.json')} for EAS Update`);
@@ -69,14 +76,14 @@ export default class UpdateConfigure extends EasCommand {
[RequestedPlatform.Android, RequestedPlatform.All].includes(platform) && [RequestedPlatform.Android, RequestedPlatform.All].includes(platform) &&
androidWorkflow === Workflow.GENERIC androidWorkflow === Workflow.GENERIC
) { ) {
await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp, { nonInteractive: true }); await syncAndroidUpdatesConfigurationAsync(projectDir, updatedExp, projectId);
Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`); Log.withTick(`Configured ${chalk.bold('AndroidManifest.xml')} for EAS Update`);
} }
if ( if (
[RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) && [RequestedPlatform.Ios, RequestedPlatform.All].includes(platform) &&
iosWorkflow === Workflow.GENERIC iosWorkflow === Workflow.GENERIC
) { ) {
await syncIosUpdatesConfigurationAsync(projectDir, updatedExp, { nonInteractive: true }); await syncIosUpdatesConfigurationAsync(projectDir, updatedExp, projectId);
Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`); Log.withTick(`Configured ${chalk.bold('Expo.plist')} for EAS Update`);
} }
@@ -90,6 +97,7 @@ async function configureAppJSONForEASUpdateAsync({
exp, exp,
platform, platform,
workflows, workflows,
projectId,
}: { }: {
projectDir: string; projectDir: string;
exp: ExpoConfig; exp: ExpoConfig;
@@ -97,9 +105,9 @@ async function configureAppJSONForEASUpdateAsync({
workflows: { workflows: {
[key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow; [key in RequestedPlatform.Android | RequestedPlatform.Ios]: Workflow;
}; };
projectId: string;
}): Promise<ExpoConfig> { }): Promise<ExpoConfig> {
// this command is non-interactive in the way it was designed // this command is non-interactive in the way it was designed
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const easUpdateURL = getEASUpdateURL(projectId); const easUpdateURL = getEASUpdateURL(projectId);
const updates = { ...exp.updates, url: easUpdateURL }; const updates = { ...exp.updates, url: easUpdateURL };
@@ -10,7 +10,7 @@ import { getEASUpdateURL } from '../../api';
import { selectBranchOnAppAsync } from '../../branch/queries'; import { selectBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils'; import { BranchNotFoundError, getDefaultBranchNameAsync } from '../../branch/utils';
import { getUpdateGroupUrl } from '../../build/utils/url'; import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination'; import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch'; import fetch from '../../fetch';
@@ -31,7 +31,6 @@ import { getExpoConfig } from '../../project/expoConfig';
import { import {
findProjectRootAsync, findProjectRootAsync,
getOwnerAccountForProjectIdAsync, getOwnerAccountForProjectIdAsync,
getProjectIdAsync,
installExpoUpdatesAsync, installExpoUpdatesAsync,
isExpoUpdatesInstalledOrAvailable, isExpoUpdatesInstalledOrAvailable,
} from '../../project/projectUtils'; } from '../../project/projectUtils';
@@ -169,6 +168,10 @@ export default class UpdatePublish extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdatePublish); const { flags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
@@ -185,6 +188,9 @@ export default class UpdatePublish extends EasCommand {
'non-interactive': nonInteractive, 'non-interactive': nonInteractive,
json: jsonFlag, json: jsonFlag,
} = flags; } = flags;
const { projectId } = await this.getContextAsync(UpdatePublish, {
nonInteractive,
});
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
@@ -234,7 +240,6 @@ export default class UpdatePublish extends EasCommand {
} }
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir); const runtimeVersions = await getRuntimeVersionObjectAsync(exp, platformFlag, projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
await checkEASUpdateURLIsSetAsync(exp, projectId); await checkEASUpdateURLIsSetAsync(exp, projectId);
if (!branchName) { if (!branchName) {
+8 -7
View File
@@ -1,15 +1,13 @@
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import { selectBranchOnAppAsync } from '../../branch/queries'; import { selectBranchOnAppAsync } from '../../branch/queries';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { import {
EasPaginatedQueryFlags, EasPaginatedQueryFlags,
getLimitFlagWithCustomValues, getLimitFlagWithCustomValues,
getPaginatedQueryOptions, getPaginatedQueryOptions,
} from '../../commandUtils/pagination'; } from '../../commandUtils/pagination';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { import {
listAndRenderUpdateGroupsOnAppAsync, listAndRenderUpdateGroupsOnAppAsync,
listAndRenderUpdateGroupsOnBranchAsync, listAndRenderUpdateGroupsOnBranchAsync,
@@ -34,19 +32,22 @@ export default class UpdateList extends EasCommand {
...EasNonInteractiveAndJsonFlags, ...EasNonInteractiveAndJsonFlags,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdateList); const { flags } = await this.parse(UpdateList);
const { branch: branchFlag, all, json: jsonFlag, 'non-interactive': nonInteractive } = flags; const { branch: branchFlag, all, json: jsonFlag, 'non-interactive': nonInteractive } = flags;
const { projectId } = await this.getContextAsync(UpdateList, {
nonInteractive,
});
const paginatedQueryOptions = getPaginatedQueryOptions(flags); const paginatedQueryOptions = getPaginatedQueryOptions(flags);
if (jsonFlag) { if (jsonFlag) {
enableJsonOutput(); enableJsonOutput();
} }
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
if (all) { if (all) {
listAndRenderUpdateGroupsOnAppAsync({ projectId, paginatedQueryOptions }); listAndRenderUpdateGroupsOnAppAsync({ projectId, paginatedQueryOptions });
} else { } else {
@@ -1,12 +1,10 @@
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags'; import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookType } from '../../graphql/generated'; import { WebhookType } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation'; import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { prepareInputParamsAsync } from '../../webhooks/input'; import { prepareInputParamsAsync } from '../../webhooks/input';
export default class WebhookCreate extends EasCommand { export default class WebhookCreate extends EasCommand {
@@ -27,15 +25,17 @@ export default class WebhookCreate extends EasCommand {
...EASNonInteractiveFlag, ...EASNonInteractiveFlag,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { flags } = await this.parse(WebhookCreate); const { flags } = await this.parse(WebhookCreate);
const { projectId } = await this.getContextAsync(WebhookCreate, {
nonInteractive: flags['non-interactive'],
});
const webhookInputParams = await prepareInputParamsAsync(flags); const webhookInputParams = await prepareInputParamsAsync(flags);
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(exp, { nonInteractive: flags['non-interactive'] });
const spinner = ora('Creating a webhook').start(); const spinner = ora('Creating a webhook').start();
try { try {
await WebhookMutation.createWebhookAsync(projectId, webhookInputParams); await WebhookMutation.createWebhookAsync(projectId, webhookInputParams);
@@ -2,15 +2,13 @@ import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { EASNonInteractiveFlag } from '../../commandUtils/flags'; import { EASNonInteractiveFlag } from '../../commandUtils/flags';
import { WebhookFragment } from '../../graphql/generated'; import { WebhookFragment } from '../../graphql/generated';
import { WebhookMutation } from '../../graphql/mutations/WebhookMutation'; import { WebhookMutation } from '../../graphql/mutations/WebhookMutation';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery'; import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, toggleConfirmAsync } from '../../prompts'; import { promptAsync, toggleConfirmAsync } from '../../prompts';
import { formatWebhook } from '../../webhooks/formatWebhook'; import { formatWebhook } from '../../webhooks/formatWebhook';
@@ -29,15 +27,18 @@ export default class WebhookDelete extends EasCommand {
...EASNonInteractiveFlag, ...EASNonInteractiveFlag,
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
let { let {
args: { ID: webhookId }, args: { ID: webhookId },
flags: { 'non-interactive': nonInteractive }, flags: { 'non-interactive': nonInteractive },
} = await this.parse(WebhookDelete); } = await this.parse(WebhookDelete);
const { projectId } = await this.getContextAsync(WebhookDelete, {
const projectDir = await findProjectRootAsync(); nonInteractive,
const exp = getExpoConfig(projectDir); });
const projectId = await getProjectIdAsync(exp, { nonInteractive });
let webhook: WebhookFragment | undefined = let webhook: WebhookFragment | undefined =
webhookId && (await WebhookQuery.byIdAsync(webhookId)); webhookId && (await WebhookQuery.byIdAsync(webhookId));
+9 -12
View File
@@ -1,17 +1,12 @@
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import chalk from 'chalk'; import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand, { EASCommandProjectIdContext } from '../../commandUtils/EasCommand';
import { WebhookType } from '../../graphql/generated'; import { WebhookType } from '../../graphql/generated';
import { WebhookQuery } from '../../graphql/queries/WebhookQuery'; import { WebhookQuery } from '../../graphql/queries/WebhookQuery';
import Log from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig'; import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import {
findProjectRootAsync,
getDisplayNameForProjectIdAsync,
getProjectIdAsync,
} from '../../project/projectUtils';
import { formatWebhook } from '../../webhooks/formatWebhook'; import { formatWebhook } from '../../webhooks/formatWebhook';
export default class WebhookList extends EasCommand { export default class WebhookList extends EasCommand {
@@ -24,16 +19,18 @@ export default class WebhookList extends EasCommand {
}), }),
}; };
static override contextDefinition = {
...EASCommandProjectIdContext,
};
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
flags: { event }, flags: { event },
} = await this.parse(WebhookList); } = await this.parse(WebhookList);
const { projectId } = await this.getContextAsync(WebhookList, {
nonInteractive: true,
});
const projectDir = await findProjectRootAsync();
const exp = getExpoConfig(projectDir);
// this command is non-interactive by design
const projectId = await getProjectIdAsync(exp, { nonInteractive: true });
const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId); const projectDisplayName = await getDisplayNameForProjectIdAsync(projectId);
const spinner = ora(`Fetching the list of webhook on project ${projectDisplayName}`).start(); const spinner = ora(`Fetching the list of webhook on project ${projectDisplayName}`).start();
@@ -8,7 +8,6 @@ import { GradleBuildContext } from '../../../project/android/gradle';
import { import {
getOwnerAccountForProjectIdAsync, getOwnerAccountForProjectIdAsync,
getProjectConfigDescription, getProjectConfigDescription,
getProjectIdAsync,
} from '../../../project/projectUtils'; } from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts'; import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context'; import { CredentialsContext } from '../../context';
@@ -91,9 +90,8 @@ export async function getAppLookupParamsFromContextAsync(
ctx: CredentialsContext, ctx: CredentialsContext,
gradleContext?: GradleBuildContext gradleContext?: GradleBuildContext
): Promise<AppLookupParams> { ): Promise<AppLookupParams> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug; const projectName = ctx.exp.slug;
const projectId = await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive }); const projectId = ctx.projectId;
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
const androidApplicationIdentifier = await getApplicationIdAsync( const androidApplicationIdentifier = await getApplicationIdAsync(
@@ -1,6 +1,5 @@
import { AccountFragment, AndroidKeystoreFragment } from '../../../graphql/generated'; import { AccountFragment, AndroidKeystoreFragment } from '../../../graphql/generated';
import Log from '../../../log'; import Log from '../../../log';
import { getProjectIdAsync } from '../../../project/projectUtils';
import { CredentialsContext } from '../../context'; import { CredentialsContext } from '../../context';
import { askForUserProvidedAsync } from '../../utils/promptForCredentials'; import { askForUserProvidedAsync } from '../../utils/promptForCredentials';
import { KeystoreWithType, keystoreSchema } from '../credentials'; import { KeystoreWithType, keystoreSchema } from '../credentials';
@@ -15,7 +14,7 @@ export class CreateKeystore {
throw new Error(`New keystore cannot be created in non-interactive mode.`); throw new Error(`New keystore cannot be created in non-interactive mode.`);
} }
const projectId = await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive }); const projectId = ctx.projectId;
const keystore = await this.provideOrGenerateAsync(projectId); const keystore = await this.provideOrGenerateAsync(projectId);
const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore); const keystoreFragment = await ctx.android.createKeystoreAsync(this.account, keystore);
Log.succeed('Created keystore'); Log.succeed('Created keystore');
+14 -38
View File
@@ -4,7 +4,6 @@ import { EasJson } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import Log from '../log'; import Log from '../log';
import { getExpoConfig } from '../project/expoConfig';
import { confirmAsync } from '../prompts'; import { confirmAsync } from '../prompts';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
import * as AndroidGraphqlClient from './android/api/GraphqlClient'; import * as AndroidGraphqlClient from './android/api/GraphqlClient';
@@ -12,6 +11,11 @@ import * as IosGraphqlClient from './ios/api/GraphqlClient';
import AppStoreApi from './ios/appstore/AppStoreApi'; import AppStoreApi from './ios/appstore/AppStoreApi';
import { AuthenticationMode } from './ios/appstore/authenticateTypes'; import { AuthenticationMode } from './ios/appstore/authenticateTypes';
export type CredentialsContextProjectInfo = {
exp: ExpoConfig;
projectId: string;
};
export class CredentialsContext { export class CredentialsContext {
public readonly android = AndroidGraphqlClient; public readonly android = AndroidGraphqlClient;
public readonly appStore = new AppStoreApi(); public readonly appStore = new AppStoreApi();
@@ -22,58 +26,30 @@ export class CredentialsContext {
public readonly easJsonCliConfig?: EasJson['cli']; public readonly easJsonCliConfig?: EasJson['cli'];
private shouldAskAuthenticateAppStore: boolean = true; private shouldAskAuthenticateAppStore: boolean = true;
private resolvedExp?: ExpoConfig;
constructor( private projectInfo: CredentialsContextProjectInfo;
private options: {
exp?: ExpoConfig; constructor(options: {
projectInfo: CredentialsContextProjectInfo;
easJsonCliConfig?: EasJson['cli']; easJsonCliConfig?: EasJson['cli'];
nonInteractive: boolean; nonInteractive: boolean;
projectDir: string; projectDir: string;
user: Actor; user: Actor;
env?: Env; env?: Env;
} }) {
) {
this.easJsonCliConfig = options.easJsonCliConfig; this.easJsonCliConfig = options.easJsonCliConfig;
this.projectDir = options.projectDir; this.projectDir = options.projectDir;
this.user = options.user; this.user = options.user;
this.nonInteractive = options.nonInteractive ?? false; this.nonInteractive = options.nonInteractive ?? false;
this.projectInfo = options.projectInfo;
this.resolvedExp = options.exp;
if (!this.resolvedExp) {
this.resolvedExp =
CredentialsContext.getExpoConfigInProject(this.projectDir, { env: options.env }) ??
undefined;
}
}
static getExpoConfigInProject(
projectDir: string,
{ env }: { env?: Env } = {}
): ExpoConfig | null {
try {
return getExpoConfig(projectDir, { env });
} catch {
// ignore error, context might be created outside of expo project
return null;
}
}
get hasProjectContext(): boolean {
return !!this.resolvedExp;
} }
get exp(): ExpoConfig { get exp(): ExpoConfig {
this.ensureProjectContext(); return this.projectInfo!.exp;
return this.resolvedExp!;
} }
public ensureProjectContext(): void { get projectId(): string {
if (this.hasProjectContext) { return this.projectInfo!.projectId;
return;
}
// trigger getConfig error
getExpoConfig(this.options.projectDir);
} }
async bestEffortAppStoreAuthenticateAsync(): Promise<void> { async bestEffortAppStoreAuthenticateAsync(): Promise<void> {
@@ -7,7 +7,7 @@ import {
IosDistributionType as GraphQLIosDistributionType, IosDistributionType as GraphQLIosDistributionType,
IosAppBuildCredentialsFragment, IosAppBuildCredentialsFragment,
} from '../../../graphql/generated'; } from '../../../graphql/generated';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../../../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { CredentialsContext } from '../../context'; import { CredentialsContext } from '../../context';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams'; import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { App, Target } from '../types'; import { App, Target } from '../types';
@@ -82,9 +82,8 @@ export async function assignBuildCredentialsAsync(
} }
export async function getAppFromContextAsync(ctx: CredentialsContext): Promise<App> { export async function getAppFromContextAsync(ctx: CredentialsContext): Promise<App> {
ctx.ensureProjectContext();
const projectName = ctx.exp.slug; const projectName = ctx.exp.slug;
const projectId = await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive }); const projectId = ctx.projectId;
const account = await getOwnerAccountForProjectIdAsync(projectId); const account = await getOwnerAccountForProjectIdAsync(projectId);
return { return {
account, account,
@@ -1,8 +1,11 @@
import Log from '../../log'; import Log from '../../log';
import { pressAnyKeyToContinueAsync } from '../../prompts'; import { pressAnyKeyToContinueAsync } from '../../prompts';
import { CredentialsContext } from '../context'; import { Actor } from '../../user/User';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
export interface Action<T = void> { export interface Action<T = void> {
actor: Actor;
projectInfo: CredentialsContextProjectInfo;
runAsync(ctx: CredentialsContext): Promise<T>; runAsync(ctx: CredentialsContext): Promise<T>;
} }
@@ -1,4 +1,3 @@
import { CredentialsContext } from '../context';
import { ActionInfo, IosActionType, Scope } from './Actions'; import { ActionInfo, IosActionType, Scope } from './Actions';
export const highLevelActions: ActionInfo[] = [ export const highLevelActions: ActionInfo[] = [
@@ -47,7 +46,7 @@ export const credentialsJsonActions: ActionInfo[] = [
}, },
]; ];
export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] { export function getPushKeyActions(): ActionInfo[] {
return [ return [
{ {
value: IosActionType.SetUpPushKey, value: IosActionType.SetUpPushKey,
@@ -57,7 +56,7 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
{ {
value: IosActionType.CreatePushKey, value: IosActionType.CreatePushKey,
title: 'Add a new push key', title: 'Add a new push key',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account, scope: Scope.Project,
}, },
{ {
value: IosActionType.UseExistingPushKey, value: IosActionType.UseExistingPushKey,
@@ -77,7 +76,7 @@ export function getPushKeyActions(ctx: CredentialsContext): ActionInfo[] {
]; ];
} }
export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] { export function getAscApiKeyActions(): ActionInfo[] {
return [ return [
{ {
value: IosActionType.SetUpAscApiKeyForSubmissions, value: IosActionType.SetUpAscApiKeyForSubmissions,
@@ -92,7 +91,7 @@ export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
{ {
value: IosActionType.CreateAscApiKeyForSubmissions, value: IosActionType.CreateAscApiKeyForSubmissions,
title: 'Add a new API Key For EAS Submit', title: 'Add a new API Key For EAS Submit',
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account, scope: Scope.Project,
}, },
{ {
value: IosActionType.RemoveAscApiKey, value: IosActionType.RemoveAscApiKey,
@@ -107,7 +106,7 @@ export function getAscApiKeyActions(ctx: CredentialsContext): ActionInfo[] {
]; ];
} }
export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[] { export function getBuildCredentialsActions(): ActionInfo[] {
return [ return [
{ {
// This command will be triggered during build to ensure all credentials are ready // This command will be triggered during build to ensure all credentials are ready
@@ -124,7 +123,7 @@ export function getBuildCredentialsActions(ctx: CredentialsContext): ActionInfo[
{ {
value: IosActionType.CreateDistributionCertificate, value: IosActionType.CreateDistributionCertificate,
title: `Distribution Certificate: Add a new one to your account`, title: `Distribution Certificate: Add a new one to your account`,
scope: ctx.hasProjectContext ? Scope.Project : Scope.Account, scope: Scope.Project,
}, },
{ {
value: IosActionType.RemoveDistributionCertificate, value: IosActionType.RemoveDistributionCertificate,
@@ -4,9 +4,7 @@ import assert from 'assert';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle'; import { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { getProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions';
import { AssignFcm } from '../android/actions/AssignFcm'; import { AssignFcm } from '../android/actions/AssignFcm';
import { AssignGoogleServiceAccountKey } from '../android/actions/AssignGoogleServiceAccountKey'; import { AssignGoogleServiceAccountKey } from '../android/actions/AssignGoogleServiceAccountKey';
import { import {
@@ -50,27 +48,23 @@ export class ManageAndroid {
constructor(private callingAction: Action, private projectDir: string) {} constructor(private callingAction: Action, private projectDir: string) {}
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> { async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir); const hasProjectContext = !!this.callingAction.projectInfo;
const buildProfile = hasProjectContext const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync() ? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.ANDROID).runAsync()
: null; : null;
const ctx = new CredentialsContext({ const ctx = new CredentialsContext({
projectDir: process.cwd(), projectDir: process.cwd(),
// this command is interactive by design projectInfo: this.callingAction.projectInfo,
user: await ensureLoggedInAsync({ nonInteractive: false }), user: this.callingAction.actor,
env: buildProfile?.env, env: buildProfile?.env,
nonInteractive: false, nonInteractive: false,
}); });
let gradleContext;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in a project context'); assert(buildProfile, 'buildProfile must be defined in a project context');
gradleContext = await this.createProjectContextAsync(ctx, buildProfile); const gradleContext = await this.createProjectContextAsync(ctx, buildProfile);
}
while (true) { while (true) {
try { try {
if (ctx.hasProjectContext) {
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext); const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync( const appCredentials = await ctx.android.getAndroidAppCredentialsWithCommonFieldsAsync(
appLookupParams appLookupParams
@@ -82,14 +76,11 @@ export class ManageAndroid {
} }
// copy legacy credentials if user is new to EAS and has legacy credentials // copy legacy credentials if user is new to EAS and has legacy credentials
const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync( const canCopyLegacyCredentials = await canCopyLegacyCredentialsAsync(ctx, appLookupParams);
ctx,
appLookupParams
);
if (canCopyLegacyCredentials) { if (canCopyLegacyCredentials) {
await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams); await promptUserAndCopyLegacyCredentialsAsync(ctx, appLookupParams);
} }
}
const { action: chosenAction } = await promptAsync({ const { action: chosenAction } = await promptAsync({
type: 'select', type: 'select',
name: 'action', name: 'action',
@@ -140,11 +131,6 @@ export class ManageAndroid {
ctx: CredentialsContext, ctx: CredentialsContext,
buildProfile: BuildProfile<Platform.ANDROID> buildProfile: BuildProfile<Platform.ANDROID>
): Promise<GradleBuildContext | undefined> { ): Promise<GradleBuildContext | undefined> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
// ensure the project exists on the EAS server
await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive });
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile); return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
} }
@@ -153,10 +139,6 @@ export class ManageAndroid {
action: AndroidActionType, action: AndroidActionType,
gradleContext?: GradleBuildContext gradleContext?: GradleBuildContext
): Promise<void> { ): Promise<void> {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext); const appLookupParams = await getAppLookupParamsFromContextAsync(ctx, gradleContext);
if (action === AndroidActionType.CreateKeystore) { if (action === AndroidActionType.CreateKeystore) {
const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials( const selectBuildCredentialsResult = await new SelectAndroidBuildCredentials(
@@ -12,9 +12,8 @@ import {
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme'; import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { resolveTargetsAsync } from '../../project/ios/target'; import { resolveTargetsAsync } from '../../project/ios/target';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { confirmAsync, promptAsync, selectAsync } from '../../prompts'; import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
import { ensureActorHasPrimaryAccount, ensureLoggedInAsync } from '../../user/actions';
import { CredentialsContext } from '../context'; import { CredentialsContext } from '../context';
import { import {
AppStoreApiKeyPurpose, AppStoreApiKeyPurpose,
@@ -58,44 +57,33 @@ export class ManageIos {
constructor(private callingAction: Action, private projectDir: string) {} constructor(private callingAction: Action, private projectDir: string) {}
async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> { async runAsync(currentActions: ActionInfo[] = highLevelActions): Promise<void> {
const hasProjectContext = !!CredentialsContext.getExpoConfigInProject(this.projectDir); const buildProfile = this.callingAction.projectInfo
const buildProfile = hasProjectContext
? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync() ? await new SelectBuildProfileFromEasJson(this.projectDir, Platform.IOS).runAsync()
: null; : null;
const ctx = new CredentialsContext({ const ctx = new CredentialsContext({
projectDir: process.cwd(), projectDir: process.cwd(),
// this command is interactive by design projectInfo: this.callingAction.projectInfo,
user: await ensureLoggedInAsync({ nonInteractive: false }), user: this.callingAction.actor,
env: buildProfile?.env, env: buildProfile?.env,
nonInteractive: false, nonInteractive: false,
}); });
const buildCredentialsActions = getBuildCredentialsActions(ctx); const buildCredentialsActions = getBuildCredentialsActions();
const pushKeyActions = getPushKeyActions(ctx); const pushKeyActions = getPushKeyActions();
const ascApiKeyActions = getAscApiKeyActions(ctx); const ascApiKeyActions = getAscApiKeyActions();
await ctx.bestEffortAppStoreAuthenticateAsync(); await ctx.bestEffortAppStoreAuthenticateAsync();
const getAccountForProjectAsync = async (): Promise<AccountFragment> => { const account = await getOwnerAccountForProjectIdAsync(
const projectId = await getProjectIdAsync(ctx.exp, { nonInteractive: false }); this.callingAction.projectInfo.projectId
return await getOwnerAccountForProjectIdAsync(projectId); );
};
const account = ctx.hasProjectContext
? await getAccountForProjectAsync()
: ensureActorHasPrimaryAccount(ctx.user);
let app = null;
let targets = null;
if (ctx.hasProjectContext) {
assert(buildProfile, 'buildProfile must be defined in project context'); assert(buildProfile, 'buildProfile must be defined in project context');
const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile); const projectContext = await this.createProjectContextAsync(ctx, account, buildProfile);
app = projectContext.app; const app = projectContext.app;
targets = projectContext.targets; const targets = projectContext.targets;
}
while (true) { while (true) {
try { try {
if (ctx.hasProjectContext) {
assert(targets && app); assert(targets && app);
const iosAppCredentialsMap: IosAppCredentialsMap = {}; const iosAppCredentialsMap: IosAppCredentialsMap = {};
for (const target of targets) { for (const target of targets) {
@@ -104,7 +92,6 @@ export class ManageIos {
await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams); await ctx.ios.getIosAppCredentialsWithCommonFieldsAsync(appLookupParams);
} }
displayIosCredentials(app, iosAppCredentialsMap, targets); displayIosCredentials(app, iosAppCredentialsMap, targets);
}
const { action: chosenAction } = await promptAsync({ const { action: chosenAction } = await promptAsync({
type: 'select', type: 'select',
@@ -139,10 +126,6 @@ export class ManageIos {
return await this.callingAction.runAsync(ctx); return await this.callingAction.runAsync(ctx);
} }
} else if (actionInfo.scope === Scope.Project) { } else if (actionInfo.scope === Scope.Project) {
assert(
ctx.hasProjectContext,
'You must be in your project directory in order to perform this action'
);
await this.runProjectSpecificActionAsync( await this.runProjectSpecificActionAsync(
ctx, ctx,
nullthrows(app, 'app must be defined in project context'), nullthrows(app, 'app must be defined in project context'),
@@ -174,11 +157,6 @@ export class ManageIos {
app: App; app: App;
targets: Target[]; targets: Target[];
}> { }> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
// ensure the project exists on the EAS server
await getProjectIdAsync(ctx.exp, { nonInteractive: ctx.nonInteractive });
const app = { account, projectName: ctx.exp.slug }; const app = { account, projectName: ctx.exp.slug };
const xcodeBuildContext = await resolveXcodeBuildContextAsync( const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{ {
@@ -1,16 +1,22 @@
import { selectPlatformAsync } from '../../platform'; import { selectPlatformAsync } from '../../platform';
import { Actor } from '../../user/User';
import { CredentialsContextProjectInfo } from '../context';
import { ManageAndroid } from './ManageAndroid'; import { ManageAndroid } from './ManageAndroid';
import { ManageIos } from './ManageIos'; import { ManageIos } from './ManageIos';
export class SelectPlatform { export class SelectPlatform {
constructor(private readonly flagPlatform?: string) {} constructor(
public readonly actor: Actor,
public readonly projectInfo: CredentialsContextProjectInfo,
private readonly flagPlatform?: string
) {}
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const platform = await selectPlatformAsync(this.flagPlatform); const platform = await selectPlatformAsync(this.flagPlatform);
if (platform === 'ios') { if (platform === 'ios') {
return await new ManageIos(new SelectPlatform(platform), process.cwd()).runAsync(); return await new ManageIos(this, process.cwd()).runAsync();
} }
return await new ManageAndroid(new SelectPlatform(platform), process.cwd()).runAsync(); return await new ManageAndroid(this, process.cwd()).runAsync();
} }
} }
@@ -44,16 +44,6 @@ describe(AccountResolver, () => {
}; };
describe('when inside project dir', () => { describe('when inside project dir', () => {
const exp = {
name: 'wat',
slug: 'wat',
extra: {
eas: {
projectId: '1234',
},
},
};
beforeEach(() => { beforeEach(() => {
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: 'test-project-id', id: 'test-project-id',
@@ -72,7 +62,7 @@ describe(AccountResolver, () => {
value: true, value: true,
})); }));
const resolver = new AccountResolver(exp, user); const resolver = new AccountResolver('1234', user);
const account = await resolver.resolveAccountAsync(); const account = await resolver.resolveAccountAsync();
expect(account).toEqual({ expect(account).toEqual({
id: user.accounts[1].id, id: user.accounts[1].id,
@@ -89,19 +79,7 @@ describe(AccountResolver, () => {
account: user.accounts[0], account: user.accounts[0],
})); }));
const resolver = new AccountResolver(exp, user); const resolver = new AccountResolver('1234', user);
const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]);
});
});
describe('when outside project dir', () => {
it('asks the user to choose the account from his account list', async () => {
jest.mocked(prompts).mockImplementationOnce(async () => ({
account: user.accounts[0],
}));
const resolver = new AccountResolver(null, user);
const account = await resolver.resolveAccountAsync(); const account = await resolver.resolveAccountAsync();
expect(account).toEqual(user.accounts[0]); expect(account).toEqual(user.accounts[0]);
}); });
+4 -15
View File
@@ -1,35 +1,24 @@
import { ExpoConfig, getConfig } from '@expo/config';
import AppStoreApi from '../credentials/ios/appstore/AppStoreApi'; import AppStoreApi from '../credentials/ios/appstore/AppStoreApi';
import { findProjectRootAsync } from '../project/projectUtils';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
export interface DeviceManagerContext { export interface DeviceManagerContext {
appStore: AppStoreApi; appStore: AppStoreApi;
exp: ExpoConfig | null;
projectDir: string | null;
user: Actor; user: Actor;
projectId: string;
} }
export async function createContextAsync({ export async function createContextAsync({
appStore, appStore,
cwd,
user, user,
projectId,
}: { }: {
appStore: AppStoreApi; appStore: AppStoreApi;
cwd?: string;
user: Actor; user: Actor;
projectId: string;
}): Promise<DeviceManagerContext> { }): Promise<DeviceManagerContext> {
const projectDir = await findProjectRootAsync({ cwd });
let exp: ExpoConfig | null = null;
if (projectDir) {
const config = getConfig(projectDir, { skipSDKVersionRequirement: true });
exp = config.exp;
}
return { return {
appStore, appStore,
projectDir,
exp,
user, user,
projectId,
}; };
} }
+5 -11
View File
@@ -1,4 +1,3 @@
import { ExpoConfig } from '@expo/config';
import assert from 'assert'; import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
@@ -6,7 +5,7 @@ import { AppleTeamMutation } from '../credentials/ios/api/graphql/mutations/Appl
import { AppleTeamQuery } from '../credentials/ios/api/graphql/queries/AppleTeamQuery'; import { AppleTeamQuery } from '../credentials/ios/api/graphql/queries/AppleTeamQuery';
import { AccountFragment, AppleTeamFragment } from '../graphql/generated'; import { AccountFragment, AppleTeamFragment } from '../graphql/generated';
import Log from '../log'; import Log from '../log';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { Choice, confirmAsync, promptAsync } from '../prompts'; import { Choice, confirmAsync, promptAsync } from '../prompts';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
import DeviceCreateAction from './actions/create/action'; import DeviceCreateAction from './actions/create/action';
@@ -38,30 +37,25 @@ export default class DeviceManager {
} }
private async resolveAccountAsync(): Promise<AccountFragment> { private async resolveAccountAsync(): Promise<AccountFragment> {
const resolver = new AccountResolver(this.ctx.exp, this.ctx.user); const resolver = new AccountResolver(this.ctx.projectId, this.ctx.user);
return await resolver.resolveAccountAsync(); return await resolver.resolveAccountAsync();
} }
} }
export class AccountResolver { export class AccountResolver {
constructor(private exp: ExpoConfig | null, private user: Actor) {} constructor(private projectId: string | null, private user: Actor) {}
public async resolveAccountAsync(): Promise<AccountFragment> { public async resolveAccountAsync(): Promise<AccountFragment> {
if (this.exp) {
const account = await this.resolveProjectAccountAsync(); const account = await this.resolveProjectAccountAsync();
if (account) { if (account) {
return account; return account;
} }
}
return await this.promptForAccountAsync(); return await this.promptForAccountAsync();
} }
private async resolveProjectAccountAsync(): Promise<AccountFragment | undefined> { private async resolveProjectAccountAsync(): Promise<AccountFragment | undefined> {
assert(this.exp, 'expo config is not set'); assert(this.projectId, 'expo config is not set');
const account = await getOwnerAccountForProjectIdAsync(this.projectId);
// this command is interactive by design
const projectId = await getProjectIdAsync(this.exp, { nonInteractive: false });
const account = await getOwnerAccountForProjectIdAsync(projectId);
const useProjectAccount = await confirmAsync({ const useProjectAccount = await confirmAsync({
message: `You're inside the project directory. Would you like to use the ${chalk.underline( message: `You're inside the project directory. Would you like to use the ${chalk.underline(
+1 -2
View File
@@ -9,7 +9,6 @@ import { getRequestContext } from '../credentials/ios/appstore/authenticate';
import { getExpoConfig } from '../project/expoConfig'; import { getExpoConfig } from '../project/expoConfig';
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier'; import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
export type MetadataContext = { export type MetadataContext = {
/** Submission profile platform to use */ /** Submission profile platform to use */
@@ -55,7 +54,7 @@ export async function createMetadataContextAsync(params: {
); );
const exp = params.exp ?? getExpoConfig(params.projectDir); const exp = params.exp ?? getExpoConfig(params.projectDir);
const user = await ensureLoggedInAsync({ nonInteractive: params.credentialsCtx.nonInteractive }); const user = params.credentialsCtx.user;
const bundleIdentifier = const bundleIdentifier =
submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp)); submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp));
@@ -1,3 +1,4 @@
import { jester } from '../../credentials/__tests__/fixtures-constants';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync'; import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
describe(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync, () => { describe(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync, () => {
@@ -5,7 +6,8 @@ describe(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync, () => {
await expect( await expect(
fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync( fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
{ accountName: 'fake', projectName: 'fake' }, { accountName: 'fake', projectName: 'fake' },
{ nonInteractive: true } { nonInteractive: true },
jester
) )
).rejects.toThrow( ).rejects.toThrow(
`Must configure EAS project by running 'eas init' before this command can be run in non-interactive mode.` `Must configure EAS project by running 'eas init' before this command can be run in non-interactive mode.`
@@ -110,7 +110,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
'/app' '/app'
); );
await expect( await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any) ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
).rejects.toThrowError(/we can't update this file programmatically/); ).rejects.toThrowError(/we can't update this file programmatically/);
}); });
it('prompts for the Android package if using app.json', async () => { it('prompts for the Android package if using app.json', async () => {
@@ -126,7 +126,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
})); }));
await expect( await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any) ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
).resolves.toBe('com.expo.notdominik'); ).resolves.toBe('com.expo.notdominik');
expect(promptAsync).toHaveBeenCalled(); expect(promptAsync).toHaveBeenCalled();
}); });
@@ -143,7 +143,7 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
})); }));
await expect( await expect(
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any) ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
).resolves.toBe('com.expo.notdominik'); ).resolves.toBe('com.expo.notdominik');
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8')); const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
expect(appJson).toMatchObject({ expect(appJson).toMatchObject({
@@ -11,7 +11,7 @@ import env from '../../env';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils'; import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions'; import { Actor } from '../../user/User';
import { resolveWorkflowAsync } from '../workflow'; import { resolveWorkflowAsync } from '../workflow';
import { GradleBuildContext } from './gradle'; import { GradleBuildContext } from './gradle';
import * as gradleUtils from './gradleUtils'; import * as gradleUtils from './gradleUtils';
@@ -20,7 +20,8 @@ export const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applica
export async function ensureApplicationIdIsDefinedForManagedProjectAsync( export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig exp: ExpoConfig,
actor: Actor
): Promise<string> { ): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects'); assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
@@ -30,7 +31,7 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
moduleName: gradleUtils.DEFAULT_MODULE_NAME, moduleName: gradleUtils.DEFAULT_MODULE_NAME,
}); });
} catch { } catch {
return await configureApplicationIdAsync(projectDir, exp); return await configureApplicationIdAsync(projectDir, exp, actor);
} }
} }
@@ -109,7 +110,11 @@ export async function getApplicationIdAsync(
} }
} }
async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig): Promise<string> { async function configureApplicationIdAsync(
projectDir: string,
exp: ExpoConfig,
actor: Actor
): Promise<string> {
const paths = getConfigFilePaths(projectDir); const paths = getConfigFilePaths(projectDir);
// we can't automatically update app.config.js // we can't automatically update app.config.js
if (paths.dynamicConfigPath) { if (paths.dynamicConfigPath) {
@@ -127,7 +132,7 @@ async function configureApplicationIdAsync(projectDir: string, exp: ExpoConfig):
)}` )}`
); );
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(exp); const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(exp, actor);
const { packageName } = await promptAsync({ const { packageName } = await promptAsync({
name: 'packageName', name: 'packageName',
type: 'text', type: 'text',
@@ -168,14 +173,17 @@ export function warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(
} }
} }
async function getSuggestedApplicationIdAsync(exp: ExpoConfig): Promise<string | undefined> { async function getSuggestedApplicationIdAsync(
exp: ExpoConfig,
actor: Actor
): Promise<string | undefined> {
// Attempt to use the ios bundle id first since it's convenient to have them aligned. // Attempt to use the ios bundle id first since it's convenient to have them aligned.
const maybeBundleId = IOSConfig.BundleIdentifier.getBundleIdentifier(exp); const maybeBundleId = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
if (maybeBundleId && isApplicationIdValid(maybeBundleId)) { if (maybeBundleId && isApplicationIdValid(maybeBundleId)) {
return maybeBundleId; return maybeBundleId;
} else { } else {
// the only callsite is heavily interactive // the only callsite is heavily interactive
const username = getUsername(exp, await ensureLoggedInAsync({ nonInteractive: false })); const username = getUsername(exp, actor);
// It's common to use dashes in your node project name, strip them from the suggested package name. // It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join(''); const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
if (isApplicationIdValid(possibleId)) { if (isApplicationIdValid(possibleId)) {
@@ -2,6 +2,7 @@ import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import { Actor } from '../user/User';
import { import {
ensureApplicationIdIsDefinedForManagedProjectAsync, ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync, getApplicationIdAsync,
@@ -16,7 +17,8 @@ export async function getApplicationIdentifierAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig, exp: ExpoConfig,
buildProfile: BuildProfile, buildProfile: BuildProfile,
platform: Platform platform: Platform,
actor: Actor
): Promise<string> { ): Promise<string> {
if (platform === Platform.ANDROID) { if (platform === Platform.ANDROID) {
const profile = buildProfile as BuildProfile<Platform.ANDROID>; const profile = buildProfile as BuildProfile<Platform.ANDROID>;
@@ -24,7 +26,7 @@ export async function getApplicationIdentifierAsync(
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile); const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
if (workflow === Workflow.MANAGED) { if (workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp); await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
} }
return await getApplicationIdAsync(projectDir, exp, gradleContext); return await getApplicationIdAsync(projectDir, exp, gradleContext);
@@ -7,7 +7,7 @@ import { AppMutation } from '../graphql/mutations/AppMutation';
import { AppQuery } from '../graphql/queries/AppQuery'; import { AppQuery } from '../graphql/queries/AppQuery';
import { ora } from '../ora'; import { ora } from '../ora';
import { confirmAsync } from '../prompts'; import { confirmAsync } from '../prompts';
import { ensureLoggedInAsync } from '../user/actions'; import { Actor } from '../user/User';
/** /**
* 1. Looks for an existing project on EAS servers. If found, ask the user whether this is the * 1. Looks for an existing project on EAS servers. If found, ask the user whether this is the
@@ -23,7 +23,8 @@ export async function fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsyn
}, },
options: { options: {
nonInteractive: boolean; nonInteractive: boolean;
} },
actor: Actor
): Promise<string> { ): Promise<string> {
const { accountName, projectName } = projectInfo; const { accountName, projectName } = projectInfo;
const projectFullName = `@${accountName}/${projectName}`; const projectFullName = `@${accountName}/${projectName}`;
@@ -34,7 +35,6 @@ export async function fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsyn
); );
} }
const actor = await ensureLoggedInAsync({ nonInteractive: options.nonInteractive });
const allAccounts = actor.accounts; const allAccounts = actor.accounts;
const accountNamesWhereUserHasSufficientPublishPermissions = new Set( const accountNamesWhereUserHasSufficientPublishPermissions = new Set(
allAccounts allAccounts
@@ -93,7 +93,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
'/app' '/app'
); );
await expect( await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any) ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
).rejects.toThrowError(/we can't update this file programmatically/); ).rejects.toThrowError(/we can't update this file programmatically/);
}); });
it('prompts for the bundle identifier if using app.json', async () => { it('prompts for the bundle identifier if using app.json', async () => {
@@ -109,7 +109,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
})); }));
await expect( await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any) ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
).resolves.toBe('com.expo.notdominik'); ).resolves.toBe('com.expo.notdominik');
expect(promptAsync).toHaveBeenCalled(); expect(promptAsync).toHaveBeenCalled();
}); });
@@ -126,7 +126,7 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
})); }));
await expect( await expect(
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any) ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
).resolves.toBe('com.expo.notdominik'); ).resolves.toBe('com.expo.notdominik');
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8')); const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
expect(appJson).toMatchObject({ expect(appJson).toMatchObject({
@@ -8,7 +8,7 @@ import fs from 'fs-extra';
import { readAppJson } from '../../build/utils/appJson'; import { readAppJson } from '../../build/utils/appJson';
import Log, { learnMore } from '../../log'; import Log, { learnMore } from '../../log';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import { ensureLoggedInAsync } from '../../user/actions'; import { Actor } from '../../user/User';
import { getProjectConfigDescription, getUsername } from '../projectUtils'; import { getProjectConfigDescription, getUsername } from '../projectUtils';
import { resolveWorkflowAsync } from '../workflow'; import { resolveWorkflowAsync } from '../workflow';
@@ -16,7 +16,8 @@ export const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle i
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync( export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig exp: ExpoConfig,
actor: Actor
): Promise<string> { ): Promise<string> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects'); assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
@@ -24,7 +25,7 @@ export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
try { try {
return await getBundleIdentifierAsync(projectDir, exp); return await getBundleIdentifierAsync(projectDir, exp);
} catch { } catch {
return await configureBundleIdentifierAsync(projectDir, exp); return await configureBundleIdentifierAsync(projectDir, exp, actor);
} }
} }
@@ -101,7 +102,8 @@ export async function getBundleIdentifierAsync(
async function configureBundleIdentifierAsync( async function configureBundleIdentifierAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig exp: ExpoConfig,
actor: Actor
): Promise<string> { ): Promise<string> {
const paths = getConfigFilePaths(projectDir); const paths = getConfigFilePaths(projectDir);
// we can't automatically update app.config.js // we can't automatically update app.config.js
@@ -120,7 +122,7 @@ async function configureBundleIdentifierAsync(
)}` )}`
); );
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(exp); const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(exp, actor);
const { bundleIdentifier } = await promptAsync({ const { bundleIdentifier } = await promptAsync({
name: 'bundleIdentifier', name: 'bundleIdentifier',
type: 'text', type: 'text',
@@ -166,14 +168,17 @@ export function isWildcardBundleIdentifier(bundleIdentifier: string): boolean {
return wildcardRegex.test(bundleIdentifier); return wildcardRegex.test(bundleIdentifier);
} }
async function getSuggestedBundleIdentifierAsync(exp: ExpoConfig): Promise<string | undefined> { async function getSuggestedBundleIdentifierAsync(
exp: ExpoConfig,
actor: Actor
): Promise<string | undefined> {
// Attempt to use the android package name first since it's convenient to have them aligned. // Attempt to use the android package name first since it's convenient to have them aligned.
const maybeAndroidPackage = AndroidConfig.Package.getPackage(exp); const maybeAndroidPackage = AndroidConfig.Package.getPackage(exp);
if (maybeAndroidPackage && isBundleIdentifierValid(maybeAndroidPackage)) { if (maybeAndroidPackage && isBundleIdentifierValid(maybeAndroidPackage)) {
return maybeAndroidPackage; return maybeAndroidPackage;
} else { } else {
// the only callsite is heavily interactive // the only callsite is heavily interactive
const username = getUsername(exp, await ensureLoggedInAsync({ nonInteractive: false })); const username = getUsername(exp, actor);
// It's common to use dashes in your node project name, strip them from the suggested package name. // It's common to use dashes in your node project name, strip them from the suggested package name.
const possibleId = `com.${username}.${exp.slug}`.split('-').join(''); const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
if (isBundleIdentifierValid(possibleId)) { if (isBundleIdentifierValid(possibleId)) {
+4 -5
View File
@@ -146,18 +146,17 @@ export async function getProjectIdAsync(
} }
}; };
const actor = await ensureLoggedInAsync({ nonInteractive: options.nonInteractive });
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync( const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
{ {
accountName: getAccountNameForEASProjectSync( accountName: getAccountNameForEASProjectSync(exp, actor),
exp,
await ensureLoggedInAsync({ nonInteractive: options.nonInteractive })
),
projectName: exp.slug, projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy), privacy: toAppPrivacy(exp.privacy),
}, },
{ {
nonInteractive: options.nonInteractive, nonInteractive: options.nonInteractive,
} },
actor
); );
const spinner = ora(`Linking local project to EAS project ${projectId}`).start(); const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
@@ -15,7 +15,7 @@ import {
} from '../../../graphql/generated'; } from '../../../graphql/generated';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation'; import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils'; import { createTestProject } from '../../../project/__tests__/project-utils';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../../../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context'; import { createSubmissionContextAsync } from '../../context';
import { getRecentBuildsForSubmissionAsync } from '../../utils/builds'; import { getRecentBuildsForSubmissionAsync } from '../../utils/builds';
import AndroidSubmitCommand from '../AndroidSubmitCommand'; import AndroidSubmitCommand from '../AndroidSubmitCommand';
@@ -78,10 +78,6 @@ describe(AndroidSubmitCommand, () => {
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]); jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]);
}); });
afterEach(() => {
jest.mocked(getProjectIdAsync).mockClear();
});
describe('non-interactive mode', () => { describe('non-interactive mode', () => {
it("throws error if didn't provide serviceAccountKeyPath in the submit profile", async () => { it("throws error if didn't provide serviceAccountKeyPath in the submit profile", async () => {
const projectId = uuidv4(); const projectId = uuidv4();
@@ -99,6 +95,7 @@ describe(AndroidSubmitCommand, () => {
changesNotSentForReview: false, changesNotSentForReview: false,
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
const command = new AndroidSubmitCommand(ctx); const command = new AndroidSubmitCommand(ctx);
await expect(command.runAsync()).rejects.toThrowError(); await expect(command.runAsync()).rejects.toThrowError();
@@ -123,6 +120,7 @@ describe(AndroidSubmitCommand, () => {
changesNotSentForReview: false, changesNotSentForReview: false,
}, },
nonInteractive: false, nonInteractive: false,
actor: mockJester,
}); });
const command = new AndroidSubmitCommand(ctx); const command = new AndroidSubmitCommand(ctx);
await command.runAsync(); await command.runAsync();
@@ -159,6 +157,7 @@ describe(AndroidSubmitCommand, () => {
changesNotSentForReview: false, changesNotSentForReview: false,
}, },
nonInteractive: false, nonInteractive: false,
actor: mockJester,
}); });
const command = new AndroidSubmitCommand(ctx); const command = new AndroidSubmitCommand(ctx);
await command.runAsync(); await command.runAsync();
@@ -153,6 +153,7 @@ describe(getServiceAccountKeyResultAsync, () => {
changesNotSentForReview: false, changesNotSentForReview: false,
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
const source: ServiceAccountSource = { const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.path, sourceType: ServiceAccountSourceType.path,
@@ -188,6 +189,7 @@ describe(getServiceAccountKeyResultAsync, () => {
changesNotSentForReview: false, changesNotSentForReview: false,
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
const source: ServiceAccountSource = { const source: ServiceAccountSource = {
sourceType: ServiceAccountSourceType.prompt, sourceType: ServiceAccountSourceType.prompt,
@@ -219,6 +221,7 @@ describe(getServiceAccountKeyResultAsync, () => {
changesNotSentForReview: false, changesNotSentForReview: false,
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, { const serviceAccountResult = await getServiceAccountKeyResultAsync(ctx, {
sourceType: ServiceAccountSourceType.credentialsService, sourceType: ServiceAccountSourceType.credentialsService,
+9 -5
View File
@@ -9,7 +9,6 @@ import { CredentialsContext } from '../credentials/context';
import { getExpoConfig } from '../project/expoConfig'; import { getExpoConfig } from '../project/expoConfig';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
import { ensureLoggedInAsync } from '../user/actions';
export interface SubmissionContext<T extends Platform> { export interface SubmissionContext<T extends Platform> {
accountName: string; accountName: string;
@@ -44,17 +43,22 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
projectDir: string; projectDir: string;
projectId: string; projectId: string;
applicationIdentifier?: string; applicationIdentifier?: string;
actor: Actor;
}): Promise<SubmissionContext<T>> { }): Promise<SubmissionContext<T>> {
const { applicationIdentifier, projectDir, nonInteractive } = params; const { applicationIdentifier, projectDir, nonInteractive, actor } = params;
const exp = getExpoConfig(projectDir, { env: params.env }); const exp = getExpoConfig(projectDir, { env: params.env });
const { env, ...rest } = params; const { env, ...rest } = params;
const user = await ensureLoggedInAsync({ nonInteractive });
const projectName = exp.slug; const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(params.projectId); const account = await getOwnerAccountForProjectIdAsync(params.projectId);
const accountId = account.id; const accountId = account.id;
let credentialsCtx: CredentialsContext | undefined = params.credentialsCtx; let credentialsCtx: CredentialsContext | undefined = params.credentialsCtx;
if (!credentialsCtx) { if (!credentialsCtx) {
credentialsCtx = new CredentialsContext({ projectDir, user, exp, nonInteractive }); credentialsCtx = new CredentialsContext({
projectDir,
user: actor,
projectInfo: { exp, projectId: params.projectId },
nonInteractive,
});
} }
const trackingCtx = { const trackingCtx = {
@@ -72,7 +76,7 @@ export async function createSubmissionContextAsync<T extends Platform>(params: {
credentialsCtx, credentialsCtx,
exp, exp,
projectName, projectName,
user, user: actor,
trackingCtx, trackingCtx,
applicationIdentifierOverride: applicationIdentifier, applicationIdentifierOverride: applicationIdentifier,
}; };
@@ -50,6 +50,7 @@ async function getIosSubmissionContextAsync(): Promise<SubmissionContext<Platfor
language: 'en-US', language: 'en-US',
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
} }
@@ -59,6 +59,7 @@ describe(getFromCredentialsServiceAsync, () => {
language: 'en-US', language: 'en-US',
}, },
nonInteractive: false, nonInteractive: false,
actor: mockJester,
}); });
jest jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync') .spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
@@ -86,6 +87,7 @@ describe(getFromCredentialsServiceAsync, () => {
language: 'en-US', language: 'en-US',
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
jest jest
.spyOn(SetUpSubmissionCredentials.prototype, 'runAsync') .spyOn(SetUpSubmissionCredentials.prototype, 'runAsync')
@@ -8,7 +8,7 @@ import {
} from '../../../credentials/__tests__/fixtures-constants'; } from '../../../credentials/__tests__/fixtures-constants';
import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation'; import { SubmissionMutation } from '../../../graphql/mutations/SubmissionMutation';
import { createTestProject } from '../../../project/__tests__/project-utils'; import { createTestProject } from '../../../project/__tests__/project-utils';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../../../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../../../project/projectUtils';
import { createSubmissionContextAsync } from '../../context'; import { createSubmissionContextAsync } from '../../context';
import IosSubmitCommand from '../IosSubmitCommand'; import IosSubmitCommand from '../IosSubmitCommand';
@@ -54,10 +54,6 @@ describe(IosSubmitCommand, () => {
jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]); jest.mocked(getOwnerAccountForProjectIdAsync).mockResolvedValue(mockJester.accounts[0]);
}); });
afterEach(() => {
jest.mocked(getProjectIdAsync).mockClear();
});
describe('non-interactive mode', () => { describe('non-interactive mode', () => {
it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => { it("throws error if didn't provide appleId and ascAppId in the submit profile", async () => {
const projectId = uuidv4(); const projectId = uuidv4();
@@ -73,6 +69,7 @@ describe(IosSubmitCommand, () => {
language: 'en-US', language: 'en-US',
}, },
nonInteractive: true, nonInteractive: true,
actor: mockJester,
}); });
const command = new IosSubmitCommand(ctx); const command = new IosSubmitCommand(ctx);
await expect(command.runAsync()).rejects.toThrowError(); await expect(command.runAsync()).rejects.toThrowError();
@@ -98,6 +95,7 @@ describe(IosSubmitCommand, () => {
ascAppId: '12345678', ascAppId: '12345678',
}, },
nonInteractive: false, nonInteractive: false,
actor: mockJester,
}); });
const command = new IosSubmitCommand(ctx); const command = new IosSubmitCommand(ctx);
await command.runAsync(); await command.runAsync();
@@ -2,16 +2,15 @@ import { ExpoConfig } from '@expo/config';
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins'; import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
import { RequestedPlatform } from '../../platform'; import { RequestedPlatform } from '../../platform';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { ensureValidVersions } from '../utils'; import { ensureValidVersions } from '../utils';
export async function syncUpdatesConfigurationAsync( export async function syncUpdatesConfigurationAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig, exp: ExpoConfig,
{ nonInteractive }: { nonInteractive: boolean } projectId: string
): Promise<void> { ): Promise<void> {
ensureValidVersions(exp, RequestedPlatform.Android); ensureValidVersions(exp, RequestedPlatform.Android);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const accountName = (await getOwnerAccountForProjectIdAsync(projectId)).name; const accountName = (await getOwnerAccountForProjectIdAsync(projectId)).name;
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir); const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins'; import { IOSConfig } from '@expo/config-plugins';
import { RequestedPlatform } from '../../platform'; import { RequestedPlatform } from '../../platform';
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../../project/projectUtils'; import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { readPlistAsync, writePlistAsync } from '../../utils/plist'; import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { getVcsClient } from '../../vcs'; import { getVcsClient } from '../../vcs';
import { ensureValidVersions } from '../utils'; import { ensureValidVersions } from '../utils';
@@ -10,10 +10,9 @@ import { ensureValidVersions } from '../utils';
export async function syncUpdatesConfigurationAsync( export async function syncUpdatesConfigurationAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig, exp: ExpoConfig,
{ nonInteractive }: { nonInteractive: boolean } projectId: string
): Promise<void> { ): Promise<void> {
ensureValidVersions(exp, RequestedPlatform.Ios); ensureValidVersions(exp, RequestedPlatform.Ios);
const projectId = await getProjectIdAsync(exp, { nonInteractive });
const accountName = (await getOwnerAccountForProjectIdAsync(projectId)).name; const accountName = (await getOwnerAccountForProjectIdAsync(projectId)).name;
const expoPlist = await readExpoPlistAsync(projectDir); const expoPlist = await readExpoPlistAsync(projectDir);
const updatedExpoPlist = IOSConfig.Updates.setUpdatesConfig( const updatedExpoPlist = IOSConfig.Updates.setUpdatesConfig(