Skip to content

Commit 60e1f8d

Browse files
Merge pull request #69 from koiralakiran1/custom-release-rule-changelog
Feat: Support Custom Changelog rules
2 parents 4d77877 + 514c51b commit 60e1f8d

File tree

8 files changed

+189
-20
lines changed

8 files changed

+189
-20
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ jobs:
4747
- **tag_prefix** _(optional)_ - A prefix to the tag name (default: `v`).
4848
- **append_to_pre_release_tag** _(optional)_ - A suffix to the pre-release tag name (default: `<branch>`).
4949

50-
#### Customize the conventional commit messages
50+
#### Customize the conventional commit messages & titles of changelog sections
5151

52-
- **custom_release_rules** _(optional)_ - Comma separated list of release rules. Format: `<keyword>:<release_type>`. Example: `hotfix:patch,pre-feat:preminor`.
52+
- **custom_release_rules** _(optional)_ - Comma separated list of release rules.
53+
54+
__Format__: `<keyword>:<release_type>:<changelog_section>` where `<changelog_section>` is optional and will default to [Angular's conventions](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
55+
56+
__Examples__:
57+
1. `hotfix:patch,pre-feat:preminor`,
58+
2. `bug:patch:Bug Fixes,chore:patch:Chores`
5359

5460
#### Debugging
5561

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@actions/github": "^4.0.0",
2727
"@semantic-release/commit-analyzer": "^8.0.1",
2828
"@semantic-release/release-notes-generator": "^9.0.1",
29+
"conventional-changelog-conventionalcommits": "^4.5.0",
2930
"semver": "^7.3.4"
3031
},
3132
"devDependencies": {

src/action.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
getLatestTag,
1111
getValidTags,
1212
mapCustomReleaseRules,
13+
mergeWithDefaultChangelogRules,
1314
} from './utils';
1415
import { createTag } from './github';
1516
import { Await } from './ts';
@@ -109,7 +110,12 @@ export default async function main() {
109110
commits = await getCommits(previousTag.commit.sha, GITHUB_SHA);
110111

111112
let bump = await analyzeCommits(
112-
{ releaseRules: mappedReleaseRules },
113+
{
114+
releaseRules: mappedReleaseRules
115+
? // analyzeCommits doesn't appreciate rules with a section /shrug
116+
mappedReleaseRules.map(({ section, ...rest }) => ({ ...rest }))
117+
: undefined,
118+
},
113119
{ commits, logger: { log: console.info.bind(console) } }
114120
);
115121

@@ -153,7 +159,12 @@ export default async function main() {
153159
core.setOutput('new_tag', newTag);
154160

155161
const changelog = await generateNotes(
156-
{},
162+
{
163+
preset: 'conventionalcommits',
164+
presetConfig: {
165+
types: mergeWithDefaultChangelogRules(mappedReleaseRules),
166+
},
167+
},
157168
{
158169
commits,
159170
logger: { log: console.info.bind(console) },

src/defaults.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
type ChangelogRule = {
2+
/**
3+
* Commit type.
4+
* Eg: feat, fix etc.
5+
*/
6+
type: string;
7+
/**
8+
* Section in changelog to group commits by type.
9+
* Eg: 'Bug Fix', 'Features' etc.
10+
*/
11+
section?: string;
12+
};
13+
14+
/**
15+
* Default sections & changelog rules mentioned in `conventional-changelog-angular` & `conventional-changelog-conventionalcommits`.
16+
* References:
17+
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-angular/writer-opts.js
18+
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-conventionalcommits/writer-opts.js
19+
*/
20+
export const defaultChangelogRules: Readonly<
21+
Record<string, ChangelogRule>
22+
> = Object.freeze({
23+
feat: { type: 'feat', section: 'Features' },
24+
fix: { type: 'fix', section: 'Bug Fixes' },
25+
perf: { type: 'perf', section: 'Performance Improvements' },
26+
revert: { type: 'revert', section: 'Reverts' },
27+
docs: { type: 'docs', section: 'Documentation' },
28+
style: { type: 'style', section: 'Styles' },
29+
refactor: { type: 'refactor', section: 'Code Refactoring' },
30+
test: { type: 'test', section: 'Tests' },
31+
build: { type: 'build', section: 'Build Systems' },
32+
ci: { type: 'ci', section: 'Continuous Integration' },
33+
});

src/utils.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { prerelease, rcompare, valid } from 'semver';
33
// @ts-ignore
44
import DEFAULT_RELEASE_TYPES from '@semantic-release/commit-analyzer/lib/default-release-types';
55
import { compareCommits, listTags } from './github';
6+
import { defaultChangelogRules } from './defaults';
67
import { Await } from './ts';
78

89
type Tags = Await<ReturnType<typeof listTags>>;
@@ -77,30 +78,59 @@ export function mapCustomReleaseRules(customReleaseTypes: string) {
7778

7879
return customReleaseTypes
7980
.split(releaseRuleSeparator)
80-
.map((customReleaseRule) => customReleaseRule.split(releaseTypeSeparator))
8181
.filter((customReleaseRule) => {
82-
if (customReleaseRule.length !== 2) {
82+
const parts = customReleaseRule.split(releaseTypeSeparator);
83+
84+
if (parts.length < 2) {
8385
core.warning(
84-
`${customReleaseRule.join(
85-
releaseTypeSeparator
86-
)} is not a valid custom release definition.`
86+
`${customReleaseRule} is not a valid custom release definition.`
87+
);
88+
return false;
89+
}
90+
91+
const defaultRule = defaultChangelogRules[parts[0].toLowerCase()];
92+
if (customReleaseRule.length !== 3) {
93+
core.debug(
94+
`${customReleaseRule} doesn't mention the section for the changelog.`
8795
);
96+
core.debug(
97+
defaultRule
98+
? `Default section (${defaultRule.section}) will be used instead.`
99+
: "The commits matching this rule won't be included in the changelog."
100+
);
101+
}
102+
103+
if (!DEFAULT_RELEASE_TYPES.includes(parts[1])) {
104+
core.warning(`${parts[1]} is not a valid release type.`);
88105
return false;
89106
}
107+
90108
return true;
91109
})
92110
.map((customReleaseRule) => {
93-
const [keyword, release] = customReleaseRule;
111+
const [type, release, section] = customReleaseRule.split(
112+
releaseTypeSeparator
113+
);
114+
const defaultRule = defaultChangelogRules[type.toLowerCase()];
115+
94116
return {
95-
type: keyword,
117+
type,
96118
release,
119+
section: section || defaultRule?.section,
97120
};
98-
})
99-
.filter((customRelease) => {
100-
if (!DEFAULT_RELEASE_TYPES.includes(customRelease.release)) {
101-
core.warning(`${customRelease.release} is not a valid release type.`);
102-
return false;
103-
}
104-
return true;
105121
});
106122
}
123+
124+
export function mergeWithDefaultChangelogRules(
125+
mappedReleaseRules: ReturnType<typeof mapCustomReleaseRules> = []
126+
) {
127+
const mergedRules = mappedReleaseRules.reduce(
128+
(acc, curr) => ({
129+
...acc,
130+
[curr.type]: curr,
131+
}),
132+
{ ...defaultChangelogRules }
133+
);
134+
135+
return Object.values(mergedRules).filter((rule) => !!rule.section);
136+
}

tests/utils.test.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as utils from '../src/utils';
22
import { getValidTags } from '../src/utils';
33
import * as core from '@actions/core';
44
import * as github from '../src/github';
5+
import { defaultChangelogRules } from '../src/defaults';
56

67
jest.spyOn(core, 'debug').mockImplementation(() => {});
78
jest.spyOn(core, 'warning').mockImplementation(() => {});
@@ -140,7 +141,8 @@ describe('utils', () => {
140141
/*
141142
* Given
142143
*/
143-
const customReleasesString = 'james:preminor,bond:premajor';
144+
const customReleasesString =
145+
'james:preminor,bond:premajor,007:major:Breaking Changes,feat:minor';
144146

145147
/*
146148
* When
@@ -153,6 +155,12 @@ describe('utils', () => {
153155
expect(mappedReleases).toEqual([
154156
{ type: 'james', release: 'preminor' },
155157
{ type: 'bond', release: 'premajor' },
158+
{ type: '007', release: 'major', section: 'Breaking Changes' },
159+
{
160+
type: 'feat',
161+
release: 'minor',
162+
section: defaultChangelogRules['feat'].section,
163+
},
156164
]);
157165
});
158166

@@ -173,4 +181,73 @@ describe('utils', () => {
173181
expect(mappedReleases).toEqual([{ type: 'bond', release: 'premajor' }]);
174182
});
175183
});
184+
185+
describe('method: mergeWithDefaultChangelogRules', () => {
186+
it('combines non-existing type rules with default rules', () => {
187+
/**
188+
* Given
189+
*/
190+
const newRule = {
191+
type: 'james',
192+
release: 'major',
193+
section: '007 Changes',
194+
};
195+
196+
/**
197+
* When
198+
*/
199+
const result = utils.mergeWithDefaultChangelogRules([newRule]);
200+
201+
/**
202+
* Then
203+
*/
204+
expect(result).toEqual([
205+
...Object.values(defaultChangelogRules),
206+
newRule,
207+
]);
208+
});
209+
210+
it('overwrites existing default type rules with provided rules', () => {
211+
/**
212+
* Given
213+
*/
214+
const newRule = {
215+
type: 'feat',
216+
release: 'minor',
217+
section: '007 Changes',
218+
};
219+
220+
/**
221+
* When
222+
*/
223+
const result = utils.mergeWithDefaultChangelogRules([newRule]);
224+
const overWrittenRule = result.find((rule) => rule.type === 'feat');
225+
226+
/**
227+
* Then
228+
*/
229+
expect(overWrittenRule?.section).toBe(newRule.section);
230+
});
231+
232+
it('returns only the rules having changelog section', () => {
233+
/**
234+
* Given
235+
*/
236+
const mappedReleaseRules = [
237+
{ type: 'james', release: 'major', section: '007 Changes' },
238+
{ type: 'bond', release: 'minor', section: undefined },
239+
];
240+
241+
/**
242+
* When
243+
*/
244+
const result = utils.mergeWithDefaultChangelogRules(mappedReleaseRules);
245+
246+
/**
247+
* Then
248+
*/
249+
expect(result).toContainEqual(mappedReleaseRules[0]);
250+
expect(result).not.toContainEqual(mappedReleaseRules[1]);
251+
});
252+
});
176253
});

types/semantic.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ declare module '@semantic-release/release-notes-generator' {
2828
preset?: string;
2929
config?: string;
3030
parserOpts?: any;
31+
writerOpts?: any;
3132
releaseRules?:
3233
| string
3334
| {
3435
type: string;
3536
release: string;
3637
scope?: string;
3738
}[];
38-
presetConfig?: string;
39+
presetConfig?: any; // Depends on used preset
3940
},
4041
args: {
4142
commits: { message: string; hash: string | null }[];

0 commit comments

Comments
 (0)