feat(nx-cloud): updates to the new onboarding flow

This commit is contained in:
Katerina Skroumpelou 2024-06-21 15:02:51 +03:00 committed by Jason Jean
parent 88fd03be3b
commit d928558bc4
9 changed files with 260 additions and 74 deletions

View File

@ -28,6 +28,11 @@
"type": "boolean", "type": "boolean",
"description": "If the user will be using GitHub as their git hosting provider", "description": "If the user will be using GitHub as their git hosting provider",
"default": false "default": false
},
"directory": {
"type": "string",
"description": "The directory where the workspace is located",
"x-priority": "internal"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -5,7 +5,7 @@ import { createSandbox } from './create-sandbox';
import { createEmptyWorkspace } from './create-empty-workspace'; import { createEmptyWorkspace } from './create-empty-workspace';
import { createPreset } from './create-preset'; import { createPreset } from './create-preset';
import { setupCI } from './utils/ci/setup-ci'; import { setupCI } from './utils/ci/setup-ci';
import { initializeGitRepo } from './utils/git/git'; import { commitChanges, initializeGitRepo } from './utils/git/git';
import { getPackageNameFromThirdPartyPreset } from './utils/preset/get-third-party-preset'; import { getPackageNameFromThirdPartyPreset } from './utils/preset/get-third-party-preset';
import { mapErrorToBodyLines } from './utils/error-utils'; import { mapErrorToBodyLines } from './utils/error-utils';
@ -51,27 +51,11 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
); );
} }
let nxCloudInstallRes; let gitSuccess = false;
if (nxCloud !== 'skip') {
nxCloudInstallRes = await setupNxCloud(
directory,
packageManager,
nxCloud,
useGitHub
);
if (nxCloud !== 'yes') {
await setupCI(
directory,
nxCloud,
packageManager,
nxCloudInstallRes?.code === 0
);
}
}
if (!skipGit && commit) { if (!skipGit && commit) {
try { try {
await initializeGitRepo(directory, { defaultBase, commit }); await initializeGitRepo(directory, { defaultBase, commit });
gitSuccess = true;
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
output.error({ output.error({
@ -84,6 +68,28 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
} }
} }
let nxCloudInstallRes;
if (nxCloud !== 'skip') {
nxCloudInstallRes = await setupNxCloud(
directory,
packageManager,
nxCloud,
useGitHub
);
if (nxCloud !== 'yes') {
const nxCIsetupRes = await setupCI(
directory,
nxCloud,
packageManager,
nxCloudInstallRes?.code === 0
);
if (nxCIsetupRes?.code === 0) {
commitChanges(directory, `feat(nx): Generated CI workflow`);
}
}
}
return { return {
nxCloudInfo: nxCloudInstallRes?.stdout, nxCloudInfo: nxCloudInstallRes?.stdout,
directory, directory,

View File

@ -84,3 +84,19 @@ export async function initializeGitRepo(
await execute(['commit', `-m "${message}"`]); await execute(['commit', `-m "${message}"`]);
} }
} }
export function commitChanges(directory: string, message: string) {
try {
execSync('git add -A', { encoding: 'utf8', stdio: 'pipe', cwd: directory });
execSync('git commit --no-verify -F -', {
encoding: 'utf8',
stdio: 'pipe',
input: message,
cwd: directory,
});
} catch (e) {
console.error(`There was an error committing your Nx Cloud token.\n
Please commit the changes manually and push to your new repository.\n
\n${e}`);
}
}

View File

@ -19,7 +19,7 @@ export async function setupNxCloud(
process.env.NX_NEW_CLOUD_ONBOARDING === 'true' process.env.NX_NEW_CLOUD_ONBOARDING === 'true'
? `${ ? `${
pmc.exec pmc.exec
} nx g nx:connect-to-nx-cloud --installationSource=create-nx-workspace ${ } nx g nx:connect-to-nx-cloud --installationSource=create-nx-workspace --directory=${directory} ${
useGitHub ? '--github' : '' useGitHub ? '--github' : ''
} --no-interactive` } --no-interactive`
: `${pmc.exec} nx g nx:connect-to-nx-cloud --no-interactive --quiet`, : `${pmc.exec} nx g nx:connect-to-nx-cloud --no-interactive --quiet`,

View File

@ -50,7 +50,9 @@ export async function connectToNxCloudIfExplicitlyAsked(
} }
} }
export async function connectToNxCloudCommand(): Promise<boolean> { export async function connectToNxCloudCommand(
command?: string
): Promise<boolean> {
const nxJson = readNxJson(); const nxJson = readNxJson();
if (isNxCloudUsed(nxJson)) { if (isNxCloudUsed(nxJson)) {
@ -85,7 +87,9 @@ export async function connectToNxCloudCommand(): Promise<boolean> {
} }
const tree = new FsTree(workspaceRoot, false, 'connect-to-nx-cloud'); const tree = new FsTree(workspaceRoot, false, 'connect-to-nx-cloud');
const callback = await connectToNxCloud(tree, {}); const callback = await connectToNxCloud(tree, {
installationSource: command ?? 'nx-connect',
});
tree.lock(); tree.lock();
flushChanges(workspaceRoot, tree.listChanges()); flushChanges(workspaceRoot, tree.listChanges());
await callback(); await callback();
@ -96,7 +100,7 @@ export async function connectToNxCloudCommand(): Promise<boolean> {
export async function connectToNxCloudWithPrompt(command: string) { export async function connectToNxCloudWithPrompt(command: string) {
const setNxCloud = await nxCloudPrompt('setupNxCloud'); const setNxCloud = await nxCloudPrompt('setupNxCloud');
const useCloud = const useCloud =
setNxCloud === 'yes' ? await connectToNxCloudCommand() : false; setNxCloud === 'yes' ? await connectToNxCloudCommand(command) : false;
await recordStat({ await recordStat({
command, command,
nxVersion, nxVersion,

View File

@ -6,7 +6,10 @@ import { readJson } from '../../../generators/utils/json';
import { NxJsonConfiguration } from '../../../config/nx-json'; import { NxJsonConfiguration } from '../../../config/nx-json';
import { readNxJson, updateNxJson } from '../../../generators/utils/nx-json'; import { readNxJson, updateNxJson } from '../../../generators/utils/nx-json';
import { formatChangedFilesWithPrettierIfAvailable } from '../../../generators/internal-utils/format-changed-files-with-prettier-if-available'; import { formatChangedFilesWithPrettierIfAvailable } from '../../../generators/internal-utils/format-changed-files-with-prettier-if-available';
import { shortenedCloudUrl } from '../../utilities/url-shorten'; import { repoUsesGithub, shortenedCloudUrl } from '../../utilities/url-shorten';
import { commitChanges } from '../../../utils/git-utils';
import * as ora from 'ora';
import * as open from 'open';
function printCloudConnectionDisabledMessage() { function printCloudConnectionDisabledMessage() {
output.error({ output.error({
@ -75,9 +78,10 @@ async function createNxCloudWorkspace(
async function printSuccessMessage( async function printSuccessMessage(
url: string, url: string,
token: string, token: string | undefined,
installationSource: string, installationSource: string,
github: boolean usesGithub?: boolean,
directory?: string
) { ) {
if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') { if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') {
let origin = 'https://nx.app'; let origin = 'https://nx.app';
@ -97,20 +101,63 @@ async function printSuccessMessage(
const connectCloudUrl = await shortenedCloudUrl( const connectCloudUrl = await shortenedCloudUrl(
installationSource, installationSource,
token, token,
github usesGithub
); );
output.note({ if (installationSource === 'nx-connect' && usesGithub) {
title: `Your Nx Cloud workspace is ready.`, try {
bodyLines: [ const cloudConnectSpinner = ora(
`To claim it, connect it to your Nx Cloud account:`, `Opening Nx Cloud ${connectCloudUrl} in your browser to connect your workspace.`
`- Commit and push your changes.`, ).start();
`- Create a pull request for the changes.`, await sleep(2000);
`- Go to the following URL to connect your workspace to Nx Cloud: open(connectCloudUrl);
cloudConnectSpinner.succeed();
} catch (e) {
output.note({
title: `Your Nx Cloud workspace is ready.`,
bodyLines: [
`To claim it, connect it to your Nx Cloud account:`,
`- Go to the following URL to connect your workspace to Nx Cloud:`,
'',
`${connectCloudUrl}`,
],
});
}
} else {
if (installationSource === 'create-nx-workspace') {
output.note({
title: `Your Nx Cloud workspace is ready.`,
bodyLines: [
`To claim it, connect it to your Nx Cloud account:`,
`- Push your repository to your git hosting provider.`,
`- Go to the following URL to connect your workspace to Nx Cloud:`,
'',
`${connectCloudUrl}`,
],
});
commitChanges(
`feat(nx): Added Nx Cloud token to your nx.json
${connectCloudUrl}`, To connect your workspace to Nx Cloud, push your repository
], to your git hosting provider and go to the following URL:
});
${connectCloudUrl}`,
directory
);
} else {
output.note({
title: `Your Nx Cloud workspace is ready.`,
bodyLines: [
`To claim it, connect it to your Nx Cloud account:`,
`- Commit and push your changes.`,
`- Create a pull request for the changes.`,
`- Go to the following URL to connect your workspace to Nx Cloud:`,
'',
`${connectCloudUrl}`,
],
});
}
}
} }
} }
@ -119,6 +166,7 @@ interface ConnectToNxCloudOptions {
installationSource?: string; installationSource?: string;
hideFormatLogs?: boolean; hideFormatLogs?: boolean;
github?: boolean; github?: boolean;
directory?: string;
} }
function addNxCloudOptionsToNxJson( function addNxCloudOptionsToNxJson(
@ -152,27 +200,70 @@ export async function connectToNxCloud(
printCloudConnectionDisabledMessage(); printCloudConnectionDisabledMessage();
}; };
} else { } else {
// TODO: Change to using loading light client when that is enabled by default if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') {
const r = await createNxCloudWorkspace( // TODO: Change to using loading light client when that is enabled by default
getRootPackageName(tree), const r = await createNxCloudWorkspace(
schema.installationSource, getRootPackageName(tree),
getNxInitDate()
);
addNxCloudOptionsToNxJson(tree, nxJson, r.token);
await formatChangedFilesWithPrettierIfAvailable(tree, {
silent: schema.hideFormatLogs,
});
return async () =>
await printSuccessMessage(
r.url,
r.token,
schema.installationSource, schema.installationSource,
schema.github getNxInitDate()
); );
addNxCloudOptionsToNxJson(tree, nxJson, r.token);
await formatChangedFilesWithPrettierIfAvailable(tree, {
silent: schema.hideFormatLogs,
});
return async () =>
await printSuccessMessage(r.url, r.token, schema.installationSource);
} else {
const usesGithub = await repoUsesGithub(schema.github);
let responseFromCreateNxCloudWorkspace:
| {
token: string;
url: string;
}
| undefined;
// do NOT create Nx Cloud token (createNxCloudWorkspace)
// if user is using github and is running nx-connect
if (!(usesGithub && schema.installationSource === 'nx-connect')) {
responseFromCreateNxCloudWorkspace = await createNxCloudWorkspace(
getRootPackageName(tree),
schema.installationSource,
getNxInitDate()
);
addNxCloudOptionsToNxJson(
tree,
nxJson,
responseFromCreateNxCloudWorkspace?.token
);
await formatChangedFilesWithPrettierIfAvailable(tree, {
silent: schema.hideFormatLogs,
});
}
const apiUrl = removeTrailingSlash(
process.env.NX_CLOUD_API ||
process.env.NRWL_API ||
`https://cloud.nx.app`
);
return async () =>
await printSuccessMessage(
responseFromCreateNxCloudWorkspace?.url ?? apiUrl,
responseFromCreateNxCloudWorkspace?.token,
schema.installationSource,
usesGithub,
schema.directory
);
}
} }
} }
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export default connectToNxCloud; export default connectToNxCloud;

View File

@ -25,6 +25,11 @@
"type": "boolean", "type": "boolean",
"description": "If the user will be using GitHub as their git hosting provider", "description": "If the user will be using GitHub as their git hosting provider",
"default": false "default": false
},
"directory": {
"type": "string",
"description": "The directory where the workspace is located",
"x-priority": "internal"
} }
}, },
"additionalProperties": false, "additionalProperties": false,

View File

@ -1,10 +1,11 @@
import { logger } from '../../devkit-exports'; import { logger } from '../../devkit-exports';
import { getGithubSlugOrNull } from '../../utils/git-utils'; import { getGithubSlugOrNull } from '../../utils/git-utils';
import { lt } from 'semver';
export async function shortenedCloudUrl( export async function shortenedCloudUrl(
installationSource: string, installationSource: string,
accessToken: string, accessToken?: string,
github?: boolean usesGithub?: boolean
) { ) {
const githubSlug = getGithubSlugOrNull(); const githubSlug = getGithubSlugOrNull();
@ -12,15 +13,11 @@ export async function shortenedCloudUrl(
process.env.NX_CLOUD_API || process.env.NRWL_API || `https://cloud.nx.app` process.env.NX_CLOUD_API || process.env.NRWL_API || `https://cloud.nx.app`
); );
const installationSupportsGitHub = await getInstallationSupportsGitHub( const version = await getNxCloudVersion(apiUrl);
apiUrl
);
const usesGithub = if (version && lt(truncateToSemver(version), '2406.11.5')) {
(githubSlug || github) && return apiUrl;
(apiUrl.includes('cloud.nx.app') || }
apiUrl.includes('eu.nx.app') ||
installationSupportsGitHub);
const source = getSource(installationSource); const source = getSource(installationSource);
@ -49,12 +46,31 @@ export async function shortenedCloudUrl(
usesGithub, usesGithub,
githubSlug, githubSlug,
apiUrl, apiUrl,
accessToken, source,
source accessToken
); );
} }
} }
export async function repoUsesGithub(github?: boolean) {
const githubSlug = getGithubSlugOrNull();
const apiUrl = removeTrailingSlash(
process.env.NX_CLOUD_API || process.env.NRWL_API || `https://cloud.nx.app`
);
const installationSupportsGitHub = await getInstallationSupportsGitHub(
apiUrl
);
return (
(githubSlug || github) &&
(apiUrl.includes('cloud.nx.app') ||
apiUrl.includes('eu.nx.app') ||
installationSupportsGitHub)
);
}
function removeTrailingSlash(apiUrl: string) { function removeTrailingSlash(apiUrl: string) {
return apiUrl[apiUrl.length - 1] === '/' ? apiUrl.slice(0, -1) : apiUrl; return apiUrl[apiUrl.length - 1] === '/' ? apiUrl.slice(0, -1) : apiUrl;
} }
@ -77,16 +93,16 @@ function getURLifShortenFailed(
usesGithub: boolean, usesGithub: boolean,
githubSlug: string, githubSlug: string,
apiUrl: string, apiUrl: string,
accessToken: string, source: string,
source: string accessToken?: string
) { ) {
if (usesGithub) { if (usesGithub) {
if (githubSlug) { if (githubSlug) {
return `${apiUrl}/setup/connect-workspace/vcs?provider=GITHUB&selectedRepositoryName=${encodeURIComponent( return `${apiUrl}/setup/connect-workspace/github/connect?name=${encodeURIComponent(
githubSlug githubSlug
)}&source=${source}`; )}&source=${source}`;
} else { } else {
return `${apiUrl}/setup/connect-workspace/vcs?provider=GITHUB&source=${source}`; return `${apiUrl}/setup/connect-workspace/github/select&source=${source}`;
} }
} }
return `${apiUrl}/setup/connect-workspace/manual?accessToken=${accessToken}&source=${source}`; return `${apiUrl}/setup/connect-workspace/manual?accessToken=${accessToken}&source=${source}`;
@ -109,3 +125,33 @@ async function getInstallationSupportsGitHub(apiUrl: string): Promise<boolean> {
return false; return false;
} }
} }
async function getNxCloudVersion(apiUrl: string): Promise<string | null> {
try {
const response = await require('axios').get(`${apiUrl}/version`, {
responseType: 'document',
});
const version = extractVersion(response.data);
if (!version) {
throw new Error('Failed to extract version from response.');
}
return version;
} catch (e) {
logger.verbose(`Failed to get version of Nx Cloud.
${e}`);
}
}
function extractVersion(htmlString: string): string | null {
// The pattern assumes 'Version' is inside an h1 tag and the version number is the next span's content
const regex =
/<h1[^>]*>Version<\/h1>\s*<div[^>]*><div[^>]*><div[^>]*><span[^>]*>([^<]+)<\/span>/;
const match = htmlString.match(regex);
return match ? match[1].trim() : null;
}
function truncateToSemver(versionString: string): string {
// version may be something like 2406.13.5.hotfix2
return versionString.split(/[\.-]/).slice(0, 3).join('.');
}

View File

@ -1,4 +1,5 @@
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { logger } from '../devkit-exports';
export function getGithubSlugOrNull(): string | null { export function getGithubSlugOrNull(): string | null {
try { try {
@ -46,16 +47,28 @@ function parseGitHubUrl(url: string): string | null {
return null; return null;
} }
export function commitChanges(commitMessage: string): string | null { export function commitChanges(
commitMessage: string,
directory?: string
): string | null {
try { try {
execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' }); execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' });
execSync('git commit --no-verify -F -', { execSync('git commit --no-verify -F -', {
encoding: 'utf8', encoding: 'utf8',
stdio: 'pipe', stdio: 'pipe',
input: commitMessage, input: commitMessage,
cwd: directory,
}); });
} catch (err) { } catch (err) {
throw new Error(`Error committing changes:\n${err.stderr}`); if (directory) {
// We don't want to throw during create-nx-workspace
// because maybe there was an error when setting up git
// initially.
logger.verbose(`Git may not be set up correctly for this new workspace.
${err}`);
} else {
throw new Error(`Error committing changes:\n${err.stderr}`);
}
} }
return getLatestCommitSha(); return getLatestCommitSha();