diff --git a/README.md b/README.md index 06c123858b..f3fabec62c 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,7 @@ This removes the immediate connection between human emotions and version numbers **semantic-release** uses the commit messages to determine the consumer impact of changes in the codebase. Following formalized conventions for commit messages, **semantic-release** automatically determines the next [semantic version](https://semver.org) number, generates a changelog and publishes the release. -By default, **semantic-release** uses [Angular Commit Message Conventions](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format). -The commit message format can be changed with the [`preset` or `config` options](docs/usage/configuration.md#options) of the [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer#options) and [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator#options) plugins. +By default **semantic-release** uses the [Conventional Commits convention](https://www.conventionalcommits.org/). The commit message format can be changed with the [`preset` or `config` options](docs/usage/configuration.md#options) of the [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer#options) and [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator#options) plugins. Tools such as [commitizen](https://github.com/commitizen/cz-cli) or [commitlint](https://github.com/conventional-changelog/commitlint) can be used to help contributors and enforce valid commit messages. diff --git a/cli.js b/cli.js index 9040236228..23fb164789 100755 --- a/cli.js +++ b/cli.js @@ -23,6 +23,7 @@ Usage: .option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'}) .option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'}) .option('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'}) + .option('preset', {describe: 'Commit message format convention', type: 'string', group: 'Options'}) .option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'}) .option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'}) .option('verify-conditions', {...stringList, group: 'Plugins'}) diff --git a/lib/get-config.js b/lib/get-config.js index 1f16962ac9..e2679bc96e 100644 --- a/lib/get-config.js +++ b/lib/get-config.js @@ -70,6 +70,7 @@ module.exports = async (context, cliOptions) => { '@semantic-release/npm', '@semantic-release/github', ], + preset: 'conventionalcommits', // Remove `null` and `undefined` options so they can be replaced with default ones ...pickBy(options, (option) => !isNil(option)), ...(options.branches ? {branches: castArray(options.branches)} : {}), diff --git a/package-lock.json b/package-lock.json index 71e440d9c8..5b0ce77eee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@semantic-release/npm": "^8.0.0", "@semantic-release/release-notes-generator": "^10.0.0", "aggregate-error": "^3.0.0", + "conventional-changelog-conventionalcommits": "^4.6.3", "cosmiconfig": "^7.0.0", "debug": "^4.0.0", "env-ci": "^5.0.0", @@ -2809,6 +2810,19 @@ "node": ">=10" } }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", + "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/conventional-changelog-writer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz", @@ -18314,6 +18328,16 @@ "q": "^1.5.1" } }, + "conventional-changelog-conventionalcommits": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", + "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, "conventional-changelog-writer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz", diff --git a/package.json b/package.json index 44dd6b6b44..39fa953016 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@semantic-release/npm": "^8.0.0", "@semantic-release/release-notes-generator": "^10.0.0", "aggregate-error": "^3.0.0", + "conventional-changelog-conventionalcommits": "^4.6.3", "cosmiconfig": "^7.0.0", "debug": "^4.0.0", "env-ci": "^5.0.0", diff --git a/test/get-config.test.js b/test/get-config.test.js index 28a9ec6297..9c42693bb8 100644 --- a/test/get-config.test.js +++ b/test/get-config.test.js @@ -118,7 +118,7 @@ test('Read options from package.json', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from package.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json @@ -140,7 +140,7 @@ test('Read options from .releaserc.yml', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from package.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json @@ -162,7 +162,7 @@ test('Read options from .releaserc.json', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from package.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json @@ -184,7 +184,7 @@ test('Read options from .releaserc.js', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from package.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json @@ -206,7 +206,7 @@ test('Read options from .releaserc.cjs', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from .releaserc.cjs t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from .releaserc.cjs @@ -228,7 +228,7 @@ test('Read options from release.config.js', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from package.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json @@ -250,7 +250,7 @@ test('Read options from release.config.cjs', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from release.config.cjs t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from release.config.cjs @@ -280,7 +280,7 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async const result = await t.context.getConfig({cwd}, options); - const expected = {...options, branches: ['branch_cli']}; + const expected = {...options, branches: ['branch_cli'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from CLI/API t.deepEqual(result.options, expected); // Verify the plugins module is called with the plugin options from CLI/API @@ -305,7 +305,7 @@ test('Read configuration from file path in "extends"', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from shareable.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from shareable.json @@ -336,7 +336,7 @@ test('Read configuration from module path in "extends"', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options, branches: ['test_branch']}; + const expected = {...options, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from shareable.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from shareable.json @@ -372,7 +372,7 @@ test('Read configuration from an array of paths in "extends"', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = {...options1, ...options2, branches: ['test_branch']}; + const expected = {...options1, ...options2, branches: ['test_branch'], preset: 'conventionalcommits'}; // Verify the options contains the plugin config from shareable1.json and shareable2.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from shareable1.json and shareable2.json @@ -410,7 +410,7 @@ test('Prioritize configuration from config file over "extends"', async (t) => { const {options: result} = await t.context.getConfig({cwd}); - const expected = omit({...options1, ...pkgOptions, branches: ['test_pkg']}, 'extends'); + const expected = omit({...options1, ...pkgOptions, branches: ['test_pkg'], preset: 'conventionalcommits'}, 'extends'); // Verify the options contains the plugin config from package.json and shareable.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json and shareable.json @@ -458,7 +458,10 @@ test('Prioritize configuration from cli/API options over "extends"', async (t) = const {options: result} = await t.context.getConfig({cwd}, cliOptions); - const expected = omit({...options2, ...pkgOptions, ...cliOptions, branches: ['branch_opts']}, 'extends'); + const expected = omit( + {...options2, ...pkgOptions, ...cliOptions, branches: ['branch_opts'], preset: 'conventionalcommits'}, + 'extends' + ); // Verify the options contains the plugin config from package.json and shareable2.json t.deepEqual(result, expected); // Verify the plugins module is called with the plugin options from package.json and shareable2.json @@ -492,6 +495,7 @@ test('Allow to unset properties defined in shareable config with "null"', async ...omit(options1, ['analyzeCommits']), ...omit(pkgOptions, ['extends', 'analyzeCommits']), plugins: DEFAULT_PLUGINS, + preset: 'conventionalcommits', }); // Verify the plugins module is called with the plugin options from shareable.json and the default `plugins` t.deepEqual(t.context.plugins.args[0][0], { @@ -499,6 +503,7 @@ test('Allow to unset properties defined in shareable config with "null"', async ...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits']), plugins: DEFAULT_PLUGINS, + preset: 'conventionalcommits', }, cwd, }); @@ -535,6 +540,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a ...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits']), branches: ['test_branch'], + preset: 'conventionalcommits', }; // Verify the options contains the plugin config from shareable.json t.deepEqual(result, expected); @@ -577,3 +583,40 @@ test('Convert "ci" option to "noCi" when set from extended config', async (t) => t.is(result.ci, false); t.is(result.noCi, true); }); + +test('Allow to override preset', async (t) => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + const pkgOptions = { + preset: '1', + }; + // Create package.json in repository root + await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); + + const { + options: {preset: result}, + } = await t.context.getConfig({cwd}); + + // Verify the preset contains the config from CLI/API + t.deepEqual(result, pkgOptions.preset); +}); + +test('Allow to override preset through CLI option --preset', async (t) => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + const pkgOptions = { + preset: '1', + }; + const cliOptions = { + preset: '2', + }; + // Create package.json in repository root + await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); + + const { + options: {preset: result}, + } = await t.context.getConfig({cwd}, cliOptions); + + // Verify the preset contains the config from CLI/API + t.deepEqual(result, cliOptions.preset); +}); diff --git a/test/index.test.js b/test/index.test.js index d2533c4919..6f7ab94bb9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -92,6 +92,7 @@ test('Plugins are called with expected values', async (t) => { repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`, + preset: 'conventionalcommits', }; const branches = [ { @@ -883,6 +884,7 @@ test('Call all "success" plugins even if one errors out', async (t) => { repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`, + preset: 'conventionalcommits', }; const options = { ...config, @@ -927,7 +929,12 @@ test('Log all "verifyConditions" errors', async (t) => { const error2 = new SemanticReleaseError('error 2', 'ERR2'); const error3 = new SemanticReleaseError('error 3', 'ERR3'); const fail = stub().resolves(); - const config = {branches: [{name: 'master'}], repositoryUrl, tagFormat: `v\${version}`}; + const config = { + branches: [{name: 'master'}], + repositoryUrl, + tagFormat: `v\${version}`, + preset: 'conventionalcommits', + }; const options = { ...config, plugins: false, @@ -971,7 +978,12 @@ test('Log all "verifyRelease" errors', async (t) => { const error1 = new SemanticReleaseError('error 1', 'ERR1'); const error2 = new SemanticReleaseError('error 2', 'ERR2'); const fail = stub().resolves(); - const config = {branches: [{name: 'master'}], repositoryUrl, tagFormat: `v\${version}`}; + const config = { + branches: [{name: 'master'}], + repositoryUrl, + tagFormat: `v\${version}`, + preset: 'conventionalcommits', + }; const options = { ...config, verifyConditions: stub().resolves(),