diff --git a/e2e/release/src/version-plans.test.ts b/e2e/release/src/version-plans.test.ts index 389dc0d028436..8b542d07e325f 100644 --- a/e2e/release/src/version-plans.test.ts +++ b/e2e/release/src/version-plans.test.ts @@ -114,7 +114,7 @@ describe('nx release version plans', () => { await ensureDir(versionPlansDir); 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, } @@ -128,7 +128,9 @@ ${pkg4}: preminor ${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) + + -+ ### 🚀 Features ++ ### 🩹 Fixes ++ ++ - **${pkg3}:** 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.` ); expect(resultWithoutDate).toContain( @@ -207,7 +211,9 @@ feat: Update the independent packages with a patch, preminor, and prerelease. + + ### 🚀 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( @@ -217,9 +223,11 @@ feat: Update the independent packages with a patch, preminor, and prerelease. + ## 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( @@ -229,7 +237,7 @@ ${pkg1}: minor ${pkg3}: patch --- -fix: Update packages in both groups with a bug fix +Update packages in both groups with a mix #1 ` ); await writeFile( @@ -240,7 +248,7 @@ ${pkg4}: preminor ${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 + -+ - Update packages in both groups with a feat ++ - **${pkg1}:** Update packages in both groups with a mix #1 + + + ### 🩹 Fixes + -+ - Update packages in both groups with a bug fix` ++ - Update packages in both groups with a mix #2` ); expect(result2WithoutDate).toContain( `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) + + -+ ### 🚀 Features -+ -+ - Update packages in both groups with a feat -+ -+ + ### 🩹 Fixes + -+ - Update packages in both groups with a bug fix ++ - Update packages in both groups with a mix #2 ` ); expect(result2WithoutDate).toContain( @@ -326,7 +329,7 @@ feat: Update packages in both groups with a feat + + ### 🩹 Fixes + -+ - Update packages in both groups with a bug fix` ++ - **${pkg3}:** Update packages in both groups with a mix #1` ); expect(result2WithoutDate).toContain( @@ -339,7 +342,7 @@ feat: Update packages in both groups with a feat + + ### 🚀 Features + -+ - Update packages in both groups with a feat` ++ - **${pkg4}:** Update packages in both groups with a mix #2` ); expect(result2WithoutDate).toContain( @@ -350,9 +353,9 @@ feat: Update packages in both groups with a feat + ## 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(); @@ -394,7 +397,7 @@ feat: Update packages in both groups with a feat 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 --- -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) + + -+ ### 🚀 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( @@ -544,7 +547,7 @@ const yargs = require('yargs'); + + ### 🚀 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( @@ -554,9 +557,9 @@ const yargs = require('yargs'); + ## 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(); @@ -569,8 +572,8 @@ ${pkg1}: minor ${pkg3}: patch --- -fix: Update packages in both groups with a bug fix -` +Update packages in both groups with a mix #1 + ` ); await writeFile( join(versionPlansDir, 'bump-mixed2.md'), @@ -580,8 +583,8 @@ ${pkg4}: preminor ${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')}`); @@ -631,12 +634,12 @@ feat: Update packages in both groups with a feat + + ### 🚀 Features + -+ - Update packages in both groups with a feat ++ - **${pkg1}:** Update packages in both groups with a mix #1 + + + ### 🩹 Fixes + -+ - Update packages in both groups with a bug fix` ++ - Update packages in both groups with a mix #2` ); expect(result2WithoutDate).toContain( `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) + + -+ ### 🚀 Features -+ -+ - Update packages in both groups with a feat -+ -+ + ### 🩹 Fixes + -+ - Update packages in both groups with a bug fix ++ - Update packages in both groups with a mix #2 ` ); expect(result2WithoutDate).toContain( @@ -666,7 +664,7 @@ feat: Update packages in both groups with a feat + + ### 🩹 Fixes + -+ - Update packages in both groups with a bug fix` ++ - **${pkg3}:** Update packages in both groups with a mix #1` ); expect(result2WithoutDate).toContain( @@ -679,7 +677,7 @@ feat: Update packages in both groups with a feat + + ### 🚀 Features + -+ - Update packages in both groups with a feat` ++ - **${pkg4}:** Update packages in both groups with a mix #2` ); expect(result2WithoutDate).toContain( @@ -690,9 +688,9 @@ feat: Update packages in both groups with a feat + ## 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(); @@ -715,7 +713,7 @@ feat: Update packages in both groups with a feat await ensureDir(versionPlansDir); 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, } diff --git a/packages/nx/release/changelog-renderer/index.ts b/packages/nx/release/changelog-renderer/index.ts index ff22f63b3779e..fcf6e9be3bad8 100644 --- a/packages/nx/release/changelog-renderer/index.ts +++ b/packages/nx/release/changelog-renderer/index.ts @@ -404,14 +404,30 @@ function formatChange( changelogRenderOptions: DefaultChangelogRenderOptions, repoSlug?: RepoSlug ): 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 = '- ' + (change.isBreaking ? '⚠️ ' : '') + (change.scope ? `**${change.scope.trim()}:** ` : '') + - change.description; + description; if (repoSlug && changelogRenderOptions.commitReferences) { changeLine += formatReferences(change.githubReferences, repoSlug); } + if (extraLinesStr) { + changeLine += '\n\n' + extraLinesStr; + } return changeLine; } diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index e2c79396bec92..1221832ea0f5c 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -2,7 +2,7 @@ import * as chalk from 'chalk'; import { prompt } from 'enquirer'; import { removeSync } from 'fs-extra'; import { readFileSync, writeFileSync } from 'node:fs'; -import { valid } from 'semver'; +import { ReleaseType, valid } from 'semver'; import { dirSync } from 'tmp'; import type { DependencyBump } from '../../../release/changelog-renderer'; import { @@ -56,7 +56,6 @@ import { gitPush, gitTag, parseCommits, - parseConventionalCommitsMessage, } from './utils/git'; import { createOrUpdateGithubRelease, getGitHubRepoSlug } from './utils/github'; import { launchEditor } from './utils/launch-editor'; @@ -281,30 +280,37 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { const releaseGroup = releaseGroups[0]; if (releaseGroup.projectsRelationship === 'fixed') { const versionPlans = releaseGroup.versionPlans as GroupVersionPlan[]; - workspaceChangelogChanges = filterHiddenChanges( - versionPlans - .map((vp) => { - const parsedMessage = parseConventionalCommitsMessage( - vp.message - ); - - // only properly formatted conventional commits messages will be included in the changelog - if (!parsedMessage) { - return null; - } - - return { - type: parsedMessage.type, - scope: parsedMessage.scope, - description: parsedMessage.description, - body: '', - isBreaking: parsedMessage.breaking, - githubReferences: [], - }; - }) - .filter(Boolean), - nxReleaseConfig.conventionalCommits - ); + workspaceChangelogChanges = versionPlans + .flatMap((vp) => { + const releaseType = versionPlanSemverReleaseTypeToChangelogType( + vp.groupVersionBump + ); + const changes: ChangelogChange | ChangelogChange[] = + !vp.triggeredByProjects + ? { + type: releaseType.type, + scope: '', + description: vp.message, + body: '', + isBreaking: releaseType.isBreaking, + githubReferences: [], + affectedProjects: '*', + } + : vp.triggeredByProjects.map((project) => { + return { + type: releaseType.type, + scope: project, + description: vp.message, + body: '', + // TODO: what about github references? + isBreaking: releaseType.isBreaking, + githubReferences: [], + affectedProjects: [project], + }; + }); + return changes; + }) + .filter(Boolean); } } } else { @@ -485,31 +491,26 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { let commits: GitCommit[]; if (releaseGroup.versionPlans) { - changes = filterHiddenChanges( - (releaseGroup.versionPlans as ProjectsVersionPlan[]) - .map((vp) => { - const parsedMessage = parseConventionalCommitsMessage( - vp.message - ); - - // only properly formatted conventional commits messages will be included in the changelog - if (!parsedMessage) { - return null; - } - - return { - type: parsedMessage.type, - scope: parsedMessage.scope, - description: parsedMessage.description, - body: '', - isBreaking: parsedMessage.breaking, - affectedProjects: Object.keys(vp.projectVersionBumps), - githubReferences: [], - }; - }) - .filter(Boolean), - nxReleaseConfig.conventionalCommits - ); + changes = (releaseGroup.versionPlans as ProjectsVersionPlan[]) + .map((vp) => { + const bumpForProject = vp.projectVersionBumps[project.name]; + if (!bumpForProject) { + return null; + } + const releaseType = + versionPlanSemverReleaseTypeToChangelogType(bumpForProject); + return { + type: releaseType.type, + scope: project.name, + description: vp.message, + body: '', + isBreaking: releaseType.isBreaking, + affectedProjects: Object.keys(vp.projectVersionBumps), + // TODO: can we include github references when using version plans? + githubReferences: [], + }; + }) + .filter(Boolean); } else { let fromRef = 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 let commits: GitCommit[] = []; if (releaseGroup.versionPlans) { - changes = filterHiddenChanges( - (releaseGroup.versionPlans as GroupVersionPlan[]) - .map((vp) => { - const parsedMessage = parseConventionalCommitsMessage( - vp.message - ); - - // only properly formatted conventional commits messages will be included in the changelog - if (!parsedMessage) { - return null; - } - - return { - type: parsedMessage.type, - scope: parsedMessage.scope, - description: parsedMessage.description, - body: '', - isBreaking: parsedMessage.breaking, - githubReferences: [], - affectedProjects: '*', - }; - }) - .filter(Boolean), - nxReleaseConfig.conventionalCommits - ); + changes = (releaseGroup.versionPlans as GroupVersionPlan[]) + .flatMap((vp) => { + const releaseType = versionPlanSemverReleaseTypeToChangelogType( + vp.groupVersionBump + ); + const changes: ChangelogChange | ChangelogChange[] = + !vp.triggeredByProjects + ? { + type: releaseType.type, + scope: '', + description: vp.message, + body: '', + isBreaking: releaseType.isBreaking, + githubReferences: [], + affectedProjects: '*', + } + : vp.triggeredByProjects.map((project) => { + return { + type: releaseType.type, + scope: project, + description: vp.message, + body: '', + // TODO: what about github references? + isBreaking: releaseType.isBreaking, + githubReferences: [], + affectedProjects: [project], + }; + }); + return changes; + }) + .filter(Boolean); } else { let fromRef = args.from || @@ -1408,3 +1415,23 @@ function createFileToProjectMap( } 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}`); + } +} diff --git a/packages/nx/src/command-line/release/config/test-files/version-plan-3.md b/packages/nx/src/command-line/release/config/test-files/version-plan-3.md index 4d5a4c9ed4465..1d731b35da473 100644 --- a/packages/nx/src/command-line/release/config/test-files/version-plan-3.md +++ b/packages/nx/src/command-line/release/config/test-files/version-plan-3.md @@ -4,3 +4,5 @@ pkg4: minor --- This is a change to packages 3 and 4 + +...and it includes multiple lines of text diff --git a/packages/nx/src/command-line/release/config/version-plans.spec.ts b/packages/nx/src/command-line/release/config/version-plans.spec.ts index 684dd532195e5..349d22bfcf765 100644 --- a/packages/nx/src/command-line/release/config/version-plans.spec.ts +++ b/packages/nx/src/command-line/release/config/version-plans.spec.ts @@ -75,7 +75,8 @@ describe('version-plans', () => { pkg1: patch, }, 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, }, { @@ -85,7 +86,8 @@ describe('version-plans', () => { pkg2: patch, }, 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, }, { @@ -95,7 +97,10 @@ describe('version-plans', () => { pkg4: minor, }, 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, }, { @@ -107,7 +112,8 @@ describe('version-plans', () => { pkg6: preminor, }, 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, }, { @@ -116,7 +122,8 @@ describe('version-plans', () => { fixed-group-1: minor, }, 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, }, { @@ -127,7 +134,8 @@ describe('version-plans', () => { pkg3: major, }, 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, }, ] @@ -786,6 +794,11 @@ describe('version-plans', () => { groupVersionBump: patch, message: plan1 message, relativePath: .nx/version-plans/plan1.md, + triggeredByProjects: [ + pkg1, + pkg2, + pkg3, + ], }, { absolutePath: /version-plans/plan2.md, @@ -794,6 +807,11 @@ describe('version-plans', () => { groupVersionBump: minor, message: plan2 message, relativePath: .nx/version-plans/plan2.md, + triggeredByProjects: [ + pkg1, + pkg2, + pkg3, + ], }, ], }, @@ -1005,6 +1023,9 @@ describe('version-plans', () => { groupVersionBump: minor, message: plan2 message, relativePath: .nx/version-plans/plan2.md, + triggeredByProjects: [ + pkg1, + ], }, ], }, @@ -1026,6 +1047,9 @@ describe('version-plans', () => { groupVersionBump: minor, message: plan2 message, relativePath: .nx/version-plans/plan2.md, + triggeredByProjects: [ + pkg2, + ], }, ], }, @@ -1047,6 +1071,9 @@ describe('version-plans', () => { groupVersionBump: minor, message: plan2 message, relativePath: .nx/version-plans/plan2.md, + triggeredByProjects: [ + pkg3, + ], }, ], }, diff --git a/packages/nx/src/command-line/release/config/version-plans.ts b/packages/nx/src/command-line/release/config/version-plans.ts index 2aa1639bc511e..0c03dc3a3ec2d 100644 --- a/packages/nx/src/command-line/release/config/version-plans.ts +++ b/packages/nx/src/command-line/release/config/version-plans.ts @@ -25,6 +25,11 @@ export interface VersionPlan extends VersionPlanFile { export interface GroupVersionPlan extends VersionPlan { 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 { @@ -54,7 +59,7 @@ export async function readRawVersionPlans(): Promise { relativePath: join(versionPlansDirectory, versionPlanFile), fileName: versionPlanFile, content: parsedContent.attributes, - message: getSingleLineMessage(parsedContent.body), + message: parsedContent.body, createdOnMs: versionPlanStats.birthtimeMs, }); } @@ -74,6 +79,12 @@ export function setVersionPlansOnGroups( const isDefaultGroup = isDefault(releaseGroups); 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)) { if (groupsByName.has(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.` ); } + } else { + existingPlan.triggeredByProjects.push(key); } } else { groupForProject.versionPlans.push({ @@ -241,7 +254,9 @@ export function setVersionPlansOnGroups( createdOnMs: rawVersionPlan.createdOnMs, message: rawVersionPlan.message, // 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, + triggeredByProjects: [key], }); } } @@ -273,8 +288,3 @@ export function getVersionPlansAbsolutePath() { function isReleaseType(value: string): value is 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]; -} diff --git a/packages/nx/src/command-line/release/plan.ts b/packages/nx/src/command-line/release/plan.ts index 3b12c73c0dec8..3fe7f4d26aafb 100644 --- a/packages/nx/src/command-line/release/plan.ts +++ b/packages/nx/src/command-line/release/plan.ts @@ -1,7 +1,8 @@ import { prompt } from 'enquirer'; -import { ensureDir, writeFile } from 'fs-extra'; -import { join } from 'path'; +import { ensureDir, readFileSync, writeFile, writeFileSync } from 'fs-extra'; +import { join } from 'node:path'; import { RELEASE_TYPES } from 'semver'; +import { dirSync } from 'tmp'; import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json'; import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; @@ -13,13 +14,13 @@ import { createNxReleaseConfig, handleNxReleaseConfigError, } from './config/config'; +import { deepMergeJson } from './config/deep-merge-json'; import { filterReleaseGroups } from './config/filter-release-groups'; import { getVersionPlansAbsolutePath } from './config/version-plans'; 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 { printConfigAndExit } from './utils/print-config'; -import { deepMergeJson } from './config/deep-merge-json'; export const releasePlanCLIHandler = (args: PlanOptions) => 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) { const group = releaseGroups[0]; if (group.projectsRelationship === 'independent') { @@ -153,12 +134,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { 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( versionPlanBumps, versionPlanMessage ); - const versionPlanFileName = `version-plan-${new Date().getTime()}.md`; + const versionPlanFileName = `${versionPlanName}.md`; if (args.dryRun) { output.logSingleLine( @@ -202,47 +185,49 @@ async function promptForVersion(message: string): Promise { } } -async function promptForMessage(): Promise { +async function promptForMessage(versionPlanName: string): Promise { let message: string; do { - message = await _promptForMessage(); + message = await _promptForMessage(versionPlanName); } while (!message); return message; } -// TODO: support non-conventional commits messages (will require significant changelog renderer changes) -async function _promptForMessage(): Promise { +async function _promptForMessage(versionPlanName: string): Promise { try { const reply = await prompt<{ message: string }>([ { name: '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', }, ]); - const conventionalCommitsMessage = parseConventionalCommitsMessage( - reply.message - ); - if (!conventionalCommitsMessage) { + let message = reply.message.trim(); + + if (!message.length) { + const tmpDir = dirSync().name; + const messageFilePath = join( + tmpDir, + `DRAFT_MESSAGE__${versionPlanName}.md` + ); + writeFileSync(messageFilePath, ''); + await launchEditor(messageFilePath); + message = readFileSync(messageFilePath, 'utf-8'); + } + + message = message.trim(); + + if (!message) { output.warn({ - 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', - ], + title: + 'A changelog message is required in order to create the version plan file', + bodyLines: [], }); - return null; } - return reply.message; + return message; } catch (e) { output.log({ title: 'Cancelled version plan creation.', diff --git a/packages/nx/src/command-line/release/utils/generate-version-plan-content.spec.ts b/packages/nx/src/command-line/release/utils/generate-version-plan-content.spec.ts index 51d3b9b488544..414075946b3f2 100644 --- a/packages/nx/src/command-line/release/utils/generate-version-plan-content.spec.ts +++ b/packages/nx/src/command-line/release/utils/generate-version-plan-content.spec.ts @@ -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 + " + `); + }); }); diff --git a/packages/nx/src/command-line/release/utils/generate-version-plan-content.ts b/packages/nx/src/command-line/release/utils/generate-version-plan-content.ts index da3b9949b8c6e..1d8860eee4891 100644 --- a/packages/nx/src/command-line/release/utils/generate-version-plan-content.ts +++ b/packages/nx/src/command-line/release/utils/generate-version-plan-content.ts @@ -2,7 +2,7 @@ export function generateVersionPlanContent( bumps: Record, message: string ): string { - return `--- + const frontMatter = `--- ${Object.entries(bumps) .filter(([_, version]) => version !== 'none') .map(([projectOrGroup, version]) => { @@ -15,7 +15,7 @@ ${Object.entries(bumps) }) .join('\n')} --- - -${message} `; + + return `${frontMatter}${message ? `\n${message}\n` : ''}`; }