diff --git a/e2e/release/src/circular-dependencies.test.ts b/e2e/release/src/circular-dependencies.test.ts index 153bc5e753..225c813550 100644 --- a/e2e/release/src/circular-dependencies.test.ts +++ b/e2e/release/src/circular-dependencies.test.ts @@ -544,7 +544,6 @@ describe('nx release circular dependencies', () => { + # 2.0.0 (YYYY-MM-DD) + - + + ### 🧱 Updated Dependencies + + - Updated {project-name} to 2.0.0 @@ -556,7 +555,6 @@ describe('nx release circular dependencies', () => { + # 2.0.0 (YYYY-MM-DD) + - + + ### 🧱 Updated Dependencies + + - Updated {project-name} to 2.0.0 @@ -867,7 +865,6 @@ describe('nx release circular dependencies', () => { + # 2.0.0 (YYYY-MM-DD) + - + + ### 🧱 Updated Dependencies + + - Updated {project-name} to 2.0.0 @@ -879,7 +876,6 @@ describe('nx release circular dependencies', () => { + # 2.0.0 (YYYY-MM-DD) + - + + ### 🧱 Updated Dependencies + + - Updated {project-name} to 2.0.0 @@ -1054,7 +1050,6 @@ describe('nx release circular dependencies', () => { + # 2.0.0 (YYYY-MM-DD) + - + + ### 🧱 Updated Dependencies + + - Updated {project-name} to 1.0.1 @@ -1066,7 +1061,6 @@ describe('nx release circular dependencies', () => { + ## 1.0.1 (YYYY-MM-DD) + - + + ### 🧱 Updated Dependencies + + - Updated {project-name} to 2.0.0 diff --git a/e2e/release/src/conventional-commits-config.test.ts b/e2e/release/src/conventional-commits-config.test.ts index 1f081101bb..458b25dcfc 100644 --- a/e2e/release/src/conventional-commits-config.test.ts +++ b/e2e/release/src/conventional-commits-config.test.ts @@ -364,12 +364,10 @@ describe('nx release conventional commits config', () => { expect(pkg1Changelog).toMatchInlineSnapshot(` # 1.0.0 (YYYY-MM-DD) - ### Custom Type - ⚠️ **{project-name}:** this is a breaking change - ### ⚠️ Breaking Changes - ⚠️ **{project-name}:** this is a breaking change @@ -379,7 +377,6 @@ describe('nx release conventional commits config', () => { expect(pkg2Changelog).toMatchInlineSnapshot(` # 1.0.0 (YYYY-MM-DD) - ### Custom Type - **{project-name}:** this is a custom type @@ -389,7 +386,6 @@ describe('nx release conventional commits config', () => { expect(pkg3Changelog).toMatchInlineSnapshot(` # 1.0.0 (YYYY-MM-DD) - ### Custom Docs Header - this is a doc @@ -408,7 +404,6 @@ describe('nx release conventional commits config', () => { expect(pkg5Changelog).toMatchInlineSnapshot(` # 1.0.0 (YYYY-MM-DD) - ### 🔥 Performance - this is a performance improvement @@ -418,12 +413,10 @@ describe('nx release conventional commits config', () => { expect(pkg6Changelog).toMatchInlineSnapshot(` # 1.0.0 (YYYY-MM-DD) - ### 💅 Refactors - this is refactor - ### 📦 Build - this is a build diff --git a/e2e/release/src/release.test.ts b/e2e/release/src/release.test.ts index fd9f3f7dd7..80ec1cc7fd 100644 --- a/e2e/release/src/release.test.ts +++ b/e2e/release/src/release.test.ts @@ -129,7 +129,6 @@ describe('nx release', () => { + ## 999.9.9 (YYYY-MM-DD) + - + + ### 🚀 Features + + - an awesome new feature ([{COMMIT_SHA}](https://github.com/nrwl/fake-repo/commit/{COMMIT_SHA})) @@ -150,7 +149,6 @@ describe('nx release', () => { expect(readFile('CHANGELOG.md')).toMatchInlineSnapshot(` ## 999.9.9 (YYYY-MM-DD) - ### 🚀 Features - an awesome new feature ([{COMMIT_SHA}](https://github.com/nrwl/fake-repo/commit/{COMMIT_SHA})) @@ -666,7 +664,7 @@ describe('nx release', () => { + ## 999.9.9 (YYYY-MM-DD) - + ### 🚀 Features NX Previewing an entry in {project-name}/CHANGELOG.md for v1000.0.0-next.0 diff --git a/e2e/release/src/version-plans.test.ts b/e2e/release/src/version-plans.test.ts index c94bf7a72d..5a533c5da1 100644 --- a/e2e/release/src/version-plans.test.ts +++ b/e2e/release/src/version-plans.test.ts @@ -174,12 +174,10 @@ Here is another line in the message. + ## 0.1.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the fixed packages with a minor release. + -+ + ### ❤️ Thank You + + - Test` @@ -190,12 +188,10 @@ Here is another line in the message. + ## 0.1.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the fixed packages with a minor release. + -+ + ### ❤️ Thank You + + - Test` @@ -206,14 +202,12 @@ Here is another line in the message. + ## 0.0.1 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update the independent packages with a patch, preminor, and prerelease. + + Here is another line in the message. + -+ + ### ❤️ Thank You + + - Test` @@ -225,14 +219,12 @@ Here is another line in the message. + ## 0.1.0-0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the independent packages with a patch, preminor, and prerelease. + + Here is another line in the message. + -+ + ### ❤️ Thank You + + - Test` @@ -244,14 +236,12 @@ Here is another line in the message. + ## 0.0.1-0 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update the independent packages with a patch, preminor, and prerelease. + + Here is another line in the message. + -+ + ### ❤️ Thank You + + - Test` @@ -323,17 +313,14 @@ Update packages in both groups with a mix #2 + ## 0.2.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update packages in both groups with a mix #1 + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test` @@ -345,12 +332,10 @@ Update packages in both groups with a mix #2 + ## 0.2.0 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test @@ -363,12 +348,10 @@ Update packages in both groups with a mix #2 + ## 0.0.2 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #1 + -+ + ### ❤️ Thank You + + - Test` @@ -381,12 +364,10 @@ Update packages in both groups with a mix #2 + ## 0.2.0-0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test` @@ -399,12 +380,10 @@ Update packages in both groups with a mix #2 + ## 0.0.1 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test` @@ -511,12 +490,16 @@ const yargs = require('yargs'); verbose: options.verbose, }); - // The returned number value from releasePublish will be zero if all projects are published successfully, non-zero if not - const publishStatus = await releasePublish({ + const publishProjectsResult = await releasePublish({ dryRun: options.dryRun, verbose: options.verbose, }); - process.exit(publishStatus); + // Derive an overall exit code from the publish projects result + process.exit( + Object.values(publishProjectsResult).every((result) => result.code === 0) + ? 0 + : 1 + ); })(); ` ); @@ -562,12 +545,10 @@ const yargs = require('yargs'); + ## 0.1.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the fixed packages with a minor release. + -+ + ### ❤️ Thank You + + - Test` @@ -578,12 +559,10 @@ const yargs = require('yargs'); + ## 0.1.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the fixed packages with a minor release. + -+ + ### ❤️ Thank You + + - Test` @@ -594,12 +573,10 @@ const yargs = require('yargs'); + ## 0.0.1 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update the independent packages with a patch, preminor, and prerelease. + -+ + ### ❤️ Thank You + + - Test` @@ -611,12 +588,10 @@ const yargs = require('yargs'); + ## 0.1.0-0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the independent packages with a patch, preminor, and prerelease. + -+ + ### ❤️ Thank You + + - Test` @@ -628,12 +603,10 @@ const yargs = require('yargs'); + ## 0.0.1-0 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update the independent packages with a patch, preminor, and prerelease. + -+ + ### ❤️ Thank You + + - Test` @@ -708,17 +681,14 @@ Update packages in both groups with a mix #2 + ## 0.2.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update packages in both groups with a mix #1 + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test` @@ -730,12 +700,10 @@ Update packages in both groups with a mix #2 + ## 0.2.0 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test @@ -748,12 +716,10 @@ Update packages in both groups with a mix #2 + ## 0.0.2 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #1 + -+ + ### ❤️ Thank You + + - Test` @@ -766,12 +732,10 @@ Update packages in both groups with a mix #2 + ## 0.2.0-0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test` @@ -784,12 +748,10 @@ Update packages in both groups with a mix #2 + ## 0.0.1 (YYYY-MM-DD) + -+ + ### 🩹 Fixes + + - Update packages in both groups with a mix #2 + -+ + ### ❤️ Thank You + + - Test` @@ -850,7 +812,6 @@ Update packages in both groups with a mix #2 + ## 0.1.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the fixed packages with a minor release.` @@ -861,7 +822,6 @@ Update packages in both groups with a mix #2 + ## 0.1.0 (YYYY-MM-DD) + -+ + ### 🚀 Features + + - Update the fixed packages with a minor release.` diff --git a/packages/nx/release/changelog-renderer/index.spec.ts b/packages/nx/release/changelog-renderer/index.spec.ts index 2f33a089dd..e1c04b8a80 100644 --- a/packages/nx/release/changelog-renderer/index.spec.ts +++ b/packages/nx/release/changelog-renderer/index.spec.ts @@ -1,6 +1,6 @@ import type { ChangelogChange } from '../../src/command-line/release/changelog'; import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits'; -import defaultChangelogRenderer from './index'; +import DefaultChangelogRenderer from './index'; jest.mock('../../src/project-graph/file-map-utils', () => ({ createFileMapUsingProjectGraph: jest.fn().mockImplementation(() => { @@ -25,17 +25,16 @@ jest.mock('../../src/project-graph/file-map-utils', () => ({ }), })); -describe('defaultChangelogRenderer()', () => { - const projectGraph = { - nodes: {}, - } as any; +describe('ChangelogRenderer', () => { const changes: ChangelogChange[] = [ { shortHash: '4130f65', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], body: '"\n\nM\tpackages/pkg-a/src/index.ts\nM\tpackages/pkg-b/src/index.ts\n"', description: 'all packages fixed', type: 'fix', @@ -52,10 +51,12 @@ describe('defaultChangelogRenderer()', () => { }, { shortHash: '7dc5ec3', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"', description: 'and another new capability', type: 'feat', @@ -72,10 +73,12 @@ describe('defaultChangelogRenderer()', () => { }, { shortHash: 'd7a58a2', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], body: '"\n\nM\tpackages/pkg-a/src/index.ts\n"', description: 'new hotness', type: 'feat', @@ -92,10 +95,12 @@ describe('defaultChangelogRenderer()', () => { }, { shortHash: 'feace4a', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"', description: 'brand new thing', type: 'feat', @@ -112,10 +117,12 @@ describe('defaultChangelogRenderer()', () => { }, { shortHash: '6301405', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], body: '"\n\nM\tpackages/pkg-a/src/index.ts\n', description: 'squashing bugs', type: 'fix', @@ -132,702 +139,738 @@ describe('defaultChangelogRenderer()', () => { }, ]; - describe('workspaceChangelog', () => { - it('should generate markdown for all projects by organizing commits by type, then grouped by scope within the type (sorted alphabetically), then chronologically within the scope group', async () => { - const markdown = await defaultChangelogRenderer({ - projectGraph, - changes, - releaseVersion: 'v1.1.0', - project: null, - entryWhenNoChanges: false, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + describe('DefaultChangelogRenderer', () => { + describe('workspaceChangelog', () => { + it('should generate markdown for all projects by organizing commits by type, then grouped by scope within the type (sorted alphabetically), then chronologically within the scope group', async () => { + const renderer = new DefaultChangelogRenderer({ + changes, + changelogEntryVersion: 'v1.1.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }); + const markdown = await renderer.render(); + expect(markdown).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-a:** new hotness + - **pkg-b:** brand new thing + - **pkg-b:** and another new capability + + ### 🩹 Fixes + + - all packages fixed + - **pkg-a:** squashing bugs + + ### ❤️ Thank You + + - James Henry" + `); }); - expect(markdown).toMatchInlineSnapshot(` - "## v1.1.0 - - ### 🚀 Features - - - **pkg-a:** new hotness - - **pkg-b:** brand new thing - - **pkg-b:** and another new capability - - ### 🩹 Fixes - - - all packages fixed - - **pkg-a:** squashing bugs - - ### ❤️ Thank You - - - James Henry" - `); - }); - - it('should not generate a Thank You section when changelogRenderOptions.authors is false', async () => { - const markdown = await defaultChangelogRenderer({ - projectGraph, - changes, - // Major version, should use single # for generated heading - releaseVersion: 'v1.0.0', - project: null, - entryWhenNoChanges: false, - changelogRenderOptions: { - authors: false, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }); - expect(markdown).toMatchInlineSnapshot(` - "# v1.0.0 - - - ### 🚀 Features - - - **pkg-a:** new hotness - - **pkg-b:** brand new thing - - **pkg-b:** and another new capability - - ### 🩹 Fixes - - - all packages fixed - - **pkg-a:** squashing bugs" - `); - }); - }); - - describe('project level configs', () => { - it('should generate markdown for the given project by organizing commits by type, then chronologically', async () => { - const otherOpts = { - projectGraph, - changes, - releaseVersion: 'v1.1.0', - entryWhenNoChanges: false as const, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }; - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - project: 'pkg-a', - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### 🚀 Features - - - **pkg-a:** new hotness - - - ### 🩹 Fixes - - - all packages fixed - - - **pkg-a:** squashing bugs - - - ### ❤️ Thank You - - - James Henry" - `); - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - project: 'pkg-a', - // test that the authors option is being respected for project changelogs and therefore no Thank You section exists + it('should not generate a Thank You section when changelogRenderOptions.authors is false', async () => { + const renderer = new DefaultChangelogRenderer({ + changes, + // Major version, should use single # for generated heading + changelogEntryVersion: 'v1.0.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, changelogRenderOptions: { authors: false, }, - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }); + const markdown = await renderer.render(); + expect(markdown).toMatchInlineSnapshot(` + "# v1.0.0 + ### 🚀 Features - ### 🚀 Features + - **pkg-a:** new hotness + - **pkg-b:** brand new thing + - **pkg-b:** and another new capability - - **pkg-a:** new hotness + ### 🩹 Fixes - - ### 🩹 Fixes - - - all packages fixed - - - **pkg-a:** squashing bugs" - `); - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - project: 'pkg-b', - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### 🚀 Features - - - **pkg-b:** brand new thing - - - **pkg-b:** and another new capability - - - ### 🩹 Fixes - - - all packages fixed - - - ### ❤️ Thank You - - - James Henry" - `); - }); - - it('should only include authors relevant to the specific project', async () => { - const changes: ChangelogChange[] = [ - { - shortHash: '4130f65', - author: { - name: 'Author 1', - email: 'author-1@example.com', - }, - body: '"\n\nM\tpackages/pkg-a/src/index.ts\nM\tpackages/pkg-b/src/index.ts\n"', - description: 'all packages fixed', - type: 'fix', - scope: '', - githubReferences: [ - { - value: '4130f65', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: [], - affectedProjects: ['pkg-a', 'pkg-b'], - }, - { - shortHash: '7dc5ec3', - author: { - name: 'Author 2', - email: 'author-2@example.com', - }, - body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"', - description: 'and another new capability', - type: 'feat', - scope: 'pkg-b', - githubReferences: [ - { - value: '7dc5ec3', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: [], - affectedProjects: ['pkg-b'], - }, - { - shortHash: 'd7a58a2', - author: { - name: 'Author 3', - email: 'author-3@example.com', - }, - body: '"\n\nM\tpackages/pkg-a/src/index.ts\n"', - description: 'new hotness', - type: 'feat', - scope: 'pkg-a', - githubReferences: [ - { - value: 'd7a58a2', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: [], - affectedProjects: ['pkg-a'], - }, - { - shortHash: 'feace4a', - author: { - name: 'Author 4', - email: 'author-4@example.com', - }, - body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"', - description: 'brand new thing', - type: 'feat', - scope: 'pkg-b', - githubReferences: [ - { - value: 'feace4a', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: [], - affectedProjects: ['pkg-b'], - }, - { - shortHash: '6301405', - author: { - name: 'Author 5', - email: 'author-5@example.com', - }, - body: '"\n\nM\tpackages/pkg-a/src/index.ts\n', - description: 'squashing bugs', - type: 'fix', - scope: 'pkg-a', - githubReferences: [ - { - value: '6301405', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: [], - affectedProjects: ['pkg-a'], - }, - ]; - - const otherOpts = { - projectGraph, - changes, - releaseVersion: 'v1.1.0', - entryWhenNoChanges: false as const, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }; - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - project: 'pkg-a', - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### 🚀 Features - - - **pkg-a:** new hotness - - - ### 🩹 Fixes - - - all packages fixed - - - **pkg-a:** squashing bugs - - - ### ❤️ Thank You - - - Author 1 - - Author 3 - - Author 5" - `); - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - project: 'pkg-b', - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### 🚀 Features - - - **pkg-b:** brand new thing - - - **pkg-b:** and another new capability - - - ### 🩹 Fixes - - - all packages fixed - - - ### ❤️ Thank You - - - Author 1 - - Author 2 - - Author 4" - `); - }); - }); - - describe('entryWhenNoChanges', () => { - it('should respect the entryWhenNoChanges option for the workspace changelog', async () => { - const otherOpts = { - projectGraph, - changes: [], - releaseVersion: 'v1.1.0', - project: null, // workspace changelog - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }; - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - entryWhenNoChanges: 'Nothing at all!', - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 - - Nothing at all!" - `); - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - entryWhenNoChanges: false, // should not create an entry - }) - ).toMatchInlineSnapshot(`""`); - }); - - it('should respect the entryWhenNoChanges option for project changelogs', async () => { - const otherOpts = { - projectGraph, - changes: [], - releaseVersion: 'v1.1.0', - project: 'pkg-a', - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }; - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - entryWhenNoChanges: 'Nothing at all!', - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 - - Nothing at all!" - `); - - expect( - await defaultChangelogRenderer({ - ...otherOpts, - entryWhenNoChanges: false, // should not create an entry - }) - ).toMatchInlineSnapshot(`""`); - }); - }); - - describe('revert commits', () => { - it('should generate a Revert section for the changelog if the reverted commit is not part of the same release', async () => { - const changesWithOnlyRevert: ChangelogChange[] = [ - { - shortHash: '6528e88aa', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, - body: 'This reverts commit 6d68236d467812aba4557a2bc7f667157de80fdb.\n"\n\nM\tpackages/js/src/generators/release-version/release-version.spec.ts\nM\tpackages/js/src/generators/release-version/release-version.ts\n', - description: - 'Revert "fix(release): do not update dependents when they already use "*" (#20607)"', - type: 'revert', - scope: 'release', - githubReferences: [ - { - type: 'pull-request', - value: '#20607', - }, - { - value: '6528e88aa', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: ['6d68236d467812aba4557a2bc7f667157de80fdb'], - affectedProjects: ['js'], - }, - ]; - - const markdown = await defaultChangelogRenderer({ - projectGraph, - changes: changesWithOnlyRevert, - releaseVersion: 'v1.1.0', - project: null, - entryWhenNoChanges: false, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + - all packages fixed + - **pkg-a:** squashing bugs" + `); }); - - expect(markdown).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### ⏪ Revert - - - **release:** Revert "fix(release): do not update dependents when they already use "*" (#20607)" - - ### ❤️ Thank You - - - James Henry" - `); }); - it('should strip both the original commit and its revert if they are both included in the current range of commits', async () => { - const changesWithRevertAndOriginal: ChangelogChange[] = [ - { - shortHash: '6528e88aa', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, - body: 'This reverts commit 6d68236d467812aba4557a2bc7f667157de80fdb.\n"\n\nM\tpackages/js/src/generators/release-version/release-version.spec.ts\nM\tpackages/js/src/generators/release-version/release-version.ts\n', - description: - 'Revert "fix(release): do not update dependents when they already use "*" (#20607)"', - type: 'revert', - scope: 'release', - githubReferences: [ - { - type: 'pull-request', - value: '#20607', - }, - { - value: '6528e88aa', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: ['6d68236d467812aba4557a2bc7f667157de80fdb'], - affectedProjects: ['js'], - }, - { - shortHash: '6d68236d4', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, - body: '"\n\nM\tpackages/js/src/generators/release-version/release-version.spec.ts\nM\tpackages/js/src/generators/release-version/release-version.ts\n', - description: 'do not update dependents when they already use "*"', - type: 'fix', - scope: 'release', - githubReferences: [ - { - type: 'pull-request', - value: '#20607', - }, - { - value: '6d68236d4', - type: 'hash', - }, - ], - isBreaking: false, - revertedHashes: [], - affectedProjects: ['js'], - }, - ]; - - const markdown = await defaultChangelogRenderer({ - projectGraph, - changes: changesWithRevertAndOriginal, - releaseVersion: 'v1.1.0', - project: null, - entryWhenNoChanges: false, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }); - - expect(markdown).toMatchInlineSnapshot(`""`); - }); - }); - - describe('breaking changes', () => { - it('should work for breaking changes with just the ! and no explanation', async () => { - const breakingChangeWithExplanation: ChangelogChange = { - shortHash: '54f2f6ed1', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, - body: - 'M\tpackages/rxjs/src/internal/observable/dom/WebSocketSubject.ts\n' + - '"', - description: 'no longer extends `Subject`.', - type: 'feat', - scope: 'WebSocketSubject', - githubReferences: [{ value: '54f2f6ed1', type: 'hash' }], - isBreaking: true, - revertedHashes: [], - affectedProjects: ['rxjs'], - }; - - const markdown = await defaultChangelogRenderer({ - projectGraph, - changes: [breakingChangeWithExplanation], - releaseVersion: 'v1.1.0', - project: null, - entryWhenNoChanges: false, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }); - - expect(markdown).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### 🚀 Features - - - ⚠️ **WebSocketSubject:** no longer extends \`Subject\`. - - ### ⚠️ Breaking Changes - - - ⚠️ **WebSocketSubject:** no longer extends \`Subject\`. - - ### ❤️ Thank You - - - James Henry" - `); - }); - - it('should extract the explanation of a breaking change and render it preferentially', async () => { - const breakingChangeWithExplanation: ChangelogChange = { - shortHash: '54f2f6ed1', - author: { - name: 'James Henry', - email: 'jh@example.com', - }, - body: - 'BREAKING CHANGE: `WebSocketSubject` is no longer `instanceof Subject`. Check for `instanceof WebSocketSubject` instead.\n' + - '"\n' + - '\n' + - 'M\tpackages/rxjs/src/internal/observable/dom/WebSocketSubject.ts\n' + - '"', - description: 'no longer extends `Subject`.', - type: 'feat', - scope: 'WebSocketSubject', - githubReferences: [{ value: '54f2f6ed1', type: 'hash' }], - isBreaking: true, - revertedHashes: [], - affectedProjects: ['rxjs'], - }; - - const markdown = await defaultChangelogRenderer({ - projectGraph, - changes: [breakingChangeWithExplanation], - releaseVersion: 'v1.1.0', - project: null, - entryWhenNoChanges: false, - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - }); - - expect(markdown).toMatchInlineSnapshot(` - "## v1.1.0 - - - ### 🚀 Features - - - ⚠️ **WebSocketSubject:** no longer extends \`Subject\`. - - ### ⚠️ Breaking Changes - - - **WebSocketSubject:** \`WebSocketSubject\` is no longer \`instanceof Subject\`. Check for \`instanceof WebSocketSubject\` instead. - - ### ❤️ Thank You - - - James Henry" - `); - }); - }); - - describe('dependency bumps', () => { - it('should render the dependency bumps in addition to the changes', async () => { - expect( - await defaultChangelogRenderer({ - projectGraph, + describe('project level configs', () => { + it('should generate markdown for the given project by organizing commits by type, then chronologically', async () => { + const otherOpts = { changes, - releaseVersion: 'v1.1.0', + changelogEntryVersion: 'v1.1.0', entryWhenNoChanges: false as const, + isVersionPlans: false, changelogRenderOptions: { authors: true, }, conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }; + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + project: 'pkg-a', + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-a:** new hotness + + ### 🩹 Fixes + + - all packages fixed + - **pkg-a:** squashing bugs + + ### ❤️ Thank You + + - James Henry" + `); + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + project: 'pkg-a', + // test that the authors option is being respected for project changelogs and therefore no Thank You section exists + changelogRenderOptions: { + authors: false, + }, + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-a:** new hotness + + ### 🩹 Fixes + + - all packages fixed + - **pkg-a:** squashing bugs" + `); + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + project: 'pkg-b', + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-b:** and another new capability + - **pkg-b:** brand new thing + + ### 🩹 Fixes + + - all packages fixed + + ### ❤️ Thank You + + - James Henry" + `); + }); + + it('should only include authors relevant to the specific project', async () => { + const changes: ChangelogChange[] = [ + { + shortHash: '4130f65', + authors: [ + { + name: 'Author 1', + email: 'author-1@example.com', + }, + ], + body: '"\n\nM\tpackages/pkg-a/src/index.ts\nM\tpackages/pkg-b/src/index.ts\n"', + description: 'all packages fixed', + type: 'fix', + scope: '', + githubReferences: [ + { + value: '4130f65', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: [], + affectedProjects: ['pkg-a', 'pkg-b'], + }, + { + shortHash: '7dc5ec3', + authors: [ + { + name: 'Author 2', + email: 'author-2@example.com', + }, + ], + body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"', + description: 'and another new capability', + type: 'feat', + scope: 'pkg-b', + githubReferences: [ + { + value: '7dc5ec3', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: [], + affectedProjects: ['pkg-b'], + }, + { + shortHash: 'd7a58a2', + authors: [ + { + name: 'Author 3', + email: 'author-3@example.com', + }, + ], + body: '"\n\nM\tpackages/pkg-a/src/index.ts\n"', + description: 'new hotness', + type: 'feat', + scope: 'pkg-a', + githubReferences: [ + { + value: 'd7a58a2', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: [], + affectedProjects: ['pkg-a'], + }, + { + shortHash: 'feace4a', + authors: [ + { + name: 'Author 4', + email: 'author-4@example.com', + }, + ], + body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"', + description: 'brand new thing', + type: 'feat', + scope: 'pkg-b', + githubReferences: [ + { + value: 'feace4a', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: [], + affectedProjects: ['pkg-b'], + }, + { + shortHash: '6301405', + authors: [ + { + name: 'Author 5', + email: 'author-5@example.com', + }, + ], + body: '"\n\nM\tpackages/pkg-a/src/index.ts\n', + description: 'squashing bugs', + type: 'fix', + scope: 'pkg-a', + githubReferences: [ + { + value: '6301405', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: [], + affectedProjects: ['pkg-a'], + }, + ]; + + const otherOpts = { + changes, + changelogEntryVersion: 'v1.1.0', + entryWhenNoChanges: false as const, + isVersionPlans: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }; + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + project: 'pkg-a', + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-a:** new hotness + + ### 🩹 Fixes + + - all packages fixed + - **pkg-a:** squashing bugs + + ### ❤️ Thank You + + - Author 1 + - Author 3 + - Author 5" + `); + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + project: 'pkg-b', + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-b:** and another new capability + - **pkg-b:** brand new thing + + ### 🩹 Fixes + + - all packages fixed + + ### ❤️ Thank You + + - Author 1 + - Author 2 + - Author 4" + `); + }); + }); + + describe('entryWhenNoChanges', () => { + it('should respect the entryWhenNoChanges option for the workspace changelog', async () => { + const otherOpts = { + changes: [], + changelogEntryVersion: 'v1.1.0', + project: null, // workspace changelog + isVersionPlans: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }; + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + entryWhenNoChanges: 'Nothing at all!', + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + Nothing at all!" + `); + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + entryWhenNoChanges: false, // should not create an entry + }).render() + ).toMatchInlineSnapshot(`""`); + }); + + it('should respect the entryWhenNoChanges option for project changelogs', async () => { + const otherOpts = { + changes: [], + changelogEntryVersion: 'v1.1.0', project: 'pkg-a', - dependencyBumps: [ + isVersionPlans: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }; + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + entryWhenNoChanges: 'Nothing at all!', + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + Nothing at all!" + `); + + expect( + await new DefaultChangelogRenderer({ + ...otherOpts, + entryWhenNoChanges: false, // should not create an entry + }).render() + ).toMatchInlineSnapshot(`""`); + }); + }); + + describe('revert commits', () => { + it('should generate a Revert section for the changelog if the reverted commit is not part of the same release', async () => { + const changesWithOnlyRevert: ChangelogChange[] = [ + { + shortHash: '6528e88aa', + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], + body: 'This reverts commit 6d68236d467812aba4557a2bc7f667157de80fdb.\n"\n\nM\tpackages/js/src/generators/release-version/release-version.spec.ts\nM\tpackages/js/src/generators/release-version/release-version.ts\n', + description: + 'Revert "fix(release): do not update dependents when they already use "*" (#20607)"', + type: 'revert', + scope: 'release', + githubReferences: [ + { + type: 'pull-request', + value: '#20607', + }, + { + value: '6528e88aa', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: ['6d68236d467812aba4557a2bc7f667157de80fdb'], + affectedProjects: ['js'], + }, + ]; + + const markdown = await new DefaultChangelogRenderer({ + changes: changesWithOnlyRevert, + changelogEntryVersion: 'v1.1.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }).render(); + + expect(markdown).toMatchInlineSnapshot(` + "## v1.1.0 + + ### ⏪ Revert + + - **release:** Revert "fix(release): do not update dependents when they already use "*" (#20607)" + + ### ❤️ Thank You + + - James Henry" + `); + }); + + it('should strip both the original commit and its revert if they are both included in the current range of commits', async () => { + const changesWithRevertAndOriginal: ChangelogChange[] = [ + { + shortHash: '6528e88aa', + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], + body: 'This reverts commit 6d68236d467812aba4557a2bc7f667157de80fdb.\n"\n\nM\tpackages/js/src/generators/release-version/release-version.spec.ts\nM\tpackages/js/src/generators/release-version/release-version.ts\n', + description: + 'Revert "fix(release): do not update dependents when they already use "*" (#20607)"', + type: 'revert', + scope: 'release', + githubReferences: [ + { + type: 'pull-request', + value: '#20607', + }, + { + value: '6528e88aa', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: ['6d68236d467812aba4557a2bc7f667157de80fdb'], + affectedProjects: ['js'], + }, + { + shortHash: '6d68236d4', + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], + body: '"\n\nM\tpackages/js/src/generators/release-version/release-version.spec.ts\nM\tpackages/js/src/generators/release-version/release-version.ts\n', + description: 'do not update dependents when they already use "*"', + type: 'fix', + scope: 'release', + githubReferences: [ + { + type: 'pull-request', + value: '#20607', + }, + { + value: '6d68236d4', + type: 'hash', + }, + ], + isBreaking: false, + revertedHashes: [], + affectedProjects: ['js'], + }, + ]; + + const markdown = await new DefaultChangelogRenderer({ + changes: changesWithRevertAndOriginal, + changelogEntryVersion: 'v1.1.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }).render(); + + expect(markdown).toMatchInlineSnapshot(`""`); + }); + }); + + describe('breaking changes', () => { + it('should work for breaking changes with just the ! and no explanation', async () => { + const breakingChangeWithExplanation: ChangelogChange = { + shortHash: '54f2f6ed1', + authors: [ { - dependencyName: 'pkg-b', - newVersion: '2.0.0', + name: 'James Henry', + email: 'jh@example.com', }, ], - }) - ).toMatchInlineSnapshot(` - "## v1.1.0 + body: + 'M\tpackages/rxjs/src/internal/observable/dom/WebSocketSubject.ts\n' + + '"', + description: 'no longer extends `Subject`.', + type: 'feat', + scope: 'WebSocketSubject', + githubReferences: [{ value: '54f2f6ed1', type: 'hash' }], + isBreaking: true, + revertedHashes: [], + affectedProjects: ['rxjs'], + }; + const markdown = await new DefaultChangelogRenderer({ + changes: [breakingChangeWithExplanation], + changelogEntryVersion: 'v1.1.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }).render(); + + expect(markdown).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - ⚠️ **WebSocketSubject:** no longer extends \`Subject\`. + + ### ⚠️ Breaking Changes + + - ⚠️ **WebSocketSubject:** no longer extends \`Subject\`. + + ### ❤️ Thank You + + - James Henry" + `); + }); + + it('should extract the explanation of a breaking change and render it preferentially', async () => { + const breakingChangeWithExplanation: ChangelogChange = { + shortHash: '54f2f6ed1', + authors: [ + { + name: 'James Henry', + email: 'jh@example.com', + }, + ], + body: + 'BREAKING CHANGE: `WebSocketSubject` is no longer `instanceof Subject`. Check for `instanceof WebSocketSubject` instead.\n' + + '"\n' + + '\n' + + 'M\tpackages/rxjs/src/internal/observable/dom/WebSocketSubject.ts\n' + + '"', + description: 'no longer extends `Subject`.', + type: 'feat', + scope: 'WebSocketSubject', + githubReferences: [{ value: '54f2f6ed1', type: 'hash' }], + isBreaking: true, + revertedHashes: [], + affectedProjects: ['rxjs'], + }; + + const markdown = await new DefaultChangelogRenderer({ + changes: [breakingChangeWithExplanation], + changelogEntryVersion: 'v1.1.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }).render(); + + expect(markdown).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - ⚠️ **WebSocketSubject:** no longer extends \`Subject\`. + + ### ⚠️ Breaking Changes + + - **WebSocketSubject:** \`WebSocketSubject\` is no longer \`instanceof Subject\`. Check for \`instanceof WebSocketSubject\` instead. + + ### ❤️ Thank You + + - James Henry" + `); + }); + }); + + describe('dependency bumps', () => { + it('should render the dependency bumps in addition to the changes', async () => { + expect( + await new DefaultChangelogRenderer({ + changes, + changelogEntryVersion: 'v1.1.0', + entryWhenNoChanges: false as const, + changelogRenderOptions: { + authors: true, + }, + isVersionPlans: false, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + project: 'pkg-a', + dependencyBumps: [ + { + dependencyName: 'pkg-b', + newVersion: '2.0.0', + }, + ], + }).render() + ).toMatchInlineSnapshot(` + "## v1.1.0 + + ### 🚀 Features + + - **pkg-a:** new hotness + + ### 🩹 Fixes + + - all packages fixed + - **pkg-a:** squashing bugs + + ### 🧱 Updated Dependencies + + - Updated pkg-b to 2.0.0 + + ### ❤️ Thank You + + - James Henry" + `); + }); + + it('should render the dependency bumps and release version title even when there are no changes', async () => { + expect( + await new DefaultChangelogRenderer({ + changes: [], + changelogEntryVersion: 'v3.1.0', + entryWhenNoChanges: + 'should not be printed because we have dependency bumps', + changelogRenderOptions: { + authors: true, + }, + isVersionPlans: false, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + project: 'pkg-a', + dependencyBumps: [ + { + dependencyName: 'pkg-b', + newVersion: '4.0.0', + }, + ], + }).render() + ).toMatchInlineSnapshot(` + "## v3.1.0 + + ### 🧱 Updated Dependencies + + - Updated pkg-b to 4.0.0" + `); + }); + }); + }); + + describe('Custom ChangelogRenderer', () => { + it('should be possible to override individual methods of the DefaultChangelogRenderer', async () => { + class CustomChangelogRenderer extends DefaultChangelogRenderer { + public renderVersionTitle(): string { + return 'Custom Version Title'; + } + } + + const renderer = new CustomChangelogRenderer({ + changes, + changelogEntryVersion: 'v1.1.0', + project: null, + isVersionPlans: false, + entryWhenNoChanges: false, + changelogRenderOptions: { + authors: true, + }, + conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, + }); + + const markdown = await renderer.render(); + expect(markdown).toMatchInlineSnapshot(` + "Custom Version Title ### 🚀 Features - **pkg-a:** new hotness - + - **pkg-b:** brand new thing + - **pkg-b:** and another new capability ### 🩹 Fixes - all packages fixed - - **pkg-a:** squashing bugs - - ### 🧱 Updated Dependencies - - - Updated pkg-b to 2.0.0 - - ### ❤️ Thank You - James Henry" `); }); - - it('should render the dependency bumps and release version title even when there are no changes', async () => { - expect( - await defaultChangelogRenderer({ - projectGraph, - changes: [], - releaseVersion: 'v3.1.0', - entryWhenNoChanges: - 'should not be printed because we have dependency bumps', - changelogRenderOptions: { - authors: true, - }, - conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG, - project: 'pkg-a', - dependencyBumps: [ - { - dependencyName: 'pkg-b', - newVersion: '4.0.0', - }, - ], - }) - ).toMatchInlineSnapshot(` - "## v3.1.0 - - - ### 🧱 Updated Dependencies - - - Updated pkg-b to 4.0.0" - `); - }); }); }); diff --git a/packages/nx/release/changelog-renderer/index.ts b/packages/nx/release/changelog-renderer/index.ts index b1eaf6667a..e4d9b91714 100644 --- a/packages/nx/release/changelog-renderer/index.ts +++ b/packages/nx/release/changelog-renderer/index.ts @@ -2,13 +2,10 @@ import { major } from 'semver'; import { ChangelogChange } from '../../src/command-line/release/changelog'; import { NxReleaseConfig } from '../../src/command-line/release/config/config'; import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits'; -import { GitCommit } from '../../src/command-line/release/utils/git'; import { GithubRepoData, - RepoSlug, formatReferences, } from '../../src/command-line/release/utils/github'; -import type { ProjectGraph } from '../../src/config/project-graph'; // axios types and values don't seem to match import _axios = require('axios'); @@ -21,7 +18,7 @@ const axios = _axios as any as (typeof _axios)['default']; export type ChangelogRenderOptions = Record; /** - * When versioning projects independently and enabling `"updateDependents": "always"`, there could + * When versioning projects independently and enabling `"updateDependents": "auto"`, there could * be additional dependency bump information that is not captured in the commit data, but that nevertheless * should be included in the rendered changelog. */ @@ -30,39 +27,6 @@ export type DependencyBump = { newVersion: string; }; -/** - * A ChangelogRenderer function takes in the extracted commits and other relevant metadata - * and returns a string, or a Promise of a string of changelog contents (usually markdown). - * - * @param {Object} config The configuration object for the ChangelogRenderer - * @param {ProjectGraph} config.projectGraph The project graph for the workspace - * @param {GitCommit[]} config.commits DEPRECATED [Use 'config.changes' instead] - The collection of extracted commits to generate a changelog for - * @param {ChangelogChange[]} config.changes The collection of changes to show in the changelog - * @param {string} config.releaseVersion The version that is being released - * @param {string | null} config.project The name of specific project to generate a changelog for, or `null` if the overall workspace changelog - * @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated - * @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation - * @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the commit data - * @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository - */ -export type ChangelogRenderer = (config: { - projectGraph: ProjectGraph; - // TODO(v20): remove 'commits' and make 'changes' required - commits?: GitCommit[]; - changes?: ChangelogChange[]; - releaseVersion: string; - project: string | null; - entryWhenNoChanges: string | false; - changelogRenderOptions: DefaultChangelogRenderOptions; - dependencyBumps?: DependencyBump[]; - // TODO(v20): remove repoSlug in favour of repoData - repoSlug?: RepoSlug; - repoData?: GithubRepoData; - // TODO(v20): Evaluate if there is a cleaner way to configure this when breaking changes are allowed - // null if version plans are being used to generate the changelog - conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null; -}) => Promise | string; - /** * The specific options available to the default implementation of the ChangelogRenderer that nx exports * for the common case. @@ -91,112 +55,209 @@ export interface DefaultChangelogRenderOptions extends ChangelogRenderOptions { versionTitleDate?: boolean; } -/** - * The default ChangelogRenderer implementation that nx exports for the common case of generating markdown - * from the given commits and other metadata. - */ -const defaultChangelogRenderer: ChangelogRenderer = async ({ - projectGraph, - changes, - releaseVersion, - project, - entryWhenNoChanges, - changelogRenderOptions, - dependencyBumps, - repoSlug, - conventionalCommitsConfig, - repoData, -}): Promise => { - const markdownLines: string[] = []; +export default class DefaultChangelogRenderer { + protected changes: ChangelogChange[]; + protected changelogEntryVersion: string; + protected project: string | null; + protected entryWhenNoChanges: string | false; + protected changelogRenderOptions: DefaultChangelogRenderOptions; + protected isVersionPlans: boolean; + protected dependencyBumps?: DependencyBump[]; + protected repoData?: GithubRepoData; + protected conventionalCommitsConfig: + | NxReleaseConfig['conventionalCommits'] + | null; + protected relevantChanges: ChangelogChange[]; + protected breakingChanges: string[]; + protected additionalChangesForAuthorsSection: ChangelogChange[]; - // If the current range of changes contains both a commit and its revert, we strip them both from the final list. Changes from version plans are unaffected, as they have no hashes. - for (const change of changes) { - if (change.type === 'revert' && change.revertedHashes) { - for (const revertedHash of change.revertedHashes) { - const revertedCommit = changes.find( - (c) => c.shortHash && revertedHash.startsWith(c.shortHash) - ); - if (revertedCommit) { - changes.splice(changes.indexOf(revertedCommit), 1); - changes.splice(changes.indexOf(change), 1); + /** + * A ChangelogRenderer class takes in the determined changes and other relevant metadata + * and returns a string, or a Promise of a string of changelog contents (usually markdown). + * + * @param {Object} config The configuration object for the ChangelogRenderer + * @param {ChangelogChange[]} config.changes The collection of changes to show in the changelog + * @param {string} config.changelogEntryVersion The version for which we are rendering the current changelog entry + * @param {string | null} config.project The name of specific project to generate a changelog entry for, or `null` if the overall workspace changelog + * @param {string | false} config.entryWhenNoChanges The (already interpolated) string to use as the changelog entry when there are no changes, or `false` if no entry should be generated + * @param {boolean} config.isVersionPlans Whether or not Nx release version plans are the source of truth for the changelog entry + * @param {ChangelogRenderOptions} config.changelogRenderOptions The options specific to the ChangelogRenderer implementation + * @param {DependencyBump[]} config.dependencyBumps Optional list of additional dependency bumps that occurred as part of the release, outside of the change data + * @param {GithubRepoData} config.repoData Resolved data for the current GitHub repository + * @param {NxReleaseConfig['conventionalCommits'] | null} config.conventionalCommitsConfig The configuration for conventional commits, or null if version plans are being used + */ + constructor(config: { + changes: ChangelogChange[]; + changelogEntryVersion: string; + project: string | null; + entryWhenNoChanges: string | false; + isVersionPlans: boolean; + changelogRenderOptions: DefaultChangelogRenderOptions; + dependencyBumps?: DependencyBump[]; + repoData?: GithubRepoData; + conventionalCommitsConfig: NxReleaseConfig['conventionalCommits'] | null; + }) { + this.changes = this.filterChanges(config.changes, config.project); + this.changelogEntryVersion = config.changelogEntryVersion; + this.project = config.project; + this.entryWhenNoChanges = config.entryWhenNoChanges; + this.isVersionPlans = config.isVersionPlans; + this.changelogRenderOptions = config.changelogRenderOptions; + this.dependencyBumps = config.dependencyBumps; + this.repoData = config.repoData; + this.conventionalCommitsConfig = config.conventionalCommitsConfig; + + this.relevantChanges = []; + this.breakingChanges = []; + this.additionalChangesForAuthorsSection = []; + } + + protected filterChanges( + changes: ChangelogChange[], + project: string | null + ): ChangelogChange[] { + if (project === null) { + return changes; + } + return changes.filter( + (c) => + c.affectedProjects && + (c.affectedProjects === '*' || c.affectedProjects.includes(project)) + ); + } + + async render(): Promise { + const sections: string[][] = []; + + this.preprocessChanges(); + + if (this.shouldRenderEmptyEntry()) { + return this.renderEmptyEntry(); + } + + sections.push([this.renderVersionTitle()]); + + const changesByType = this.renderChangesByType(); + if (changesByType.length > 0) { + sections.push(changesByType); + } + + if (this.hasBreakingChanges()) { + sections.push(this.renderBreakingChanges()); + } + + if (this.hasDependencyBumps()) { + sections.push(this.renderDependencyBumps()); + } + + if (this.shouldRenderAuthors()) { + sections.push(await this.renderAuthors()); + } + + // Join sections with double newlines, and trim any extra whitespace + return sections + .filter((section) => section.length > 0) + .map((section) => section.join('\n').trim()) + .join('\n\n') + .trim(); + } + + protected preprocessChanges(): void { + this.relevantChanges = [...this.changes]; + this.breakingChanges = []; + this.additionalChangesForAuthorsSection = []; + + // Filter out reverted changes + for (let i = this.relevantChanges.length - 1; i >= 0; i--) { + const change = this.relevantChanges[i]; + if (change.type === 'revert' && change.revertedHashes) { + for (const revertedHash of change.revertedHashes) { + const revertedCommitIndex = this.relevantChanges.findIndex( + (c) => c.shortHash && revertedHash.startsWith(c.shortHash) + ); + if (revertedCommitIndex !== -1) { + this.relevantChanges.splice(revertedCommitIndex, 1); + this.relevantChanges.splice(i, 1); + i--; + break; + } + } + } + } + + if (this.isVersionPlans) { + this.conventionalCommitsConfig = { + types: { + feat: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.feat, + fix: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.fix, + }, + }; + + for (let i = this.relevantChanges.length - 1; i >= 0; i--) { + if (this.relevantChanges[i].isBreaking) { + const change = this.relevantChanges[i]; + this.additionalChangesForAuthorsSection.push(change); + const line = this.formatChange(change); + this.breakingChanges.push(line); + this.relevantChanges.splice(i, 1); + } + } + } else { + for (const change of this.relevantChanges) { + if (change.isBreaking) { + const breakingChangeExplanation = + this.extractBreakingChangeExplanation(change.body); + this.breakingChanges.push( + breakingChangeExplanation + ? `- ${ + change.scope ? `**${change.scope.trim()}:** ` : '' + }${breakingChangeExplanation}` + : this.formatChange(change) + ); } } } } - let relevantChanges = changes; - const breakingChanges = []; - - // For now to keep the interface of the changelog renderer non-breaking for v19 releases we have a somewhat indirect check for whether or not we are generating a changelog for version plans - const isVersionPlans = !conventionalCommitsConfig; - - // Only applicable for version plans - const additionalChangesForAuthorsSection = []; - - // Provide a default configuration for version plans to allow most of the subsequent logic to work in the same way it would for conventional commits - // NOTE: The one exception is breaking/major changes, where we do not follow the same structure and instead only show the changes once - if (isVersionPlans) { - conventionalCommitsConfig = { - types: { - feat: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.feat, - fix: DEFAULT_CONVENTIONAL_COMMITS_CONFIG.types.fix, - }, - }; - // Trim down "relevant changes" to only include non-breaking ones so that we can render them differently under version plans, - // but keep track of the changes for the purposes of the authors section - // TODO(v20): Clean this abstraction up as part of the larger overall refactor of changelog rendering - for (let i = 0; i < relevantChanges.length; i++) { - if (relevantChanges[i].isBreaking) { - const change = relevantChanges[i]; - additionalChangesForAuthorsSection.push(change); - const line = formatChange( - change, - changelogRenderOptions, - isVersionPlans, - repoData - ); - breakingChanges.push(line); - relevantChanges.splice(i, 1); - } - } + protected shouldRenderEmptyEntry(): boolean { + return ( + this.relevantChanges.length === 0 && + this.breakingChanges.length === 0 && + !this.hasDependencyBumps() + ); } - const changeTypes = conventionalCommitsConfig.types; - - // workspace root level changelog - if (project === null) { - // No changes for the workspace - if (relevantChanges.length === 0 && breakingChanges.length === 0) { - if (dependencyBumps?.length) { - applyAdditionalDependencyBumps({ - markdownLines, - dependencyBumps, - releaseVersion, - changelogRenderOptions, - }); - } else if (entryWhenNoChanges) { - markdownLines.push( - '', - `${createVersionTitle( - releaseVersion, - changelogRenderOptions - )}\n\n${entryWhenNoChanges}`, - '' - ); - } - return markdownLines.join('\n').trim(); + protected renderEmptyEntry(): string { + if (this.hasDependencyBumps()) { + return [ + this.renderVersionTitle(), + '', + ...this.renderDependencyBumps(), + ].join('\n'); + } else if (this.entryWhenNoChanges) { + return `${this.renderVersionTitle()}\n\n${this.entryWhenNoChanges}`; } + return ''; + } - const typeGroups: Record = groupBy( - relevantChanges, - 'type' - ); + protected renderVersionTitle(): string { + const isMajorVersion = + `${major(this.changelogEntryVersion)}.0.0` === + this.changelogEntryVersion.replace(/^v/, ''); + let maybeDateStr = ''; + if (this.changelogRenderOptions.versionTitleDate) { + const dateStr = new Date().toISOString().slice(0, 10); + maybeDateStr = ` (${dateStr})`; + } + return isMajorVersion + ? `# ${this.changelogEntryVersion}${maybeDateStr}` + : `## ${this.changelogEntryVersion}${maybeDateStr}`; + } - markdownLines.push( - '', - createVersionTitle(releaseVersion, changelogRenderOptions), - '' - ); + protected renderChangesByType(): string[] { + const markdownLines: string[] = []; + const typeGroups = this.groupChangesByType(); + const changeTypes = this.conventionalCommitsConfig.types; for (const type of Object.keys(changeTypes)) { const group = typeGroups[type]; @@ -204,37 +265,41 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ continue; } - markdownLines.push('', '### ' + changeTypes[type].changelog.title, ''); + markdownLines.push('', `### ${changeTypes[type].changelog.title}`, ''); - /** - * In order to make the final changelog most readable, we organize changes as follows: - * - By scope, where scopes are in alphabetical order (changes with no scope are listed first) - * - Within a particular scope grouping, we list changes in chronological order - */ - const changesInChronologicalOrder = group.reverse(); - const changesGroupedByScope: Record = groupBy( - changesInChronologicalOrder, - 'scope' - ); - const scopesSortedAlphabetically = Object.keys( - changesGroupedByScope - ).sort(); + if (this.project === null) { + const changesGroupedByScope = this.groupChangesByScope(group); + const scopesSortedAlphabetically = Object.keys( + changesGroupedByScope + ).sort(); - for (const scope of scopesSortedAlphabetically) { - const changes = changesGroupedByScope[scope]; - for (const change of changes) { - const line = formatChange( - change, - changelogRenderOptions, - isVersionPlans, - repoData - ); + for (const scope of scopesSortedAlphabetically) { + const changes = changesGroupedByScope[scope]; + for (const change of changes.reverse()) { + const line = this.formatChange(change); + markdownLines.push(line); + if (change.isBreaking && !this.isVersionPlans) { + const breakingChangeExplanation = + this.extractBreakingChangeExplanation(change.body); + this.breakingChanges.push( + breakingChangeExplanation + ? `- ${ + change.scope ? `**${change.scope.trim()}:** ` : '' + }${breakingChangeExplanation}` + : line + ); + } + } + } + } else { + // For project-specific changelogs, maintain the original order + for (const change of group) { + const line = this.formatChange(change); markdownLines.push(line); - if (change.isBreaking) { - const breakingChangeExplanation = extractBreakingChangeExplanation( - change.body - ); - breakingChanges.push( + if (change.isBreaking && !this.isVersionPlans) { + const breakingChangeExplanation = + this.extractBreakingChangeExplanation(change.body); + this.breakingChanges.push( breakingChangeExplanation ? `- ${ change.scope ? `**${change.scope.trim()}:** ` : '' @@ -245,123 +310,68 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ } } } - } else { - // project level changelog - relevantChanges = relevantChanges.filter( - (c) => - c.affectedProjects && - (c.affectedProjects === '*' || c.affectedProjects.includes(project)) - ); - // Generating for a named project, but that project has no relevant changes in the current set of commits, exit early - if (relevantChanges.length === 0 && breakingChanges.length === 0) { - if (dependencyBumps?.length) { - applyAdditionalDependencyBumps({ - markdownLines, - dependencyBumps, - releaseVersion, - changelogRenderOptions, - }); - } else if (entryWhenNoChanges) { - markdownLines.push( - '', - `${createVersionTitle( - releaseVersion, - changelogRenderOptions - )}\n\n${entryWhenNoChanges}`, - '' - ); - } - return markdownLines.join('\n').trim(); - } - - markdownLines.push( - '', - createVersionTitle(releaseVersion, changelogRenderOptions), - '' - ); - - const typeGroups: Record = groupBy( - // Sort the relevant changes to have the unscoped changes first, before grouping by type - relevantChanges.sort((a, b) => (b.scope ? 1 : 0) - (a.scope ? 1 : 0)), - 'type' - ); - for (const type of Object.keys(changeTypes)) { - const group = typeGroups[type]; - if (!group || group.length === 0) { - continue; - } - - markdownLines.push('', `### ${changeTypes[type].changelog.title}`, ''); - - const changesInChronologicalOrder = group.reverse(); - for (const change of changesInChronologicalOrder) { - const line = formatChange( - change, - changelogRenderOptions, - isVersionPlans, - repoData - ); - markdownLines.push(line + '\n'); - if (change.isBreaking) { - const breakingChangeExplanation = extractBreakingChangeExplanation( - change.body - ); - breakingChanges.push( - breakingChangeExplanation - ? `- ${ - change.scope ? `**${change.scope.trim()}:** ` : '' - }${breakingChangeExplanation}` - : line - ); - } - } - } + return markdownLines; } - if (breakingChanges.length > 0) { - markdownLines.push('', '### ⚠️ Breaking Changes', '', ...breakingChanges); + protected hasBreakingChanges(): boolean { + return this.breakingChanges.length > 0; } - if (dependencyBumps?.length) { - applyAdditionalDependencyBumps({ - markdownLines, - dependencyBumps, - releaseVersion, - changelogRenderOptions, + protected renderBreakingChanges(): string[] { + const uniqueBreakingChanges = Array.from(new Set(this.breakingChanges)); + return ['### ⚠️ Breaking Changes', '', ...uniqueBreakingChanges]; + } + + protected hasDependencyBumps(): boolean { + return this.dependencyBumps && this.dependencyBumps.length > 0; + } + + protected renderDependencyBumps(): string[] { + const markdownLines = ['', '### 🧱 Updated Dependencies', '']; + this.dependencyBumps.forEach(({ dependencyName, newVersion }) => { + markdownLines.push(`- Updated ${dependencyName} to ${newVersion}`); }); + return markdownLines; } - if (changelogRenderOptions.authors) { + protected shouldRenderAuthors(): boolean { + return this.changelogRenderOptions.authors; + } + + protected async renderAuthors(): Promise { + const markdownLines: string[] = []; const _authors = new Map; github?: string }>(); for (const change of [ - ...relevantChanges, - ...additionalChangesForAuthorsSection, + ...this.relevantChanges, + ...this.additionalChangesForAuthorsSection, ]) { - if (!change.author) { + if (!change.authors) { continue; } - const name = formatName(change.author.name); - if (!name || name.includes('[bot]')) { - continue; - } - if (_authors.has(name)) { - const entry = _authors.get(name); - entry.email.add(change.author.email); - } else { - _authors.set(name, { email: new Set([change.author.email]) }); + for (const author of change.authors) { + const name = this.formatName(author.name); + if (!name || name.includes('[bot]')) { + continue; + } + if (_authors.has(name)) { + const entry = _authors.get(name); + entry.email.add(author.email); + } else { + _authors.set(name, { email: new Set([author.email]) }); + } } } - // Try to map authors to github usernames - if (repoData && changelogRenderOptions.mapAuthorsToGitHubUsernames) { + if ( + this.repoData && + this.changelogRenderOptions.mapAuthorsToGitHubUsernames + ) { await Promise.all( [..._authors.keys()].map(async (authorName) => { const meta = _authors.get(authorName); for (const email of meta.email) { - // For these pseudo-anonymized emails we can just extract the Github username from before the @ - // It could either be in the format: username@ or github_id+username@ if (email.endsWith('@users.noreply.github.com')) { const match = email.match( /^(\d+\+)?([^@]+)@users\.noreply\.github\.com$/ @@ -371,7 +381,6 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ break; } } - // Look up any other emails against the ungh.cc API const { data } = await axios .get( `https://ungh.cc/users/find/${email}` @@ -397,147 +406,93 @@ const defaultChangelogRenderer: ChangelogRenderer = async ({ '### ' + '❤️ Thank You', '', ...authors - // Sort the contributors by name .sort((a, b) => a.name.localeCompare(b.name)) .map((i) => { - // Tag the author's Github username if we were able to resolve it so that Github adds them as a contributor const github = i.github ? ` @${i.github}` : ''; return `- ${i.name}${github}`; }) ); } + + return markdownLines; } - return markdownLines.join('\n').trim(); -}; + protected formatChange(change: ChangelogChange): string { + let description = change.description; + let extraLines = []; + let extraLinesStr = ''; + if (description.includes('\n')) { + [description, ...extraLines] = description.split('\n'); + const indentation = ' '; + extraLinesStr = extraLines + .filter((l) => l.trim().length > 0) + .map((l) => `${indentation}${l}`) + .join('\n'); + } -export default defaultChangelogRenderer; - -function applyAdditionalDependencyBumps({ - markdownLines, - dependencyBumps, - releaseVersion, - changelogRenderOptions, -}: { - markdownLines: string[]; - dependencyBumps: DependencyBump[]; - releaseVersion: string; - changelogRenderOptions: DefaultChangelogRenderOptions; -}) { - if (markdownLines.length === 0) { - markdownLines.push( - '', - `${createVersionTitle(releaseVersion, changelogRenderOptions)}\n`, - '' - ); - } else { - markdownLines.push(''); + let changeLine = + '- ' + + (!this.isVersionPlans && change.isBreaking ? '⚠️ ' : '') + + (!this.isVersionPlans && change.scope + ? `**${change.scope.trim()}:** ` + : '') + + description; + if (this.repoData && this.changelogRenderOptions.commitReferences) { + changeLine += formatReferences(change.githubReferences, this.repoData); + } + if (extraLinesStr) { + changeLine += '\n\n' + extraLinesStr; + } + return changeLine; + } + + protected groupChangesByType(): Record { + const typeGroups: Record = {}; + for (const change of this.relevantChanges) { + typeGroups[change.type] = typeGroups[change.type] || []; + typeGroups[change.type].push(change); + } + return typeGroups; + } + + protected groupChangesByScope( + changes: ChangelogChange[] + ): Record { + const scopeGroups: Record = {}; + for (const change of changes) { + const scope = change.scope || ''; + scopeGroups[scope] = scopeGroups[scope] || []; + scopeGroups[scope].push(change); + } + return scopeGroups; + } + + protected extractBreakingChangeExplanation(message: string): string | null { + if (!message) { + return null; + } + + const breakingChangeIdentifier = 'BREAKING CHANGE:'; + const startIndex = message.indexOf(breakingChangeIdentifier); + + if (startIndex === -1) { + return null; + } + + const startOfBreakingChange = startIndex + breakingChangeIdentifier.length; + const endOfBreakingChange = message.indexOf('\n', startOfBreakingChange); + + if (endOfBreakingChange === -1) { + return message.substring(startOfBreakingChange).trim(); + } + + return message.substring(startOfBreakingChange, endOfBreakingChange).trim(); + } + + protected formatName(name = ''): string { + return name + .split(' ') + .map((p) => p.trim()) + .join(' '); } - markdownLines.push('### 🧱 Updated Dependencies\n'); - dependencyBumps.forEach(({ dependencyName, newVersion }) => { - markdownLines.push(`- Updated ${dependencyName} to ${newVersion}`); - }); - markdownLines.push(''); -} - -function formatName(name = '') { - return name - .split(' ') - .map((p) => p.trim()) - .join(' '); -} - -function groupBy(items: any[], key: string) { - const groups = {}; - for (const item of items) { - groups[item[key]] = groups[item[key]] || []; - groups[item[key]].push(item); - } - return groups; -} - -function formatChange( - change: ChangelogChange, - changelogRenderOptions: DefaultChangelogRenderOptions, - isVersionPlans: boolean, - repoData?: GithubRepoData -): 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'); - } - - /** - * In version plans changelogs: - * - don't repeat the breaking change icon - * - don't render the scope - */ - let changeLine = - '- ' + - (!isVersionPlans && change.isBreaking ? '⚠️ ' : '') + - (!isVersionPlans && change.scope ? `**${change.scope.trim()}:** ` : '') + - description; - if (repoData && changelogRenderOptions.commitReferences) { - changeLine += formatReferences(change.githubReferences, repoData); - } - if (extraLinesStr) { - changeLine += '\n\n' + extraLinesStr; - } - return changeLine; -} - -/** - * It is common to add further information about a breaking change in the commit body, - * and it is naturally that information that should be included in the BREAKING CHANGES - * section of changelog, rather than repeating the commit title/description. - */ -function extractBreakingChangeExplanation(message: string): string | null { - if (!message) { - return null; - } - - const breakingChangeIdentifier = 'BREAKING CHANGE:'; - const startIndex = message.indexOf(breakingChangeIdentifier); - - if (startIndex === -1) { - // "BREAKING CHANGE:" not found in the message - return null; - } - - const startOfBreakingChange = startIndex + breakingChangeIdentifier.length; - const endOfBreakingChange = message.indexOf('\n', startOfBreakingChange); - - if (endOfBreakingChange === -1) { - // No newline character found, extract till the end of the message - return message.substring(startOfBreakingChange).trim(); - } - - // Extract and return the breaking change message - return message.substring(startOfBreakingChange, endOfBreakingChange).trim(); -} - -function createVersionTitle( - version: string, - changelogRenderOptions: DefaultChangelogRenderOptions -) { - // Normalize by removing any leading `v` during comparison - const isMajorVersion = `${major(version)}.0.0` === version.replace(/^v/, ''); - let maybeDateStr = ''; - if (changelogRenderOptions.versionTitleDate) { - // YYYY-MM-DD - const dateStr = new Date().toISOString().slice(0, 10); - maybeDateStr = ` (${dateStr})`; - } - if (isMajorVersion) { - return `# ${version}${maybeDateStr}`; - } - return `## ${version}${maybeDateStr}`; } diff --git a/packages/nx/src/command-line/release/changelog.ts b/packages/nx/src/command-line/release/changelog.ts index 6f23caf9b7..fd97f74adb 100644 --- a/packages/nx/src/command-line/release/changelog.ts +++ b/packages/nx/src/command-line/release/changelog.ts @@ -96,8 +96,7 @@ export interface ChangelogChange { body?: string; isBreaking?: boolean; githubReferences?: Reference[]; - // TODO(v20): This should be an array of one or more authors (Co-authored-by is supported at the commit level and should have been supported here) - author?: { name: string; email: string }; + authors?: { name: string; email: string }[]; shortHash?: string; revertedHashes?: string[]; } @@ -303,7 +302,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: '', isBreaking: releaseType.isBreaking, githubReferences, - author, + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors: [author], affectedProjects: '*', } : vp.triggeredByProjects.map((project) => { @@ -314,7 +314,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: '', isBreaking: releaseType.isBreaking, githubReferences, - author, + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors: [author], affectedProjects: [project], }; }); @@ -362,7 +363,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: c.body, isBreaking: c.isBreaking, githubReferences: c.references, - author: c.author, + authors: [c.author], shortHash: c.shortHash, revertedHashes: c.revertedHashes, affectedProjects: '*', @@ -515,13 +516,14 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { const releaseType = versionPlanSemverReleaseTypeToChangelogType(bumpForProject); let githubReferences = []; - let author = undefined; + let authors = []; const parsedCommit = vp.commit ? parseGitCommit(vp.commit, true) : null; if (parsedCommit) { githubReferences = parsedCommit.references; - author = parsedCommit.author; + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors = [parsedCommit.author]; } return { type: releaseType.type, @@ -531,8 +533,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { isBreaking: releaseType.isBreaking, affectedProjects: Object.keys(vp.projectVersionBumps), githubReferences, - author, - }; + authors, + } as ChangelogChange; }) .filter(Boolean); } else { @@ -589,7 +591,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: c.body, isBreaking: c.isBreaking, githubReferences: c.references, - author: c.author, + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors: [c.author], shortHash: c.shortHash, revertedHashes: c.revertedHashes, affectedProjects: commitChangesNonProjectFiles( @@ -606,18 +609,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { const projectChangelogs = await generateChangelogForProjects({ tree, args, - projectGraph, changes, projectsVersionData, releaseGroup, projects: [project], nxReleaseConfig, projectToAdditionalDependencyBumps, - // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits - commits: filterHiddenCommits( - commits, - nxReleaseConfig.conventionalCommits - ), }); let hasPushed = false; @@ -688,7 +685,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: '', isBreaking: releaseType.isBreaking, githubReferences, - author, + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors: [author], affectedProjects: '*', } : vp.triggeredByProjects.map((project) => { @@ -699,7 +697,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: '', isBreaking: releaseType.isBreaking, githubReferences, - author, + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors: [author], affectedProjects: [project], }; }); @@ -745,7 +744,8 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { body: c.body, isBreaking: c.isBreaking, githubReferences: c.references, - author: c.author, + // TODO(JamesHenry): Implement support for Co-authored-by and adding multiple authors + authors: [c.author], shortHash: c.shortHash, revertedHashes: c.revertedHashes, affectedProjects: commitChangesNonProjectFiles( @@ -762,18 +762,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) { const projectChangelogs = await generateChangelogForProjects({ tree, args, - projectGraph, changes, projectsVersionData, releaseGroup, projects: projectNodes, nxReleaseConfig, projectToAdditionalDependencyBumps, - // TODO: remove this after the changelog renderer is refactored to remove coupling with git commits - commits: filterHiddenCommits( - commits, - nxReleaseConfig.conventionalCommits - ), }); let hasPushed = false; @@ -1094,7 +1088,7 @@ async function generateChangelogForWorkspace({ const dryRun = !!args.dryRun; const gitRemote = args.gitRemote; - const changelogRenderer = resolveChangelogRenderer(config.renderer); + const ChangelogRendererClass = resolveChangelogRenderer(config.renderer); let interpolatedTreePath = config.file || ''; if (interpolatedTreePath) { @@ -1121,18 +1115,17 @@ async function generateChangelogForWorkspace({ const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease); - let contents = await changelogRenderer({ - projectGraph, + const changelogRenderer = new ChangelogRendererClass({ changes, - commits, - releaseVersion: releaseVersion.rawVersion, + changelogEntryVersion: releaseVersion.rawVersion, project: null, - repoSlug: githubRepoData?.slug, + isVersionPlans: false, repoData: githubRepoData, entryWhenNoChanges: config.entryWhenNoChanges, changelogRenderOptions: config.renderOptions, conventionalCommitsConfig: nxReleaseConfig.conventionalCommits, }); + let contents = await changelogRenderer.render(); /** * If interactive mode, make the changelog contents available for the user to modify in their editor of choice, @@ -1191,9 +1184,7 @@ async function generateChangelogForWorkspace({ async function generateChangelogForProjects({ tree, args, - projectGraph, changes, - commits, projectsVersionData, releaseGroup, projects, @@ -1202,9 +1193,7 @@ async function generateChangelogForProjects({ }: { tree: Tree; args: ChangelogOptions; - projectGraph: ProjectGraph; changes: ChangelogChange[]; - commits: GitCommit[]; projectsVersionData: VersionData; releaseGroup: ReleaseGroupWithName; projects: ProjectGraphProjectNode[]; @@ -1223,7 +1212,7 @@ async function generateChangelogForProjects({ const dryRun = !!args.dryRun; const gitRemote = args.gitRemote; - const changelogRenderer = resolveChangelogRenderer(config.renderer); + const ChangelogRendererClass = resolveChangelogRenderer(config.renderer); const projectChangelogs: NxReleaseChangelogResult['projectChangelogs'] = {}; @@ -1262,13 +1251,10 @@ async function generateChangelogForProjects({ const githubRepoData = getGitHubRepoData(gitRemote, config.createRelease); - let contents = await changelogRenderer({ - projectGraph, + const changelogRenderer = new ChangelogRendererClass({ changes, - commits, - releaseVersion: releaseVersion.rawVersion, + changelogEntryVersion: releaseVersion.rawVersion, project: project.name, - repoSlug: githubRepoData?.slug, repoData: githubRepoData, entryWhenNoChanges: typeof config.entryWhenNoChanges === 'string' @@ -1279,11 +1265,13 @@ async function generateChangelogForProjects({ }) : false, changelogRenderOptions: config.renderOptions, + isVersionPlans: !!releaseGroup.versionPlans, conventionalCommitsConfig: releaseGroup.versionPlans ? null : nxReleaseConfig.conventionalCommits, dependencyBumps: projectToAdditionalDependencyBumps.get(project.name), }); + let contents = await changelogRenderer.render(); /** * If interactive mode, make the changelog contents available for the user to modify in their editor of choice, diff --git a/packages/nx/src/command-line/release/utils/resolve-changelog-renderer.ts b/packages/nx/src/command-line/release/utils/resolve-changelog-renderer.ts index 0f9ea95e1f..43d1c73ee0 100644 --- a/packages/nx/src/command-line/release/utils/resolve-changelog-renderer.ts +++ b/packages/nx/src/command-line/release/utils/resolve-changelog-renderer.ts @@ -1,4 +1,4 @@ -import type { ChangelogRenderer } from '../../../../release/changelog-renderer'; +import type ChangelogRenderer from '../../../../release/changelog-renderer'; import { registerTsProject } from '../../../plugins/js/utils/register'; import { getRootTsConfigPath } from '../../../plugins/js/utils/typescript'; import { interpolate } from '../../../tasks-runner/utils'; @@ -6,13 +6,13 @@ import { workspaceRoot } from '../../../utils/workspace-root'; export function resolveChangelogRenderer( changelogRendererPath: string -): ChangelogRenderer { +): typeof ChangelogRenderer { const interpolatedChangelogRendererPath = interpolate(changelogRendererPath, { workspaceRoot, }); // Try and load the provided (or default) changelog renderer - let changelogRenderer: ChangelogRenderer; + let ChangelogRendererClass: typeof ChangelogRenderer; let cleanupTranspiler = () => {}; try { const rootTsconfigPath = getRootTsConfigPath(); @@ -20,11 +20,11 @@ export function resolveChangelogRenderer( cleanupTranspiler = registerTsProject(rootTsconfigPath); } const r = require(interpolatedChangelogRendererPath); - changelogRenderer = r.default || r; + ChangelogRendererClass = r.default || r; } catch (err) { throw err; } finally { cleanupTranspiler(); } - return changelogRenderer; + return ChangelogRendererClass; }