fix(release): allow version plans to have multi-line, arbitrarily formatted messages (#27323)

This commit is contained in:
James Henry 2024-08-08 01:27:34 +04:00 committed by GitHub
parent 555182353b
commit 7e2266177d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 276 additions and 187 deletions

View File

@ -114,7 +114,7 @@ describe('nx release version plans', () => {
await ensureDir(versionPlansDir); await ensureDir(versionPlansDir);
runCLI( runCLI(
'release plan minor -g fixed-group -m "feat: Update the fixed packages with a minor release." --verbose', 'release plan minor -g fixed-group -m "Update the fixed packages with a minor release." --verbose',
{ {
silenceError: true, silenceError: true,
} }
@ -128,7 +128,9 @@ ${pkg4}: preminor
${pkg5}: prerelease ${pkg5}: prerelease
--- ---
feat: Update the independent packages with a patch, preminor, and prerelease. Update the independent packages with a patch, preminor, and prerelease.
Here is another line in the message.
` `
); );
@ -193,9 +195,11 @@ feat: Update the independent packages with a patch, preminor, and prerelease.
+ ## 0.0.1 (YYYY-MM-DD) + ## 0.0.1 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features + ### 🩹 Fixes
+ +
+ - Update the independent packages with a patch, preminor, and prerelease.` + - **${pkg3}:** Update the independent packages with a patch, preminor, and prerelease.
+
+ Here is another line in the message.`
); );
expect(resultWithoutDate).toContain( expect(resultWithoutDate).toContain(
@ -207,7 +211,9 @@ feat: Update the independent packages with a patch, preminor, and prerelease.
+ +
+ ### 🚀 Features + ### 🚀 Features
+ +
+ - Update the independent packages with a patch, preminor, and prerelease.` + - **${pkg4}:** Update the independent packages with a patch, preminor, and prerelease.
+
+ Here is another line in the message.`
); );
expect(resultWithoutDate).toContain( expect(resultWithoutDate).toContain(
@ -217,9 +223,11 @@ feat: Update the independent packages with a patch, preminor, and prerelease.
+ ## 0.0.1-0 (YYYY-MM-DD) + ## 0.0.1-0 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features + ### 🩹 Fixes
+ +
+ - Update the independent packages with a patch, preminor, and prerelease.` + - **${pkg5}:** Update the independent packages with a patch, preminor, and prerelease.
+
+ Here is another line in the message.`
); );
await writeFile( await writeFile(
@ -229,7 +237,7 @@ ${pkg1}: minor
${pkg3}: patch ${pkg3}: patch
--- ---
fix: Update packages in both groups with a bug fix Update packages in both groups with a mix #1
` `
); );
await writeFile( await writeFile(
@ -240,7 +248,7 @@ ${pkg4}: preminor
${pkg5}: patch ${pkg5}: patch
--- ---
feat: Update packages in both groups with a feat Update packages in both groups with a mix #2
` `
); );
@ -291,12 +299,12 @@ feat: Update packages in both groups with a feat
+ +
+ ### 🚀 Features + ### 🚀 Features
+ +
+ - Update packages in both groups with a feat + - **${pkg1}:** Update packages in both groups with a mix #1
+ +
+ +
+ ### 🩹 Fixes + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a bug fix` + - Update packages in both groups with a mix #2`
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
`NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0 `NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0
@ -306,14 +314,9 @@ feat: Update packages in both groups with a feat
+ ## 0.2.0 (YYYY-MM-DD) + ## 0.2.0 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features
+
+ - Update packages in both groups with a feat
+
+
+ ### 🩹 Fixes + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a bug fix + - Update packages in both groups with a mix #2
` `
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
@ -326,7 +329,7 @@ feat: Update packages in both groups with a feat
+ +
+ ### 🩹 Fixes + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a bug fix` + - **${pkg3}:** Update packages in both groups with a mix #1`
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
@ -339,7 +342,7 @@ feat: Update packages in both groups with a feat
+ +
+ ### 🚀 Features + ### 🚀 Features
+ +
+ - Update packages in both groups with a feat` + - **${pkg4}:** Update packages in both groups with a mix #2`
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
@ -350,9 +353,9 @@ feat: Update packages in both groups with a feat
+ ## 0.0.1 (YYYY-MM-DD) + ## 0.0.1 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a feat` + - **${pkg5}:** Update packages in both groups with a mix #2`
); );
expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy(); expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy();
@ -394,7 +397,7 @@ feat: Update packages in both groups with a feat
fixed-group: minor fixed-group: minor
--- ---
feat: Update the fixed packages with a minor release. Update the fixed packages with a minor release.
` `
); );
@ -406,7 +409,7 @@ ${pkg4}: preminor
${pkg5}: prerelease ${pkg5}: prerelease
--- ---
feat: Update the independent packages with a patch, preminor, and prerelease. Update the independent packages with a patch, preminor, and prerelease.
` `
); );
@ -530,9 +533,9 @@ const yargs = require('yargs');
+ ## 0.0.1 (YYYY-MM-DD) + ## 0.0.1 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features + ### 🩹 Fixes
+ +
+ - Update the independent packages with a patch, preminor, and prerelease.` + - **${pkg3}:** Update the independent packages with a patch, preminor, and prerelease.`
); );
expect(resultWithoutDate).toContain( expect(resultWithoutDate).toContain(
@ -544,7 +547,7 @@ const yargs = require('yargs');
+ +
+ ### 🚀 Features + ### 🚀 Features
+ +
+ - Update the independent packages with a patch, preminor, and prerelease.` + - **${pkg4}:** Update the independent packages with a patch, preminor, and prerelease.`
); );
expect(resultWithoutDate).toContain( expect(resultWithoutDate).toContain(
@ -554,9 +557,9 @@ const yargs = require('yargs');
+ ## 0.0.1-0 (YYYY-MM-DD) + ## 0.0.1-0 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features + ### 🩹 Fixes
+ +
+ - Update the independent packages with a patch, preminor, and prerelease.` + - **${pkg5}:** Update the independent packages with a patch, preminor, and prerelease.`
); );
expect(exists(join(versionPlansDir, 'bump-fixed.md'))).toBeFalsy(); expect(exists(join(versionPlansDir, 'bump-fixed.md'))).toBeFalsy();
@ -569,8 +572,8 @@ ${pkg1}: minor
${pkg3}: patch ${pkg3}: patch
--- ---
fix: Update packages in both groups with a bug fix Update packages in both groups with a mix #1
` `
); );
await writeFile( await writeFile(
join(versionPlansDir, 'bump-mixed2.md'), join(versionPlansDir, 'bump-mixed2.md'),
@ -580,8 +583,8 @@ ${pkg4}: preminor
${pkg5}: patch ${pkg5}: patch
--- ---
feat: Update packages in both groups with a feat Update packages in both groups with a mix #2
` `
); );
await runCommandAsync(`git add ${join(versionPlansDir, 'bump-mixed1.md')}`); await runCommandAsync(`git add ${join(versionPlansDir, 'bump-mixed1.md')}`);
@ -631,12 +634,12 @@ feat: Update packages in both groups with a feat
+ +
+ ### 🚀 Features + ### 🚀 Features
+ +
+ - Update packages in both groups with a feat + - **${pkg1}:** Update packages in both groups with a mix #1
+ +
+ +
+ ### 🩹 Fixes + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a bug fix` + - Update packages in both groups with a mix #2`
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
`NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0 `NX Generating an entry in ${pkg2}/CHANGELOG.md for v0.2.0
@ -646,14 +649,9 @@ feat: Update packages in both groups with a feat
+ ## 0.2.0 (YYYY-MM-DD) + ## 0.2.0 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features
+
+ - Update packages in both groups with a feat
+
+
+ ### 🩹 Fixes + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a bug fix + - Update packages in both groups with a mix #2
` `
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
@ -666,7 +664,7 @@ feat: Update packages in both groups with a feat
+ +
+ ### 🩹 Fixes + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a bug fix` + - **${pkg3}:** Update packages in both groups with a mix #1`
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
@ -679,7 +677,7 @@ feat: Update packages in both groups with a feat
+ +
+ ### 🚀 Features + ### 🚀 Features
+ +
+ - Update packages in both groups with a feat` + - **${pkg4}:** Update packages in both groups with a mix #2`
); );
expect(result2WithoutDate).toContain( expect(result2WithoutDate).toContain(
@ -690,9 +688,9 @@ feat: Update packages in both groups with a feat
+ ## 0.0.1 (YYYY-MM-DD) + ## 0.0.1 (YYYY-MM-DD)
+ +
+ +
+ ### 🚀 Features + ### 🩹 Fixes
+ +
+ - Update packages in both groups with a feat` + - **${pkg5}:** Update packages in both groups with a mix #2`
); );
expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy(); expect(exists(join(versionPlansDir, 'bump-mixed1.md'))).toBeFalsy();
@ -715,7 +713,7 @@ feat: Update packages in both groups with a feat
await ensureDir(versionPlansDir); await ensureDir(versionPlansDir);
runCLI( runCLI(
'release plan minor -m "feat: Update the fixed packages with a minor release." --verbose', 'release plan minor -m "Update the fixed packages with a minor release." --verbose',
{ {
silenceError: true, silenceError: true,
} }

View File

@ -404,14 +404,30 @@ function formatChange(
changelogRenderOptions: DefaultChangelogRenderOptions, changelogRenderOptions: DefaultChangelogRenderOptions,
repoSlug?: RepoSlug repoSlug?: RepoSlug
): string { ): string {
let description = change.description;
let extraLines = [];
let extraLinesStr = '';
if (description.includes('\n')) {
[description, ...extraLines] = description.split('\n');
// Align the extra lines with the start of the description for better readability
const indentation = ' ';
extraLinesStr = extraLines
.filter((l) => l.trim().length > 0)
.map((l) => `${indentation}${l}`)
.join('\n');
}
let changeLine = let changeLine =
'- ' + '- ' +
(change.isBreaking ? '⚠️ ' : '') + (change.isBreaking ? '⚠️ ' : '') +
(change.scope ? `**${change.scope.trim()}:** ` : '') + (change.scope ? `**${change.scope.trim()}:** ` : '') +
change.description; description;
if (repoSlug && changelogRenderOptions.commitReferences) { if (repoSlug && changelogRenderOptions.commitReferences) {
changeLine += formatReferences(change.githubReferences, repoSlug); changeLine += formatReferences(change.githubReferences, repoSlug);
} }
if (extraLinesStr) {
changeLine += '\n\n' + extraLinesStr;
}
return changeLine; return changeLine;
} }

View File

@ -2,7 +2,7 @@ import * as chalk from 'chalk';
import { prompt } from 'enquirer'; import { prompt } from 'enquirer';
import { removeSync } from 'fs-extra'; import { removeSync } from 'fs-extra';
import { readFileSync, writeFileSync } from 'node:fs'; import { readFileSync, writeFileSync } from 'node:fs';
import { valid } from 'semver'; import { ReleaseType, valid } from 'semver';
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import type { DependencyBump } from '../../../release/changelog-renderer'; import type { DependencyBump } from '../../../release/changelog-renderer';
import { import {
@ -56,7 +56,6 @@ import {
gitPush, gitPush,
gitTag, gitTag,
parseCommits, parseCommits,
parseConventionalCommitsMessage,
} from './utils/git'; } from './utils/git';
import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github'; import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github';
import { launchEditor } from './utils/launch-editor'; import { launchEditor } from './utils/launch-editor';
@ -281,30 +280,37 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
const releaseGroup = releaseGroups[0]; const releaseGroup = releaseGroups[0];
if (releaseGroup.projectsRelationship === 'fixed') { if (releaseGroup.projectsRelationship === 'fixed') {
const versionPlans = releaseGroup.versionPlans as GroupVersionPlan[]; const versionPlans = releaseGroup.versionPlans as GroupVersionPlan[];
workspaceChangelogChanges = filterHiddenChanges( workspaceChangelogChanges = versionPlans
versionPlans .flatMap((vp) => {
.map((vp) => { const releaseType = versionPlanSemverReleaseTypeToChangelogType(
const parsedMessage = parseConventionalCommitsMessage( vp.groupVersionBump
vp.message );
); const changes: ChangelogChange | ChangelogChange[] =
!vp.triggeredByProjects
// only properly formatted conventional commits messages will be included in the changelog ? {
if (!parsedMessage) { type: releaseType.type,
return null; scope: '',
} description: vp.message,
body: '',
return <ChangelogChange>{ isBreaking: releaseType.isBreaking,
type: parsedMessage.type, githubReferences: [],
scope: parsedMessage.scope, affectedProjects: '*',
description: parsedMessage.description, }
body: '', : vp.triggeredByProjects.map((project) => {
isBreaking: parsedMessage.breaking, return {
githubReferences: [], type: releaseType.type,
}; scope: project,
}) description: vp.message,
.filter(Boolean), body: '',
nxReleaseConfig.conventionalCommits // TODO: what about github references?
); isBreaking: releaseType.isBreaking,
githubReferences: [],
affectedProjects: [project],
};
});
return changes;
})
.filter(Boolean);
} }
} }
} else { } else {
@ -485,31 +491,26 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
let commits: GitCommit[]; let commits: GitCommit[];
if (releaseGroup.versionPlans) { if (releaseGroup.versionPlans) {
changes = filterHiddenChanges( changes = (releaseGroup.versionPlans as ProjectsVersionPlan[])
(releaseGroup.versionPlans as ProjectsVersionPlan[]) .map((vp) => {
.map((vp) => { const bumpForProject = vp.projectVersionBumps[project.name];
const parsedMessage = parseConventionalCommitsMessage( if (!bumpForProject) {
vp.message return null;
); }
const releaseType =
// only properly formatted conventional commits messages will be included in the changelog versionPlanSemverReleaseTypeToChangelogType(bumpForProject);
if (!parsedMessage) { return {
return null; type: releaseType.type,
} scope: project.name,
description: vp.message,
return { body: '',
type: parsedMessage.type, isBreaking: releaseType.isBreaking,
scope: parsedMessage.scope, affectedProjects: Object.keys(vp.projectVersionBumps),
description: parsedMessage.description, // TODO: can we include github references when using version plans?
body: '', githubReferences: [],
isBreaking: parsedMessage.breaking, };
affectedProjects: Object.keys(vp.projectVersionBumps), })
githubReferences: [], .filter(Boolean);
};
})
.filter(Boolean),
nxReleaseConfig.conventionalCommits
);
} else { } else {
let fromRef = let fromRef =
args.from || args.from ||
@ -637,31 +638,37 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
// TODO: remove this after the changelog renderer is refactored to remove coupling with git commits // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits
let commits: GitCommit[] = []; let commits: GitCommit[] = [];
if (releaseGroup.versionPlans) { if (releaseGroup.versionPlans) {
changes = filterHiddenChanges( changes = (releaseGroup.versionPlans as GroupVersionPlan[])
(releaseGroup.versionPlans as GroupVersionPlan[]) .flatMap((vp) => {
.map((vp) => { const releaseType = versionPlanSemverReleaseTypeToChangelogType(
const parsedMessage = parseConventionalCommitsMessage( vp.groupVersionBump
vp.message );
); const changes: ChangelogChange | ChangelogChange[] =
!vp.triggeredByProjects
// only properly formatted conventional commits messages will be included in the changelog ? {
if (!parsedMessage) { type: releaseType.type,
return null; scope: '',
} description: vp.message,
body: '',
return <ChangelogChange>{ isBreaking: releaseType.isBreaking,
type: parsedMessage.type, githubReferences: [],
scope: parsedMessage.scope, affectedProjects: '*',
description: parsedMessage.description, }
body: '', : vp.triggeredByProjects.map((project) => {
isBreaking: parsedMessage.breaking, return {
githubReferences: [], type: releaseType.type,
affectedProjects: '*', scope: project,
}; description: vp.message,
}) body: '',
.filter(Boolean), // TODO: what about github references?
nxReleaseConfig.conventionalCommits isBreaking: releaseType.isBreaking,
); githubReferences: [],
affectedProjects: [project],
};
});
return changes;
})
.filter(Boolean);
} else { } else {
let fromRef = let fromRef =
args.from || args.from ||
@ -1408,3 +1415,23 @@ function createFileToProjectMap(
} }
return fileToProjectMap; return fileToProjectMap;
} }
function versionPlanSemverReleaseTypeToChangelogType(bump: ReleaseType): {
type: 'fix' | 'feat';
isBreaking: boolean;
} {
switch (bump) {
case 'premajor':
case 'major':
return { type: 'feat', isBreaking: true };
case 'preminor':
case 'minor':
return { type: 'feat', isBreaking: false };
case 'prerelease':
case 'prepatch':
case 'patch':
return { type: 'fix', isBreaking: false };
default:
throw new Error(`Invalid semver bump type: ${bump}`);
}
}

View File

@ -4,3 +4,5 @@ pkg4: minor
--- ---
This is a change to packages 3 and 4 This is a change to packages 3 and 4
...and it includes multiple lines of text

View File

@ -75,7 +75,8 @@ describe('version-plans', () => {
pkg1: patch, pkg1: patch,
}, },
fileName: plan1.md, fileName: plan1.md,
message: This is a change to just package 1, message: This is a change to just package 1
,
relativePath: .nx/version-plans/plan1.md, relativePath: .nx/version-plans/plan1.md,
}, },
{ {
@ -85,7 +86,8 @@ describe('version-plans', () => {
pkg2: patch, pkg2: patch,
}, },
fileName: plan2.md, fileName: plan2.md,
message: This is a change to package 1 and package 2, message: This is a change to package 1 and package 2
,
relativePath: .nx/version-plans/plan2.md, relativePath: .nx/version-plans/plan2.md,
}, },
{ {
@ -95,7 +97,10 @@ describe('version-plans', () => {
pkg4: minor, pkg4: minor,
}, },
fileName: plan3.md, fileName: plan3.md,
message: This is a change to packages 3 and 4, message: This is a change to packages 3 and 4
...and it includes multiple lines of text
,
relativePath: .nx/version-plans/plan3.md, relativePath: .nx/version-plans/plan3.md,
}, },
{ {
@ -107,7 +112,8 @@ describe('version-plans', () => {
pkg6: preminor, pkg6: preminor,
}, },
fileName: plan4.md, fileName: plan4.md,
message: This is a change to packages 3, 4, 5, and 6, message: This is a change to packages 3, 4, 5, and 6
,
relativePath: .nx/version-plans/plan4.md, relativePath: .nx/version-plans/plan4.md,
}, },
{ {
@ -116,7 +122,8 @@ describe('version-plans', () => {
fixed-group-1: minor, fixed-group-1: minor,
}, },
fileName: plan5.md, fileName: plan5.md,
message: This is a change to fixed-group-1, message: This is a change to fixed-group-1
,
relativePath: .nx/version-plans/plan5.md, relativePath: .nx/version-plans/plan5.md,
}, },
{ {
@ -127,7 +134,8 @@ describe('version-plans', () => {
pkg3: major, pkg3: major,
}, },
fileName: plan6.md, fileName: plan6.md,
message: This is a major change to fixed-group-1 and pkg3 and a minor change to fixed-group-2, message: This is a major change to fixed-group-1 and pkg3 and a minor change to fixed-group-2
,
relativePath: .nx/version-plans/plan6.md, relativePath: .nx/version-plans/plan6.md,
}, },
] ]
@ -786,6 +794,11 @@ describe('version-plans', () => {
groupVersionBump: patch, groupVersionBump: patch,
message: plan1 message, message: plan1 message,
relativePath: .nx/version-plans/plan1.md, relativePath: .nx/version-plans/plan1.md,
triggeredByProjects: [
pkg1,
pkg2,
pkg3,
],
}, },
{ {
absolutePath: <workspace-root>/version-plans/plan2.md, absolutePath: <workspace-root>/version-plans/plan2.md,
@ -794,6 +807,11 @@ describe('version-plans', () => {
groupVersionBump: minor, groupVersionBump: minor,
message: plan2 message, message: plan2 message,
relativePath: .nx/version-plans/plan2.md, relativePath: .nx/version-plans/plan2.md,
triggeredByProjects: [
pkg1,
pkg2,
pkg3,
],
}, },
], ],
}, },
@ -1005,6 +1023,9 @@ describe('version-plans', () => {
groupVersionBump: minor, groupVersionBump: minor,
message: plan2 message, message: plan2 message,
relativePath: .nx/version-plans/plan2.md, relativePath: .nx/version-plans/plan2.md,
triggeredByProjects: [
pkg1,
],
}, },
], ],
}, },
@ -1026,6 +1047,9 @@ describe('version-plans', () => {
groupVersionBump: minor, groupVersionBump: minor,
message: plan2 message, message: plan2 message,
relativePath: .nx/version-plans/plan2.md, relativePath: .nx/version-plans/plan2.md,
triggeredByProjects: [
pkg2,
],
}, },
], ],
}, },
@ -1047,6 +1071,9 @@ describe('version-plans', () => {
groupVersionBump: minor, groupVersionBump: minor,
message: plan2 message, message: plan2 message,
relativePath: .nx/version-plans/plan2.md, relativePath: .nx/version-plans/plan2.md,
triggeredByProjects: [
pkg3,
],
}, },
], ],
}, },

