feat(release): allow projects shorthand for single release group (#20560)

This commit is contained in:
James Henry 2023-12-05 01:58:16 +04:00 committed by GitHub
parent 57cef5d979
commit 16dfccc0de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 173 additions and 45 deletions

View File

@ -11,7 +11,7 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import * as chalk from 'chalk'; import * as chalk from 'chalk';
import { exec } from 'child_process'; 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 { getLatestGitTagForPattern } from 'nx/src/command-line/release/utils/git';
import { import {
resolveSemverSpecifierFromConventionalCommits, resolveSemverSpecifierFromConventionalCommits,
@ -271,7 +271,9 @@ To fix this you will either need to add a package.json file at that location, or
case 'prompt': { case 'prompt': {
// Only add the release group name to the log if it is one set by the user, otherwise it is useless noise // 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 => { 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;
} }
return `${log} within release group "${options.releaseGroup.name}"`; return `${log} within release group "${options.releaseGroup.name}"`;

View File

@ -671,7 +671,7 @@ describe('createNxReleaseConfig()', () => {
}); });
describe('user config -> top level version', () => { 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, { const res = await createNxReleaseConfig(projectGraph, {
version: { version: {
// only modifying options, use default generator // 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', () => { 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, { const res = await createNxReleaseConfig(projectGraph, {
releaseTagPattern: '{projectName}__{version}', releaseTagPattern: '{projectName}__{version}',
}); });

View File

@ -11,10 +11,7 @@
* defaults and user overrides, as well as handling common errors, up front to produce a single, consistent, * 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. * and easy to consume config object for all the `nx release` command implementations.
*/ */
import { import { NxJsonConfiguration } from '../../../config/nx-json';
NxJsonConfiguration,
NxReleaseChangelogConfiguration,
} from '../../../config/nx-json';
import { output, type ProjectGraph } from '../../../devkit-exports'; import { output, type ProjectGraph } from '../../../devkit-exports';
import { findMatchingProjects } from '../../../utils/find-matching-projects'; import { findMatchingProjects } from '../../../utils/find-matching-projects';
import { projectHasTarget } from '../../../utils/project-graph-utils'; import { projectHasTarget } from '../../../utils/project-graph-utils';
@ -38,7 +35,7 @@ type RemoveTrueFromPropertiesOnEach<T, K extends keyof T[keyof T]> = {
[U in keyof T]: RemoveTrueFromProperties<T[U], K>; [U in keyof T]: RemoveTrueFromProperties<T[U], K>;
}; };
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 * 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 * 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). * pattern such as directories and globs).
*/ */
export type NxReleaseConfig = DeepRequired< export type NxReleaseConfig = Omit<
NxJsonConfiguration['release'] & { DeepRequired<
groups: DeepRequired< NxJsonConfiguration['release'] & {
RemoveTrueFromPropertiesOnEach< groups: DeepRequired<
EnsureProjectsArray<NxJsonConfiguration['release']['groups']>, RemoveTrueFromPropertiesOnEach<
'changelog' EnsureProjectsArray<NxJsonConfiguration['release']['groups']>,
> 'changelog'
>; >
// Remove the true shorthand from the changelog config types, it will be normalized to a default object >;
changelog: RemoveTrueFromProperties< // Remove the true shorthand from the changelog config types, it will be normalized to a default object
DeepRequired<NxJsonConfiguration['release']['changelog']>, changelog: RemoveTrueFromProperties<
'workspaceChangelog' | 'projectChangelogs' DeepRequired<NxJsonConfiguration['release']['changelog']>,
>; '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 // We explicitly handle some possible errors in order to provide the best possible DX
export interface CreateNxReleaseConfigError { export interface CreateNxReleaseConfigError {
code: code:
| 'PROJECTS_AND_GROUPS_DEFINED'
| 'RELEASE_GROUP_MATCHES_NO_PROJECTS' | 'RELEASE_GROUP_MATCHES_NO_PROJECTS'
| 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE' | 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE'
| 'PROJECT_MATCHES_MULTIPLE_GROUPS' | 'PROJECT_MATCHES_MULTIPLE_GROUPS'
@ -85,6 +87,16 @@ export async function createNxReleaseConfig(
error: null | CreateNxReleaseConfigError; error: null | CreateNxReleaseConfigError;
nxReleaseConfig: NxReleaseConfig | null; nxReleaseConfig: NxReleaseConfig | null;
}> { }> {
if (userConfig.projects && userConfig.groups) {
return {
error: {
code: 'PROJECTS_AND_GROUPS_DEFINED',
data: {},
},
nxReleaseConfig: null,
};
}
const gitDefaults = { const gitDefaults = {
commit: false, commit: false,
commitMessage: '', commitMessage: '',
@ -212,23 +224,26 @@ export async function createNxReleaseConfig(
const rootVersionWithoutGit = { ...rootVersionConfig }; const rootVersionWithoutGit = { ...rootVersionConfig };
delete rootVersionWithoutGit.git; 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'] = const groups: NxReleaseConfig['groups'] =
userConfig.groups && Object.keys(userConfig.groups).length userConfig.groups && Object.keys(userConfig.groups).length
? ensureProjectsConfigIsArray(userConfig.groups) ? ensureProjectsConfigIsArray(userConfig.groups)
: /** : /**
* No user specified release groups, so we treat all projects as being in one release group * No user specified release groups, so we treat all projects (or any any user-defined subset via the top level "projects" property)
* together in which all projects are released in lock step. * 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, 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 * 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 * 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], [GROUP_DEFAULTS.version],
rootVersionWithoutGit 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: releaseTagPattern:
userConfig.releaseTagPattern || GROUP_DEFAULTS.releaseTagPattern, userConfig.releaseTagPattern || GROUP_DEFAULTS.releaseTagPattern,
// Directly inherit the root level config for projectChangelogs, if set // Directly inherit the root level config for projectChangelogs, if set
@ -390,6 +405,18 @@ export async function handleNxReleaseConfigError(
error: CreateNxReleaseConfigError error: CreateNxReleaseConfigError
): Promise<never> { ): Promise<never> {
switch (error.code) { 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': case 'RELEASE_GROUP_MATCHES_NO_PROJECTS':
{ {
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([ const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
@ -465,14 +492,16 @@ function ensureProjectsConfigIsArray(
for (const [groupName, groupConfig] of Object.entries(groups)) { for (const [groupName, groupConfig] of Object.entries(groups)) {
result[groupName] = { result[groupName] = {
...groupConfig, ...groupConfig,
projects: Array.isArray(groupConfig.projects) projects: ensureArray(groupConfig.projects),
? groupConfig.projects
: [groupConfig.projects],
}; };
} }
return result as NxReleaseConfig['groups']; return result as NxReleaseConfig['groups'];
} }
function ensureArray(value: string | string[]): string[] {
return Array.isArray(value) ? value : [value];
}
function ensureProjectsHaveTarget( function ensureProjectsHaveTarget(
projects: string[], projects: string[],
projectGraph: ProjectGraph, projectGraph: ProjectGraph,

View File

@ -1,5 +1,5 @@
import { type ProjectGraph } from '../../../devkit-exports'; 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'; import { filterReleaseGroups } from './filter-release-groups';
describe('filterReleaseGroups()', () => { 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 = { nxReleaseConfig.groups = {
[CATCH_ALL_RELEASE_GROUP]: { [IMPLICIT_DEFAULT_RELEASE_GROUP]: {
projectsRelationship: 'fixed', projectsRelationship: 'fixed',
projects: ['lib-a', 'lib-a'], projects: ['lib-a', 'lib-a'],
changelog: false, changelog: false,

View File

@ -1,7 +1,7 @@
import { ProjectGraph } from '../../../config/project-graph'; import { ProjectGraph } from '../../../config/project-graph';
import { findMatchingProjects } from '../../../utils/find-matching-projects'; import { findMatchingProjects } from '../../../utils/find-matching-projects';
import { output } from '../../../utils/output'; 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] & { export type ReleaseGroupWithName = NxReleaseConfig['groups'][string] & {
name: string; name: string;
@ -112,10 +112,11 @@ export function filterReleaseGroups(
(rg) => rg.projectsRelationship !== 'independent' (rg) => rg.projectsRelationship !== 'independent'
); );
if (releaseGroupsThatAreNotIndependent.length) { 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 ( if (
releaseGroupsThatAreNotIndependent.length === 1 && releaseGroupsThatAreNotIndependent.length === 1 &&
releaseGroupsThatAreNotIndependent[0].name === CATCH_ALL_RELEASE_GROUP releaseGroupsThatAreNotIndependent[0].name ===
IMPLICIT_DEFAULT_RELEASE_GROUP
) { ) {
return { return {
error: { error: {
@ -143,7 +144,7 @@ export function filterReleaseGroups(
title: `Your filter "${projectsFilter}" matched the following projects:`, title: `Your filter "${projectsFilter}" matched the following projects:`,
bodyLines: matchingProjectsForFilter.map((p) => { bodyLines: matchingProjectsForFilter.map((p) => {
const releaseGroupForProject = filteredProjectToReleaseGroup.get(p); const releaseGroupForProject = filteredProjectToReleaseGroup.get(p);
if (releaseGroupForProject.name === CATCH_ALL_RELEASE_GROUP) { if (releaseGroupForProject.name === IMPLICIT_DEFAULT_RELEASE_GROUP) {
return `- ${p}`; return `- ${p}`;
} }
return `- ${p} (release group "${releaseGroupForProject.name}")`; return `- ${p} (release group "${releaseGroupForProject.name}")`;

View File

@ -139,7 +139,12 @@ export interface NxReleaseGitConfiguration {
*/ */
interface NxReleaseConfiguration { 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. * if they were in a release group together with a fixed relationship.
*/ */
groups?: Record< groups?: Record<