feat: support sharable configuration

Adds the options `extends`, which can be defined via configuration file or CLI arguments to a single path or an array of paths of shareable configuration.
A shareable configuration is a file or a module that can be loaded with `require`.
Options is defined by merging in the following order of priority:
- CLI/API
- Configuration file
- Shareable configuration (from right to left)

Options set in a shareable configuration can be unset by setting it to `null` or `undefined` in the main configuration file. If a default value applies to this property it will be used.
This commit is contained in:
Pierre Vanduynslager
2017-12-22 14:22:30 -05:00
parent 2fc538b607
commit 754b420fd6
14 changed files with 508 additions and 59 deletions
+2
View File
@@ -0,0 +1,2 @@
module.exports = () => {};
+265 -9
View File
@@ -1,5 +1,7 @@
import {format} from 'util';
import test from 'ava';
import {writeFile, writeJson} from 'fs-extra';
import {writeFile, outputJson} from 'fs-extra';
import {omit} from 'lodash';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import yaml from 'js-yaml';
@@ -30,7 +32,7 @@ test.serial('Default values, reading repositoryUrl from package.json', async t =
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
// Create package.json in repository root
await writeJson('./package.json', pkg);
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
@@ -57,7 +59,7 @@ test.serial('Default values, reading repositoryUrl (http url) from package.json
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', pkg);
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
@@ -78,7 +80,7 @@ test.serial('Read options from package.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', {release});
await outputJson('./package.json', {release});
const {options} = await t.context.getConfig();
@@ -118,7 +120,7 @@ test.serial('Read options from .releaserc.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('.releaserc.json', release);
await outputJson('.releaserc.json', release);
const {options} = await t.context.getConfig();
@@ -168,7 +170,7 @@ test.serial('Read options from release.config.js', async t => {
t.deepEqual(t.context.plugins.args[0][0], release);
});
test.serial('Prioritise cli parameters over file configuration and git repo', async t => {
test.serial('Prioritise CLI/API parameters over file configuration and git repo', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_pkg'},
branch: 'branch_pkg',
@@ -185,12 +187,266 @@ test.serial('Prioritise cli parameters over file configuration and git repo', as
// Create a clone
await gitShallowClone(repo);
// Create package.json in repository root
await writeJson('./package.json', pkg);
await outputJson('./package.json', pkg);
const result = await t.context.getConfig(options);
// Verify the options contains the plugin config from cli
// Verify the options contains the plugin config from CLI/API
t.deepEqual(result.options, options);
// Verify the plugins module is called with the plugin options from cli
// Verify the plugins module is called with the plugin options from CLI/API
t.deepEqual(t.context.plugins.args[0][0], options);
});
test.serial('Read configuration from file path in "extends"', async t => {
const release = {extends: './shareable.json'};
const shareable = {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotes',
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable.json', shareable);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, shareable);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], shareable);
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: './shareable.json',
generateNotes: './shareable.json',
getLastRelease: './shareable.json',
});
});
test.serial('Read configuration from module path in "extends"', async t => {
const release = {extends: 'shareable'};
const shareable = {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotes',
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./node_modules/shareable/index.json', shareable);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, shareable);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], shareable);
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: 'shareable',
generateNotes: 'shareable',
getLastRelease: 'shareable',
});
});
test.serial('Read configuration from an array of paths in "extends"', async t => {
const release = {extends: ['./shareable1.json', './shareable2.json']};
const shareable1 = {
analyzeCommits: 'analyzeCommits1',
getLastRelease: {path: 'getLastRelease1', param: 'getLastRelease_param1'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
const shareable2 = {
analyzeCommits: 'analyzeCommits2',
generateNotes: 'generateNotes2',
getLastRelease: {path: 'getLastRelease2', param: 'getLastRelease_param2'},
branch: 'test_branch',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable1.json', shareable1);
await outputJson('./shareable2.json', shareable2);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from shareable1.json and shareable2.json
t.deepEqual(options, {...shareable1, ...shareable2});
// Verify the plugins module is called with the plugin options from shareable1.json and shareable2.json
t.deepEqual(t.context.plugins.args[0][0], {...shareable1, ...shareable2});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits1: './shareable1.json',
analyzeCommits2: './shareable2.json',
generateNotes2: './shareable2.json',
getLastRelease1: './shareable1.json',
getLastRelease2: './shareable2.json',
});
});
test.serial('Prioritize configuration from config file over "extends"', async t => {
const release = {
extends: './shareable.json',
branch: 'test_pkg',
generateNotes: 'generateNotes',
publish: [{path: 'publishPkg', param: 'publishPkg_param'}],
};
const shareable = {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotesShareable',
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable.json', shareable);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json and shareable.json
t.deepEqual(options, omit({...shareable, ...release}, 'extends'));
// Verify the plugins module is called with the plugin options from package.json and shareable.json
t.deepEqual(t.context.plugins.args[0][0], omit({...shareable, ...release}, 'extends'));
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: './shareable.json',
generateNotesShareable: './shareable.json',
publishShareable: './shareable.json',
});
});
test.serial('Prioritize configuration from cli/API options over "extends"', async t => {
const opts = {
extends: './shareable2.json',
branch: 'branch_opts',
publish: [{path: 'publishOpts', param: 'publishOpts_param'}],
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
const release = {
extends: './shareable1.json',
branch: 'branch_pkg',
generateNotes: 'generateNotes',
publish: [{path: 'publishPkg', param: 'publishPkg_param'}],
};
const shareable1 = {
analyzeCommits: 'analyzeCommits1',
generateNotes: 'generateNotesShareable1',
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
branch: 'test_branch1',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
const shareable2 = {
analyzeCommits: 'analyzeCommits2',
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
branch: 'test_branch2',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json, shareable1.json and shareable2.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable1.json', shareable1);
await outputJson('./shareable2.json', shareable2);
const {options} = await t.context.getConfig(opts);
// Verify the options contains the plugin config from package.json and shareable2.json
t.deepEqual(options, omit({...shareable2, ...release, ...opts}, 'extends'));
// Verify the plugins module is called with the plugin options from package.json and shareable2.json
t.deepEqual(t.context.plugins.args[0][0], omit({...shareable2, ...release, ...opts}, 'extends'));
});
test.serial('Allow to unset properties defined in shareable config with "null"', async t => {
const release = {
extends: './shareable.json',
getLastRelease: null,
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable.json', shareable);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, {...omit(shareable, 'getLastRelease'), ...omit(release, ['extends', 'getLastRelease'])});
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], {
...omit(shareable, 'getLastRelease'),
...omit(release, ['extends', 'getLastRelease']),
});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
getLastRelease: './shareable.json',
});
});
test.serial('Allow to unset properties defined in shareable config with "undefined"', async t => {
const release = {
extends: './shareable.json',
getLastRelease: undefined,
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and release.config.js in repository root
// await outputJson('./package.json', {release});
await writeFile('release.config.js', `module.exports = ${format(release)}`);
await outputJson('./shareable.json', shareable);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, {...omit(shareable, 'getLastRelease'), ...omit(release, ['extends', 'getLastRelease'])});
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], {
...omit(shareable, 'getLastRelease'),
...omit(release, ['extends', 'getLastRelease']),
});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
getLastRelease: './shareable.json',
});
});
test.serial('Throw an Error if one of the shareable config cannot be found', async t => {
const release = {extends: ['./shareable1.json', 'non-existing-path']};
const shareable = {analyzeCommits: 'analyzeCommits'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json and shareable.json in repository root
await outputJson('./package.json', {release});
await outputJson('./shareable1.json', shareable);
const error = await t.throws(t.context.getConfig(), Error);
t.is(error.message, "Cannot find module 'non-existing-path'");
t.is(error.code, 'MODULE_NOT_FOUND');
});
+2 -2
View File
@@ -120,9 +120,9 @@ test.serial('Return git remote repository url set while cloning', async t => {
t.is(await repoUrl(), fileUrl(repo));
});
test.serial('Return "null" if git repository url is not set', async t => {
test.serial('Return "undefined" if git repository url is not set', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
t.is(await repoUrl(), null);
t.is(await repoUrl(), undefined);
});
+39
View File
@@ -402,6 +402,45 @@ test.serial('Exit with 1 if a plugin is not found', async t => {
t.regex(stderr, /Cannot find module/);
});
test.serial('Exit with 1 if a shareable config is not found', async t => {
const packageName = 'test-config-not-found';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {extends: 'non-existing-path'},
});
const {code, stderr} = await t.throws(execa(cli, [], {env}));
t.is(code, 1);
t.regex(stderr, /Cannot find module/);
});
test.serial('Exit with 1 if a shareable config reference a not found plugin', async t => {
const packageName = 'test-config-ref-not-found';
const owner = 'test-repo';
const shareable = {getLastRelease: 'non-existing-path'};
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {extends: './shareable.json'},
});
await writeJson('./shareable.json', shareable);
const {code, stderr} = await t.throws(execa(cli, [], {env}));
t.is(code, 1);
t.regex(stderr, /Cannot find module/);
});
test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', async t => {
const packageName = 'test-recovery';
const owner = 'test-repo';
+32 -15
View File
@@ -10,27 +10,45 @@ test.beforeEach(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.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
});
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.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']);
});
test('Normalize and load plugin from a base file path', t => {
const plugin = normalize(
'verifyConditions',
{'./plugin-noop': './test/fixtures'},
{},
'./plugin-noop',
t.context.logger
);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], [
'Load plugin %s from %s in shareable config %s',
'verifyConditions',
'./plugin-noop',
'./test/fixtures',
]);
});
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');
});
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.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
@@ -38,7 +56,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 => {
const pluginFunction = stub().resolves(1);
const plugin = normalize('', {}, pluginFunction, t.context.logger, {
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger, {
validator: output => output === 1,
message: 'The output must be 1.',
});
@@ -54,7 +72,7 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi
const pluginFunction = stub().resolves();
const conf = {path: pluginFunction, conf: 'confValue'};
const globalConf = {global: 'globalValue'};
const plugin = normalize('', globalConf, conf, t.context.logger);
const plugin = normalize('', {}, globalConf, conf, t.context.logger);
await plugin('param');
t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
@@ -66,7 +84,7 @@ test('Prevent plugins to modify "pluginConfig"', async t => {
});
const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
const globalConf = {globalConf: {globalSubConf: 'originalGlobalConf'}};
const plugin = normalize('', globalConf, conf, t.context.logger);
const plugin = normalize('', {}, globalConf, conf, t.context.logger);
await plugin();
t.is(conf.conf.subConf, 'originalConf');
@@ -78,7 +96,7 @@ test('Prevent plugins to modify its input', async t => {
options.param.subParam = 'otherParam';
});
const input = {param: {subParam: 'originalSubParam'}};
const plugin = normalize('', {}, pluginFunction, t.context.logger);
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger);
await plugin(input);
t.is(input.param.subParam, 'originalSubParam');
@@ -92,7 +110,7 @@ test('Return noop if the plugin is not defined', 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
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();
t.deepEqual(pluginResult.pluginConfig, {});
@@ -100,18 +118,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 => {
// 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();
t.deepEqual(pluginResult.pluginConfig, {});
});
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));
t.is(error.code, 'EPLUGINCONF');
t.is(error.name, 'SemanticReleaseError');
t.is(
error.message,
'The inexistantPlugin plugin must be a function, or an object with a function in the property inexistantPlugin.'
@@ -119,7 +136,7 @@ test('Throws an error if the plugin return an object without the expected plugin
});
test('Throws an error if the plugin is not found', t => {
const error = t.throws(() => normalize('inexistantPlugin', {}, 'non-existing-path', t.context.logger), Error);
const error = t.throws(() => normalize('inexistantPlugin', {}, {}, 'non-existing-path', t.context.logger), Error);
t.is(error.message, "Cannot find module 'non-existing-path'");
t.is(error.code, 'MODULE_NOT_FOUND');
+77 -6
View File
@@ -1,15 +1,26 @@
import path from 'path';
import test from 'ava';
import {copy, outputFile} from 'fs-extra';
import {stub} from 'sinon';
import tempy from 'tempy';
import getPlugins from '../../lib/plugins';
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test.afterEach.always(() => {
// Restore the current working directory
process.chdir(cwd);
});
test('Export default plugins', t => {
const plugins = getPlugins({}, t.context.logger);
const plugins = getPlugins({}, {}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
@@ -28,6 +39,62 @@ test('Export plugins based on config', t => {
analyzeCommits: {path: './test/fixtures/plugin-noop'},
verifyRelease: () => {},
},
{},
t.context.logger
);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.getLastRelease, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.publish, 'function');
});
test.serial('Export plugins loaded from the dependency of a shareable config module', async t => {
const temp = tempy.directory();
await copy(
'./test/fixtures/plugin-noop.js',
path.join(temp, 'node_modules/shareable-config/node_modules/custom-plugin/index.js')
);
await outputFile(path.join(temp, 'node_modules/shareable-config/index.js'), '');
process.chdir(temp);
const plugins = getPlugins(
{
verifyConditions: ['custom-plugin', {path: 'custom-plugin'}],
getLastRelease: 'custom-plugin',
analyzeCommits: {path: 'custom-plugin'},
verifyRelease: () => {},
},
{'custom-plugin': 'shareable-config'},
t.context.logger
);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.getLastRelease, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.publish, 'function');
});
test.serial('Export plugins loaded from the dependency of a shareable config file', async t => {
const temp = tempy.directory();
await copy('./test/fixtures/plugin-noop.js', path.join(temp, 'plugin/plugin-noop.js'));
await outputFile(path.join(temp, 'shareable-config.js'), '');
process.chdir(temp);
const plugins = getPlugins(
{
verifyConditions: ['./plugin/plugin-noop', {path: './plugin/plugin-noop'}],
getLastRelease: './plugin/plugin-noop',
analyzeCommits: {path: './plugin/plugin-noop'},
verifyRelease: () => {},
},
{'./plugin/plugin-noop': './shareable-config.js'},
t.context.logger
);
@@ -41,7 +108,7 @@ test('Export plugins based on config', t => {
});
test('Use default when only options are passed for a single plugin', t => {
const plugins = getPlugins({getLastRelease: {}, analyzeCommits: {}}, t.context.logger);
const plugins = getPlugins({getLastRelease: {}, analyzeCommits: {}}, {}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.getLastRelease, 'function');
@@ -55,6 +122,7 @@ test('Merge global options with plugin options', async t => {
otherOpt: 'globally-defined',
getLastRelease: {path: './test/fixtures/plugin-result-config', localOpt: 'local', otherOpt: 'locally-defined'},
},
{},
t.context.logger
);
@@ -64,8 +132,10 @@ test('Merge global options with plugin options', async 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));
t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EPLUGINCONF');
t.is(
error.message,
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
@@ -73,11 +143,12 @@ test('Throw an error if plugin configuration is missing a path for plugin pipeli
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(
() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, t.context.logger),
Error
const error = t.throws(() =>
getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)
);
t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EPLUGINCONF');
t.is(
error.message,
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'