From 5fe8d843960c303eaf0b9112d58091f3be3726fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Jona=C5=A1?= Date: Wed, 20 Apr 2022 20:51:15 +0200 Subject: [PATCH] feat(core): add CI generation to create-nx-workspace (#9611) --- docs/generated/cli/create-nx-workspace.md | 10 + docs/generated/packages/workspace.json | 42 ++++ docs/map.json | 5 + docs/packages.json | 3 +- docs/shared/monorepo-ci-azure.md | 17 +- docs/shared/monorepo-ci-circle-ci.md | 5 +- docs/shared/monorepo-ci-github-actions.md | 2 + packages/create-nx-workspace/bin/ci.ts | 1 + .../bin/create-nx-workspace.ts | 161 +++++++++++--- packages/nx/src/utils/package-manager.ts | 4 + packages/workspace/generators.json | 6 + .../__snapshots__/ci-workflow.spec.ts.snap | 198 ++++++++++++++++++ .../ci-workflow/ci-workflow.spec.ts | 40 ++++ .../src/generators/ci-workflow/ci-workflow.ts | 45 ++++ .../files/azure/azure-pipelines.yml__tmpl__ | 67 ++++++ .../circleci/.circleci/config.yml__tmpl__ | 76 +++++++ .../__workflowFileName__.yml__tmpl__ | 26 +++ .../src/generators/ci-workflow/schema.json | 35 ++++ 18 files changed, 706 insertions(+), 37 deletions(-) create mode 100644 packages/create-nx-workspace/bin/ci.ts create mode 100644 packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap create mode 100644 packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts create mode 100644 packages/workspace/src/generators/ci-workflow/ci-workflow.ts create mode 100644 packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ create mode 100644 packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ create mode 100644 packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ create mode 100644 packages/workspace/src/generators/ci-workflow/schema.json diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index 19c9bbad49976..0ee3eeae27f98 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -31,6 +31,16 @@ Type: string The name of the application when a preset with pregenerated app is selected +### ci + +Type: array + +Choices: [github, circleci, azure] + +Default: [] + +Generate a CI workflow file + ### cli Type: string diff --git a/docs/generated/packages/workspace.json b/docs/generated/packages/workspace.json index 6c7f15bd35e94..1903db84a5a10 100644 --- a/docs/generated/packages/workspace.json +++ b/docs/generated/packages/workspace.json @@ -654,6 +654,48 @@ "aliases": [], "hidden": false, "path": "/packages/workspace/src/generators/npm-package/schema.json" + }, + { + "name": "ci-workflow", + "factory": "./src/generators/ci-workflow/ci-workflow#ciWorkflowGenerator", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "NxWorkspaceCIWorkflow", + "title": "Generate a CI workflow.", + "description": "Generate a CI workflow.", + "cli": "nx", + "type": "object", + "properties": { + "ci": { + "type": "string", + "description": "CI provider.", + "enum": ["github", "circleci", "azure"], + "x-prompt": { + "message": "What is your target CI provider?", + "type": "list", + "items": [ + { "value": "github", "label": "Github Actions" }, + { "value": "circleci", "label": "Circle CI" }, + { "value": "azure", "label": "Azure DevOps" } + ] + } + }, + "name": { + "type": "string", + "description": "Workflow name.", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "How should we name your workflow?", + "pattern": "^[a-zA-Z].*$" + } + }, + "required": ["ci"], + "presets": [] + }, + "description": "Generete a CI workflow.", + "implementation": "/packages/workspace/src/generators/ci-workflow/ci-workflow#ciWorkflowGenerator.ts", + "aliases": [], + "hidden": false, + "path": "/packages/workspace/src/generators/ci-workflow/schema.json" } ], "executors": [ diff --git a/docs/map.json b/docs/map.json index ec812b2044846..656d94a430570 100644 --- a/docs/map.json +++ b/docs/map.json @@ -498,6 +498,11 @@ "id": "workspace-generator", "path": "/packages/workspace/generators/workspace-generator" }, + { + "name": "ci-workflow generator", + "id": "ci-workflow-generator", + "path": "/packages/workspace/generators/ci-workflow" + }, { "name": "convert-to-nx-project generator", "id": "convert-to-nx-project-generator", diff --git a/docs/packages.json b/docs/packages.json index 12d19fec4e779..d9c4c61a27c41 100644 --- a/docs/packages.json +++ b/docs/packages.json @@ -269,7 +269,8 @@ "workspace-generator", "run-commands", "convert-to-nx-project", - "npm-package" + "npm-package", + "ci-workflow" ] } } diff --git a/docs/shared/monorepo-ci-azure.md b/docs/shared/monorepo-ci-azure.md index 78a0596e71366..6a32792e4c023 100644 --- a/docs/shared/monorepo-ci-azure.md +++ b/docs/shared/monorepo-ci-azure.md @@ -40,6 +40,8 @@ variables: value: $(git merge-base $(TARGET_BRANCH) HEAD) ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: value: $(git rev-parse HEAD~1) + - name: HEAD_SHA + value: $(git rev-parse HEAD) jobs: - job: main @@ -88,12 +90,13 @@ variables: value: $(git merge-base $(TARGET_BRANCH) HEAD) ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: value: $(git rev-parse HEAD~1) + - name: HEAD_SHA + value: $(git rev-parse HEAD) jobs: - job: agents strategy: - matrix: - agent: [1, 2, 3] + parallel: 3 displayName: 'Agent $(imageName)' pool: vmImage: 'ubuntu-latest' @@ -109,13 +112,15 @@ jobs: - script: npx nx-cloud start-ci-run - script: npx nx workspace-lint - - script: npx nx format:check - - script: npx nx affected --base=$(BASE_SHA) --target=lint --parallel=3 - - script: npx nx affected --base=$(BASE_SHA) --target=test --parallel=3 --ci --code-coverage - - script: npx nx affected --base=$(BASE_SHA) --target=build --parallel=3 + - script: npx nx format:check --base=$(BASE_SHA) --head=$(HEAD_SHA) + - script: npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=lint --parallel=3 + - script: npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=test --parallel=3 --ci --code-coverage + - script: npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=build --parallel=3 - script: npx nx-cloud stop-all-agents condition: always() ``` +You can also use our [ci-workflow generator](https://nx.app/packages/workspace/generators/ci-workflow) to generate the pipeline file. + Learn more about [configuring your CI](https://nx.app/docs/configuring-ci) environment using Nx Cloud with [Distributed Caching](https://nx.app/docs/distributed-caching) and [Distributed Task Execution](https://nx.app/docs/distributed-execution) in the Nx Cloud docs. diff --git a/docs/shared/monorepo-ci-circle-ci.md b/docs/shared/monorepo-ci-circle-ci.md index 89cae1f0b7353..4e5f5aaed6ded 100644 --- a/docs/shared/monorepo-ci-circle-ci.md +++ b/docs/shared/monorepo-ci-circle-ci.md @@ -71,7 +71,6 @@ jobs: ordinal: type: integer steps: - - run: echo "export NX_RUN_GROUP=\"run-group-$CIRCLE_WORKFLOW_ID\";" >> $BASH_ENV - checkout - run: npm ci - run: @@ -83,7 +82,6 @@ jobs: environment: NX_CLOUD_DISTRIBUTED_EXECUTION: 'true' steps: - - run: echo "export NX_RUN_GROUP=\"run-group-$CIRCLE_WORKFLOW_ID\";" >> $BASH_ENV - checkout - run: npm ci - nx/set-shas @@ -96,6 +94,7 @@ jobs: - run: npx nx affected --base=$NX_BASE --head=$NX_HEAD --target=build --parallel=3 - run: npx nx-cloud stop-all-agents + when: always workflows: build: jobs: @@ -106,4 +105,6 @@ workflows: - main ``` +You can also use our [ci-workflow generator](https://nx.app/packages/workspace/generators/ci-workflow) to generate the configuration file. + Learn more about [configuring your CI](https://nx.app/docs/configuring-ci) environment using Nx Cloud with [Distributed Caching](https://nx.app/docs/distributed-caching) and [Distributed Task Execution](https://nx.app/docs/distributed-execution) in the Nx Cloud docs. diff --git a/docs/shared/monorepo-ci-github-actions.md b/docs/shared/monorepo-ci-github-actions.md index a88c384747596..ba22b36cfe978 100644 --- a/docs/shared/monorepo-ci-github-actions.md +++ b/docs/shared/monorepo-ci-github-actions.md @@ -82,4 +82,6 @@ jobs: number-of-agents: 3 ``` +You can also use our [ci-workflow generator](https://nx.app/packages/workspace/generators/ci-workflow) to generate the workflow file. + Learn more about [configuring your CI](https://nx.app/docs/configuring-ci) environment using Nx Cloud with [Distributed Caching](https://nx.app/docs/distributed-caching) and [Distributed Task Execution](https://nx.app/docs/distributed-execution) in the Nx Cloud docs. diff --git a/packages/create-nx-workspace/bin/ci.ts b/packages/create-nx-workspace/bin/ci.ts new file mode 100644 index 0000000000000..a559af7aa0e9a --- /dev/null +++ b/packages/create-nx-workspace/bin/ci.ts @@ -0,0 +1 @@ +export const ciList = ['github', 'circleci', 'azure'] as const; diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 8817154e7c6b7..9c6f8904aa13e 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -19,6 +19,7 @@ import { deduceDefaultBase } from './default-base'; import { stringifyCollection } from './utils'; import { yargsDecorator } from './decorator'; import chalk = require('chalk'); +import { ciList } from './ci'; type Arguments = { name: string; @@ -30,6 +31,7 @@ type Arguments = { allPrompts: boolean; packageManager: string; defaultBase: string; + ci: string[]; }; enum Preset { @@ -162,6 +164,12 @@ export const commandsObject: yargs.Argv = yargs describe: chalk.dim`Use Nx Cloud`, type: 'boolean', }) + .option('ci', { + describe: `Generate a CI workflow file`, + choices: ciList, + defaultDescription: '[]', + type: 'array', + }) .option('allPrompts', { alias: 'a', describe: chalk.dim`Show all prompts`, @@ -209,6 +217,7 @@ async function main(parsedArgs: yargs.Arguments) { nxCloud, packageManager, defaultBase, + ci, } = parsedArgs; output.log({ @@ -238,6 +247,14 @@ async function main(parsedArgs: yargs.Arguments) { packageManager as PackageManager ); } + if (ci && ci.length) { + await setupCI( + name, + ci, + packageManager as PackageManager, + nxCloud && nxCloudInstallRes.code === 0 + ); + } showNxWarning(name); pointToTutorialAndCourse(preset as Preset); @@ -266,6 +283,7 @@ async function getConfiguration( const packageManager = await determinePackageManager(argv); const defaultBase = await determineDefaultBase(argv); const nxCloud = await determineNxCloud(argv); + const ci = await determineCI(argv, nxCloud); Object.assign(argv, { name, @@ -276,6 +294,7 @@ async function getConfiguration( nxCloud, packageManager, defaultBase, + ci, }); } catch (e) { console.error(e); @@ -517,7 +536,7 @@ async function determineCli( async function determineStyle( preset: Preset, parsedArgs: yargs.Arguments -) { +): Promise { if ( preset === Preset.Apps || preset === Preset.Core || @@ -605,6 +624,75 @@ async function determineStyle( return Promise.resolve(parsedArgs.style); } +async function determineNxCloud( + parsedArgs: yargs.Arguments +): Promise { + if (parsedArgs.nxCloud === undefined) { + return enquirer + .prompt([ + { + name: 'NxCloud', + message: `Use Nx Cloud? (It's free and doesn't require registration.)`, + type: 'select', + choices: [ + { + name: 'Yes', + hint: 'Faster builds, run details, GitHub integration. Learn more at https://nx.app', + }, + + { + name: 'No', + }, + ], + initial: 'Yes' as any, + }, + ]) + .then((a: { NxCloud: 'Yes' | 'No' }) => a.NxCloud === 'Yes'); + } else { + return parsedArgs.nxCloud; + } +} + +async function determineCI( + parsedArgs: yargs.Arguments, + nxCloud: boolean +): Promise { + if (!nxCloud) { + if (parsedArgs.ci && parsedArgs.ci.length > 0) { + output.warn({ + title: 'Invalid CI value', + bodyLines: [ + `CI option only works when Nx Cloud is enabled.`, + `The value provided will be ignored.`, + ], + }); + } + return []; + } + + if (parsedArgs.ci) { + return parsedArgs.ci; + } + + if (parsedArgs.allPrompts) { + return enquirer + .prompt([ + { + name: 'CI', + message: `Autogenerate CI workflow file (multi-select)?`, + type: 'multiselect', + choices: [ + { message: 'GitHub Actions', name: 'github' }, + { message: 'Circle CI', name: 'circleci' }, + { message: 'Azure DevOps', name: 'azure' }, + ], + }, + ]) + .then((a: { CI: string[] }) => a.CI); + } + return []; +} + async function createSandbox(packageManager: string) { const installSpinner = ora( `Installing dependencies with ${packageManager}` @@ -720,6 +808,50 @@ async function setupNxCloud(name: string, packageManager: PackageManager) { } } +async function setupCI( + name: string, + ci: string[], + packageManager: PackageManager, + nxCloudSuccessfullyInstalled: boolean +) { + if (!nxCloudSuccessfullyInstalled) { + output.error({ + title: `CI workflow(s) generation skipped`, + bodyLines: [ + `Nx Cloud was not (successfully) installed`, + `The autogenerated CI workflows require Nx Cloud to be set-up.`, + ], + }); + } + const ciSpinner = ora(`Generating CI workflow(s)`).start(); + try { + const pmc = getPackageManagerCommand(packageManager); + // GENERATE WORKFLOWS HERE based on `ci` and `packageManager` + const res = Promise.allSettled( + ci.map( + async (provider) => + await execAndWait( + `${pmc.exec} nx g @nrwl/workspace:ci-workflow --ci=${provider}`, + path.join(process.cwd(), name) + ) + ) + ); + ciSpinner.succeed('CI workflow(s) have been generated successfully'); + return res; + } catch (e) { + ciSpinner.fail(); + + output.error({ + title: `Nx failed to generate CI workflow(s)`, + bodyLines: mapErrorToBodyLines(e), + }); + + process.exit(1); + } finally { + ciSpinner.stop(); + } +} + function printNxCloudSuccessMessage(nxCloudOut: string) { const bodyLines = nxCloudOut.split('Nx Cloud has been enabled')[1].trim(); output.note({ @@ -758,33 +890,6 @@ function execAndWait(command: string, cwd: string) { }); } -async function determineNxCloud(parsedArgs: yargs.Arguments) { - if (parsedArgs.nxCloud === undefined) { - return enquirer - .prompt([ - { - name: 'NxCloud', - message: `Use Nx Cloud? (It's free and doesn't require registration.)`, - type: 'select', - choices: [ - { - name: 'Yes', - hint: 'Faster builds, run details, GitHub integration. Learn more at https://nx.app', - }, - - { - name: 'No', - }, - ], - initial: 'Yes' as any, - }, - ]) - .then((a: { NxCloud: 'Yes' | 'No' }) => a.NxCloud === 'Yes'); - } else { - return parsedArgs.nxCloud; - } -} - function pointToTutorialAndCourse(preset: Preset) { const title = `First time using Nx? Check out this interactive Nx tutorial.`; switch (preset) { diff --git a/packages/nx/src/utils/package-manager.ts b/packages/nx/src/utils/package-manager.ts index 7f5fe6ff323d7..617bb63585920 100644 --- a/packages/nx/src/utils/package-manager.ts +++ b/packages/nx/src/utils/package-manager.ts @@ -13,6 +13,7 @@ export type PackageManager = 'yarn' | 'pnpm' | 'npm'; export interface PackageManagerCommands { install: string; + ciInstall: string; add: string; addDev: string; addGlobal: string; @@ -50,6 +51,7 @@ export function getPackageManagerCommand( const commands: { [pm in PackageManager]: () => PackageManagerCommands } = { yarn: () => ({ install: 'yarn', + ciInstall: 'yarn --frozen-lockfile', add: 'yarn add -W', addDev: 'yarn add -D -W', addGlobal: 'yarn global add', @@ -66,6 +68,7 @@ export function getPackageManagerCommand( } return { install: 'pnpm install --no-frozen-lockfile', // explicitly disable in case of CI + ciInstall: 'pnpm install --frozen-lockfile', add: 'pnpm add', addDev: 'pnpm add -D', addGlobal: 'pnpm add -g', @@ -80,6 +83,7 @@ export function getPackageManagerCommand( return { install: 'npm install', + ciInstall: 'npm ci', add: 'npm install', addDev: 'npm install -D', addGlobal: 'npm install -g', diff --git a/packages/workspace/generators.json b/packages/workspace/generators.json index 2efa5daf3845e..3d2233f6feb4a 100644 --- a/packages/workspace/generators.json +++ b/packages/workspace/generators.json @@ -141,6 +141,12 @@ "schema": "./src/generators/npm-package/schema.json", "description": "Create a minimal NPM package.", "x-type": "library" + }, + + "ci-workflow": { + "factory": "./src/generators/ci-workflow/ci-workflow#ciWorkflowGenerator", + "schema": "./src/generators/ci-workflow/schema.json", + "description": "Generete a CI workflow." } } } diff --git a/packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap b/packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap new file mode 100644 index 0000000000000..4f4945c78c0b4 --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap @@ -0,0 +1,198 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`lib should generate azure CI config 1`] = ` +"name: build + +trigger: + - main +pr: + - main + +variables: + - name: NX_CLOUD_DISTRIBUTED_EXECUTION + value: 'true' + - name: NX_BRANCH + \${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + value: $(System.PullRequest.PullRequestNumber) + \${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + value: $(Build.SourceBranchName) + - name: TARGET_BRANCH + \${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + value: $[replace(variables['System.PullRequest.TargetBranch'],'refs/heads/','origin/')] + - name: BASE_SHA + \${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + value: $(git merge-base $(TARGET_BRANCH) HEAD) + \${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + value: $(git rev-parse HEAD~1) + - name: HEAD_SHA + value: $(git rev-parse HEAD) + +jobs: + - job: agents + strategy: + parallel: 3 + displayName: 'Nx Cloud Agent' + pool: + vmImage: 'ubuntu-latest' + steps: + - script: yarn --frozen-lockfile + displayName: NPM Install Dependencies + - script: npx nx-cloud start-agent + displayName: 'Start Nx-Cloud agent + + - job: main + displayName: 'Nx Cloud Main' + pool: + vmImage: 'ubuntu-latest' + steps: + - script: yarn --frozen-lockfile + displayName: NPM Install Dependencies + - script: yarn nx-cloud start-ci-run + displayName: Start CI run + - script: yarn nx workspace-lint + displayName: Run workspace lint + - script: yarn nx format:check --base=$(BASE_SHA) --head=$(HEAD_SHA) + displayName: Check format + - script: yarn nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=lint --parallel=3 + displayName: Run lint + - script: yarn nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=test --parallel=3 --ci --code-coverage + displayName: Run test + - script: yarn nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=build --parallel=3 + displayName: Run build + - script: yarn nx-cloud stop-all-agents + condition: always() + displayName: Stop all Nx-Cloud agents +" +`; + +exports[`lib should generate circleci CI config 1`] = ` +"version: 2.1 + +orbs: + nx: nrwl/nx@1.3.0 + +jobs: + agent: + docker: + - image: cimg/node:lts-browsers + parameters: + ordinal: + type: integer + steps: + - checkout + - run: + name: Install dependencies + command: yarn --frozen-lockfile + - run: + name: Start the agent << parameters.ordinal >> + command: yarn nx-cloud start-agent + no_output_timeout: 60m + main: + docker: + - image: cimg/node:lts-browsers + environment: + NX_CLOUD_DISTRIBUTED_EXECUTION: 'true' + steps: + - checkout + - run: + name: Install dependencies + command: yarn --frozen-lockfile + - nx/set-shas: + main-branch-name: 'main' + - run: + name: Initialize the Nx Cloud distributed CI run + command: yarn nx-cloud start-ci-run + - run: + name: Run workspace lint + command: yarn nx workspace-lint + - run: + name: Check format + command: yarn nx format:check --base=$NX_BASE --head=$NX_HEAD + - run: + name: Run lint + command: yarn nx affected --base=$NX_BASE --head=$NX_HEAD --target=lint --parallel=3 + - run: + name: Run test + command: yarn nx affected --base=$NX_BASE --head=$NX_HEAD --target=test --parallel=3 --ci --code-coverage + - run: + name: Run build + command: yarn nx affected --base=$NX_BASE --head=$NX_HEAD --target=build --parallel=3 + - run: + name: Stop all agents + command: yarn nx-cloud stop-all-agents + when: always + +workflows: + version: 2 + + build: + jobs: + - agent: + name: Nx Cloud Agent << matrix.ordinal >> + matrix: + parameters: + ordinal: [1, 2, 3] + - main + name: Nx Cloud Main +" +`; + +exports[`lib should generate github CI config 1`] = ` +"name: build + +on: + push: + branches: + - main + pull_request: + +jobs: + main: + name: Nx Cloud - Main Job + uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.2 + with: + parallel-commands: | + yarn nx workspace-lint + yarn nx format:check + parallel-commands-on-agents: | + yarn nx affected --target=lint --parallel=3 + yarn nx affected --target=test --parallel=3 --ci --code-coverage + yarn nx affected --target=build --parallel=3 + + agents: + name: Nx Cloud - Agents + uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.2 + with: + number-of-agents: 3 +" +`; + +exports[`lib should generate github CI config with custom name 1`] = ` +"name: My custom-workflow + +on: + push: + branches: + - main + pull_request: + +jobs: + main: + name: Nx Cloud - Main Job + uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.2 + with: + parallel-commands: | + yarn nx workspace-lint + yarn nx format:check + parallel-commands-on-agents: | + yarn nx affected --target=lint --parallel=3 + yarn nx affected --target=test --parallel=3 --ci --code-coverage + yarn nx affected --target=build --parallel=3 + + agents: + name: Nx Cloud - Agents + uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.2 + with: + number-of-agents: 3 +" +`; diff --git a/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts b/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts new file mode 100644 index 0000000000000..d5acaf26263bf --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts @@ -0,0 +1,40 @@ +import { Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { ciWorkflowGenerator } from './ci-workflow'; + +describe('lib', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should generate github CI config', async () => { + await ciWorkflowGenerator(tree, { ci: 'github' }); + + expect(tree.read('.github/workflows/build.yml', 'utf-8')).toMatchSnapshot(); + }); + + it('should generate circleci CI config', async () => { + await ciWorkflowGenerator(tree, { ci: 'circleci' }); + + expect(tree.read('.circleci/config.yml', 'utf-8')).toMatchSnapshot(); + }); + + it('should generate azure CI config', async () => { + await ciWorkflowGenerator(tree, { ci: 'azure' }); + + expect(tree.read('azure-pipelines.yml', 'utf-8')).toMatchSnapshot(); + }); + + it('should generate github CI config with custom name', async () => { + await ciWorkflowGenerator(tree, { + ci: 'github', + name: 'My custom-workflow', + }); + + expect( + tree.read('.github/workflows/my-custom-workflow.yml', 'utf-8') + ).toMatchSnapshot(); + }); +}); diff --git a/packages/workspace/src/generators/ci-workflow/ci-workflow.ts b/packages/workspace/src/generators/ci-workflow/ci-workflow.ts new file mode 100644 index 0000000000000..c7328f84448f9 --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/ci-workflow.ts @@ -0,0 +1,45 @@ +import { + Tree, + names, + generateFiles, + joinPathFragments, + getPackageManagerCommand, +} from '@nrwl/devkit'; +import { deduceDefaultBase } from '../../utilities/default-base'; + +export interface Schema { + name?: string; + ci: 'github' | 'azure' | 'circleci'; +} + +export async function ciWorkflowGenerator(host: Tree, schema: Schema) { + const ci = schema.ci; + const options = normalizeOptions(schema); + + generateFiles(host, joinPathFragments(__dirname, 'files', ci), '', options); +} + +interface Substitutes { + mainBranch: string; + workflowName: string; + workflowFileName: string; + packageManagerInstall: string; + packageManagerPrefix: string; + tmpl: ''; +} + +function normalizeOptions(options: Schema): Substitutes { + const { name: workflowName, fileName: workflowFileName } = names( + options.name || 'build' + ); + const { exec: packageManagerPrefix, ciInstall: packageManagerInstall } = + getPackageManagerCommand(); + return { + workflowName, + workflowFileName, + packageManagerInstall, + packageManagerPrefix, + mainBranch: deduceDefaultBase(), + tmpl: '', + }; +} diff --git a/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ new file mode 100644 index 0000000000000..933996dddd53a --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ @@ -0,0 +1,67 @@ +name: <%= workflowName %> + +trigger: + - <%= mainBranch %> +pr: + - <%= mainBranch %> + +variables: + - name: NX_CLOUD_DISTRIBUTED_EXECUTION + value: 'true' + - name: NX_BRANCH + ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + value: $(System.PullRequest.PullRequestNumber) + ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + value: $(Build.SourceBranchName) + - name: TARGET_BRANCH + ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + value: $[replace(variables['System.PullRequest.TargetBranch'],'refs/heads/','origin/')] + - name: BASE_SHA + ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + value: $(git merge-base $(TARGET_BRANCH) HEAD) + ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + value: $(git rev-parse HEAD~1) + - name: HEAD_SHA + value: $(git rev-parse HEAD) + +jobs: + - job: agents + strategy: + parallel: 3 + displayName: 'Nx Cloud Agent' + pool: + vmImage: 'ubuntu-latest' + steps: + <% if(packageManagerPrefix == 'pnpm exec'){ %> + - script: npm install --prefix=$HOME/.local -g pnpm@6.32.4 + displayName: Install PNPM + <% } %>- script: <%= packageManagerInstall %> + displayName: NPM Install Dependencies + - script: npx nx-cloud start-agent + displayName: 'Start Nx-Cloud agent + + - job: main + displayName: 'Nx Cloud Main' + pool: + vmImage: 'ubuntu-latest' + steps: + <% if(packageManagerPrefix == 'pnpm exec'){ %> + - script: npm install --prefix=$HOME/.local -g pnpm@6.32.4 + displayName: Install PNPM + <% } %>- script: <%= packageManagerInstall %> + displayName: NPM Install Dependencies + - script: <%= packageManagerPrefix %> nx-cloud start-ci-run + displayName: Start CI run + - script: <%= packageManagerPrefix %> nx workspace-lint + displayName: Run workspace lint + - script: <%= packageManagerPrefix %> nx format:check --base=$(BASE_SHA) --head=$(HEAD_SHA) + displayName: Check format + - script: <%= packageManagerPrefix %> nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=lint --parallel=3 + displayName: Run lint + - script: <%= packageManagerPrefix %> nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=test --parallel=3 --ci --code-coverage + displayName: Run test + - script: <%= packageManagerPrefix %> nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) --target=build --parallel=3 + displayName: Run build + - script: <%= packageManagerPrefix %> nx-cloud stop-all-agents + condition: always() + displayName: Stop all Nx-Cloud agents diff --git a/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ new file mode 100644 index 0000000000000..004f5b320dead --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ @@ -0,0 +1,76 @@ +version: 2.1 + +orbs: + nx: nrwl/nx@1.3.0 + +jobs: + agent: + docker: + - image: cimg/node:lts-browsers + parameters: + ordinal: + type: integer + steps: + - checkout + <% if(packageManagerPrefix == 'pnpm exec'){ %> + - run: + name: Install PNPM + command: npm install --prefix=$HOME/.local -g pnpm@6.32.4 + <% } %>- run: + name: Install dependencies + command: <%= packageManagerInstall %> + - run: + name: Start the agent << parameters.ordinal >> + command: <%= packageManagerPrefix %> nx-cloud start-agent + no_output_timeout: 60m + main: + docker: + - image: cimg/node:lts-browsers + environment: + NX_CLOUD_DISTRIBUTED_EXECUTION: 'true' + steps: + - checkout + <% if(packageManagerPrefix == 'pnpm exec'){ %> + - run: + name: Install PNPM + command: npm install --prefix=$HOME/.local -g pnpm@6.32.4 + <% } %>- run: + name: Install dependencies + command: <%= packageManagerInstall %> + - nx/set-shas: + main-branch-name: '<%= mainBranch %>' + - run: + name: Initialize the Nx Cloud distributed CI run + command: <%= packageManagerPrefix %> nx-cloud start-ci-run + - run: + name: Run workspace lint + command: <%= packageManagerPrefix %> nx workspace-lint + - run: + name: Check format + command: <%= packageManagerPrefix %> nx format:check --base=$NX_BASE --head=$NX_HEAD + - run: + name: Run lint + command: <%= packageManagerPrefix %> nx affected --base=$NX_BASE --head=$NX_HEAD --target=lint --parallel=3 + - run: + name: Run test + command: <%= packageManagerPrefix %> nx affected --base=$NX_BASE --head=$NX_HEAD --target=test --parallel=3 --ci --code-coverage + - run: + name: Run build + command: <%= packageManagerPrefix %> nx affected --base=$NX_BASE --head=$NX_HEAD --target=build --parallel=3 + - run: + name: Stop all agents + command: <%= packageManagerPrefix %> nx-cloud stop-all-agents + when: always + +workflows: + version: 2 + + <%= workflowFileName %>: + jobs: + - agent: + name: Nx Cloud Agent << matrix.ordinal >> + matrix: + parameters: + ordinal: [1, 2, 3] + - main + name: Nx Cloud Main diff --git a/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ new file mode 100644 index 0000000000000..7f4b2f8de8cf0 --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ @@ -0,0 +1,26 @@ +name: <%= workflowName %> + +on: + push: + branches: + - <%= mainBranch %> + pull_request: + +jobs: + main: + name: Nx Cloud - Main Job + uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.2 + with: + parallel-commands: | + <%= packageManagerPrefix %> nx workspace-lint + <%= packageManagerPrefix %> nx format:check + parallel-commands-on-agents: | + <%= packageManagerPrefix %> nx affected --target=lint --parallel=3 + <%= packageManagerPrefix %> nx affected --target=test --parallel=3 --ci --code-coverage + <%= packageManagerPrefix %> nx affected --target=build --parallel=3 + + agents: + name: Nx Cloud - Agents + uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.2 + with: + number-of-agents: 3 diff --git a/packages/workspace/src/generators/ci-workflow/schema.json b/packages/workspace/src/generators/ci-workflow/schema.json new file mode 100644 index 0000000000000..9d2ff30136eeb --- /dev/null +++ b/packages/workspace/src/generators/ci-workflow/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "NxWorkspaceCIWorkflow", + "title": "Generate a CI workflow.", + "description": "Generate a CI workflow.", + "cli": "nx", + "type": "object", + "properties": { + "ci": { + "type": "string", + "description": "CI provider.", + "enum": ["github", "circleci", "azure"], + "x-prompt": { + "message": "What is your target CI provider?", + "type": "list", + "items": [ + { "value": "github", "label": "Github Actions" }, + { "value": "circleci", "label": "Circle CI" }, + { "value": "azure", "label": "Azure DevOps" } + ] + } + }, + "name": { + "type": "string", + "description": "Workflow name.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "How should we name your workflow?", + "pattern": "^[a-zA-Z].*$" + } + }, + "required": ["ci"] +}