feat: allow to define plugin options globally

This commit is contained in:
Pierre Vanduynslager 2017-12-10 17:34:59 -05:00
parent d28b7e3e07
commit f707b1a90a
6 changed files with 64 additions and 35 deletions

View File

@ -173,14 +173,12 @@ These options are currently available:
_A few notes on `npm` config_: _A few notes on `npm` config_:
1. The `npm` token can only be defined in the environment as `NPM_TOKEN`, because thats where `npm` itself is going to read it from. 1. The `npm` token can only be defined in the environment as `NPM_TOKEN`, because thats where `npm` itself is going to read it from.
2. In order to publish to a different `npm` registry you can specify that inside the `package.json`s [`publishConfig`](https://docs.npmjs.com/files/package.json#publishconfig) field. 2. In order to publish to a different `npm` registry you can specify that inside the `package.json`s [`publishConfig`](https://docs.npmjs.com/files/package.json#publishconfig) field.
3. If you want to use another dist-tag for your publishes than `'latest'` you can specify that inside the `package.json`s [`publishConfig`](https://docs.npmjs.com/files/package.json#publishconfig) field. 3. If you want to use another dist-tag for your publishes than `'latest'` you can specify that inside the `package.json`s [`publishConfig`](https://docs.npmjs.com/files/package.json#publishconfig) field.
## Plugins ## Plugins
There are numerous steps where you can customize `semantic-release`s behaviour using plugins. A plugin is a regular [option](#options), but passed inside the `release` block of `package.json`: There are numerous steps where you can customize `semantic-release`s behavior using plugins. A plugin is a regular [option](#options), but passed inside the `release` block of `package.json`:
```json ```json
{ {
@ -190,12 +188,13 @@ There are numerous steps where you can customize `semantic-release`s behaviou
"verifyConditions": { "verifyConditions": {
"path": "./path/to/a/module", "path": "./path/to/a/module",
"additional": "config" "additional": "config"
} },
"globalPluginOptions": "globalConfig"
} }
} }
``` ```
``` ```bash
semantic-release --analyze-commits="npm-module-name" semantic-release --analyze-commits="npm-module-name"
``` ```
@ -205,9 +204,9 @@ A plugin itself is an async function that always receives three arguments.
module.exports = function (pluginConfig, config, callback) {} module.exports = function (pluginConfig, config, callback) {}
``` ```
- `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then its this object. - `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then its this object. Options defined directly under `release` will be passed to each plugins. Options defined within a plugin will passed only to that instance of the plugin.
- `config`: A config object containing a lot of information to act upon. - `config`: A config object containing a lot of information to act upon.
- `options`: `semantic-release` options like `debug`, or `branch` - `options`: `semantic-release` options like `repositoryUrl`, or `branch`
- For certain plugins the `config` object contains even more information. See below. - For certain plugins the `config` object contains even more information. See below.
### `analyzeCommits` ### `analyzeCommits`
@ -222,9 +221,9 @@ Have a look at the [default implementation](https://github.com/semantic-release/
This plugins is responsible for verifying that a release should happen in the first place. This plugins is responsible for verifying that a release should happen in the first place.
The default implementations are: The default implementations are:
- [travis](https://github.com/semantic-release/condition-travis/): verifies that the publish is happening on Travis, that its the right branch, and that all other build jobs succeeded. - [travis](https://github.com/semantic-release/condition-travis/): verifies that the publish is happening on Travis, that its the right branch, and that all other build jobs succeeded.
- [github](https://github.com/semantic-release/github/): verifies a Github authentication is set and valid. - [github](https://github.com/semantic-release/github/): verifies a Github authentication is set and valid.
- [npm](https://github.com/semantic-release/npm/): verifies an npm authentication is set and valid. - [npm](https://github.com/semantic-release/npm/): verifies an npm authentication is set and valid.
Passing an array of plugins will run them in series. Passing an array of plugins will run them in series.
@ -245,7 +244,7 @@ It receives a `commits` array, the `lastRelease` and `nextRelease` inside `confi
### `publish` ### `publish`
This plugins is responsible for publishing the release. The default implementations publish on [npm](https://github.com/semantic-release/npm) and [github](https://github.com/semantic-release/github). This plugins is responsible for publishing the release. The default implementations publish on [npm](https://github.com/semantic-release/npm) and [github](https://github.com/semantic-release/github).
Passing an array of plugins will run them in series. Passing an array of plugins will run them in series.

View File

@ -1,4 +1,4 @@
const {isArray, isObject} = require('lodash'); const {isArray, isObject, omit} = require('lodash');
const DEFINITIONS = require('./definitions'); const DEFINITIONS = require('./definitions');
const pipeline = require('./pipeline'); const pipeline = require('./pipeline');
const normalize = require('./normalize'); const normalize = require('./normalize');
@ -20,9 +20,11 @@ module.exports = (options, logger) =>
pluginConfs = def; pluginConfs = def;
} }
const globalOpts = omit(options, Object.keys(DEFINITIONS));
plugins[pluginType] = isArray(pluginConfs) plugins[pluginType] = isArray(pluginConfs)
? pipeline(pluginConfs.map(conf => normalize(pluginType, conf, logger, output))) ? pipeline(pluginConfs.map(conf => normalize(pluginType, globalOpts, conf, logger, output)))
: normalize(pluginType, pluginConfs, logger, output); : normalize(pluginType, globalOpts, pluginConfs, logger, output);
return plugins; return plugins;
}, {}); }, {});

View File

@ -2,21 +2,22 @@ const {inspect} = require('util');
const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash'); const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash');
const importFrom = require('import-from'); const importFrom = require('import-from');
module.exports = (pluginType, pluginConfig, logger, validator) => { module.exports = (pluginType, globalOpts, pluginOpts, logger, validator) => {
if (!pluginConfig) { if (!pluginOpts) {
return noop; return noop;
} }
const {path, ...config} = isString(pluginConfig) || isFunction(pluginConfig) ? {path: pluginConfig} : pluginConfig; const {path, ...config} = isString(pluginOpts) || isFunction(pluginOpts) ? {path: pluginOpts} : pluginOpts;
if (!isFunction(pluginConfig)) { if (!isFunction(pluginOpts)) {
logger.log('Load plugin %s from %s', pluginType, path); logger.log('Load plugin %s from %s', pluginType, path);
} }
const plugin = isFunction(path) ? path : importFrom.silent(__dirname, path) || importFrom(process.cwd(), path); const plugin = isFunction(path) ? path : importFrom.silent(__dirname, path) || importFrom(process.cwd(), path);
let func; let func;
if (isFunction(plugin)) { if (isFunction(plugin)) {
func = plugin.bind(null, cloneDeep(config)); func = plugin.bind(null, cloneDeep({...globalOpts, ...config}));
} else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) { } else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
func = plugin[pluginType].bind(null, cloneDeep(config)); func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config}));
} else { } else {
throw new Error( throw new Error(
`The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.` `The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.`

View File

@ -50,9 +50,9 @@ test.serial('Plugins are called with expected values', async t => {
const generateNotes = stub().resolves(notes); const generateNotes = stub().resolves(notes);
const publish = stub().resolves(); const publish = stub().resolves();
const config = {branch: 'master', repositoryUrl: 'git@hostname.com:owner/module.git', globalOpt: 'global'};
const options = { const options = {
branch: 'master', ...config,
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions1, verifyConditions2], verifyConditions: [verifyConditions1, verifyConditions2],
getLastRelease, getLastRelease,
analyzeCommits, analyzeCommits,
@ -64,14 +64,17 @@ test.serial('Plugins are called with expected values', async t => {
await t.context.semanticRelease(options); await t.context.semanticRelease(options);
t.is(verifyConditions1.callCount, 1); t.is(verifyConditions1.callCount, 1);
t.deepEqual(verifyConditions1.args[0][0], config);
t.deepEqual(verifyConditions1.args[0][1], {options, logger: t.context.logger}); t.deepEqual(verifyConditions1.args[0][1], {options, logger: t.context.logger});
t.is(verifyConditions2.callCount, 1); t.is(verifyConditions2.callCount, 1);
t.deepEqual(verifyConditions2.args[0][1], {options, logger: t.context.logger}); t.deepEqual(verifyConditions2.args[0][1], {options, logger: t.context.logger});
t.is(getLastRelease.callCount, 1); t.is(getLastRelease.callCount, 1);
t.deepEqual(getLastRelease.args[0][0], config);
t.deepEqual(getLastRelease.args[0][1], {options, logger: t.context.logger}); t.deepEqual(getLastRelease.args[0][1], {options, logger: t.context.logger});
t.is(analyzeCommits.callCount, 1); t.is(analyzeCommits.callCount, 1);
t.deepEqual(analyzeCommits.args[0][0], config);
t.deepEqual(analyzeCommits.args[0][1].options, options); t.deepEqual(analyzeCommits.args[0][1].options, options);
t.deepEqual(analyzeCommits.args[0][1].logger, t.context.logger); t.deepEqual(analyzeCommits.args[0][1].logger, t.context.logger);
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease); t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
@ -79,6 +82,7 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[0].message); t.deepEqual(analyzeCommits.args[0][1].commits[0].message, commits[0].message);
t.is(verifyRelease.callCount, 1); t.is(verifyRelease.callCount, 1);
t.deepEqual(verifyRelease.args[0][0], config);
t.deepEqual(verifyRelease.args[0][1].options, options); t.deepEqual(verifyRelease.args[0][1].options, options);
t.deepEqual(verifyRelease.args[0][1].logger, t.context.logger); t.deepEqual(verifyRelease.args[0][1].logger, t.context.logger);
t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease); t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease);
@ -87,6 +91,7 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease); t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
t.is(generateNotes.callCount, 1); t.is(generateNotes.callCount, 1);
t.deepEqual(generateNotes.args[0][0], config);
t.deepEqual(generateNotes.args[0][1].options, options); t.deepEqual(generateNotes.args[0][1].options, options);
t.deepEqual(generateNotes.args[0][1].logger, t.context.logger); t.deepEqual(generateNotes.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes.args[0][1].lastRelease, lastRelease); t.deepEqual(generateNotes.args[0][1].lastRelease, lastRelease);
@ -95,6 +100,7 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease); t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(publish.callCount, 1); t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][0], config);
t.deepEqual(publish.args[0][1].options, options); t.deepEqual(publish.args[0][1].options, options);
t.deepEqual(publish.args[0][1].logger, t.context.logger); t.deepEqual(publish.args[0][1].logger, t.context.logger);
t.deepEqual(publish.args[0][1].lastRelease, lastRelease); t.deepEqual(publish.args[0][1].lastRelease, lastRelease);

View File

@ -10,27 +10,27 @@ test.beforeEach(t => {
}); });
test('Normalize and load plugin from string', t => { test('Normalize and load plugin from string', t => {
const plugin = normalize('verifyConditions', './test/fixtures/plugin-noop', t.context.logger); const plugin = normalize('verifyConditions', {}, './test/fixtures/plugin-noop', t.context.logger);
t.is(typeof plugin, 'function'); t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']); t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
}); });
test('Normalize and load plugin from object', t => { test('Normalize and load plugin from object', t => {
const plugin = normalize('publish', {path: './test/fixtures/plugin-noop'}, t.context.logger); const plugin = normalize('publish', {}, {path: './test/fixtures/plugin-noop'}, t.context.logger);
t.is(typeof plugin, 'function'); t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']); t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']);
}); });
test('Normalize and load plugin from function', t => { test('Normalize and load plugin from function', t => {
const plugin = normalize('', () => {}, t.context.logger); const plugin = normalize('', {}, () => {}, t.context.logger);
t.is(typeof plugin, 'function'); t.is(typeof plugin, 'function');
}); });
test('Normalize and load plugin that retuns multiple functions', t => { test('Normalize and load plugin that retuns multiple functions', t => {
const plugin = normalize('verifyConditions', './test/fixtures/multi-plugin', t.context.logger); const plugin = normalize('verifyConditions', {}, './test/fixtures/multi-plugin', t.context.logger);
t.is(typeof plugin, 'function'); t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']); t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
@ -38,7 +38,7 @@ test('Normalize and load plugin that retuns multiple functions', t => {
test('Wrap plugin in a function that validate the output of the plugin', async t => { test('Wrap plugin in a function that validate the output of the plugin', async t => {
const pluginFunction = stub().resolves(1); const pluginFunction = stub().resolves(1);
const plugin = normalize('', pluginFunction, t.context.logger, { const plugin = normalize('', {}, pluginFunction, t.context.logger, {
validator: output => output === 1, validator: output => output === 1,
message: 'The output must be 1.', message: 'The output must be 1.',
}); });
@ -50,13 +50,14 @@ test('Wrap plugin in a function that validate the output of the plugin', async t
t.is(error.message, 'The output must be 1. Received: 2'); t.is(error.message, 'The output must be 1. Received: 2');
}); });
test('Plugin is called with "pluginConfig" (omitting "path") and input', async t => { test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
const pluginFunction = stub().resolves(); const pluginFunction = stub().resolves();
const conf = {path: pluginFunction, conf: 'confValue'}; const conf = {path: pluginFunction, conf: 'confValue'};
const plugin = normalize('', conf, t.context.logger); const globalConf = {global: 'globalValue'};
const plugin = normalize('', globalConf, conf, t.context.logger);
await plugin('param'); await plugin('param');
t.true(pluginFunction.calledWith({conf: 'confValue'}, 'param')); t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
}); });
test('Prevent plugins to modify "pluginConfig"', async t => { test('Prevent plugins to modify "pluginConfig"', async t => {
@ -64,10 +65,12 @@ test('Prevent plugins to modify "pluginConfig"', async t => {
pluginConfig.conf.subConf = 'otherConf'; pluginConfig.conf.subConf = 'otherConf';
}); });
const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}}; const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
const plugin = normalize('', conf, t.context.logger); const globalConf = {globalConf: {globalSubConf: 'originalGlobalConf'}};
const plugin = normalize('', globalConf, conf, t.context.logger);
await plugin(); await plugin();
t.is(conf.conf.subConf, 'originalConf'); t.is(conf.conf.subConf, 'originalConf');
t.is(globalConf.globalConf.globalSubConf, 'originalGlobalConf');
}); });
test('Prevent plugins to modify its input', async t => { test('Prevent plugins to modify its input', async t => {
@ -75,7 +78,7 @@ test('Prevent plugins to modify its input', async t => {
options.param.subParam = 'otherParam'; options.param.subParam = 'otherParam';
}); });
const input = {param: {subParam: 'originalSubParam'}}; const input = {param: {subParam: 'originalSubParam'}};
const plugin = normalize('', pluginFunction, t.context.logger); const plugin = normalize('', {}, pluginFunction, t.context.logger);
await plugin(input); await plugin(input);
t.is(input.param.subParam, 'originalSubParam'); t.is(input.param.subParam, 'originalSubParam');
@ -89,7 +92,7 @@ test('Return noop if the plugin is not defined', t => {
test('Always pass a defined "pluginConfig" for plugin defined with string', async t => { test('Always pass a defined "pluginConfig" for plugin defined with string', async t => {
// Call the normalize function with the path of a plugin that returns its config // Call the normalize function with the path of a plugin that returns its config
const plugin = normalize('', './test/fixtures/plugin-result-config', t.context.logger); const plugin = normalize('', {}, './test/fixtures/plugin-result-config', t.context.logger);
const pluginResult = await plugin(); const pluginResult = await plugin();
t.deepEqual(pluginResult.pluginConfig, {}); t.deepEqual(pluginResult.pluginConfig, {});
@ -97,14 +100,17 @@ test('Always pass a defined "pluginConfig" for plugin defined with string', asyn
test('Always pass a defined "pluginConfig" for plugin defined with path', async t => { test('Always pass a defined "pluginConfig" for plugin defined with path', async t => {
// Call the normalize function with the path of a plugin that returns its config // Call the normalize function with the path of a plugin that returns its config
const plugin = normalize('', {path: './test/fixtures/plugin-result-config'}, t.context.logger); const plugin = normalize('', {}, {path: './test/fixtures/plugin-result-config'}, t.context.logger);
const pluginResult = await plugin(); const pluginResult = await plugin();
t.deepEqual(pluginResult.pluginConfig, {}); t.deepEqual(pluginResult.pluginConfig, {});
}); });
test('Throws an error if the plugin return an object without the expected plugin function', t => { test('Throws an error if the plugin return an object without the expected plugin function', t => {
const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger), Error); const error = t.throws(
() => normalize('inexistantPlugin', {}, './test/fixtures/multi-plugin', t.context.logger),
Error
);
t.is( t.is(
error.message, error.message,

View File

@ -48,6 +48,21 @@ test('Use default when only options are passed for a single plugin', t => {
t.is(typeof plugins.analyzeCommits, 'function'); t.is(typeof plugins.analyzeCommits, 'function');
}); });
test('Merge global options with plugin options', async t => {
const plugins = getPlugins(
{
globalOpt: 'global',
otherOpt: 'globally-defined',
getLastRelease: {path: './test/fixtures/plugin-result-config', localOpt: 'local', otherOpt: 'locally-defined'},
},
t.context.logger
);
const result = await plugins.getLastRelease();
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
});
test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => { test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger), Error); const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger), Error);