From 5180001ae6adba7a849d61bf2d5615a417350e71 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Mon, 12 Nov 2018 14:57:52 -0500 Subject: [PATCH] feat: support multiple plugins for the `analyzeCommits` step In case multiple plugins with a `analyzeCommits` step are configured, all of them will be executed and the highest release type (`major` > `minor`, `patch`) will be used. --- docs/usage/plugins.md | 20 +++---- lib/definitions/constants.js | 2 +- lib/definitions/errors.js | 6 +- lib/definitions/plugins.js | 18 +++--- lib/plugins/index.js | 11 ++-- lib/plugins/utils.js | 43 +++++++------- test/definitions/plugins.test.js | 10 ++++ test/plugins/plugins.test.js | 20 ------- test/plugins/utils.test.js | 98 +------------------------------- 9 files changed, 58 insertions(+), 170 deletions(-) diff --git a/docs/usage/plugins.md b/docs/usage/plugins.md index 21668b2e..a1f792ab 100644 --- a/docs/usage/plugins.md +++ b/docs/usage/plugins.md @@ -4,16 +4,16 @@ Each [release step](../../README.md#release-steps) is implemented by configurabl A plugin is a npm module that can implement one or more of the following steps: -| Step | Accept multiple | Required | Description | -|--------------------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `verifyConditions` | Yes | No | Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc... | -| `analyzeCommits` | No | Yes | Responsible for determining the type of the next release (`major`, `minor` or `patch`). | -| `verifyRelease` | Yes | No | Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. | -| `generateNotes` | Yes | No | Responsible for generating the content of the release note. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of each plugin output. | -| `prepare` | Yes | No | Responsible for preparing the release, for example creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets and pushing a commit. | -| `publish` | Yes | No | Responsible for publishing the release. | -| `success` | Yes | No | Responsible for notifying of a new release. | -| `fail` | Yes | No | Responsible for notifying of a failed release. | +| Step | Required | Description | +|--------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `verifyConditions` | No | Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc... | +| `analyzeCommits` | Yes | Responsible for determining the type of the next release (`major`, `minor` or `patch`). If multiple plugins with a `analyzeCommits` step are defined, the release type will be the highest one among plugins output. | +| `verifyRelease` | No | Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. | +| `generateNotes` | No | Responsible for generating the content of the release note. If multiple plugins with a `generateNotes` step are defined, the release notes will be the result of the concatenation of each plugin output. | +| `prepare` | No | Responsible for preparing the release, for example creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets and pushing a commit. | +| `publish` | No | Responsible for publishing the release. | +| `success` | No | Responsible for notifying of a new release. | +| `fail` | No | Responsible for notifying of a failed release. | **Note:** If no plugin with a `analyzeCommits` step is defined `@semantic-release/commit-analyzer` will be used. diff --git a/lib/definitions/constants.js b/lib/definitions/constants.js index 947ec28b..300733c8 100644 --- a/lib/definitions/constants.js +++ b/lib/definitions/constants.js @@ -1,4 +1,4 @@ -const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']; +const RELEASE_TYPE = ['prerelease', 'prepatch', 'patch', 'preminor', 'minor', 'premajor', 'major']; const FIRST_RELEASE = '1.0.0'; diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index ccc60ef1..d5eddb29 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -55,13 +55,11 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.` Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`, }), - EPLUGINCONF: ({type, multiple, required, pluginConf}) => ({ + EPLUGINCONF: ({type, required, pluginConf}) => ({ message: `The \`${type}\` plugin configuration is invalid.`, details: `The [${type} plugin configuration](${linkify(`docs/usage/plugins.md#${toLower(type)}-plugin`)}) ${ required ? 'is required and ' : '' - }must be ${ - multiple ? 'a single or an array of plugins' : 'a single plugin' - } definition. A plugin definition is an npm module name, optionnaly wrapped in an array with an object. + } must be a single or an array of plugins definition. A plugin definition is an npm module name, optionnaly wrapped in an array with an object. Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`, }), diff --git a/lib/definitions/plugins.js b/lib/definitions/plugins.js index fd15f3ad..7fb6fe6e 100644 --- a/lib/definitions/plugins.js +++ b/lib/definitions/plugins.js @@ -6,28 +6,30 @@ const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants'); module.exports = { verifyConditions: { - multiple: true, required: false, pipelineConfig: () => ({settleAll: true}), }, analyzeCommits: { default: ['@semantic-release/commit-analyzer'], - multiple: false, required: true, outputValidator: output => !output || RELEASE_TYPE.includes(output), preprocess: ({commits, ...inputs}) => ({ ...inputs, commits: commits.filter(commit => !/\[skip\s+release\]|\[release\s+skip\]/i.test(commit.message)), }), - postprocess: ([result]) => result, + postprocess: results => + RELEASE_TYPE[ + results.reduce((highest, result) => { + const typeIndex = RELEASE_TYPE.indexOf(result); + return typeIndex > highest ? typeIndex : highest; + }, -1) + ], }, verifyRelease: { - multiple: true, required: false, pipelineConfig: () => ({settleAll: true}), }, generateNotes: { - multiple: true, required: false, outputValidator: output => !output || isString(output), pipelineConfig: () => ({ @@ -42,9 +44,8 @@ module.exports = { postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)), }, prepare: { - multiple: true, required: false, - pipelineConfig: ({generateNotes}, logger) => ({ + pipelineConfig: ({generateNotes}) => ({ getNextInput: async context => { const newGitHead = await gitHead({cwd: context.cwd}); // If previous prepare plugin has created a commit (gitHead changed) @@ -59,7 +60,6 @@ module.exports = { }), }, publish: { - multiple: true, required: false, outputValidator: output => !output || isPlainObject(output), pipelineConfig: () => ({ @@ -72,13 +72,11 @@ module.exports = { }), }, success: { - multiple: true, required: false, pipelineConfig: () => ({settleAll: true}), preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}), }, fail: { - multiple: true, required: false, pipelineConfig: () => ({settleAll: true}), preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}), diff --git a/lib/plugins/index.js b/lib/plugins/index.js index e3ee2025..abbc754c 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -24,7 +24,7 @@ module.exports = (context, pluginsPath) => { writable: false, enumerable: true, }); - plugins[type] = [...(PLUGINS_DEFINITIONS[type].multiple ? plugins[type] || [] : []), [func, config]]; + plugins[type] = [...(plugins[type] || []), [func, config]]; } }); } else { @@ -45,10 +45,7 @@ module.exports = (context, pluginsPath) => { options = {...plugins, ...options}; const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce( - ( - pluginsConf, - [type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}] - ) => { + (pluginsConf, [type, {required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]) => { let pluginOpts; if (isNil(options[type]) && def) { @@ -60,8 +57,8 @@ module.exports = (context, pluginsPath) => { plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin ); } - if (!validateStep({multiple, required}, options[type])) { - errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]})); + if (!validateStep({required}, options[type])) { + errors.push(getError('EPLUGINCONF', {type, required, pluginConf: options[type]})); return pluginsConf; } pluginOpts = options[type]; diff --git a/lib/plugins/utils.js b/lib/plugins/utils.js index 8a9421d4..2a213400 100644 --- a/lib/plugins/utils.js +++ b/lib/plugins/utils.js @@ -2,28 +2,25 @@ const {dirname} = require('path'); const {isString, isFunction, castArray, isArray, isPlainObject, isNil} = require('lodash'); const resolveFrom = require('resolve-from'); -const validateStepArrayDefinition = conf => - isArray(conf) && - (conf.length === 1 || conf.length === 2) && - (isString(conf[0]) || isFunction(conf[0])) && - (isNil(conf[1]) || isPlainObject(conf[1])); +const validateSteps = conf => { + return conf.every(conf => { + if ( + isArray(conf) && + (conf.length === 1 || conf.length === 2) && + (isString(conf[0]) || isFunction(conf[0])) && + (isNil(conf[1]) || isPlainObject(conf[1])) + ) { + return true; + } + conf = castArray(conf); -const validateSingleStep = conf => { - if (validateStepArrayDefinition(conf)) { - return true; - } - conf = castArray(conf); + if (conf.length !== 1) { + return false; + } - if (conf.length !== 1) { - return false; - } - - const [name, config] = parseConfig(conf[0]); - return (isString(name) || isFunction(name)) && isPlainObject(config); -}; - -const validateMultipleStep = conf => { - return conf.every(conf => validateSingleStep(conf)); + const [name, config] = parseConfig(conf[0]); + return (isString(name) || isFunction(name)) && isPlainObject(config); + }); }; function validatePlugin(conf) { @@ -37,12 +34,12 @@ function validatePlugin(conf) { ); } -function validateStep({multiple, required}, conf) { +function validateStep({required}, conf) { conf = castArray(conf).filter(Boolean); if (required) { - return conf.length >= 1 && (multiple ? validateMultipleStep : validateSingleStep)(conf); + return conf.length >= 1 && validateSteps(conf); } - return conf.length === 0 || (multiple ? validateMultipleStep : validateSingleStep)(conf); + return conf.length === 0 || validateSteps(conf); } function loadPlugin({cwd}, name, pluginsPath) { diff --git a/test/definitions/plugins.test.js b/test/definitions/plugins.test.js index 61a86c7f..9f869d9a 100644 --- a/test/definitions/plugins.test.js +++ b/test/definitions/plugins.test.js @@ -51,3 +51,13 @@ test('The "generateNotes" plugins output are concatenated with separator and sen `Note 1: Exposing token ${SECRET_REPLACEMENT}${RELEASE_NOTES_SEPARATOR}Note 2: Exposing token ${SECRET_REPLACEMENT}` ); }); + +test('The "analyzeCommits" plugins output are reduced to the highest release type', t => { + t.is(plugins.analyzeCommits.postprocess(['major', 'minor']), 'major'); + t.is(plugins.analyzeCommits.postprocess(['', 'minor']), 'minor'); + t.is(plugins.analyzeCommits.postprocess([undefined, 'patch']), 'patch'); + t.is(plugins.analyzeCommits.postprocess([null, 'patch']), 'patch'); + t.is(plugins.analyzeCommits.postprocess(['wrong_type', 'minor']), 'minor'); + t.is(plugins.analyzeCommits.postprocess([]), undefined); + t.is(plugins.analyzeCommits.postprocess(['wrong_type']), undefined); +}); diff --git a/test/plugins/plugins.test.js b/test/plugins/plugins.test.js index d33f4e8b..a0020623 100644 --- a/test/plugins/plugins.test.js +++ b/test/plugins/plugins.test.js @@ -105,26 +105,6 @@ test('Export plugins based on "plugins" config (single definition)', async t => t.is(typeof plugins.fail, 'function'); }); -test('Use only last definition of single plugin steps declared in "plugins" config', async t => { - const plugin1 = {analyzeCommits: stub()}; - const plugin2 = {analyzeCommits: stub()}; - const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1, plugin2]}}, {}); - - await plugins.analyzeCommits({commits: []}); - t.true(plugin1.analyzeCommits.notCalled); - t.true(plugin2.analyzeCommits.calledOnce); - - // Verify the module returns a function for each plugin - t.is(typeof plugins.verifyConditions, 'function'); - t.is(typeof plugins.analyzeCommits, 'function'); - t.is(typeof plugins.verifyRelease, 'function'); - t.is(typeof plugins.generateNotes, 'function'); - t.is(typeof plugins.prepare, 'function'); - t.is(typeof plugins.publish, 'function'); - t.is(typeof plugins.success, 'function'); - t.is(typeof plugins.fail, 'function'); -}); - test('Merge global options, "plugins" options and step options', async t => { const plugin1 = [{verifyConditions: stub(), publish: stub()}, {pluginOpt1: 'plugin1'}]; const plugin2 = [{verifyConditions: stub()}, {pluginOpt2: 'plugin2'}]; diff --git a/test/plugins/utils.test.js b/test/plugins/utils.test.js index b7c31e39..e781316c 100644 --- a/test/plugins/utils.test.js +++ b/test/plugins/utils.test.js @@ -25,7 +25,7 @@ test('validatePlugin', t => { t.false(validatePlugin({path: 1}), 'Object definition, wrong path'); }); -test('validateStep: multiple/optional plugin configuration', t => { +test('validateStep: optional plugin configuration', t => { const type = {multiple: true, required: false}; // Empty config @@ -107,8 +107,8 @@ test('validateStep: multiple/optional plugin configuration', t => { ); }); -test('validateStep: multiple/required plugin configuration', t => { - const type = {multiple: true, required: true}; +test('validateStep: required plugin configuration', t => { + const type = {required: true}; // Empty config t.false(validateStep(type)); @@ -189,98 +189,6 @@ test('validateStep: multiple/required plugin configuration', t => { ); }); -test('validateStep: single/required plugin configuration', t => { - const type = {multiple: false, required: true}; - - // Empty config - t.false(validateStep(type)); - t.false(validateStep(type, [])); - - // Single value definition - t.true(validateStep(type, 'plugin-path.js')); - t.true(validateStep(type, () => {})); - t.true(validateStep(type, ['plugin-path.js'])); - t.true(validateStep(type, [() => {}])); - t.false(validateStep(type, {})); - t.false(validateStep(type, [{}])); - - // Array type definition - t.true(validateStep(type, [['plugin-path.js']])); - t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]])); - t.true(validateStep(type, [[() => {}, {options: 'value'}]])); - t.false(validateStep(type, [['plugin-path.js', 1]])); - - // Object type definition - t.true(validateStep(type, {path: 'plugin-path.js'})); - t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'})); - t.true(validateStep(type, {path: () => {}, options: 'value'})); - t.false(validateStep(type, {path: null})); - - // Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type - t.true(validateStep(type, [() => {}, {options: 'value'}])); - t.true(validateStep(type, ['plugin-path.js', {options: 'value'}])); - - // Multiple definitions - t.false( - validateStep(type, [ - 'plugin-path.js', - () => {}, - ['plugin-path.js'], - ['plugin-path.js', {options: 'value'}], - [() => {}, {options: 'value'}], - {path: 'plugin-path.js'}, - {path: 'plugin-path.js', options: 'value'}, - {path: () => {}, options: 'value'}, - ]) - ); -}); - -test('validateStep: single/optional plugin configuration', t => { - const type = {multiple: false, required: false}; - - // Empty config - t.true(validateStep(type)); - t.true(validateStep(type, [])); - - // Single value definition - t.true(validateStep(type, 'plugin-path.js')); - t.true(validateStep(type, () => {})); - t.true(validateStep(type, ['plugin-path.js'])); - t.true(validateStep(type, [() => {}])); - t.false(validateStep(type, {})); - t.false(validateStep(type, [{}])); - - // Array type definition - t.true(validateStep(type, [['plugin-path.js']])); - t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]])); - t.true(validateStep(type, [[() => {}, {options: 'value'}]])); - t.false(validateStep(type, [['plugin-path.js', 1]])); - - // Object type definition - t.true(validateStep(type, {path: 'plugin-path.js'})); - t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'})); - t.true(validateStep(type, {path: () => {}, options: 'value'})); - t.false(validateStep(type, {path: null})); - - // Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type - t.true(validateStep(type, [() => {}, {options: 'value'}])); - t.true(validateStep(type, ['plugin-path.js', {options: 'value'}])); - - // Multiple definitions - t.false( - validateStep(type, [ - 'plugin-path.js', - () => {}, - ['plugin-path.js'], - ['plugin-path.js', {options: 'value'}], - [() => {}, {options: 'value'}], - {path: 'plugin-path.js'}, - {path: 'plugin-path.js', options: 'value'}, - {path: () => {}, options: 'value'}, - ]) - ); -}); - test('loadPlugin', t => { const cwd = process.cwd(); const func = () => {};