From f8239debd06e39d89cad05e01d618c0b9e8bc1ac Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Wed, 5 Jun 2024 11:45:22 +0300 Subject: [PATCH] feat(nx-cloud): new cloud onboarding flow (#26262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Nx cloud onboarding flow. ![Screenshot 2024-05-30 at 5 29 43 PM](https://github.com/nrwl/nx/assets/6603745/c26f6416-f54b-4b6d-8b3c-4d4c1acfcbb1) ![Screenshot 2024-05-30 at 5 30 18 PM](https://github.com/nrwl/nx/assets/6603745/acb2b1d1-8437-4d8b-8b87-602cc918f12b) ![Screenshot 2024-05-30 at 5 31 26 PM](https://github.com/nrwl/nx/assets/6603745/e6b1f595-e3c1-4c09-83e7-9f71b5383a35) --- docs/generated/cli/create-nx-workspace.md | 8 ++ .../nx/documents/create-nx-workspace.md | 8 ++ .../nx/generators/connect-to-nx-cloud.json | 5 + .../bin/create-nx-workspace.ts | 32 +++-- .../src/create-workspace-options.ts | 1 + .../src/create-workspace.ts | 8 +- .../src/internal-utils/prompts.ts | 18 +++ .../src/internal-utils/yargs-options.ts | 8 ++ .../src/utils/nx/nx-cloud.ts | 12 +- .../connect/connect-to-nx-cloud.ts | 38 ++++-- packages/nx/src/command-line/init/init-v2.ts | 3 +- .../nx/src/command-line/migrate/migrate.ts | 27 +---- .../connect-to-nx-cloud.ts | 61 +++++++--- .../connect-to-nx-cloud/schema.json | 5 + .../nx/src/nx-cloud/utilities/url-shorten.ts | 111 ++++++++++++++++++ packages/nx/src/utils/git-utils.spec.ts | 84 +++++++++++++ packages/nx/src/utils/git-utils.ts | 73 ++++++++++++ 17 files changed, 442 insertions(+), 60 deletions(-) create mode 100644 packages/nx/src/nx-cloud/utilities/url-shorten.ts create mode 100644 packages/nx/src/utils/git-utils.spec.ts create mode 100644 packages/nx/src/utils/git-utils.ts diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index e5de260ffc..7401524683 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -183,6 +183,14 @@ Type: `string` Stylesheet type to be used with certain stacks +### useGitHub + +Type: `boolean` + +Default: `false` + +Will you be using GitHub as your git hosting provider? + ### version Type: `boolean` diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index e5de260ffc..7401524683 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -183,6 +183,14 @@ Type: `string` Stylesheet type to be used with certain stacks +### useGitHub + +Type: `boolean` + +Default: `false` + +Will you be using GitHub as your git hosting provider? + ### version Type: `boolean` 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 c9c3e2eff6..7177ec9837 100644 --- a/docs/generated/packages/nx/generators/connect-to-nx-cloud.json +++ b/docs/generated/packages/nx/generators/connect-to-nx-cloud.json @@ -23,6 +23,11 @@ "type": "boolean", "description": "Hide formatting logs", "x-priority": "internal" + }, + "github": { + "type": "boolean", + "description": "If the user will be using GitHub as their git hosting provider", + "default": false } }, "additionalProperties": false, diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index f84dfe8533..0bf443ac58 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -13,12 +13,14 @@ import { yargsDecorator } from './decorator'; import { getPackageNameFromThirdPartyPreset } from '../src/utils/preset/get-third-party-preset'; import { determineDefaultBase, + determineIfGitHubWillBeUsed, determineNxCloud, determinePackageManager, } from '../src/internal-utils/prompts'; import { withAllPrompts, withGitOptions, + withUseGitHub, withNxCloud, withOptions, withPackageManager, @@ -183,6 +185,7 @@ export const commandsObject: yargs.Argv = yargs type: 'string', }), withNxCloud, + withUseGitHub, withAllPrompts, withPackageManager, withGitOptions @@ -280,13 +283,28 @@ async function normalizeArgsMiddleware( const packageManager = await determinePackageManager(argv); const defaultBase = await determineDefaultBase(argv); - const nxCloud = await determineNxCloud(argv); - - Object.assign(argv, { - nxCloud, - packageManager, - defaultBase, - }); + if (process.env.NX_NEW_CLOUD_ONBOARDING === 'true') { + const nxCloud = + argv.skipGit === true ? 'skip' : await determineNxCloud(argv); + const useGitHub = + nxCloud === 'skip' + ? undefined + : nxCloud === 'github' || + (await determineIfGitHubWillBeUsed(nxCloud)); + Object.assign(argv, { + nxCloud, + useGitHub, + packageManager, + defaultBase, + }); + } else { + const nxCloud = await determineNxCloud(argv); + Object.assign(argv, { + nxCloud, + packageManager, + defaultBase, + }); + } } catch (e) { console.error(e); process.exit(1); diff --git a/packages/create-nx-workspace/src/create-workspace-options.ts b/packages/create-nx-workspace/src/create-workspace-options.ts index 3cccbbec38..b84e2e60d4 100644 --- a/packages/create-nx-workspace/src/create-workspace-options.ts +++ b/packages/create-nx-workspace/src/create-workspace-options.ts @@ -5,6 +5,7 @@ export interface CreateWorkspaceOptions { name: string; // Workspace name (e.g. org name) packageManager: PackageManager; // Package manager to use nxCloud: NxCloud; // Enable Nx Cloud + useGitHub?: boolean; // Will you be using GitHub as your git hosting provider? /** * @description Enable interactive mode with presets * @default true diff --git a/packages/create-nx-workspace/src/create-workspace.ts b/packages/create-nx-workspace/src/create-workspace.ts index dbb0bd9da3..56fd2da441 100644 --- a/packages/create-nx-workspace/src/create-workspace.ts +++ b/packages/create-nx-workspace/src/create-workspace.ts @@ -21,6 +21,7 @@ export async function createWorkspace( defaultBase = 'main', commit, cliName, + useGitHub, } = options; if (cliName) { @@ -52,7 +53,12 @@ export async function createWorkspace( let nxCloudInstallRes; if (nxCloud !== 'skip') { - nxCloudInstallRes = await setupNxCloud(directory, packageManager, nxCloud); + nxCloudInstallRes = await setupNxCloud( + directory, + packageManager, + nxCloud, + useGitHub + ); if (nxCloud !== 'yes') { await setupCI( diff --git a/packages/create-nx-workspace/src/internal-utils/prompts.ts b/packages/create-nx-workspace/src/internal-utils/prompts.ts index abb6ef2f81..93414a9604 100644 --- a/packages/create-nx-workspace/src/internal-utils/prompts.ts +++ b/packages/create-nx-workspace/src/internal-utils/prompts.ts @@ -22,6 +22,24 @@ export async function determineNxCloud( } } +export async function determineIfGitHubWillBeUsed( + nxCloud: NxCloud +): Promise { + if (nxCloud === 'yes' || nxCloud === 'circleci') { + const reply = await enquirer.prompt<{ github: 'Yes' | 'No' }>([ + { + name: 'github', + message: 'Will you be using GitHub as your git hosting provider?', + type: 'autocomplete', + choices: [{ name: 'Yes' }, { name: 'No' }], + initial: 0, + }, + ]); + return reply.github === 'Yes'; + } + return false; +} + async function nxCloudPrompt(key: MessageKey): Promise { const { message, choices, initial, fallback, footer, hint } = messages.getPrompt(key); diff --git a/packages/create-nx-workspace/src/internal-utils/yargs-options.ts b/packages/create-nx-workspace/src/internal-utils/yargs-options.ts index 2404969725..a43145fd76 100644 --- a/packages/create-nx-workspace/src/internal-utils/yargs-options.ts +++ b/packages/create-nx-workspace/src/internal-utils/yargs-options.ts @@ -15,6 +15,14 @@ export function withNxCloud(argv: yargs.Argv) { return result; } +export function withUseGitHub(argv: yargs.Argv) { + return argv.option('useGitHub', { + describe: chalk.dim`Will you be using GitHub as your git hosting provider?`, + type: 'boolean', + default: false, + }); +} + export function withAllPrompts(argv: yargs.Argv) { return argv.option('allPrompts', { alias: 'a', 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 aad7508120..11adbd3853 100644 --- a/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts +++ b/packages/create-nx-workspace/src/utils/nx/nx-cloud.ts @@ -9,15 +9,23 @@ export type NxCloud = 'yes' | 'github' | 'circleci' | 'skip'; export async function setupNxCloud( directory: string, packageManager: PackageManager, - nxCloud: NxCloud + nxCloud: NxCloud, + useGitHub?: boolean ) { const nxCloudSpinner = ora(`Setting up Nx Cloud`).start(); try { const pmc = getPackageManagerCommand(packageManager); const res = await execAndWait( - `${pmc.exec} nx g nx:connect-to-nx-cloud --no-interactive --quiet`, + process.env.NX_NEW_CLOUD_ONBOARDING === 'true' + ? `${ + pmc.exec + } nx g nx:connect-to-nx-cloud --installationSource=create-nx-workspace ${ + useGitHub ? '--github' : '' + } --no-interactive` + : `${pmc.exec} nx g nx:connect-to-nx-cloud --no-interactive --quiet`, directory ); + if (nxCloud !== 'yes') { nxCloudSpinner.succeed( 'CI workflow with Nx Cloud has been generated successfully' 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 a0d0392031..544ad543eb 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 @@ -2,6 +2,7 @@ import { output } from '../../utils/output'; import { readNxJson } from '../../config/configuration'; import { FsTree, flushChanges } from '../../generators/tree'; import { connectToNxCloud } from '../../nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud'; +import { shortenedCloudUrl } from '../../nx-cloud/utilities/url-shorten'; import { getNxCloudUrl, isNxCloudUsed } from '../../utils/nx-cloud-utils'; import { runNxSync } from '../../utils/child-process'; import { NxJsonConfiguration } from '../../config/nx-json'; @@ -51,14 +52,35 @@ export async function connectToNxCloudIfExplicitlyAsked( export async function connectToNxCloudCommand(): Promise { const nxJson = readNxJson(); + if (isNxCloudUsed(nxJson)) { - output.log({ - title: '✔ This workspace already has Nx Cloud set up', - bodyLines: [ - 'If you have not done so already, connect your workspace to your Nx Cloud account:', - `- Login at ${getNxCloudUrl(nxJson)} to connect your repository`, - ], - }); + if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') { + output.log({ + title: '✔ This workspace already has Nx Cloud set up', + bodyLines: [ + 'If you have not done so already, connect your workspace to your Nx Cloud account:', + `- Login at ${getNxCloudUrl(nxJson)} to connect your repository`, + ], + }); + } else { + const token = + process.env.NX_CLOUD_ACCESS_TOKEN || nxJson.nxCloudAccessToken; + if (!token) { + throw new Error( + `Unable to authenticate. Either define accessToken in nx.json or set the NX_CLOUD_ACCESS_TOKEN env variable.` + ); + } + const connectCloudUrl = await shortenedCloudUrl('nx-connect', token); + output.log({ + title: '✔ This workspace already has Nx Cloud set up', + bodyLines: [ + 'If you have not done so already, connect your workspace to your Nx Cloud account:', + `- Connect with Nx Cloud at: + + ${connectCloudUrl}`, + ], + }); + } return false; } @@ -66,7 +88,7 @@ export async function connectToNxCloudCommand(): Promise { const callback = await connectToNxCloud(tree, {}); tree.lock(); flushChanges(workspaceRoot, tree.listChanges()); - callback(); + await callback(); return true; } diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index b3834d5648..725a49c4b0 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -5,7 +5,7 @@ import { output } from '../../utils/output'; import { getPackageManagerCommand } from '../../utils/package-manager'; import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts'; import { runNxSync } from '../../utils/child-process'; -import { readJsonFile, writeJsonFile } from '../../utils/fileutils'; +import { readJsonFile } from '../../utils/fileutils'; import { nxVersion } from '../../utils/versions'; import { addDepsToPackageJson, @@ -22,7 +22,6 @@ import { globWithWorkspaceContext } from '../../utils/workspace-context'; import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud'; import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo'; import { addNxToMonorepo } from './implementation/add-nx-to-monorepo'; -import { join } from 'path'; export interface InitArgs { interactive: boolean; diff --git a/packages/nx/src/command-line/migrate/migrate.ts b/packages/nx/src/command-line/migrate/migrate.ts index 90541db2a2..8e15984ace 100644 --- a/packages/nx/src/command-line/migrate/migrate.ts +++ b/packages/nx/src/command-line/migrate/migrate.ts @@ -32,6 +32,7 @@ import { writeJsonFile, } from '../../utils/fileutils'; import { logger } from '../../utils/logger'; +import { commitChanges } from '../../utils/git-utils'; import { ArrayPackageGroup, NxMigrationsConfiguration, @@ -1573,32 +1574,6 @@ function getStringifiedPackageJsonDeps(root: string): string { } } -function commitChanges(commitMessage: string): string | null { - try { - execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' }); - execSync('git commit --no-verify -F -', { - encoding: 'utf8', - stdio: 'pipe', - input: commitMessage, - }); - } catch (err) { - throw new Error(`Error committing changes:\n${err.stderr}`); - } - - return getLatestCommitSha(); -} - -function getLatestCommitSha(): string | null { - try { - return execSync('git rev-parse HEAD', { - encoding: 'utf8', - stdio: 'pipe', - }).trim(); - } catch { - return null; - } -} - async function runNxMigration( root: string, collectionPath: string, 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 2f07596b49..cad9ca7ede 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,6 +6,7 @@ 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'; function printCloudConnectionDisabledMessage() { output.error({ @@ -72,26 +73,52 @@ async function createNxCloudWorkspace( return response.data; } -function printSuccessMessage(url: string) { - let origin = 'https://nx.app'; - try { - origin = new URL(url).origin; - } catch (e) {} +async function printSuccessMessage( + url: string, + token: string, + installationSource: string, + github: boolean +) { + if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') { + let origin = 'https://nx.app'; + try { + origin = new URL(url).origin; + } catch (e) {} - output.note({ - title: `Your Nx Cloud workspace is public`, - bodyLines: [ - `To restrict access, connect it to your Nx Cloud account:`, - `- Push your changes`, - `- Login at ${origin} to connect your repository`, - ], - }); + output.note({ + title: `Your Nx Cloud workspace is public`, + bodyLines: [ + `To restrict access, connect it to your Nx Cloud account:`, + `- Push your changes`, + `- Login at ${origin} to connect your repository`, + ], + }); + } else { + const connectCloudUrl = await shortenedCloudUrl( + installationSource, + token, + github + ); + + 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}`, + ], + }); + } } interface ConnectToNxCloudOptions { analytics?: boolean; installationSource?: string; hideFormatLogs?: boolean; + github?: boolean; } function addNxCloudOptionsToNxJson( @@ -138,7 +165,13 @@ export async function connectToNxCloud( silent: schema.hideFormatLogs, }); - return () => printSuccessMessage(r.url); + return async () => + await printSuccessMessage( + r.url, + r.token, + schema.installationSource, + schema.github + ); } } 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 4e7453f606..f2c5fd8c85 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 @@ -20,6 +20,11 @@ "type": "boolean", "description": "Hide formatting logs", "x-priority": "internal" + }, + "github": { + "type": "boolean", + "description": "If the user will be using GitHub as their git hosting provider", + "default": false } }, "additionalProperties": false, diff --git a/packages/nx/src/nx-cloud/utilities/url-shorten.ts b/packages/nx/src/nx-cloud/utilities/url-shorten.ts new file mode 100644 index 0000000000..2330913f51 --- /dev/null +++ b/packages/nx/src/nx-cloud/utilities/url-shorten.ts @@ -0,0 +1,111 @@ +import { logger } from '../../devkit-exports'; +import { getGithubSlugOrNull } from '../../utils/git-utils'; + +export async function shortenedCloudUrl( + installationSource: string, + accessToken: string, + 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 + ); + + const usesGithub = + (githubSlug || github) && + (apiUrl.includes('cloud.nx.app') || + apiUrl.includes('eu.nx.app') || + installationSupportsGitHub); + + const source = getSource(installationSource); + + try { + const response = await require('axios').post( + `${apiUrl}/nx-cloud/onboarding`, + { + type: usesGithub ? 'GITHUB' : 'MANUAL', + source, + accessToken: usesGithub ? null : accessToken, + selectedRepositoryName: githubSlug, + } + ); + + if (!response?.data || response.data.message) { + throw new Error( + response?.data?.message ?? 'Failed to shorten Nx Cloud URL' + ); + } + + return `${apiUrl}/connect/${response.data}`; + } catch (e) { + logger.verbose(`Failed to shorten Nx Cloud URL. + ${e}`); + return getURLifShortenFailed( + usesGithub, + githubSlug, + apiUrl, + accessToken, + source + ); + } +} + +function removeTrailingSlash(apiUrl: string) { + return apiUrl[apiUrl.length - 1] === '/' ? apiUrl.slice(0, -1) : apiUrl; +} + +function getSource( + installationSource: string +): 'nx-init' | 'nx-connect' | 'create-nx-workspace' | 'other' { + if (installationSource.includes('nx-init')) { + return 'nx-init'; + } else if (installationSource.includes('nx-connect')) { + return 'nx-connect'; + } else if (installationSource.includes('create-nx-workspace')) { + return 'create-nx-workspace'; + } else { + return 'other'; + } +} + +function getURLifShortenFailed( + usesGithub: boolean, + githubSlug: string, + apiUrl: string, + accessToken: string, + source: string +) { + if (usesGithub) { + if (githubSlug) { + return `${apiUrl}/setup/connect-workspace/vcs?provider=GITHUB&selectedRepositoryName=${encodeURIComponent( + githubSlug + )}&source=${source}`; + } else { + return `${apiUrl}/setup/connect-workspace/vcs?provider=GITHUB&source=${source}`; + } + } + return `${apiUrl}/setup/connect-workspace/manual?accessToken=${accessToken}&source=${source}`; +} + +async function getInstallationSupportsGitHub(apiUrl: string): Promise { + try { + const response = await require('axios').get(`${apiUrl}/vcs-integrations`); + if (!response?.data || response.data.message) { + throw new Error( + response?.data?.message ?? 'Failed to shorten Nx Cloud URL' + ); + } + return !!response.data.github; + } catch (e) { + if (process.env.NX_VERBOSE_LOGGING) { + logger.warn(`Failed to access vcs-integrations endpoint. + ${e}`); + } + return false; + } +} diff --git a/packages/nx/src/utils/git-utils.spec.ts b/packages/nx/src/utils/git-utils.spec.ts new file mode 100644 index 0000000000..22b9057fdd --- /dev/null +++ b/packages/nx/src/utils/git-utils.spec.ts @@ -0,0 +1,84 @@ +import { extractUserAndRepoFromGitHubUrl } from './git-utils'; + +describe('extractUserAndRepoFromGitHubUrl', () => { + describe('ssh cases', () => { + it('should return the github user + repo info for origin', () => { + expect( + extractUserAndRepoFromGitHubUrl( + ` + upstream git@github.com:upstream-user/repo-name.git (fetch) + upstream git@github.com:upstream-user/repo-name.git (push) + origin git@github.com:origin-user/repo-name.git (fetch) + origin git@github.com:origin-user/repo-name.git (push) + ` + ) + ).toBe('origin-user/repo-name'); + }); + + it('should return the github user + repo info for the first one since no origin', () => { + expect( + extractUserAndRepoFromGitHubUrl( + ` + upstream git@github.com:upstream-user/repo-name.git (fetch) + upstream git@github.com:upstream-user/repo-name.git (push) + other git@github.com:other-user/repo-name.git (fetch) + other git@github.com:other-user/repo-name.git (push) + ` + ) + ).toBe('upstream-user/repo-name'); + }); + + it('should return null since no github', () => { + expect( + extractUserAndRepoFromGitHubUrl( + ` + upstream git@random.com:upstream-user/repo-name.git (fetch) + upstream git@random.com:upstream-user/repo-name.git (push) + origin git@random.com:other-user/repo-name.git (fetch) + origin git@random.com:other-user/repo-name.git (push) + ` + ) + ).toBe(null); + }); + }); + describe('https cases', () => { + it('should return the github user + repo info for origin', () => { + expect( + extractUserAndRepoFromGitHubUrl( + ` + upstream https://github.com/upstream-user/repo-name.git (fetch) + upstream https://github.com/upstream-user/repo-name.git (push) + origin https://github.com/origin-user/repo-name.git (fetch) + origin https://github.com/origin-user/repo-name.git (push) + ` + ) + ).toBe('origin-user/repo-name'); + }); + + it('should return the github user + repo info for the first one since no origin', () => { + expect( + extractUserAndRepoFromGitHubUrl( + ` + upstream https://github.com/upstream-user/repo-name.git (fetch) + upstream https://github.com/upstream-user/repo-name.git (push) + other https://github.com/other-user/repo-name.git (fetch) + other https://github.com/other-user/repo-name.git (push) + ` + ) + ).toBe('upstream-user/repo-name'); + }); + + it('should return null since no github', () => { + expect( + extractUserAndRepoFromGitHubUrl( + ` + upstream https://other.com/upstream-user/repo-name.git (fetch) + upstream https://other.com/upstream-user/repo-name.git (push) + origin https://other.com/other-user/repo-name.git (fetch) + origin https://other.com/other-user/repo-name.git (push) + ` + ) + ).toBe(null); + }); + }); +}); diff --git a/packages/nx/src/utils/git-utils.ts b/packages/nx/src/utils/git-utils.ts new file mode 100644 index 0000000000..3bfaff25f7 --- /dev/null +++ b/packages/nx/src/utils/git-utils.ts @@ -0,0 +1,73 @@ +import { execSync } from 'child_process'; + +export function getGithubSlugOrNull(): string | null { + try { + const gitRemote = execSync('git remote -v').toString(); + return extractUserAndRepoFromGitHubUrl(gitRemote); + } catch (e) { + return null; + } +} + +export function extractUserAndRepoFromGitHubUrl( + gitRemotes: string +): string | null { + const regex = + /^\s*(\w+)\s+(git@github\.com:|https:\/\/github\.com\/)([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git/gm; + let firstGitHubUrl: string | null = null; + let match; + + while ((match = regex.exec(gitRemotes)) !== null) { + const remoteName = match[1]; + const url = match[2] + match[3] + '/' + match[4] + '.git'; + + if (remoteName === 'origin') { + return parseGitHubUrl(url); + } + + if (!firstGitHubUrl) { + firstGitHubUrl = url; + } + } + + return firstGitHubUrl ? parseGitHubUrl(firstGitHubUrl) : null; +} + +function parseGitHubUrl(url: string): string | null { + const sshPattern = + /git@github\.com:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git/; + const httpsPattern = + /https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\.git/; + let match = url.match(sshPattern) || url.match(httpsPattern); + + if (match) { + return `${match[1]}/${match[2]}`; + } + return null; +} + +export function commitChanges(commitMessage: string): string | null { + try { + execSync('git add -A', { encoding: 'utf8', stdio: 'pipe' }); + execSync('git commit --no-verify -F -', { + encoding: 'utf8', + stdio: 'pipe', + input: commitMessage, + }); + } catch (err) { + throw new Error(`Error committing changes:\n${err.stderr}`); + } + + return getLatestCommitSha(); +} + +export function getLatestCommitSha(): string | null { + try { + return execSync('git rev-parse HEAD', { + encoding: 'utf8', + stdio: 'pipe', + }).trim(); + } catch { + return null; + } +}