diff --git a/docs/generated/cli/release.md b/docs/generated/cli/release.md index f1f3cd6c88aa3..557f4c3237736 100644 --- a/docs/generated/cli/release.md +++ b/docs/generated/cli/release.md @@ -15,9 +15,9 @@ nx release Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`. -## Options +## Shared Options -### dryRun +### dry-run Type: `boolean` @@ -57,6 +57,46 @@ Show version number ## Subcommands +### Base Command Options + +Create a version and release for the workspace, generate a changelog, and optionally publish the packages + +```shell +nx release [specifier] +``` + +#### Options + +##### help + +Type: `boolean` + +Show help + +##### skip-publish + +Type: `boolean` + +Skip publishing by automatically answering no to the confirmation prompt for publishing + +##### specifier + +Type: `string` + +Exact version or semver keyword to apply to the selected release group. + +##### version + +Type: `boolean` + +Show version number + +##### yes + +Type: `boolean` + +Automatically answer yes to the confirmation prompt for publishing + ### version Create a version and release for one or more applications and libraries @@ -169,6 +209,14 @@ Type: `string` Custom git commit message to use when committing the changes made by this command. {version} will be dynamically interpolated when performing fixed releases, interpolated tags will be appended to the commit body when performing independent releases. +##### git-remote + +Type: `string` + +Default: `origin` + +Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing) + ##### git-tag Type: `boolean` @@ -187,14 +235,6 @@ Type: `string` Custom git tag message to use when tagging the changes made by this command. This defaults to be the same value as the tag itself. -##### gitRemote - -Type: `string` - -Default: `origin` - -Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing) - ##### help Type: `boolean` diff --git a/docs/generated/cli/show.md b/docs/generated/cli/show.md index b4f24626a5376..3597e627d633a 100644 --- a/docs/generated/cli/show.md +++ b/docs/generated/cli/show.md @@ -65,7 +65,7 @@ Show information about "my-app" in a human readable format.: nx show project my-app --json false ``` -## Options +## Shared Options ### help diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index ae1c7064cc3b6..832ad674ca926 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -638,6 +638,14 @@ "children": [], "disableCollapsible": false }, + { + "name": "Manage Releases", + "path": "/core-features/manage-releases", + "id": "manage-releases", + "isExternal": false, + "children": [], + "disableCollapsible": false + }, { "name": "Plugin Features", "path": "/core-features/plugin-features", @@ -730,6 +738,14 @@ "children": [], "disableCollapsible": false }, + { + "name": "Manage Releases", + "path": "/core-features/manage-releases", + "id": "manage-releases", + "isExternal": false, + "children": [], + "disableCollapsible": false + }, { "name": "Plugin Features", "path": "/core-features/plugin-features", diff --git a/docs/generated/manifests/nx.json b/docs/generated/manifests/nx.json index c9f8d0486e26b..d6075de68c1bd 100644 --- a/docs/generated/manifests/nx.json +++ b/docs/generated/manifests/nx.json @@ -870,6 +870,17 @@ "path": "/core-features/integrate-with-editors", "tags": ["integrate-with-editors"] }, + { + "id": "manage-releases", + "name": "Manage Releases", + "description": "Learn how Nx provides tools to help you manage releasing your projects.", + "mediaImage": "", + "file": "shared/core-features/manage-releases", + "itemList": [], + "isExternal": false, + "path": "/core-features/manage-releases", + "tags": ["manage-releases"] + }, { "id": "plugin-features", "name": "Plugin Features", @@ -997,6 +1008,17 @@ "path": "/core-features/integrate-with-editors", "tags": ["integrate-with-editors"] }, + "/core-features/manage-releases": { + "id": "manage-releases", + "name": "Manage Releases", + "description": "Learn how Nx provides tools to help you manage releasing your projects.", + "mediaImage": "", + "file": "shared/core-features/manage-releases", + "itemList": [], + "isExternal": false, + "path": "/core-features/manage-releases", + "tags": ["manage-releases"] + }, "/core-features/plugin-features": { "id": "plugin-features", "name": "Plugin Features", diff --git a/docs/generated/manifests/tags.json b/docs/generated/manifests/tags.json index dcb9a920650f2..7702d9a0b9ddc 100644 --- a/docs/generated/manifests/tags.json +++ b/docs/generated/manifests/tags.json @@ -396,6 +396,15 @@ "path": "/recipes/nx-console/console-troubleshooting" } ], + "manage-releases": [ + { + "description": "Learn how Nx provides tools to help you manage releasing your projects.", + "file": "shared/core-features/manage-releases", + "id": "manage-releases", + "name": "Manage Releases", + "path": "/core-features/manage-releases" + } + ], "use-task-executors": [ { "description": "", diff --git a/docs/generated/packages/nx/documents/release.md b/docs/generated/packages/nx/documents/release.md index f1f3cd6c88aa3..557f4c3237736 100644 --- a/docs/generated/packages/nx/documents/release.md +++ b/docs/generated/packages/nx/documents/release.md @@ -15,9 +15,9 @@ nx release Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`. -## Options +## Shared Options -### dryRun +### dry-run Type: `boolean` @@ -57,6 +57,46 @@ Show version number ## Subcommands +### Base Command Options + +Create a version and release for the workspace, generate a changelog, and optionally publish the packages + +```shell +nx release [specifier] +``` + +#### Options + +##### help + +Type: `boolean` + +Show help + +##### skip-publish + +Type: `boolean` + +Skip publishing by automatically answering no to the confirmation prompt for publishing + +##### specifier + +Type: `string` + +Exact version or semver keyword to apply to the selected release group. + +##### version + +Type: `boolean` + +Show version number + +##### yes + +Type: `boolean` + +Automatically answer yes to the confirmation prompt for publishing + ### version Create a version and release for one or more applications and libraries @@ -169,6 +209,14 @@ Type: `string` Custom git commit message to use when committing the changes made by this command. {version} will be dynamically interpolated when performing fixed releases, interpolated tags will be appended to the commit body when performing independent releases. +##### git-remote + +Type: `string` + +Default: `origin` + +Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing) + ##### git-tag Type: `boolean` @@ -187,14 +235,6 @@ Type: `string` Custom git tag message to use when tagging the changes made by this command. This defaults to be the same value as the tag itself. -##### gitRemote - -Type: `string` - -Default: `origin` - -Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing) - ##### help Type: `boolean` diff --git a/docs/generated/packages/nx/documents/show.md b/docs/generated/packages/nx/documents/show.md index b4f24626a5376..3597e627d633a 100644 --- a/docs/generated/packages/nx/documents/show.md +++ b/docs/generated/packages/nx/documents/show.md @@ -65,7 +65,7 @@ Show information about "my-app" in a human readable format.: nx show project my-app --json false ``` -## Options +## Shared Options ### help diff --git a/docs/map.json b/docs/map.json index e0b3a5a5dc594..b22c4a268069d 100644 --- a/docs/map.json +++ b/docs/map.json @@ -244,6 +244,13 @@ "tags": ["integrate-with-editors"], "file": "shared/core-features/integrate-with-editors" }, + { + "name": "Manage Releases", + "description": "Learn how Nx provides tools to help you manage releasing your projects.", + "id": "manage-releases", + "tags": ["manage-releases"], + "file": "shared/core-features/manage-releases" + }, { "name": "Plugin Features", "id": "plugin-features", diff --git a/docs/shared/core-features/manage-releases.md b/docs/shared/core-features/manage-releases.md new file mode 100644 index 0000000000000..c241f2738d86b --- /dev/null +++ b/docs/shared/core-features/manage-releases.md @@ -0,0 +1,128 @@ +# Manage Releases - `nx release` + +Once you have leveraged Nx's powerful code generation and task running capabilities to build your libraries and applications, you will want to share them with your users. + +Nx provides a set of tools to help you manage your releases called `nx release`. + +> We recommend always starting with --dry-run, because publishing is difficult to undo + +```shell +nx release --dry-run +``` + +## What makes up a release? + +A release can be thought about in three main phases: + +1. **Versioning** - The process of determining the next version of your projects, and updating any projects that depend on them to use the new version. +2. **Changelog** - The process of deriving a changelog from your commit messages, which can be used to communicate the changes to your users. +3. **Publishing** - The process of publishing your projects to a registry, such as npm for TypeScript/JavaScript libraries. + +## Running releases + +The `nx release` command is used to run the release process from end to end. It is a wrapper around the three main phases of a release to provide maximum convenience and ease of use. + +By default if you just run `nx release` it will prompt you for a semver-compatible version number, or semver keyword (such as major, minor, patch, etc.) and then run the three phases of the release process, including publishing. + +As with most Nx commands, when trying it out for the first time, it is strongly recommended to use the `--dry-run` flag to see what changes will be made before actually making them. + +```shell +nx release --dry-run +``` + +{% callout type="note" title="Establishing the previous release" %} +If you are working with a brand new workspace, or one that has never been released before, you will need to establish the previous release before running `nx release`. This is because Nx needs to know what the previous version of your projects was in order to know what to use as the start of the new release's changelog commit range. To do this, run `git tag` with an appropriate initial version. For example, if you have a brand new workspace, you might run `git tag 0.0.0` to establish the initial version. +{% /callout %} + +## Customizing releases + +The `nx release` command is highly customizable. You can customize the versioning, changelog, and publishing phases of the release process independently through a mixture of configuration and CLI arguments. + +The configuration lives in your `nx.json` file under the `"release"` section. + +```jsonc {% fileName="nx.json" %} +{ + // ... more nx.json config + "release": { + // For example, configures nx release to target all projects + // except the one called "ignore-me" + "projects": ["*", "!ignore-me"] + // ... nx release config + } +} +``` + +## Using nx release subcommands independently + +As explained above, `nx release` is a wrapper around the three main phases of a release. + +If you need more advanced or granular control over your release process you can also run these phases independently using the `nx release version`, `nx release changelog`, and `nx release publish` subcommands. + +Each of these subcommands has their own CLI arguments which you can explore using the `--help` flag. + +```shell +nx release version --help +nx release changelog --help +nx release publish --help +``` + +## Using the programmatic API for nx release + +For the maximum control and power over your release process, it is recommended to use the programmatic API for `nx release` in your own custom scripts. + +Here is a full working example of creating a custom script which processes its own CLI arguments (with `--dry-run` true by default) and then calls the `nx release` programmatic API. + +```ts {% fileName="tools/scripts/release.ts" %} +import { + releaseChangelog, + releasePublish, + releaseVersion, +} from 'nx/src/command-line/release'; +import * as yargs from 'yargs'; + +(async () => { + const options = await yargs + .version(false) // don't use the default meaning of version in yargs + .option('version', { + description: + 'Explicit version specifier to use, if overriding conventional commits', + type: 'string', + }) + .option('dryRun', { + alias: 'd', + description: + 'Whether or not to perform a dry-run of the release process, defaults to true', + type: 'boolean', + default: true, + }) + .option('verbose', { + description: + 'Whether or not to enable verbose logging, defaults to false', + type: 'boolean', + default: false, + }) + .parseAsync(); + + const { workspaceVersion, projectsVersionData } = await releaseVersion({ + specifier: options.version, + // stage package.json updates to be committed later by the changelog command + stageChanges: true, + dryRun: options.dryRun, + verbose: options.verbose, + }); + + await releaseChangelog({ + versionData: projectsVersionData, + version: workspaceVersion, + dryRun: options.dryRun, + verbose: options.verbose, + }); + + await releasePublish({ + dryRun: options.dryRun, + verbose: options.verbose, + }); + + process.exit(0); +})(); +``` diff --git a/docs/shared/reference/nx-json.md b/docs/shared/reference/nx-json.md index 8dc972d55b683..2c11963ca83e4 100644 --- a/docs/shared/reference/nx-json.md +++ b/docs/shared/reference/nx-json.md @@ -31,7 +31,25 @@ The following is an expanded example showing all options. Your `nx.json` will li } }, "parallel": 4, - "cacheDirectory": "tmp/my-nx-cache" + "cacheDirectory": "tmp/my-nx-cache", + "release": { + "version": { + "generatorOptions": { + "currentVersionResolver": "git-tag", + "specifierSource": "conventional-commits" + } + }, + "changelog": { + "git": { + "commit": true, + "tag": true + }, + "workspaceChangelog": { + "createRelease": "github" + }, + "projectChangelogs": true + } + } } ``` @@ -259,3 +277,181 @@ As of Nx 17, if you only use one tasks runner, you can specify these properties You can configure `parallel` in `nx.json`, but you can also pass them in the terminal `nx run-many -t test --parallel=5`. + +### Release + +The `release` property in `nx.json` configures the `nx release` command. It is an optional property, as `nx release` is capable of working with zero config, but when present it is used to configure the versioning, changelog, and publishing phases of the release process. + +For more information on how `nx release` works, see [manage releases](/core-features/manage-releases). + +The full list of configuration options available for `"release"` can be found here: [https://github.com/nrwl/nx/blob/master/packages/nx/src/config/nx-json.ts](https://github.com/nrwl/nx/blob/master/packages/nx/src/config/nx-json.ts) under `NxReleaseConfiguration`. + +#### Projects + +If you want to limit the projects that `nx release` targets, you can use the `projects` property in `nx.json` to do so. This property is either a string, or an array of strings. The strings can be project names, glob patterns, directories, tag references or anything else that is supported by the `--projects` filter you may know from other commands such as `nx run`. + +```jsonc {% fileName="nx.json" %} +{ + "release": { + // Here we are configuring nx release to target all projects + // except the one called "ignore-me" + "projects": ["*", "!ignore-me"] + } +} +``` + +#### Projects Relationship + +The `projectsRelationship` property tells Nx whether to release projects independently or together. By default Nx will release all your projects together in lock step, which is an equivalent of `"projectRelationships": "fixed"`. If you want to release projects independently, you can set `"projectsRelationship": "independent"`. + +```jsonc {% fileName="nx.json" %} +{ + "release": { + // Here we are configuring nx release to release projects + // independently, as opposed to the default of "fixed" + "projectsRelationship": "independent" + } +} +``` + +#### Release Tag Pattern + +Optionally override the git/release tag pattern to use. This field is the source of truth for changelog generation and release tagging, as well as for conventional commits parsing. + +It supports interpolating the version as `{version}` and (if releasing independently or forcing project level version control system releases) the project name as `{projectName}` within the string. + +The default `"releaseTagPattern"` for fixed/unified releases is: `v{version}` + +The default `"releaseTagPattern"` for independent releases at the project level is: `{projectName}@v{version}` + +```jsonc {% fileName="nx.json" %} +{ + "release": { + // Here we are configuring nx release to use a custom release + // tag pattern (we have dropped the v prefix from the default) + "releaseTagPattern": "{version}" + } +} +``` + +#### Version + +The `version` property configures the versioning phase of the release process. It is used to determine the next version of your projects, and update any projects that depend on them to use the new version. + +Behind the scenes, the `version` logic is powered by an Nx generator. Out of the box Nx wires up the most widely applicable generator implementation for you, which is `@nx/js:release-version` provided by the `@nx/js` plugin. + +It is therefore a common requirement to be able to tweak the options given to that generator. This can be done by configuring the `release.version.generatorOptions` property in `nx.json`: + +```jsonc {% fileName="nx.json" %} +{ + "release": { + "version": { + "generatorOptions": { + // Here we are configuring the generator to use git tags as the + // source of truth for a project's current version + "currentVersionResolver": "git-tag", + // Here we are configuring the generator to use conventional + // commits as the source of truth for how to determine the + // relevant version bump for the next version + "specifierSource": "conventional-commits" + } + } + } +} +``` + +For a full reference of the available options for the `@nx/js:release-version` generator, see the [release version generator reference](/nx-api/js/generators/release-version). + +#### Changelog + +The `changelog` property configures the changelog phase of the release process. It is used to generate a changelog for your projects, and commit it to your repository. + +There are two types of possible changelog that can be generated: + +- **Workspace Changelog**: A changelog that contains all changes across all projects in your workspace. This is not applicable when releasing projects independently. + +- **Project Changelogs**: A changelog that contains all changes for a given project. + +The `changelog` property is used to configure both of these changelogs. + +##### Workspace Changelog + +The `changelog.workspaceChangelog` property configures the workspace changelog. It is used to determine if and how the workspace changelog is generated. + +```jsonc {% fileName="nx.json" %} +{ + "release": { + "changelog": { + // This disables the workspace changelog + "workspaceChangelog": false + } + } +} +``` + +```jsonc {% fileName="nx.json" %} +{ + "release": { + "changelog": { + "workspaceChangelog": { + // This will create a GitHub release containing the workspace + // changelog contents + "createRelease": "github", + // This will disable creating a workspace CHANGELOG.md file + "file": false + } + } + } +} +``` + +##### Project Changelogs + +The `changelog.projectChangelogs` property configures the project changelogs. It is used to determine if and how the project changelogs are generated. + +```jsonc {% fileName="nx.json" %} +{ + "release": { + "changelog": { + // This enables project changelogs with the default options + "projectChangelogs": true + } + } +} +``` + +```jsonc {% fileName="nx.json" %} +{ + "release": { + "changelog": { + "projectChangelogs": { + // This will create one GitHub release per project containing + // the project changelog contents + "createRelease": "github", + // This will disable creating any project level CHANGELOG.md + // files + "file": false + } + } + } +} +``` + +#### Git + +The `git` property configures the automated git operations that take place as part of the release process. + +```jsonc {% fileName="nx.json" %} +{ + "release": { + "git": { + // This will enable committing any changes (e.g. package.json + // updates, CHANGELOG.md files) to git + "commit": true, + // This will enable create a git for the overall release, or + // one tag per project for independent project releases + "tag": false + } + } +} +``` diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index ea92a51eea16f..1d7421f9a076e 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -40,6 +40,7 @@ - [Automate Updating Dependencies](/core-features/automate-updating-dependencies) - [Enforce Module Boundaries](/core-features/enforce-module-boundaries) - [Integrate with Editors](/core-features/integrate-with-editors) + - [Manage Releases](/core-features/manage-releases) - [Plugin Features](/core-features/plugin-features) - [Use Task Executors](/core-features/plugin-features/use-task-executors) - [Use Code Generators](/core-features/plugin-features/use-code-generators) diff --git a/e2e/release/src/independent-projects.test.ts b/e2e/release/src/independent-projects.test.ts index 98fbf5e412937..1f9063d6f1b18 100644 --- a/e2e/release/src/independent-projects.test.ts +++ b/e2e/release/src/independent-projects.test.ts @@ -649,4 +649,61 @@ describe('nx release - independent projects', () => { `); }); }); + + describe('release command', () => { + beforeEach(() => { + updateJson('nx.json', () => { + return { + release: { + projectsRelationship: 'independent', + releaseTagPattern: '{projectName}@v{version}', + version: { + generatorOptions: { + currentVersionResolver: 'git-tag', + }, + }, + changelog: { + projectChangelogs: {}, + }, + }, + }; + }); + }); + + it('should allow versioning projects independently', async () => { + runCommand(`git tag ${pkg1}@v1.2.0`); + runCommand(`git tag ${pkg2}@v1.4.0`); + runCommand(`git tag ${pkg3}@v1.6.0`); + + const releaseOutput = runCLI(`release patch -y`); + + expect( + releaseOutput.match(new RegExp(`New version 1\.2\.1 written`, 'g')) + .length + ).toEqual(1); + + expect( + releaseOutput.match(new RegExp(`New version 1\.4\.1 written`, 'g')) + .length + ).toEqual(1); + + expect( + releaseOutput.match(new RegExp(`New version 1\.6\.1 written`, 'g')) + .length + ).toEqual(1); + + expect( + releaseOutput.match(new RegExp(`Generating an entry in `, 'g')).length + ).toEqual(3); + + expect( + releaseOutput.match( + new RegExp( + `Successfully ran target nx-release-publish for 3 projects`, + 'g' + ) + ).length + ).toEqual(1); + }); + }); }); diff --git a/e2e/release/src/release.test.ts b/e2e/release/src/release.test.ts index 7473c3122ebad..a9b444527d345 100644 --- a/e2e/release/src/release.test.ts +++ b/e2e/release/src/release.test.ts @@ -874,5 +874,42 @@ ${{ /New version 1101.0.0 written to my-pkg-\d*\/package.json/g ).length ).toEqual(3); + + // Reset the nx release config to something basic for testing the release command + updateJson('nx.json', (nxJson) => { + nxJson.release = { + groups: { + default: { + // @proj/source will be added as a project by the verdaccio setup, but we aren't versioning or publishing it, so we exclude it here + projects: ['*', '!@proj/source'], + releaseTagPattern: 'xx{version}', + }, + }, + }; + return nxJson; + }); + + const releaseOutput = runCLI(`release 1200.0.0 -y`); + + expect( + releaseOutput.match( + new RegExp(`Running release version for project: `, 'g') + ).length + ).toEqual(3); + + expect( + releaseOutput.match( + new RegExp(`Generating an entry in CHANGELOG\.md for v1200\.0\.0`, 'g') + ).length + ).toEqual(1); + + expect( + releaseOutput.match( + new RegExp( + `Successfully ran target nx-release-publish for 3 projects`, + 'g' + ) + ).length + ).toEqual(1); }, 500000); }); diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index 4feadf44e9dc5..c232fa438c045 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -423,8 +423,9 @@ async function generateChangelogForWorkspace( postGitTasks: PostGitTask[] ) { const config = nxReleaseConfig.changelog.workspaceChangelog; + const isEnabled = args.workspaceChangelog ?? config; // The entire feature is disabled at the workspace level, exit early - if (config === false) { + if (isEnabled === false) { return; } @@ -433,7 +434,14 @@ async function generateChangelogForWorkspace( return; } - if (!workspaceChangelogVersion) { + // The user explicitly passed workspaceChangelog=true but does not have a workspace changelog config in nx.json + if (!config) { + throw new Error( + `Workspace changelog is enabled but no configuration was provided. Please provide a workspaceChangelog object in your nx.json` + ); + } + + if (!workspaceChangelogVersion && args.workspaceChangelog) { throw new Error( `Workspace changelog is enabled but no overall version was provided. Please provide an explicit version using --version` ); diff --git a/packages/nx/src/command-line/release/command-object.ts b/packages/nx/src/command-line/release/command-object.ts index 1c2d28bf250a2..c5aaa7f337cae 100644 --- a/packages/nx/src/command-line/release/command-object.ts +++ b/packages/nx/src/command-line/release/command-object.ts @@ -42,6 +42,7 @@ export type ChangelogOptions = NxReleaseArgs & from?: string; interactive?: string; gitRemote?: string; + workspaceChangelog?: boolean; }; export type PublishOptions = NxReleaseArgs & @@ -51,6 +52,11 @@ export type PublishOptions = NxReleaseArgs & otp?: number; }; +export type ReleaseOptions = NxReleaseArgs & { + yes?: boolean; + skipPublish?: boolean; +}; + export const yargsReleaseCommand: CommandModule< Record, NxReleaseArgs @@ -60,6 +66,7 @@ export const yargsReleaseCommand: CommandModule< '**ALPHA**: Orchestrate versioning and publishing of applications and libraries', builder: (yargs) => yargs + .command(releaseCommand) .command(versionCommand) .command(changelogCommand) .command(publishCommand) @@ -80,7 +87,7 @@ export const yargsReleaseCommand: CommandModule< describe: 'Projects to run. (comma/space delimited project names and/or patterns)', }) - .option('dryRun', { + .option('dry-run', { describe: 'Preview the changes without updating files/creating releases', alias: 'd', @@ -116,6 +123,47 @@ export const yargsReleaseCommand: CommandModule< }, }; +const releaseCommand: CommandModule = { + command: '$0 [specifier]', + describe: + 'Create a version and release for the workspace, generate a changelog, and optionally publish the packages', + builder: (yargs) => + yargs + .positional('specifier', { + type: 'string', + describe: + 'Exact version or semver keyword to apply to the selected release group.', + }) + .option('yes', { + type: 'boolean', + alias: 'y', + description: + 'Automatically answer yes to the confirmation prompt for publishing', + }) + .option('skip-publish', { + type: 'boolean', + description: + 'Skip publishing by automatically answering no to the confirmation prompt for publishing', + }) + .check((argv) => { + if (argv.yes !== undefined && argv.skipPublish !== undefined) { + throw new Error( + 'The --yes and --skip-publish options are mutually exclusive, please use one or the other.' + ); + } + return true; + }), + handler: (args) => + import('./release') + .then((m) => m.releaseCLIHandler(args)) + .then((versionDataOrExitCode) => { + if (typeof versionDataOrExitCode === 'number') { + return process.exit(versionDataOrExitCode); + } + process.exit(0); + }), +}; + const versionCommand: CommandModule = { command: 'version [specifier]', aliases: ['v'], @@ -185,7 +233,7 @@ const changelogCommand: CommandModule = { 'Interactively modify changelog markdown contents in your code editor before applying the changes. You can set it to be interactive for all changelogs, or only the workspace level, or only the project level', choices: ['all', 'workspace', 'projects'], }) - .option('gitRemote', { + .option('git-remote', { type: 'string', description: 'Alternate git remote in the form {user}/{repo} on which to create the Github release (useful for testing)', diff --git a/packages/nx/src/command-line/release/index.ts b/packages/nx/src/command-line/release/index.ts index 63aaf53701112..069178938852e 100644 --- a/packages/nx/src/command-line/release/index.ts +++ b/packages/nx/src/command-line/release/index.ts @@ -10,3 +10,7 @@ export { releasePublish } from './publish'; * @public */ export { releaseVersion } from './version'; +/** + * @public + */ +export { release } from './release'; diff --git a/packages/nx/src/command-line/release/release.ts b/packages/nx/src/command-line/release/release.ts new file mode 100644 index 0000000000000..57e154c820f1f --- /dev/null +++ b/packages/nx/src/command-line/release/release.ts @@ -0,0 +1,103 @@ +import { prompt } from 'enquirer'; +import { readNxJson } from '../../config/nx-json'; +import { output } from '../../devkit-exports'; +import { createProjectGraphAsync } from '../../project-graph/project-graph'; +import { handleErrors } from '../../utils/params'; +import { releaseChangelog } from './changelog'; +import { ReleaseOptions, VersionOptions } from './command-object'; +import { + createNxReleaseConfig, + handleNxReleaseConfigError, +} from './config/config'; +import { releasePublish } from './publish'; +import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message'; +import { NxReleaseVersionResult, releaseVersion } from './version'; + +export const releaseCLIHandler = (args: VersionOptions) => + handleErrors(args.verbose, () => release(args)); + +export async function release( + args: ReleaseOptions +): Promise { + const projectGraph = await createProjectGraphAsync({ exitOnError: true }); + const nxJson = readNxJson(); + + if (args.verbose) { + process.env.NX_VERBOSE_LOGGING = 'true'; + } + + const hasVersionGitConfig = + Object.keys(nxJson.release?.version?.git ?? {}).length > 0; + const hasChangelogGitConfig = + Object.keys(nxJson.release?.changelog?.git ?? {}).length > 0; + if (hasVersionGitConfig || hasChangelogGitConfig) { + const jsonConfigErrorPath = hasVersionGitConfig + ? ['release', 'version', 'git'] + : ['release', 'changelog', 'git']; + const nxJsonMessage = await resolveNxJsonConfigErrorMessage( + jsonConfigErrorPath + ); + output.error({ + title: `The 'release' top level command cannot be used with granular git configuration. Instead, configure git options in the 'release.git' property in nx.json.`, + bodyLines: [nxJsonMessage], + }); + process.exit(1); + } + + // Apply default configuration to any optional user configuration + const { error: configError, nxReleaseConfig } = await createNxReleaseConfig( + projectGraph, + nxJson.release, + 'nx-release-publish' + ); + if (configError) { + return await handleNxReleaseConfigError(configError); + } + + const versionResult: NxReleaseVersionResult = await releaseVersion({ + ...args, + // if enabled, committing and tagging will be handled by the changelog + // command, so we should only stage the changes in the version command + stageChanges: nxReleaseConfig.git?.commit, + gitCommit: false, + gitTag: false, + }); + + await releaseChangelog({ + ...args, + versionData: versionResult.projectsVersionData, + version: versionResult.workspaceVersion, + workspaceChangelog: versionResult.workspaceVersion !== undefined, + }); + + let shouldPublish = !!args.yes && !args.skipPublish; + const shouldPromptPublishing = !args.yes && !args.skipPublish && !args.dryRun; + + if (shouldPromptPublishing) { + shouldPublish = await promptForPublish(); + } + + if (shouldPublish) { + await releasePublish(args); + } else { + console.log('Skipped publishing packages.'); + } + + return versionResult; +} + +async function promptForPublish(): Promise { + console.log('\n'); + + const reply = await prompt<{ confirmation: boolean }>([ + { + name: 'confirmation', + message: 'Do you want to publish these versions?', + type: 'confirm', + }, + ]); + + console.log('\n'); + + return reply.confirmation; +} diff --git a/packages/nx/src/command-line/release/version.ts b/packages/nx/src/command-line/release/version.ts index 6fa99b14a3e01..9ca258ed8f0ef 100644 --- a/packages/nx/src/command-line/release/version.ts +++ b/packages/nx/src/command-line/release/version.ts @@ -58,7 +58,7 @@ export interface ReleaseVersionGeneratorSchema { currentVersionResolverMetadata?: Record; } -interface NxReleaseVersionResult { +export interface NxReleaseVersionResult { /** * In one specific (and very common) case, an overall workspace version is relevant, for example when there is * only a single release group in which all projects have a fixed relationship to each other. In this case, the diff --git a/packages/nx/src/config/nx-json.ts b/packages/nx/src/config/nx-json.ts index 1ca77ec58e3f1..13db03fce4d91 100644 --- a/packages/nx/src/config/nx-json.ts +++ b/packages/nx/src/config/nx-json.ts @@ -223,13 +223,13 @@ interface NxReleaseConfiguration { }; /** * Optionally override the git/release tag pattern to use. This field is the source of truth - * for changelog generation and release tagging, as well as for conventional-commits parsing. + * for changelog generation and release tagging, as well as for conventional commits parsing. * * It supports interpolating the version as {version} and (if releasing independently or forcing * project level version control system releases) the project name as {projectName} within the string. * - * The default releaseTagPattern for unified releases is: "v{version}" - * The default releaseTagPattern for releases at the project level is: "{projectName}@v{version}" + * The default releaseTagPattern for fixed/unified releases is: "v{version}" + * The default releaseTagPattern for independent releases at the project level is: "{projectName}@v{version}" */ releaseTagPattern?: string; /** diff --git a/scripts/documentation/generators/generate-cli-data.ts b/scripts/documentation/generators/generate-cli-data.ts index 0450723fc8b48..3bfc4f2ed85a1 100644 --- a/scripts/documentation/generators/generate-cli-data.ts +++ b/scripts/documentation/generators/generate-cli-data.ts @@ -70,10 +70,13 @@ description: "${command.description}" templateLines.push(h2('Subcommands')); for (const subcommand of command.subcommands) { templateLines.push( - h3(subcommand.name), + h3(subcommand.name.replace('$0', 'Base Command Options')), formatDescription(subcommand.description, subcommand.deprecated), codeBlock( - `nx ${command.commandString} ${subcommand.commandString}`, + `nx ${command.commandString} ${subcommand.commandString.replace( + '$0 ', + '' + )}`, 'shell' ), generateOptionsMarkdown(subcommand, 2) diff --git a/scripts/documentation/utils.ts b/scripts/documentation/utils.ts index 7b9d580b54163..0a5af52f36169 100644 --- a/scripts/documentation/utils.ts +++ b/scripts/documentation/utils.ts @@ -231,7 +231,12 @@ export function generateOptionsMarkdown( ): string { const lines: string[] = []; if (Array.isArray(command.options) && !!command.options.length) { - lines.push(h(2 + extraHeadingLevels, 'Options')); + lines.push( + h( + 2 + extraHeadingLevels, + command.subcommands?.length ? 'Shared Options' : 'Options' + ) + ); command.options .sort((a, b) => sortAlphabeticallyFunction(a.name, b.name))