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';
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}"`;

View File

@ -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}',
});

View File

@ -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<T, K extends keyof T[keyof T]> = {
[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
@ -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<NxJsonConfiguration['release']['groups']>,
'changelog'
>
>;
// Remove the true shorthand from the changelog config types, it will be normalized to a default object
changelog: RemoveTrueFromProperties<
DeepRequired<NxJsonConfiguration['release']['changelog']>,
'workspaceChangelog' | 'projectChangelogs'
>;
}
export type NxReleaseConfig = Omit<
DeepRequired<
NxJsonConfiguration['release'] & {
groups: DeepRequired<
RemoveTrueFromPropertiesOnEach<
EnsureProjectsArray<NxJsonConfiguration['release']['groups']>,
'changelog'
>
>;
// Remove the true shorthand from the changelog config types, it will be normalized to a default object
changelog: RemoveTrueFromProperties<
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
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<never> {
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,

View File

@ -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,

View File

@ -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}")`;

View File

@ -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<