Skip to content

Commit

Permalink
feat(release): allow projects shorthand for single release group (#20560
Browse files Browse the repository at this point in the history
)
  • Loading branch information
JamesHenry authored Dec 4, 2023
1 parent 57cef5d commit 16dfccc
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 45 deletions.
6 changes: 4 additions & 2 deletions packages/js/src/generators/release-version/release-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}"`;
Expand Down
95 changes: 93 additions & 2 deletions packages/nx/src/command-line/release/config/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}',
});
Expand Down
95 changes: 62 additions & 33 deletions packages/nx/src/command-line/release/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -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'
Expand All @@ -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: '',
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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()', () => {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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}")`;
Expand Down
7 changes: 6 additions & 1 deletion packages/nx/src/config/nx-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down

0 comments on commit 16dfccc

Please sign in to comment.