Skip to content

Commit

Permalink
Merge pull request #69 from koiralakiran1/custom-release-rule-changelog
Browse files Browse the repository at this point in the history
Feat: Support Custom Changelog rules
  • Loading branch information
mathieudutour authored Apr 1, 2021
2 parents 4d77877 + 514c51b commit 60e1f8d
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 20 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ jobs:
- **tag_prefix** _(optional)_ - A prefix to the tag name (default: `v`).
- **append_to_pre_release_tag** _(optional)_ - A suffix to the pre-release tag name (default: `<branch>`).

#### Customize the conventional commit messages
#### Customize the conventional commit messages & titles of changelog sections

- **custom_release_rules** _(optional)_ - Comma separated list of release rules. Format: `<keyword>:<release_type>`. Example: `hotfix:patch,pre-feat:preminor`.
- **custom_release_rules** _(optional)_ - Comma separated list of release rules.

__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).

__Examples__:
1. `hotfix:patch,pre-feat:preminor`,
2. `bug:patch:Bug Fixes,chore:patch:Chores`

#### Debugging

Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@actions/github": "^4.0.0",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/release-notes-generator": "^9.0.1",
"conventional-changelog-conventionalcommits": "^4.5.0",
"semver": "^7.3.4"
},
"devDependencies": {
Expand Down
15 changes: 13 additions & 2 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getLatestTag,
getValidTags,
mapCustomReleaseRules,
mergeWithDefaultChangelogRules,
} from './utils';
import { createTag } from './github';
import { Await } from './ts';
Expand Down Expand Up @@ -109,7 +110,12 @@ export default async function main() {
commits = await getCommits(previousTag.commit.sha, GITHUB_SHA);

let bump = await analyzeCommits(
{ releaseRules: mappedReleaseRules },
{
releaseRules: mappedReleaseRules
? // analyzeCommits doesn't appreciate rules with a section /shrug
mappedReleaseRules.map(({ section, ...rest }) => ({ ...rest }))
: undefined,
},
{ commits, logger: { log: console.info.bind(console) } }
);

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

const changelog = await generateNotes(
{},
{
preset: 'conventionalcommits',
presetConfig: {
types: mergeWithDefaultChangelogRules(mappedReleaseRules),
},
},
{
commits,
logger: { log: console.info.bind(console) },
Expand Down
33 changes: 33 additions & 0 deletions src/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type ChangelogRule = {
/**
* Commit type.
* Eg: feat, fix etc.
*/
type: string;
/**
* Section in changelog to group commits by type.
* Eg: 'Bug Fix', 'Features' etc.
*/
section?: string;
};

/**
* Default sections & changelog rules mentioned in `conventional-changelog-angular` & `conventional-changelog-conventionalcommits`.
* References:
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-angular/writer-opts.js
* https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-conventionalcommits/writer-opts.js
*/
export const defaultChangelogRules: Readonly<
Record<string, ChangelogRule>
> = Object.freeze({
feat: { type: 'feat', section: 'Features' },
fix: { type: 'fix', section: 'Bug Fixes' },
perf: { type: 'perf', section: 'Performance Improvements' },
revert: { type: 'revert', section: 'Reverts' },
docs: { type: 'docs', section: 'Documentation' },
style: { type: 'style', section: 'Styles' },
refactor: { type: 'refactor', section: 'Code Refactoring' },
test: { type: 'test', section: 'Tests' },
build: { type: 'build', section: 'Build Systems' },
ci: { type: 'ci', section: 'Continuous Integration' },
});
58 changes: 44 additions & 14 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { prerelease, rcompare, valid } from 'semver';
// @ts-ignore
import DEFAULT_RELEASE_TYPES from '@semantic-release/commit-analyzer/lib/default-release-types';
import { compareCommits, listTags } from './github';
import { defaultChangelogRules } from './defaults';
import { Await } from './ts';

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

return customReleaseTypes
.split(releaseRuleSeparator)
.map((customReleaseRule) => customReleaseRule.split(releaseTypeSeparator))
.filter((customReleaseRule) => {
if (customReleaseRule.length !== 2) {
const parts = customReleaseRule.split(releaseTypeSeparator);

if (parts.length < 2) {
core.warning(
`${customReleaseRule.join(
releaseTypeSeparator
)} is not a valid custom release definition.`
`${customReleaseRule} is not a valid custom release definition.`
);
return false;
}

const defaultRule = defaultChangelogRules[parts[0].toLowerCase()];
if (customReleaseRule.length !== 3) {
core.debug(
`${customReleaseRule} doesn't mention the section for the changelog.`
);
core.debug(
defaultRule
? `Default section (${defaultRule.section}) will be used instead.`
: "The commits matching this rule won't be included in the changelog."
);
}

if (!DEFAULT_RELEASE_TYPES.includes(parts[1])) {
core.warning(`${parts[1]} is not a valid release type.`);
return false;
}

return true;
})
.map((customReleaseRule) => {
const [keyword, release] = customReleaseRule;
const [type, release, section] = customReleaseRule.split(
releaseTypeSeparator
);
const defaultRule = defaultChangelogRules[type.toLowerCase()];

return {
type: keyword,
type,
release,
section: section || defaultRule?.section,
};
})
.filter((customRelease) => {
if (!DEFAULT_RELEASE_TYPES.includes(customRelease.release)) {
core.warning(`${customRelease.release} is not a valid release type.`);
return false;
}
return true;
});
}

export function mergeWithDefaultChangelogRules(
mappedReleaseRules: ReturnType<typeof mapCustomReleaseRules> = []
) {
const mergedRules = mappedReleaseRules.reduce(
(acc, curr) => ({
...acc,
[curr.type]: curr,
}),
{ ...defaultChangelogRules }
);

return Object.values(mergedRules).filter((rule) => !!rule.section);
}
79 changes: 78 additions & 1 deletion tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as utils from '../src/utils';
import { getValidTags } from '../src/utils';
import * as core from '@actions/core';
import * as github from '../src/github';
import { defaultChangelogRules } from '../src/defaults';

jest.spyOn(core, 'debug').mockImplementation(() => {});
jest.spyOn(core, 'warning').mockImplementation(() => {});
Expand Down Expand Up @@ -140,7 +141,8 @@ describe('utils', () => {
/*
* Given
*/
const customReleasesString = 'james:preminor,bond:premajor';
const customReleasesString =
'james:preminor,bond:premajor,007:major:Breaking Changes,feat:minor';

/*
* When
Expand All @@ -153,6 +155,12 @@ describe('utils', () => {
expect(mappedReleases).toEqual([
{ type: 'james', release: 'preminor' },
{ type: 'bond', release: 'premajor' },
{ type: '007', release: 'major', section: 'Breaking Changes' },
{
type: 'feat',
release: 'minor',
section: defaultChangelogRules['feat'].section,
},
]);
});

Expand All @@ -173,4 +181,73 @@ describe('utils', () => {
expect(mappedReleases).toEqual([{ type: 'bond', release: 'premajor' }]);
});
});

describe('method: mergeWithDefaultChangelogRules', () => {
it('combines non-existing type rules with default rules', () => {
/**
* Given
*/
const newRule = {
type: 'james',
release: 'major',
section: '007 Changes',
};

/**
* When
*/
const result = utils.mergeWithDefaultChangelogRules([newRule]);

/**
* Then
*/
expect(result).toEqual([
...Object.values(defaultChangelogRules),
newRule,
]);
});

it('overwrites existing default type rules with provided rules', () => {
/**
* Given
*/
const newRule = {
type: 'feat',
release: 'minor',
section: '007 Changes',
};

/**
* When
*/
const result = utils.mergeWithDefaultChangelogRules([newRule]);
const overWrittenRule = result.find((rule) => rule.type === 'feat');

/**
* Then
*/
expect(overWrittenRule?.section).toBe(newRule.section);
});

it('returns only the rules having changelog section', () => {
/**
* Given
*/
const mappedReleaseRules = [
{ type: 'james', release: 'major', section: '007 Changes' },
{ type: 'bond', release: 'minor', section: undefined },
];

/**
* When
*/
const result = utils.mergeWithDefaultChangelogRules(mappedReleaseRules);

/**
* Then
*/
expect(result).toContainEqual(mappedReleaseRules[0]);
expect(result).not.toContainEqual(mappedReleaseRules[1]);
});
});
});
3 changes: 2 additions & 1 deletion types/semantic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ declare module '@semantic-release/release-notes-generator' {
preset?: string;
config?: string;
parserOpts?: any;
writerOpts?: any;
releaseRules?:
| string
| {
type: string;
release: string;
scope?: string;
}[];
presetConfig?: string;
presetConfig?: any; // Depends on used preset
},
args: {
commits: { message: string; hash: string | null }[];
Expand Down

0 comments on commit 60e1f8d

Please sign in to comment.