From d928558bc48076d3f5d1aba7921cd328958b735c Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Fri, 21 Jun 2024 15:02:51 +0300 Subject: [PATCH] feat(nx-cloud): updates to the new onboarding flow --- .../nx/generators/connect-to-nx-cloud.json | 5 + .../src/create-workspace.ts | 36 +++-- .../create-nx-workspace/src/utils/git/git.ts | 16 ++ .../src/utils/nx/nx-cloud.ts | 2 +- .../connect/connect-to-nx-cloud.ts | 10 +- .../connect-to-nx-cloud.ts | 153 ++++++++++++++---- .../connect-to-nx-cloud/schema.json | 5 + .../nx/src/nx-cloud/utilities/url-shorten.ts | 78 +++++++-- packages/nx/src/utils/git-utils.ts | 17 +- 9 files changed, 254 insertions(+), 68 deletions(-) diff --git a/docs/generated/packages/nx/generators/connect-to-nx-cloud.json b/docs/generated/packages/nx/generators/connect-to-nx-cloud.json index 7177ec98373a7..7090a0a83976d 100644 --- a/docs/generated/packages/nx/generators/connect-to-nx-cloud.json +++ b/docs/generated/packages/nx/generators/connect-to-nx-cloud.json @@ -28,6 +28,11 @@ "type": "boolean", "description": "If the user will be using GitHub as their git hosting provider", "default": false + }, + "directory": { + "type": "string", + "description": "The directory where the workspace is located", + "x-priority": "internal" } }, "additionalProperties": false, diff --git a/packages/create-nx-workspace/src/create-workspace.ts b/packages/create-nx-workspace/src/create-workspace.ts index 56fd2da441990..92dbd58108565 100644 --- a/packages/create-nx-workspace/src/create-workspace.ts +++ b/packages/create-nx-workspace/src/create-workspace.ts @@ -5,7 +5,7 @@ import { createSandbox } from './create-sandbox'; import { createEmptyWorkspace } from './create-empty-workspace'; import { createPreset } from './create-preset'; 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 { mapErrorToBodyLines } from './utils/error-utils'; @@ -51,6 +51,23 @@ export async function createWorkspace( ); } + let gitSuccess = false; + if (!skipGit && commit) { + try { + await initializeGitRepo(directory, { defaultBase, commit }); + gitSuccess = true; + } catch (e) { + if (e instanceof Error) { + output.error({ + title: 'Could not initialize git repository', + bodyLines: mapErrorToBodyLines(e), + }); + } else { + console.error(e); + } + } + } + let nxCloudInstallRes; if (nxCloud !== 'skip') { nxCloudInstallRes = await setupNxCloud( @@ -61,25 +78,14 @@ export async function createWorkspace( ); if (nxCloud !== 'yes') { - await setupCI( + const nxCIsetupRes = await setupCI( directory, nxCloud, packageManager, nxCloudInstallRes?.code === 0 ); - } - } - if (!skipGit && commit) { - try { - await initializeGitRepo(directory, { defaultBase, commit }); - } catch (e) { - if (e instanceof Error) { - output.error({ - title: 'Could not initialize git repository', - bodyLines: mapErrorToBodyLines(e), - }); - } else { - console.error(e); + if (nxCIsetupRes?.code === 0) { + commitChanges(directory, `feat(nx): Generated CI workflow`); } } } diff --git a/packages/create-nx-workspace/src/utils/git/git.ts b/packages/create-nx-workspace/src/utils/git/git.ts index 9855b68e62dad..4747f65a1a68b 100644 --- a/packages/create-nx-workspace/src/utils/git/git.ts +++ b/packages/create-nx-workspace/src/utils/git/git.ts @@ -84,3 +84,19 @@ export async function initializeGitRepo( 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}`); + } +} diff --git a/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts b/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts index 11adbd385367c..a5178f6a0292e 100644 --- a/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts +++ b/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts @@ -19,7 +19,7 @@ export async function setupNxCloud( process.env.NX_NEW_CLOUD_ONBOARDING === 'true' ? `${ 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' : '' } --no-interactive` : `${pmc.exec} nx g nx:connect-to-nx-cloud --no-interactive --quiet`, diff --git a/packages/nx/src/command-line/connect/connect-to-nx-cloud.ts b/packages/nx/src/command-line/connect/connect-to-nx-cloud.ts index 544ad543eb2b1..722d2b642ae38 100644 --- a/packages/nx/src/command-line/connect/connect-to-nx-cloud.ts +++ b/packages/nx/src/command-line/connect/connect-to-nx-cloud.ts @@ -50,7 +50,9 @@ export async function connectToNxCloudIfExplicitlyAsked( } } -export async function connectToNxCloudCommand(): Promise { +export async function connectToNxCloudCommand( + command?: string +): Promise { const nxJson = readNxJson(); if (isNxCloudUsed(nxJson)) { @@ -85,7 +87,9 @@ export async function connectToNxCloudCommand(): Promise { } 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(); flushChanges(workspaceRoot, tree.listChanges()); await callback(); @@ -96,7 +100,7 @@ export async function connectToNxCloudCommand(): Promise { export async function connectToNxCloudWithPrompt(command: string) { const setNxCloud = await nxCloudPrompt('setupNxCloud'); const useCloud = - setNxCloud === 'yes' ? await connectToNxCloudCommand() : false; + setNxCloud === 'yes' ? await connectToNxCloudCommand(command) : false; await recordStat({ command, nxVersion, diff --git a/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.ts b/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.ts index cad9ca7ede29f..99772cfb4ec22 100644 --- a/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.ts +++ b/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud.ts @@ -6,7 +6,10 @@ import { readJson } from '../../../generators/utils/json'; import { NxJsonConfiguration } from '../../../config/nx-json'; import { readNxJson, updateNxJson } from '../../../generators/utils/nx-json'; 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() { output.error({ @@ -75,9 +78,10 @@ async function createNxCloudWorkspace( async function printSuccessMessage( url: string, - token: string, + token: string | undefined, installationSource: string, - github: boolean + usesGithub?: boolean, + directory?: string ) { if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') { let origin = 'https://nx.app'; @@ -97,20 +101,63 @@ async function printSuccessMessage( const connectCloudUrl = await shortenedCloudUrl( installationSource, token, - github + usesGithub ); - 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}`, - ], - }); + if (installationSource === 'nx-connect' && usesGithub) { + try { + const cloudConnectSpinner = ora( + `Opening Nx Cloud ${connectCloudUrl} in your browser to connect your workspace.` + ).start(); + await sleep(2000); + 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 + + 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; hideFormatLogs?: boolean; github?: boolean; + directory?: string; } function addNxCloudOptionsToNxJson( @@ -152,27 +200,70 @@ export async function connectToNxCloud( printCloudConnectionDisabledMessage(); }; } else { - // TODO: Change to using loading light client when that is enabled by default - const r = await createNxCloudWorkspace( - getRootPackageName(tree), - schema.installationSource, - getNxInitDate() - ); + if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') { + // TODO: Change to using loading light client when that is enabled by default + const r = await createNxCloudWorkspace( + getRootPackageName(tree), + schema.installationSource, + getNxInitDate() + ); - addNxCloudOptionsToNxJson(tree, nxJson, r.token); + addNxCloudOptionsToNxJson(tree, nxJson, r.token); - await formatChangedFilesWithPrettierIfAvailable(tree, { - silent: schema.hideFormatLogs, - }); + await formatChangedFilesWithPrettierIfAvailable(tree, { + silent: schema.hideFormatLogs, + }); - return async () => - await printSuccessMessage( - r.url, - r.token, - schema.installationSource, - schema.github + 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; diff --git a/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/schema.json b/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/schema.json index f2c5fd8c851c6..e6c75887efcda 100644 --- a/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/schema.json +++ b/packages/nx/src/nx-cloud/generators/connect-to-nx-cloud/schema.json @@ -25,6 +25,11 @@ "type": "boolean", "description": "If the user will be using GitHub as their git hosting provider", "default": false + }, + "directory": { + "type": "string", + "description": "The directory where the workspace is located", + "x-priority": "internal" } }, "additionalProperties": false, diff --git a/packages/nx/src/nx-cloud/utilities/url-shorten.ts b/packages/nx/src/nx-cloud/utilities/url-shorten.ts index 2330913f51510..d5ce9bb68e795 100644 --- a/packages/nx/src/nx-cloud/utilities/url-shorten.ts +++ b/packages/nx/src/nx-cloud/utilities/url-shorten.ts @@ -1,10 +1,11 @@ import { logger } from '../../devkit-exports'; import { getGithubSlugOrNull } from '../../utils/git-utils'; +import { lt } from 'semver'; export async function shortenedCloudUrl( installationSource: string, - accessToken: string, - github?: boolean + accessToken?: string, + usesGithub?: boolean ) { const githubSlug = getGithubSlugOrNull(); @@ -12,15 +13,11 @@ export async function shortenedCloudUrl( process.env.NX_CLOUD_API || process.env.NRWL_API || `https://cloud.nx.app` ); - const installationSupportsGitHub = await getInstallationSupportsGitHub( - apiUrl - ); + const version = await getNxCloudVersion(apiUrl); - const usesGithub = - (githubSlug || github) && - (apiUrl.includes('cloud.nx.app') || - apiUrl.includes('eu.nx.app') || - installationSupportsGitHub); + if (version && lt(truncateToSemver(version), '2406.11.5')) { + return apiUrl; + } const source = getSource(installationSource); @@ -49,12 +46,31 @@ export async function shortenedCloudUrl( usesGithub, githubSlug, 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) { return apiUrl[apiUrl.length - 1] === '/' ? apiUrl.slice(0, -1) : apiUrl; } @@ -77,16 +93,16 @@ function getURLifShortenFailed( usesGithub: boolean, githubSlug: string, apiUrl: string, - accessToken: string, - source: string + source: string, + accessToken?: string ) { if (usesGithub) { if (githubSlug) { - return `${apiUrl}/setup/connect-workspace/vcs?provider=GITHUB&selectedRepositoryName=${encodeURIComponent( + return `${apiUrl}/setup/connect-workspace/github/connect?name=${encodeURIComponent( githubSlug )}&source=${source}`; } 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}`; @@ -109,3 +125,33 @@ async function getInstallationSupportsGitHub(apiUrl: string): Promise { return false; } } + +async function getNxCloudVersion(apiUrl: string): Promise { + 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 = + /]*>Version<\/h1>\s*]*>]*>]*>]*>([^<]+)<\/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('.'); +} diff --git a/packages/nx/src/utils/git-utils.ts b/packages/nx/src/utils/git-utils.ts index 3bfaff25f791c..2014e2c19b668 100644 --- a/packages/nx/src/utils/git-utils.ts +++ b/packages/nx/src/utils/git-utils.ts @@ -1,4 +1,5 @@ import { execSync } from 'child_process'; +import { logger } from '../devkit-exports'; export function getGithubSlugOrNull(): string | null { try { @@ -46,16 +47,28 @@ function parseGitHubUrl(url: string): string | null { return null; } -export function commitChanges(commitMessage: string): string | null { +export function commitChanges( + commitMessage: string, + directory?: string +): string | null { try { execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' }); execSync('git commit --no-verify -F -', { encoding: 'utf8', stdio: 'pipe', input: commitMessage, + cwd: directory, }); } 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();