feat(nx-cloud): new cloud onboarding flow (#26262)
New Nx cloud onboarding flow.   
This commit is contained in:
parent
260562e484
commit
f8239debd0
@ -183,6 +183,14 @@ Type: `string`
|
|||||||
|
|
||||||
Stylesheet type to be used with certain stacks
|
Stylesheet type to be used with certain stacks
|
||||||
|
|
||||||
|
### useGitHub
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Will you be using GitHub as your git hosting provider?
|
||||||
|
|
||||||
### version
|
### version
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|||||||
@ -183,6 +183,14 @@ Type: `string`
|
|||||||
|
|
||||||
Stylesheet type to be used with certain stacks
|
Stylesheet type to be used with certain stacks
|
||||||
|
|
||||||
|
### useGitHub
|
||||||
|
|
||||||
|
Type: `boolean`
|
||||||
|
|
||||||
|
Default: `false`
|
||||||
|
|
||||||
|
Will you be using GitHub as your git hosting provider?
|
||||||
|
|
||||||
### version
|
### version
|
||||||
|
|
||||||
Type: `boolean`
|
Type: `boolean`
|
||||||
|
|||||||
@ -23,6 +23,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Hide formatting logs",
|
"description": "Hide formatting logs",
|
||||||
"x-priority": "internal"
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If the user will be using GitHub as their git hosting provider",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@ -13,12 +13,14 @@ import { yargsDecorator } from './decorator';
|
|||||||
import { getPackageNameFromThirdPartyPreset } from '../src/utils/preset/get-third-party-preset';
|
import { getPackageNameFromThirdPartyPreset } from '../src/utils/preset/get-third-party-preset';
|
||||||
import {
|
import {
|
||||||
determineDefaultBase,
|
determineDefaultBase,
|
||||||
|
determineIfGitHubWillBeUsed,
|
||||||
determineNxCloud,
|
determineNxCloud,
|
||||||
determinePackageManager,
|
determinePackageManager,
|
||||||
} from '../src/internal-utils/prompts';
|
} from '../src/internal-utils/prompts';
|
||||||
import {
|
import {
|
||||||
withAllPrompts,
|
withAllPrompts,
|
||||||
withGitOptions,
|
withGitOptions,
|
||||||
|
withUseGitHub,
|
||||||
withNxCloud,
|
withNxCloud,
|
||||||
withOptions,
|
withOptions,
|
||||||
withPackageManager,
|
withPackageManager,
|
||||||
@ -183,6 +185,7 @@ export const commandsObject: yargs.Argv<Arguments> = yargs
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
}),
|
}),
|
||||||
withNxCloud,
|
withNxCloud,
|
||||||
|
withUseGitHub,
|
||||||
withAllPrompts,
|
withAllPrompts,
|
||||||
withPackageManager,
|
withPackageManager,
|
||||||
withGitOptions
|
withGitOptions
|
||||||
@ -280,13 +283,28 @@ async function normalizeArgsMiddleware(
|
|||||||
|
|
||||||
const packageManager = await determinePackageManager(argv);
|
const packageManager = await determinePackageManager(argv);
|
||||||
const defaultBase = await determineDefaultBase(argv);
|
const defaultBase = await determineDefaultBase(argv);
|
||||||
const nxCloud = await determineNxCloud(argv);
|
if (process.env.NX_NEW_CLOUD_ONBOARDING === 'true') {
|
||||||
|
const nxCloud =
|
||||||
Object.assign(argv, {
|
argv.skipGit === true ? 'skip' : await determineNxCloud(argv);
|
||||||
nxCloud,
|
const useGitHub =
|
||||||
packageManager,
|
nxCloud === 'skip'
|
||||||
defaultBase,
|
? 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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export interface CreateWorkspaceOptions {
|
|||||||
name: string; // Workspace name (e.g. org name)
|
name: string; // Workspace name (e.g. org name)
|
||||||
packageManager: PackageManager; // Package manager to use
|
packageManager: PackageManager; // Package manager to use
|
||||||
nxCloud: NxCloud; // Enable Nx Cloud
|
nxCloud: NxCloud; // Enable Nx Cloud
|
||||||
|
useGitHub?: boolean; // Will you be using GitHub as your git hosting provider?
|
||||||
/**
|
/**
|
||||||
* @description Enable interactive mode with presets
|
* @description Enable interactive mode with presets
|
||||||
* @default true
|
* @default true
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
|
|||||||
defaultBase = 'main',
|
defaultBase = 'main',
|
||||||
commit,
|
commit,
|
||||||
cliName,
|
cliName,
|
||||||
|
useGitHub,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
if (cliName) {
|
if (cliName) {
|
||||||
@ -52,7 +53,12 @@ export async function createWorkspace<T extends CreateWorkspaceOptions>(
|
|||||||
|
|
||||||
let nxCloudInstallRes;
|
let nxCloudInstallRes;
|
||||||
if (nxCloud !== 'skip') {
|
if (nxCloud !== 'skip') {
|
||||||
nxCloudInstallRes = await setupNxCloud(directory, packageManager, nxCloud);
|
nxCloudInstallRes = await setupNxCloud(
|
||||||
|
directory,
|
||||||
|
packageManager,
|
||||||
|
nxCloud,
|
||||||
|
useGitHub
|
||||||
|
);
|
||||||
|
|
||||||
if (nxCloud !== 'yes') {
|
if (nxCloud !== 'yes') {
|
||||||
await setupCI(
|
await setupCI(
|
||||||
|
|||||||
@ -22,6 +22,24 @@ export async function determineNxCloud(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function determineIfGitHubWillBeUsed(
|
||||||
|
nxCloud: NxCloud
|
||||||
|
): Promise<boolean> {
|
||||||
|
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<NxCloud> {
|
async function nxCloudPrompt(key: MessageKey): Promise<NxCloud> {
|
||||||
const { message, choices, initial, fallback, footer, hint } =
|
const { message, choices, initial, fallback, footer, hint } =
|
||||||
messages.getPrompt(key);
|
messages.getPrompt(key);
|
||||||
|
|||||||
@ -15,6 +15,14 @@ export function withNxCloud<T = unknown>(argv: yargs.Argv<T>) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function withUseGitHub<T = unknown>(argv: yargs.Argv<T>) {
|
||||||
|
return argv.option('useGitHub', {
|
||||||
|
describe: chalk.dim`Will you be using GitHub as your git hosting provider?`,
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function withAllPrompts<T = unknown>(argv: yargs.Argv<T>) {
|
export function withAllPrompts<T = unknown>(argv: yargs.Argv<T>) {
|
||||||
return argv.option('allPrompts', {
|
return argv.option('allPrompts', {
|
||||||
alias: 'a',
|
alias: 'a',
|
||||||
|
|||||||
@ -9,15 +9,23 @@ export type NxCloud = 'yes' | 'github' | 'circleci' | 'skip';
|
|||||||
export async function setupNxCloud(
|
export async function setupNxCloud(
|
||||||
directory: string,
|
directory: string,
|
||||||
packageManager: PackageManager,
|
packageManager: PackageManager,
|
||||||
nxCloud: NxCloud
|
nxCloud: NxCloud,
|
||||||
|
useGitHub?: boolean
|
||||||
) {
|
) {
|
||||||
const nxCloudSpinner = ora(`Setting up Nx Cloud`).start();
|
const nxCloudSpinner = ora(`Setting up Nx Cloud`).start();
|
||||||
try {
|
try {
|
||||||
const pmc = getPackageManagerCommand(packageManager);
|
const pmc = getPackageManagerCommand(packageManager);
|
||||||
const res = await execAndWait(
|
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
|
directory
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nxCloud !== 'yes') {
|
if (nxCloud !== 'yes') {
|
||||||
nxCloudSpinner.succeed(
|
nxCloudSpinner.succeed(
|
||||||
'CI workflow with Nx Cloud has been generated successfully'
|
'CI workflow with Nx Cloud has been generated successfully'
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { output } from '../../utils/output';
|
|||||||
import { readNxJson } from '../../config/configuration';
|
import { readNxJson } from '../../config/configuration';
|
||||||
import { FsTree, flushChanges } from '../../generators/tree';
|
import { FsTree, flushChanges } from '../../generators/tree';
|
||||||
import { connectToNxCloud } from '../../nx-cloud/generators/connect-to-nx-cloud/connect-to-nx-cloud';
|
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 { getNxCloudUrl, isNxCloudUsed } from '../../utils/nx-cloud-utils';
|
||||||
import { runNxSync } from '../../utils/child-process';
|
import { runNxSync } from '../../utils/child-process';
|
||||||
import { NxJsonConfiguration } from '../../config/nx-json';
|
import { NxJsonConfiguration } from '../../config/nx-json';
|
||||||
@ -51,14 +52,35 @@ export async function connectToNxCloudIfExplicitlyAsked(
|
|||||||
|
|
||||||
export async function connectToNxCloudCommand(): Promise<boolean> {
|
export async function connectToNxCloudCommand(): Promise<boolean> {
|
||||||
const nxJson = readNxJson();
|
const nxJson = readNxJson();
|
||||||
|
|
||||||
if (isNxCloudUsed(nxJson)) {
|
if (isNxCloudUsed(nxJson)) {
|
||||||
output.log({
|
if (process.env.NX_NEW_CLOUD_ONBOARDING !== 'true') {
|
||||||
title: '✔ This workspace already has Nx Cloud set up',
|
output.log({
|
||||||
bodyLines: [
|
title: '✔ This workspace already has Nx Cloud set up',
|
||||||
'If you have not done so already, connect your workspace to your Nx Cloud account:',
|
bodyLines: [
|
||||||
`- Login at ${getNxCloudUrl(nxJson)} to connect your repository`,
|
'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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +88,7 @@ export async function connectToNxCloudCommand(): Promise<boolean> {
|
|||||||
const callback = await connectToNxCloud(tree, {});
|
const callback = await connectToNxCloud(tree, {});
|
||||||
tree.lock();
|
tree.lock();
|
||||||
flushChanges(workspaceRoot, tree.listChanges());
|
flushChanges(workspaceRoot, tree.listChanges());
|
||||||
callback();
|
await callback();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { output } from '../../utils/output';
|
|||||||
import { getPackageManagerCommand } from '../../utils/package-manager';
|
import { getPackageManagerCommand } from '../../utils/package-manager';
|
||||||
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
|
import { generateDotNxSetup } from './implementation/dot-nx/add-nx-scripts';
|
||||||
import { runNxSync } from '../../utils/child-process';
|
import { runNxSync } from '../../utils/child-process';
|
||||||
import { readJsonFile, writeJsonFile } from '../../utils/fileutils';
|
import { readJsonFile } from '../../utils/fileutils';
|
||||||
import { nxVersion } from '../../utils/versions';
|
import { nxVersion } from '../../utils/versions';
|
||||||
import {
|
import {
|
||||||
addDepsToPackageJson,
|
addDepsToPackageJson,
|
||||||
@ -22,7 +22,6 @@ import { globWithWorkspaceContext } from '../../utils/workspace-context';
|
|||||||
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
|
import { connectExistingRepoToNxCloudPrompt } from '../connect/connect-to-nx-cloud';
|
||||||
import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
|
import { addNxToNpmRepo } from './implementation/add-nx-to-npm-repo';
|
||||||
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
|
import { addNxToMonorepo } from './implementation/add-nx-to-monorepo';
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export interface InitArgs {
|
export interface InitArgs {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import {
|
|||||||
writeJsonFile,
|
writeJsonFile,
|
||||||
} from '../../utils/fileutils';
|
} from '../../utils/fileutils';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { commitChanges } from '../../utils/git-utils';
|
||||||
import {
|
import {
|
||||||
ArrayPackageGroup,
|
ArrayPackageGroup,
|
||||||
NxMigrationsConfiguration,
|
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(
|
async function runNxMigration(
|
||||||
root: string,
|
root: string,
|
||||||
collectionPath: string,
|
collectionPath: string,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ 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';
|
||||||
|
|
||||||
function printCloudConnectionDisabledMessage() {
|
function printCloudConnectionDisabledMessage() {
|
||||||
output.error({
|
output.error({
|
||||||
@ -72,26 +73,52 @@ async function createNxCloudWorkspace(
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printSuccessMessage(url: string) {
|
async function printSuccessMessage(
|
||||||
let origin = 'https://nx.app';
|
url: string,
|
||||||
try {
|
token: string,
|
||||||
origin = new URL(url).origin;
|
installationSource: string,
|
||||||
} catch (e) {}
|
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({
|
output.note({
|
||||||
title: `Your Nx Cloud workspace is public`,
|
title: `Your Nx Cloud workspace is public`,
|
||||||
bodyLines: [
|
bodyLines: [
|
||||||
`To restrict access, connect it to your Nx Cloud account:`,
|
`To restrict access, connect it to your Nx Cloud account:`,
|
||||||
`- Push your changes`,
|
`- Push your changes`,
|
||||||
`- Login at ${origin} to connect your repository`,
|
`- 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 {
|
interface ConnectToNxCloudOptions {
|
||||||
analytics?: boolean;
|
analytics?: boolean;
|
||||||
installationSource?: string;
|
installationSource?: string;
|
||||||
hideFormatLogs?: boolean;
|
hideFormatLogs?: boolean;
|
||||||
|
github?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNxCloudOptionsToNxJson(
|
function addNxCloudOptionsToNxJson(
|
||||||
@ -138,7 +165,13 @@ export async function connectToNxCloud(
|
|||||||
silent: schema.hideFormatLogs,
|
silent: schema.hideFormatLogs,
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => printSuccessMessage(r.url);
|
return async () =>
|
||||||
|
await printSuccessMessage(
|
||||||
|
r.url,
|
||||||
|
r.token,
|
||||||
|
schema.installationSource,
|
||||||
|
schema.github
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Hide formatting logs",
|
"description": "Hide formatting logs",
|
||||||
"x-priority": "internal"
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If the user will be using GitHub as their git hosting provider",
|
||||||
|
"default": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
111
packages/nx/src/nx-cloud/utilities/url-shorten.ts
Normal file
111
packages/nx/src/nx-cloud/utilities/url-shorten.ts
Normal file
@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
84
packages/nx/src/utils/git-utils.spec.ts
Normal file
84
packages/nx/src/utils/git-utils.spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
73
packages/nx/src/utils/git-utils.ts
Normal file
73
packages/nx/src/utils/git-utils.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user