View File

@ -25,6 +25,11 @@ export interface VersionPlan extends VersionPlanFile {
export interface GroupVersionPlan extends VersionPlan { export interface GroupVersionPlan extends VersionPlan {
groupVersionBump: ReleaseType; groupVersionBump: ReleaseType;
/**
* Will not be set if the group name was the trigger, otherwise will be a list of
* all the individual project names explicitly found in the version plan file.
*/
triggeredByProjects?: string[];
} }
export interface ProjectsVersionPlan extends VersionPlan { export interface ProjectsVersionPlan extends VersionPlan {
@ -54,7 +59,7 @@ export async function readRawVersionPlans(): Promise<RawVersionPlan[]> {
relativePath: join(versionPlansDirectory, versionPlanFile), relativePath: join(versionPlansDirectory, versionPlanFile),
fileName: versionPlanFile, fileName: versionPlanFile,
content: parsedContent.attributes, content: parsedContent.attributes,
message: getSingleLineMessage(parsedContent.body), message: parsedContent.body,
createdOnMs: versionPlanStats.birthtimeMs, createdOnMs: versionPlanStats.birthtimeMs,
}); });
} }
@ -74,6 +79,12 @@ export function setVersionPlansOnGroups(
const isDefaultGroup = isDefault(releaseGroups); const isDefaultGroup = isDefault(releaseGroups);
for (const rawVersionPlan of rawVersionPlans) { for (const rawVersionPlan of rawVersionPlans) {
if (!rawVersionPlan.message) {
throw new Error(
`Please add a changelog message to version plan: '${rawVersionPlan.fileName}'`
);
}
for (const [key, value] of Object.entries(rawVersionPlan.content)) { for (const [key, value] of Object.entries(rawVersionPlan.content)) {
if (groupsByName.has(key)) { if (groupsByName.has(key)) {
const group = groupsByName.get(key); const group = groupsByName.get(key);
@ -232,6 +243,8 @@ export function setVersionPlansOnGroups(
`Found a version bump for project '${key}' in '${rawVersionPlan.fileName}' that conflicts with another project's version bump in the same release group '${groupForProject.name}'. When the group is in fixed versioning mode, all projects' version bumps within the same group must match.` `Found a version bump for project '${key}' in '${rawVersionPlan.fileName}' that conflicts with another project's version bump in the same release group '${groupForProject.name}'. When the group is in fixed versioning mode, all projects' version bumps within the same group must match.`
); );
} }
} else {
existingPlan.triggeredByProjects.push(key);
} }
} else { } else {
groupForProject.versionPlans.push(<GroupVersionPlan>{ groupForProject.versionPlans.push(<GroupVersionPlan>{
@ -241,7 +254,9 @@ export function setVersionPlansOnGroups(
createdOnMs: rawVersionPlan.createdOnMs, createdOnMs: rawVersionPlan.createdOnMs,
message: rawVersionPlan.message, message: rawVersionPlan.message,
// This is a fixed group, so the version bump is for the group, even if a project within it was specified // This is a fixed group, so the version bump is for the group, even if a project within it was specified
// but we track the projects that triggered the version bump so that we can accurately produce changelog entries.
groupVersionBump: value, groupVersionBump: value,
triggeredByProjects: [key],
}); });
} }
} }
@ -273,8 +288,3 @@ export function getVersionPlansAbsolutePath() {
function isReleaseType(value: string): value is ReleaseType { function isReleaseType(value: string): value is ReleaseType {
return RELEASE_TYPES.includes(value as ReleaseType); return RELEASE_TYPES.includes(value as ReleaseType);
} }
// changelog messages may only be a single line long, so ignore anything else
function getSingleLineMessage(message: string) {
return message.trim().split('\n')[0];
}

View File

@ -1,7 +1,8 @@
import { prompt } from 'enquirer'; import { prompt } from 'enquirer';
import { ensureDir, writeFile } from 'fs-extra'; import { ensureDir, readFileSync, writeFile, writeFileSync } from 'fs-extra';
import { join } from 'path'; import { join } from 'node:path';
import { RELEASE_TYPES } from 'semver'; import { RELEASE_TYPES } from 'semver';
import { dirSync } from 'tmp';
import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json'; import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { createProjectGraphAsync } from '../../project-graph/project-graph';
@ -13,13 +14,13 @@ import {
createNxReleaseConfig, createNxReleaseConfig,
handleNxReleaseConfigError, handleNxReleaseConfigError,
} from './config/config'; } from './config/config';
import { deepMergeJson } from './config/deep-merge-json';
import { filterReleaseGroups } from './config/filter-release-groups'; import { filterReleaseGroups } from './config/filter-release-groups';
import { getVersionPlansAbsolutePath } from './config/version-plans'; import { getVersionPlansAbsolutePath } from './config/version-plans';
import { generateVersionPlanContent } from './utils/generate-version-plan-content'; import { generateVersionPlanContent } from './utils/generate-version-plan-content';
import { parseConventionalCommitsMessage } from './utils/git'; import { launchEditor } from './utils/launch-editor';
import { printDiff } from './utils/print-changes'; import { printDiff } from './utils/print-changes';
import { printConfigAndExit } from './utils/print-config'; import { printConfigAndExit } from './utils/print-config';
import { deepMergeJson } from './config/deep-merge-json';
export const releasePlanCLIHandler = (args: PlanOptions) => export const releasePlanCLIHandler = (args: PlanOptions) =>
handleErrors(args.verbose, () => createAPI({})(args)); handleErrors(args.verbose, () => createAPI({})(args));
@ -79,26 +80,6 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
} }
}; };
if (args.message) {
const message = parseConventionalCommitsMessage(args.message);
if (!message) {
output.error({
title: 'Changelog message is not in conventional commits format.',
bodyLines: [
'Please ensure your message is in the form of:',
' type(optional scope): description',
'',
'For example:',
' feat(pkg-b): add new feature',
' fix(pkg-a): correct a bug',
' chore: update build process',
' fix(core)!: breaking change in core package',
],
});
process.exit(1);
}
}
if (releaseGroups[0].name === IMPLICIT_DEFAULT_RELEASE_GROUP) { if (releaseGroups[0].name === IMPLICIT_DEFAULT_RELEASE_GROUP) {
const group = releaseGroups[0]; const group = releaseGroups[0];
if (group.projectsRelationship === 'independent') { if (group.projectsRelationship === 'independent') {
@ -153,12 +134,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
return 0; return 0;
} }
const versionPlanMessage = args.message || (await promptForMessage()); const versionPlanName = `version-plan-${new Date().getTime()}`;
const versionPlanMessage =
args.message || (await promptForMessage(versionPlanName));
const versionPlanFileContent = generateVersionPlanContent( const versionPlanFileContent = generateVersionPlanContent(
versionPlanBumps, versionPlanBumps,
versionPlanMessage versionPlanMessage
); );
const versionPlanFileName = `version-plan-${new Date().getTime()}.md`; const versionPlanFileName = `${versionPlanName}.md`;
if (args.dryRun) { if (args.dryRun) {
output.logSingleLine( output.logSingleLine(
@ -202,47 +185,49 @@ async function promptForVersion(message: string): Promise<string> {
} }
} }
async function promptForMessage(): Promise<string> { async function promptForMessage(versionPlanName: string): Promise<string> {
let message: string; let message: string;
do { do {
message = await _promptForMessage(); message = await _promptForMessage(versionPlanName);
} while (!message); } while (!message);
return message; return message;
} }
// TODO: support non-conventional commits messages (will require significant changelog renderer changes) async function _promptForMessage(versionPlanName: string): Promise<string> {
async function _promptForMessage(): Promise<string> {
try { try {
const reply = await prompt<{ message: string }>([ const reply = await prompt<{ message: string }>([
{ {
name: 'message', name: 'message',
message: message:
'What changelog message would you like associated with this change?', 'What changelog message would you like associated with this change? (Leave blank to open an external editor for multi-line messages/easier editing)',
type: 'input', type: 'input',
}, },
]); ]);
const conventionalCommitsMessage = parseConventionalCommitsMessage( let message = reply.message.trim();
reply.message
); if (!message.length) {
if (!conventionalCommitsMessage) { const tmpDir = dirSync().name;
output.warn({ const messageFilePath = join(
title: 'Changelog message is not in conventional commits format.', tmpDir,
bodyLines: [ `DRAFT_MESSAGE__${versionPlanName}.md`
'Please ensure your message is in the form of:', );
' type(optional scope): description', writeFileSync(messageFilePath, '');
'', await launchEditor(messageFilePath);
'For example:', message = readFileSync(messageFilePath, 'utf-8');
' feat(pkg-b): add new feature',
' fix(pkg-a): correct a bug',
' chore: update build process',
' fix(core)!: breaking change in core package',
],
});
return null;
} }
return reply.message; message = message.trim();
if (!message) {
output.warn({
title:
'A changelog message is required in order to create the version plan file',
bodyLines: [],
});
}
return message;
} catch (e) { } catch (e) {
output.log({ output.log({
title: 'Cancelled version plan creation.', title: 'Cancelled version plan creation.',

View File

@ -29,4 +29,28 @@ describe('generateVersionPlanContent()', () => {
" "
`); `);
}); });
it('should work without a message', () => {
expect(generateVersionPlanContent({ proj: '1.0.0' }, ''))
.toMatchInlineSnapshot(`
"---
proj: 1.0.0
---
"
`);
});
it('should work with multi-line messages', () => {
expect(generateVersionPlanContent({ proj: '1.0.0' }, 'foo\nbar\nbaz'))
.toMatchInlineSnapshot(`
"---
proj: 1.0.0
---
foo
bar
baz
"
`);
});
}); });

View File

@ -2,7 +2,7 @@ export function generateVersionPlanContent(
bumps: Record<string, string>, bumps: Record<string, string>,
message: string message: string
): string { ): string {
return `--- const frontMatter = `---
${Object.entries(bumps) ${Object.entries(bumps)
.filter(([_, version]) => version !== 'none') .filter(([_, version]) => version !== 'none')
.map(([projectOrGroup, version]) => { .map(([projectOrGroup, version]) => {
@ -15,7 +15,7 @@ ${Object.entries(bumps)
}) })
.join('\n')} .join('\n')}
--- ---
${message}
`; `;
return `${frontMatter}${message ? `\n${message}\n` : ''}`;
} }