Austin Fahsl cbb88f0d29
feat(release): add conventional commits configurability for version and changelog (#22004)
Co-authored-by: Isaac Mann <isaacplmann@users.noreply.github.com>
2024-03-13 18:19:24 -06:00

675 lines
18 KiB
TypeScript

import { DEFAULT_CONVENTIONAL_COMMITS_CONFIG } from '../../src/command-line/release/config/conventional-commits';
import type { GitCommit } from '../../src/command-line/release/utils/git';
import defaultChangelogRenderer from './index';
jest.mock('../../src/project-graph/file-map-utils', () => ({
createFileMapUsingProjectGraph: jest.fn().mockImplementation(() => {
return Promise.resolve({
allWorkspaceFiles: [],
fileMap: {
nonProjectFiles: [],
projectFileMap: {
'pkg-a': [
{
file: 'packages/pkg-a/src/index.ts',
},
],
'pkg-b': [
{
file: 'packages/pkg-b/src/index.ts',
},
],
},
},
});
}),
}));
describe('defaultChangelogRenderer()', () => {
const projectGraph = {
nodes: {},
} as any;
const commits: GitCommit[] = [
{
message: 'fix: all packages fixed',
shortHash: '4130f65',
author: {
name: 'James Henry',
email: 'jh@example.com',
},
body: '"\n\nM\tpackages/pkg-a/src/index.ts\nM\tpackages/pkg-b/src/index.ts\n"',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'all packages fixed',
type: 'fix',
scope: '',
references: [
{
value: '4130f65',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: [],
affectedFiles: [
'packages/pkg-a/src/index.ts',
'packages/pkg-b/src/index.ts',
],
},
{
message: 'feat(pkg-b): and another new capability',
shortHash: '7dc5ec3',
author: {
name: 'James Henry',
email: 'jh@example.com',
},
body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'and another new capability',
type: 'feat',
scope: 'pkg-b',
references: [
{
value: '7dc5ec3',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: [],
affectedFiles: ['packages/pkg-b/src/index.ts'],
},
{
message: 'feat(pkg-a): new hotness',
shortHash: 'd7a58a2',
author: {
name: 'James Henry',
email: 'jh@example.com',
},
body: '"\n\nM\tpackages/pkg-a/src/index.ts\n"',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'new hotness',
type: 'feat',
scope: 'pkg-a',
references: [
{
value: 'd7a58a2',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: [],
affectedFiles: ['packages/pkg-a/src/index.ts'],
},
{
message: 'feat(pkg-b): brand new thing',
shortHash: 'feace4a',
author: {
name: 'James Henry',
email: 'jh@example.com',
},
body: '"\n\nM\tpackages/pkg-b/src/index.ts\n"',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'brand new thing',
type: 'feat',
scope: 'pkg-b',
references: [
{
value: 'feace4a',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: [],
affectedFiles: ['packages/pkg-b/src/index.ts'],
},
{
message: 'fix(pkg-a): squashing bugs',
shortHash: '6301405',
author: {
name: 'James Henry',
email: 'jh@example.com',
},
body: '"\n\nM\tpackages/pkg-a/src/index.ts\n',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'squashing bugs',
type: 'fix',
scope: 'pkg-a',
references: [
{
value: '6301405',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: [],
affectedFiles: ['packages/pkg-a/src/index.ts'],
},
];
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,
commits,
releaseVersion: 'v1.1.0',
project: null,
entryWhenNoChanges: false,
changelogRenderOptions: {
authors: true,
},
conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG,
});
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,
commits,
// 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,
commits,
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
changelogRenderOptions: {
authors: false,
},
})
).toMatchInlineSnapshot(`
"## v1.1.0
### 🚀 Features
- **pkg-a:** new hotness
### 🩹 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"
`);
});
});
describe('entryWhenNoChanges', () => {
it('should respect the entryWhenNoChanges option for the workspace changelog', async () => {
const otherOpts = {
projectGraph,
commits: [],
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,
commits: [],
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 commitsWithOnlyRevert: GitCommit[] = [
{
message:
'Revert "fix(release): do not update dependents when they already use "*" (#20607)"',
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',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description:
'Revert "fix(release): do not update dependents when they already use "*" (#20607)"',
type: 'revert',
scope: 'release',
references: [
{
type: 'pull-request',
value: '#20607',
},
{
value: '6528e88aa',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: ['6d68236d467812aba4557a2bc7f667157de80fdb'],
affectedFiles: [
'packages/js/src/generators/release-version/release-version.spec.ts',
'packages/js/src/generators/release-version/release-version.ts',
],
},
];
const markdown = await defaultChangelogRenderer({
projectGraph,
commits: commitsWithOnlyRevert,
releaseVersion: 'v1.1.0',
project: null,
entryWhenNoChanges: false,
changelogRenderOptions: {
authors: true,
},
conventionalCommitsConfig: DEFAULT_CONVENTIONAL_COMMITS_CONFIG,
});
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 commitsWithRevertAndOriginal: GitCommit[] = [
{
message:
'Revert "fix(release): do not update dependents when they already use "*" (#20607)"',
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',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description:
'Revert "fix(release): do not update dependents when they already use "*" (#20607)"',
type: 'revert',
scope: 'release',
references: [
{
type: 'pull-request',
value: '#20607',
},
{
value: '6528e88aa',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: ['6d68236d467812aba4557a2bc7f667157de80fdb'],
affectedFiles: [
'packages/js/src/generators/release-version/release-version.spec.ts',
'packages/js/src/generators/release-version/release-version.ts',
],
},
{
message:
'fix(release): do not update dependents when they already use "*" (#20607)',
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',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'do not update dependents when they already use "*"',
type: 'fix',
scope: 'release',
references: [
{
type: 'pull-request',
value: '#20607',
},
{
value: '6d68236d4',
type: 'hash',
},
],
isBreaking: false,
revertedHashes: [],
affectedFiles: [
'packages/js/src/generators/release-version/release-version.spec.ts',
'packages/js/src/generators/release-version/release-version.ts',
],
},
];
const markdown = await defaultChangelogRenderer({
projectGraph,
commits: commitsWithRevertAndOriginal,
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 breakingChangeCommitWithExplanation: GitCommit = {
// ! after the type, no BREAKING CHANGE: in the body
message: 'feat(WebSocketSubject)!: no longer extends `Subject`.',
shortHash: '54f2f6ed1',
author: {
name: 'James Henry',
email: 'jh@example.com',
},
body:
'M\tpackages/rxjs/src/internal/observable/dom/WebSocketSubject.ts\n' +
'"',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'no longer extends `Subject`.',
type: 'feat',
scope: 'WebSocketSubject',
references: [{ value: '54f2f6ed1', type: 'hash' }],
isBreaking: true,
revertedHashes: [],
affectedFiles: [
'packages/rxjs/src/internal/observable/dom/WebSocketSubject.ts',
],
};
const markdown = await defaultChangelogRenderer({
projectGraph,
commits: [breakingChangeCommitWithExplanation],
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 breakingChangeCommitWithExplanation: GitCommit = {
// No ! after the type, but BREAKING CHANGE: in the body
message: 'feat(WebSocketSubject): no longer extends `Subject`.',
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' +
'"',
authors: [
{
name: 'James Henry',
email: 'jh@example.com',
},
],
description: 'no longer extends `Subject`.',
type: 'feat',
scope: 'WebSocketSubject',
references: [{ value: '54f2f6ed1', type: 'hash' }],
isBreaking: true,
revertedHashes: [],
affectedFiles: [
'packages/rxjs/src/internal/observable/dom/WebSocketSubject.ts',
],
};
const markdown = await defaultChangelogRenderer({
projectGraph,
commits: [breakingChangeCommitWithExplanation],
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"
`);
});
});
});