feat(release): allow projects shorthand for single release group (#20560)
This commit is contained in:
parent
57cef5d979
commit
16dfccc0de
@ -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}"`;
|
||||||
|
|||||||
@ -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}',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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}")`;
|
||||||
|
|||||||
@ -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<
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user