From 16dfccc0de00e15b9e3ebe7c5ec39f25d59c6b2e Mon Sep 17 00:00:00 2001 From: James Henry Date: Tue, 5 Dec 2023 01:58:16 +0400 Subject: [PATCH] feat(release): allow projects shorthand for single release group (#20560) --- .../release-version/release-version.ts | 6 +- .../release/config/config.spec.ts | 95 ++++++++++++++++++- .../src/command-line/release/config/config.ts | 95 ++++++++++++------- .../config/filter-release-groups.spec.ts | 6 +- .../release/config/filter-release-groups.ts | 9 +- packages/nx/src/config/nx-json.ts | 7 +- 6 files changed, 173 insertions(+), 45 deletions(-) diff --git a/packages/js/src/generators/release-version/release-version.ts b/packages/js/src/generators/release-version/release-version.ts index 4a2a4b875f71a..86594f25d25b7 100644 --- a/packages/js/src/generators/release-version/release-version.ts +++ b/packages/js/src/generators/release-version/release-version.ts @@ -11,7 +11,7 @@ import { } from '@nx/devkit'; import * as chalk from 'chalk'; import { exec } from 'child_process'; -import { CATCH_ALL_RELEASE_GROUP } from 'nx/src/command-line/release/config/config'; +import { IMPLICIT_DEFAULT_RELEASE_GROUP } from 'nx/src/command-line/release/config/config'; import { getLatestGitTagForPattern } from 'nx/src/command-line/release/utils/git'; import { resolveSemverSpecifierFromConventionalCommits, @@ -271,7 +271,9 @@ To fix this you will either need to add a package.json file at that location, or case 'prompt': { // Only add the release group name to the log if it is one set by the user, otherwise it is useless noise const maybeLogReleaseGroup = (log: string): string => { - if (options.releaseGroup.name === CATCH_ALL_RELEASE_GROUP) { + if ( + options.releaseGroup.name === IMPLICIT_DEFAULT_RELEASE_GROUP + ) { return log; } return `${log} within release group "${options.releaseGroup.name}"`; diff --git a/packages/nx/src/command-line/release/config/config.spec.ts b/packages/nx/src/command-line/release/config/config.spec.ts index dd9f51ab80901..b12c1052f777a 100644 --- a/packages/nx/src/command-line/release/config/config.spec.ts +++ b/packages/nx/src/command-line/release/config/config.spec.ts @@ -671,7 +671,7 @@ describe('createNxReleaseConfig()', () => { }); describe('user config -> top level version', () => { - it('should respect modifying version at the top level and it should be inherited by the catch all group', async () => { + it('should respect modifying version at the top level and it should be inherited by the implicit default group', async () => { const res = await createNxReleaseConfig(projectGraph, { version: { // only modifying options, use default generator @@ -906,8 +906,99 @@ describe('createNxReleaseConfig()', () => { }); }); + describe('user config -> top level projects', () => { + it('should return an error when both "projects" and "groups" are specified', async () => { + const res = await createNxReleaseConfig(projectGraph, { + projects: ['lib-a'], + groups: { + 'group-1': { + projects: ['lib-a'], + }, + }, + }); + expect(res).toMatchInlineSnapshot(` + { + "error": { + "code": "PROJECTS_AND_GROUPS_DEFINED", + "data": {}, + }, + "nxReleaseConfig": null, + } + `); + }); + + it('should influence the projects configured for the implicit default group', async () => { + const res = await createNxReleaseConfig(projectGraph, { + projects: ['lib-a'], + }); + expect(res).toMatchInlineSnapshot(` + { + "error": null, + "nxReleaseConfig": { + "changelog": { + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "", + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "projectChangelogs": false, + "workspaceChangelog": { + "createRelease": false, + "entryWhenNoChanges": "This was a version bump only, there were no code changes.", + "file": "{workspaceRoot}/CHANGELOG.md", + "renderOptions": { + "includeAuthors": true, + }, + "renderer": "nx/changelog-renderer", + }, + }, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "", + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + "groups": { + "__default__": { + "changelog": false, + "projects": [ + "lib-a", + ], + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "generator": "@nx/js:release-version", + "generatorOptions": {}, + }, + }, + }, + "projectsRelationship": "fixed", + "releaseTagPattern": "v{version}", + "version": { + "generator": "@nx/js:release-version", + "generatorOptions": {}, + "git": { + "commit": false, + "commitArgs": "", + "commitMessage": "", + "tag": false, + "tagArgs": "", + "tagMessage": "", + }, + }, + }, + } + `); + }); + }); + describe('user config -> top level releaseTagPattern', () => { - it('should respect modifying releaseTagPattern at the top level and it should be inherited by the catch all group', async () => { + it('should respect modifying releaseTagPattern at the top level and it should be inherited by the implicit default group', async () => { const res = await createNxReleaseConfig(projectGraph, { releaseTagPattern: '{projectName}__{version}', }); diff --git a/packages/nx/src/command-line/release/config/config.ts b/packages/nx/src/command-line/release/config/config.ts index 35f662f82161b..296af37b1bd6c 100644 --- a/packages/nx/src/command-line/release/config/config.ts +++ b/packages/nx/src/command-line/release/config/config.ts @@ -11,10 +11,7 @@ * defaults and user overrides, as well as handling common errors, up front to produce a single, consistent, * and easy to consume config object for all the `nx release` command implementations. */ -import { - NxJsonConfiguration, - NxReleaseChangelogConfiguration, -} from '../../../config/nx-json'; +import { NxJsonConfiguration } from '../../../config/nx-json'; import { output, type ProjectGraph } from '../../../devkit-exports'; import { findMatchingProjects } from '../../../utils/find-matching-projects'; import { projectHasTarget } from '../../../utils/project-graph-utils'; @@ -38,7 +35,7 @@ type RemoveTrueFromPropertiesOnEach = { [U in keyof T]: RemoveTrueFromProperties; }; -export const CATCH_ALL_RELEASE_GROUP = '__default__'; +export const IMPLICIT_DEFAULT_RELEASE_GROUP = '__default__'; /** * Our source of truth is a deeply required variant of the user-facing config interface, so that command @@ -49,25 +46,30 @@ export const CATCH_ALL_RELEASE_GROUP = '__default__'; * it easier to work with (the user could be specifying a single string, and they can also use any valid matcher * pattern such as directories and globs). */ -export type NxReleaseConfig = DeepRequired< - NxJsonConfiguration['release'] & { - groups: DeepRequired< - RemoveTrueFromPropertiesOnEach< - EnsureProjectsArray, - 'changelog' - > - >; - // Remove the true shorthand from the changelog config types, it will be normalized to a default object - changelog: RemoveTrueFromProperties< - DeepRequired, - 'workspaceChangelog' | 'projectChangelogs' - >; - } +export type NxReleaseConfig = Omit< + DeepRequired< + NxJsonConfiguration['release'] & { + groups: DeepRequired< + RemoveTrueFromPropertiesOnEach< + EnsureProjectsArray, + 'changelog' + > + >; + // Remove the true shorthand from the changelog config types, it will be normalized to a default object + changelog: RemoveTrueFromProperties< + DeepRequired, + 'workspaceChangelog' | 'projectChangelogs' + >; + } + >, + // projects is just a shorthand for the default group's projects configuration, it does not exist in the final config + 'projects' >; // We explicitly handle some possible errors in order to provide the best possible DX export interface CreateNxReleaseConfigError { code: + | 'PROJECTS_AND_GROUPS_DEFINED' | 'RELEASE_GROUP_MATCHES_NO_PROJECTS' | 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE' | 'PROJECT_MATCHES_MULTIPLE_GROUPS' @@ -85,6 +87,16 @@ export async function createNxReleaseConfig( error: null | CreateNxReleaseConfigError; nxReleaseConfig: NxReleaseConfig | null; }> { + if (userConfig.projects && userConfig.groups) { + return { + error: { + code: 'PROJECTS_AND_GROUPS_DEFINED', + data: {}, + }, + nxReleaseConfig: null, + }; + } + const gitDefaults = { commit: false, commitMessage: '', @@ -212,23 +224,26 @@ export async function createNxReleaseConfig( const rootVersionWithoutGit = { ...rootVersionConfig }; delete rootVersionWithoutGit.git; - const allProjects = findMatchingProjects(['*'], projectGraph.nodes).filter( - // only include libs by default when the user has no groups config, - // because the default implementation assumes npm js packages - // and these will usually be libs - (project) => projectGraph.nodes[project].type === 'lib' - ); const groups: NxReleaseConfig['groups'] = userConfig.groups && Object.keys(userConfig.groups).length ? ensureProjectsConfigIsArray(userConfig.groups) : /** - * No user specified release groups, so we treat all projects as being in one release group - * together in which all projects are released in lock step. + * No user specified release groups, so we treat all projects (or any any user-defined subset via the top level "projects" property) + * as being in one release group together in which the projects are released in lock step. */ { - [CATCH_ALL_RELEASE_GROUP]: { + [IMPLICIT_DEFAULT_RELEASE_GROUP]: { projectsRelationship: GROUP_DEFAULTS.projectsRelationship, - projects: allProjects, + projects: userConfig.projects + ? // user-defined top level "projects" config takes priority if set + findMatchingProjects( + ensureArray(userConfig.projects), + projectGraph.nodes + ) + : // default to all library projects in the workspace + findMatchingProjects(['*'], projectGraph.nodes).filter( + (project) => projectGraph.nodes[project].type === 'lib' + ), /** * For properties which are overriding config at the root, we use the root level config as the * default values to merge with so that the group that matches a specific project will always @@ -238,7 +253,7 @@ export async function createNxReleaseConfig( [GROUP_DEFAULTS.version], rootVersionWithoutGit ), - // If the user has set something custom for releaseTagPattern at the top level, respect it for the catch all default group + // If the user has set something custom for releaseTagPattern at the top level, respect it for the implicit default group releaseTagPattern: userConfig.releaseTagPattern || GROUP_DEFAULTS.releaseTagPattern, // Directly inherit the root level config for projectChangelogs, if set @@ -390,6 +405,18 @@ export async function handleNxReleaseConfigError( error: CreateNxReleaseConfigError ): Promise { switch (error.code) { + case 'PROJECTS_AND_GROUPS_DEFINED': + { + const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ + 'release', + 'projects', + ]); + output.error({ + title: `"projects" is not valid when explicitly defining release groups, and everything should be expressed within "groups" in that case. If you are using "groups" then you should remove the "projects" property`, + bodyLines: [nxJsonMessage], + }); + } + break; case 'RELEASE_GROUP_MATCHES_NO_PROJECTS': { const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ @@ -465,14 +492,16 @@ function ensureProjectsConfigIsArray( for (const [groupName, groupConfig] of Object.entries(groups)) { result[groupName] = { ...groupConfig, - projects: Array.isArray(groupConfig.projects) - ? groupConfig.projects - : [groupConfig.projects], + projects: ensureArray(groupConfig.projects), }; } return result as NxReleaseConfig['groups']; } +function ensureArray(value: string | string[]): string[] { + return Array.isArray(value) ? value : [value]; +} + function ensureProjectsHaveTarget( projects: string[], projectGraph: ProjectGraph, diff --git a/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts b/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts index 06e1bb1f76fdd..8ce0de5f7ff2a 100644 --- a/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts +++ b/packages/nx/src/command-line/release/config/filter-release-groups.spec.ts @@ -1,5 +1,5 @@ import { type ProjectGraph } from '../../../devkit-exports'; -import { CATCH_ALL_RELEASE_GROUP, NxReleaseConfig } from './config'; +import { IMPLICIT_DEFAULT_RELEASE_GROUP, NxReleaseConfig } from './config'; import { filterReleaseGroups } from './filter-release-groups'; describe('filterReleaseGroups()', () => { @@ -201,9 +201,9 @@ describe('filterReleaseGroups()', () => { `); }); - it('should produce an appropriately formatted error for the CATCH_ALL_RELEASE_GROUP', () => { + it('should produce an appropriately formatted error for the IMPLICIT_DEFAULT_RELEASE_GROUP', () => { nxReleaseConfig.groups = { - [CATCH_ALL_RELEASE_GROUP]: { + [IMPLICIT_DEFAULT_RELEASE_GROUP]: { projectsRelationship: 'fixed', projects: ['lib-a', 'lib-a'], changelog: false, diff --git a/packages/nx/src/command-line/release/config/filter-release-groups.ts b/packages/nx/src/command-line/release/config/filter-release-groups.ts index a6441160a6db8..4cb92b66670bd 100644 --- a/packages/nx/src/command-line/release/config/filter-release-groups.ts +++ b/packages/nx/src/command-line/release/config/filter-release-groups.ts @@ -1,7 +1,7 @@ import { ProjectGraph } from '../../../config/project-graph'; import { findMatchingProjects } from '../../../utils/find-matching-projects'; import { output } from '../../../utils/output'; -import { CATCH_ALL_RELEASE_GROUP, NxReleaseConfig } from './config'; +import { IMPLICIT_DEFAULT_RELEASE_GROUP, NxReleaseConfig } from './config'; export type ReleaseGroupWithName = NxReleaseConfig['groups'][string] & { name: string; @@ -112,10 +112,11 @@ export function filterReleaseGroups( (rg) => rg.projectsRelationship !== 'independent' ); if (releaseGroupsThatAreNotIndependent.length) { - // Special handling for CATCH_ALL_RELEASE_GROUP (which the user did not explicitly configure) + // Special handling for IMPLICIT_DEFAULT_RELEASE_GROUP if ( releaseGroupsThatAreNotIndependent.length === 1 && - releaseGroupsThatAreNotIndependent[0].name === CATCH_ALL_RELEASE_GROUP + releaseGroupsThatAreNotIndependent[0].name === + IMPLICIT_DEFAULT_RELEASE_GROUP ) { return { error: { @@ -143,7 +144,7 @@ export function filterReleaseGroups( title: `Your filter "${projectsFilter}" matched the following projects:`, bodyLines: matchingProjectsForFilter.map((p) => { const releaseGroupForProject = filteredProjectToReleaseGroup.get(p); - if (releaseGroupForProject.name === CATCH_ALL_RELEASE_GROUP) { + if (releaseGroupForProject.name === IMPLICIT_DEFAULT_RELEASE_GROUP) { return `- ${p}`; } return `- ${p} (release group "${releaseGroupForProject.name}")`; diff --git a/packages/nx/src/config/nx-json.ts b/packages/nx/src/config/nx-json.ts index 1db6e925e8296..1ca77ec58e3f1 100644 --- a/packages/nx/src/config/nx-json.ts +++ b/packages/nx/src/config/nx-json.ts @@ -139,7 +139,12 @@ export interface NxReleaseGitConfiguration { */ interface NxReleaseConfiguration { /** - * @note: When no groups are configured at all (the default), all projects in the workspace are treated as + * Shorthand for amending the projects which will be included in the implicit default release group (all projects by default). + * @note Only one of `projects` or `groups` can be specified, the cannot be used together. + */ + projects?: string[] | string; + /** + * @note When no projects or groups are configured at all (the default), all projects in the workspace are treated as * if they were in a release group together with a fixed relationship. */ groups?: Record<