Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-12115][eas-cli] add eas onboarding flow #2338

Merged
merged 9 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,366 changes: 2,049 additions & 1,317 deletions packages/eas-cli/graphql.schema.json

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions packages/eas-cli/src/build/utils/formatBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
value: build.releaseChannel,
},
{
label: 'Channel',
value: build.channel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe(getProjectIdAsync, () => {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';

import { createGraphqlClient } from './createGraphqlClient';
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
Expand Down Expand Up @@ -87,6 +87,21 @@ export async function getProjectIdAsync(
});
const graphqlClient = createGraphqlClient(authenticationInfo);

const projectId = await validateOrSetProjectIdAsync({ exp, graphqlClient, actor, options });
return projectId;
}

export async function validateOrSetProjectIdAsync({
exp,
graphqlClient,
actor,
options,
}: {
exp: ExpoConfig;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options: { env?: Env; nonInteractive: boolean };
}): Promise<string> {
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
if (typeof localProjectId !== 'string') {
Expand Down
3 changes: 2 additions & 1 deletion packages/eas-cli/src/commands/credentials/configure-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ export default class InitializeBuildCredentials extends EasCommand {
privateProjectConfig ?? null,
getDynamicPrivateProjectConfigAsync,
platform,
buildProfile
buildProfile,
process.cwd()
).runAsync();
}
}
224 changes: 224 additions & 0 deletions packages/eas-cli/src/commands/project/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { Platform } from '@expo/eas-build-job';

import { reviewAndCommitChangesAsync } from '../../build/utils/repository';
import EasCommand from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { validateOrSetProjectIdAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
import { CredentialsContextProjectInfo } from '../../credentials/context';
import { SetUpBuildCredentialsCommandAction } from '../../credentials/manager/SetUpBuildCredentialsCommandAction';
import { AppPlatform, OnboardingDeviceType, OnboardingEnvironment } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import Log from '../../log';
import { runGitCloneAsync, runGitPushAsync } from '../../onboarding/git';
import { installDependenciesAsync } from '../../onboarding/installDependencies';
import { ExpoConfigOptions, getPrivateExpoConfig } from '../../project/expoConfig';
import { confirmAsync } from '../../prompts';
import { Actor } from '../../user/User';
import GitClient from '../../vcs/clients/git';

export default class Onboarding extends EasCommand {
static override hidden = true;

static override aliases = ['init:onboarding', 'onboarding'];

static override description = 'continue onboarding process started on the expo.dev website';

static override flags = {};

static override args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];

static override contextDefinition = {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
};

async runAsync(): Promise<void> {
const {
args: { TARGET_PROJECT_DIRECTORY: targetProjectDirInput },
} = await this.parse(Onboarding);

const {
loggedIn: { actor, graphqlClient },
analytics,
} = await this.getContextAsync(Onboarding, {
nonInteractive: false,
});

if (actor.__typename === 'Robot') {
throw new Error(
'This command is not available for robot users. Make sure you are not using robot token and try again.'
szdziedzic marked this conversation as resolved.
Show resolved Hide resolved
);
}

if (!actor.preferences.onboarding) {
throw new Error(
'This command is a continuation of the onboarding process started on the Expo website. Start the onboarding process on the website before running this command. Visit https:/expo.new to create an account and start the onboarding process.'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: https:/expo.new -> https://expo.new

szdziedzic marked this conversation as resolved.
Show resolved Hide resolved
);
}

if (!actor.preferences.onboarding.platform) {
throw new Error(
'This command is a continuation of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the onboarding command (selected platform). Continue the onboarding process on the Expo website.'
);
}
if (!actor.preferences.onboarding.environment) {
throw new Error(
'This command is a continuation of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the onboarding command (selected environment). Continue the onboarding process on the Expo website.'
);
}
if (!actor.preferences.onboarding.deviceType) {
throw new Error(
'This command is a continuation of the onboarding process started on the Expo website. It seems like you started an onboarding process, but we are missing some information needed to be filled in before running the onboarding command (selected device type). Continue the onboarding process on the Expo website.'
);
}

const platform =
actor.preferences.onboarding.platform === AppPlatform.Android
? Platform.ANDROID
: Platform.IOS;

const app = await AppQuery.byIdAsync(graphqlClient, actor.preferences.onboarding.appId);

const githubUsername = app.githubRepository
? app.githubRepository.metadata.githubRepoOwnerName
: 'expo';
const githubRepositoryName = app.githubRepository
? app.githubRepository.metadata.githubRepoName
: 'expo-default-template';
Comment on lines +95 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if one is defined, but not the other we'd try to clone sjchmiela/expo-default-template? Seems like we shouldn't

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both githubRepoOwnerName and githubRepoName are reguired on metadata object

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I believe there should never be a case when one exists and other not

const initialTargetProjectDir: string = targetProjectDirInput ?? `./${githubRepositoryName}`;
szdziedzic marked this conversation as resolved.
Show resolved Hide resolved

Log.log(`👋 Welcome to Expo, ${actor.username}!`);
Log.log('🚀 We will continue your onboarding process in EAS CLI');
Log.log();
Log.log(
`🔎 Let's start by cloning ${
app.githubRepository
? `your project (${githubUsername}/${githubRepositoryName})`
: `default expo template project (${githubUsername}/${githubRepositoryName})`
} from GitHub and installing dependencies.`
);
Log.log();
const shouldContinue = await confirmAsync({ message: 'Do you want to continue?' });
if (!shouldContinue) {
throw new Error("Aborting, run the command again once you're ready.");
}

const { targetProjectDir } = await runGitCloneAsync({
githubUsername,
githubRepositoryName,
targetProjectDir: initialTargetProjectDir,
});

const vcsClient = new GitClient(targetProjectDir);
await installDependenciesAsync({
projectDir: targetProjectDir,
});
await vcsClient.trackFileAsync('package-lock.json');

const shouldSetupCredentials =
actor.preferences.onboarding.deviceType === OnboardingDeviceType.Device &&
actor.preferences.onboarding.environment === OnboardingEnvironment.DevBuild;
if (shouldSetupCredentials) {
Log.log('🔑 Now we need to set up build credentials for your project:');
await new SetUpBuildCredentialsCommandAction(
actor,
graphqlClient,
vcsClient,
analytics,
await getPrivateExpoConfigWithProjectIdAsync({
projectDir: targetProjectDir,
graphqlClient,
actor,
}),
getDynamicPrivateProjectConfigGetter({
projectDir: targetProjectDir,
graphqlClient,
actor,
}),
platform,
'development',
targetProjectDir
).runAsync();
}

if (await vcsClient.hasUncommittedChangesAsync()) {
Log.log(
'📦 We will now commit the changes made by the configuration process and push them to GitHub:'
);
Log.log();
Log.log('🔍 Checking for changes in the repository...');
await vcsClient.showChangedFilesAsync();
await reviewAndCommitChangesAsync(
vcsClient,
`[EAS onboarding] Install dependencies${
szdziedzic marked this conversation as resolved.
Show resolved Hide resolved
shouldSetupCredentials ? 'and set up build credentials' : ''
}`,
{ nonInteractive: false }
);
Log.log('📤 Pushing changes to GitHub...');
await runGitPushAsync({
targetProjectDir,
});
}

Log.log();
Log.log('🎉 We finished configuring your project.');
Log.log('🚀 You can now go back to the website to continue.');
szdziedzic marked this conversation as resolved.
Show resolved Hide resolved
}
}

// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
async function getPrivateExpoConfigWithProjectIdAsync({
projectDir,
graphqlClient,
actor,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
}): Promise<CredentialsContextProjectInfo> {
const expBefore = getPrivateExpoConfig(projectDir);
const projectId = await validateOrSetProjectIdAsync({
exp: expBefore,
graphqlClient,
actor,
options: {
nonInteractive: false,
},
});
const exp = getPrivateExpoConfig(projectDir);
return {
exp,
projectId,
};
}

// we can't get this automated by using command context because when we run a command the project directory doesn't exist yet
function getDynamicPrivateProjectConfigGetter({
projectDir,
graphqlClient,
actor,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
}): DynamicConfigContextFn {
return async (options?: ExpoConfigOptions) => {
const expBefore = getPrivateExpoConfig(projectDir, options);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not reuse getPrivateExpoConfigWithProjectIdAsync here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, sure

const projectId = await validateOrSetProjectIdAsync({
exp: expBefore,
graphqlClient,
actor,
options: {
nonInteractive: false,
},
});
const exp = getPrivateExpoConfig(projectDir, options);
return {
exp,
projectDir,
projectId,
};
};
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import assert from 'node:assert';

import { AppFragment, Role } from '../../graphql/generated';
import { Actor } from '../../user/User';

Expand All @@ -24,6 +26,7 @@ export const jester: Actor = {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};

export const jester2 = {
Expand All @@ -41,6 +44,7 @@ export const jester2 = {
featureGates: {},
};

assert(jester.__typename === 'User');
export const testUsername = jester.username;
export const testSlug = 'testApp';
export const testProjectId = '7ef93448-3bc7-4b57-be32-99326dcf24f0';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ export class SetUpBuildCredentialsCommandAction {
public readonly projectInfo: CredentialsContextProjectInfo | null,
public readonly getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn,
private readonly platform: Platform,
private readonly profileName: string
private readonly profileName: string,
private readonly projectDir: string
) {}

async runAsync(): Promise<void> {
if (this.platform === Platform.IOS) {
return await new SetUpIosBuildCredentials(this, process.cwd(), this.profileName).runAsync();
return await new SetUpIosBuildCredentials(this, this.projectDir, this.profileName).runAsync();
}
return await new SetUpAndroidBuildCredentials(this, process.cwd(), this.profileName).runAsync();
return await new SetUpAndroidBuildCredentials(
this,
this.projectDir,
this.profileName
).runAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class SetUpIosBuildCredentials extends ManageIos {
}

const ctx = new CredentialsContext({
projectDir: process.cwd(),
projectDir: this.projectDir,
projectInfo,
user: this.callingAction.actor,
graphqlClient: this.callingAction.graphqlClient,
Expand Down
1 change: 1 addition & 0 deletions packages/eas-cli/src/devices/__tests__/manager-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe(AccountResolver, () => {
],
isExpoAdmin: false,
featureGates: {},
preferences: {},
};

describe('when inside project dir', () => {
Expand Down
Loading
Loading