feat: add success and fail notification plugins
- Allow `publish` plugins to return an `Object` with information related to the releases - Add the `success` plugin hook, called when all `publish` are successful, receiving a list of release - Add the `fail` plugin hook, called when an error happens at any point, receiving a list of errors - Add detailed message for each error
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import test from 'ava';
|
||||
import plugins from '../../lib/definitions/plugins';
|
||||
import errors from '../../lib/definitions/errors';
|
||||
|
||||
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyConditions.config.validator({}));
|
||||
t.false(plugins.verifyConditions.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.verifyConditions.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyConditions.config.validator());
|
||||
t.true(plugins.verifyConditions.config.validator('plugin-path.js'));
|
||||
t.true(plugins.verifyConditions.config.validator(() => {}));
|
||||
t.true(plugins.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
|
||||
t.false(plugins.analyzeCommits.config.validator({}));
|
||||
t.false(plugins.analyzeCommits.config.validator({path: null}));
|
||||
t.false(plugins.analyzeCommits.config.validator([]));
|
||||
t.false(plugins.analyzeCommits.config.validator());
|
||||
|
||||
t.true(plugins.analyzeCommits.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.analyzeCommits.config.validator('plugin-path.js'));
|
||||
t.true(plugins.analyzeCommits.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyRelease.config.validator({}));
|
||||
t.false(plugins.verifyRelease.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyRelease.config.validator());
|
||||
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
|
||||
t.true(plugins.verifyRelease.config.validator(() => {}));
|
||||
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
|
||||
t.false(plugins.generateNotes.config.validator({}));
|
||||
t.false(plugins.generateNotes.config.validator({path: null}));
|
||||
t.false(plugins.generateNotes.config.validator([]));
|
||||
|
||||
t.true(plugins.generateNotes.config.validator());
|
||||
t.true(plugins.generateNotes.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.generateNotes.config.validator('plugin-path.js'));
|
||||
t.true(plugins.generateNotes.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.publish.config.validator({}));
|
||||
t.false(plugins.publish.config.validator({path: null}));
|
||||
t.false(plugins.publish.config.validator());
|
||||
|
||||
t.true(plugins.publish.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.publish.config.validator('plugin-path.js'));
|
||||
t.true(plugins.publish.config.validator(() => {}));
|
||||
t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.success.config.validator({}));
|
||||
t.false(plugins.success.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.success.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.success.config.validator());
|
||||
t.true(plugins.success.config.validator('plugin-path.js'));
|
||||
t.true(plugins.success.config.validator(() => {}));
|
||||
t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.fail.config.validator({}));
|
||||
t.false(plugins.fail.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.fail.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.fail.config.validator());
|
||||
t.true(plugins.fail.config.validator('plugin-path.js'));
|
||||
t.true(plugins.fail.config.validator(() => {}));
|
||||
t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||
t.false(plugins.analyzeCommits.output.validator('invalid'));
|
||||
t.false(plugins.analyzeCommits.output.validator(1));
|
||||
t.false(plugins.analyzeCommits.output.validator({}));
|
||||
|
||||
t.true(plugins.analyzeCommits.output.validator());
|
||||
t.true(plugins.analyzeCommits.output.validator(null));
|
||||
t.true(plugins.analyzeCommits.output.validator('major'));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin output, if defined, must be a string', t => {
|
||||
t.false(plugins.generateNotes.output.validator(1));
|
||||
t.false(plugins.generateNotes.output.validator({}));
|
||||
|
||||
t.true(plugins.generateNotes.output.validator());
|
||||
t.true(plugins.generateNotes.output.validator(null));
|
||||
t.true(plugins.generateNotes.output.validator(''));
|
||||
t.true(plugins.generateNotes.output.validator('string'));
|
||||
});
|
||||
|
||||
test('The "publish" plugin output, if defined, must be an object', t => {
|
||||
t.false(plugins.publish.output.validator(1));
|
||||
t.false(plugins.publish.output.validator('string'));
|
||||
|
||||
t.true(plugins.publish.output.validator({}));
|
||||
t.true(plugins.publish.output.validator());
|
||||
t.true(plugins.publish.output.validator(null));
|
||||
t.true(plugins.publish.output.validator(''));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin output definition return an existing error code', t => {
|
||||
t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin output definition return an existing error code', t => {
|
||||
t.true(Object.keys(errors).includes(plugins.generateNotes.output.error));
|
||||
});
|
||||
|
||||
test('The "publish" plugin output definition return an existing error code', t => {
|
||||
t.true(Object.keys(errors).includes(plugins.publish.output.error));
|
||||
});
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
const AggregateError = require('aggregate-error');
|
||||
|
||||
module.exports = () => {
|
||||
throw new AggregateError([new Error('a'), new Error('b')]);
|
||||
};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
module.exports = (pluginConfig, options) => options;
|
||||
+234
-86
@@ -1,11 +1,10 @@
|
||||
import test from 'ava';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import tempy from 'tempy';
|
||||
import {spy, stub} from 'sinon';
|
||||
import clearModule from 'clear-module';
|
||||
import AggregateError from 'aggregate-error';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
import DEFINITIONS from '../lib/plugins/definitions';
|
||||
import DEFINITIONS from '../lib/definitions/plugins';
|
||||
import {
|
||||
gitHead as getGitHead,
|
||||
gitTagHead,
|
||||
@@ -21,10 +20,10 @@ import {
|
||||
const envBackup = Object.assign({}, process.env);
|
||||
// Save the current working diretory
|
||||
const cwd = process.cwd();
|
||||
const pluginNoop = require.resolve('./fixtures/plugin-noop');
|
||||
|
||||
test.beforeEach(t => {
|
||||
clearModule('../lib/hide-sensitive');
|
||||
|
||||
// Delete environment variables that could have been set on the machine running the tests
|
||||
delete process.env.GIT_CREDENTIALS;
|
||||
delete process.env.GH_TOKEN;
|
||||
@@ -32,8 +31,8 @@ test.beforeEach(t => {
|
||||
delete process.env.GL_TOKEN;
|
||||
delete process.env.GITLAB_TOKEN;
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.error = stub();
|
||||
t.context.log = spy();
|
||||
t.context.error = spy();
|
||||
t.context.logger = {log: t.context.log, error: t.context.error};
|
||||
t.context.stdout = stub(process.stdout, 'write');
|
||||
t.context.stderr = stub(process.stderr, 'write');
|
||||
@@ -67,7 +66,9 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const release1 = {name: 'Release 1', url: 'https://release1.com'};
|
||||
const publish1 = stub().resolves(release1);
|
||||
const success = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
@@ -76,7 +77,8 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
publish: [publish1, pluginNoop],
|
||||
success,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -117,14 +119,27 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
t.deepEqual(generateNotes.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
|
||||
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].logger, t.context.logger);
|
||||
t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
|
||||
t.deepEqual(publish.args[0][1].commits[0].hash, commits[0].hash);
|
||||
t.deepEqual(publish.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(publish.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.is(publish1.callCount, 1);
|
||||
t.deepEqual(publish1.args[0][0], config);
|
||||
t.deepEqual(publish1.args[0][1].options, options);
|
||||
t.deepEqual(publish1.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(publish1.args[0][1].lastRelease, lastRelease);
|
||||
t.deepEqual(publish1.args[0][1].commits[0].hash, commits[0].hash);
|
||||
t.deepEqual(publish1.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
|
||||
t.is(success.callCount, 1);
|
||||
t.deepEqual(success.args[0][0], config);
|
||||
t.deepEqual(success.args[0][1].options, options);
|
||||
t.deepEqual(success.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(success.args[0][1].lastRelease, lastRelease);
|
||||
t.deepEqual(success.args[0][1].commits[0].hash, commits[0].hash);
|
||||
t.deepEqual(success.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
t.deepEqual(success.args[0][1].releases, [
|
||||
{...release1, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
||||
{...nextRelease, ...{notes}, ...{pluginName: pluginNoop}},
|
||||
]);
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead);
|
||||
@@ -139,20 +154,16 @@ test.serial('Use custom tag format', async t => {
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions,
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits: stub().resolves(nextRelease.type),
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(notes),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -193,6 +204,8 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes,
|
||||
publish: [publish1, publish2],
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -205,19 +218,69 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
|
||||
t.is(generateNotes.callCount, 2);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
t.is(publish1.callCount, 1);
|
||||
t.deepEqual(publish1.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
|
||||
nextRelease.gitHead = await getGitHead();
|
||||
|
||||
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, {...nextRelease, ...{notes}});
|
||||
t.is(publish2.callCount, 1);
|
||||
t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.deepEqual(publish2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the last gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash);
|
||||
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), commits[0].hash);
|
||||
});
|
||||
|
||||
test.serial('Call all "success" plugins even if one errors out', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
await gitCommits(['Second']);
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions1 = stub().resolves();
|
||||
const verifyConditions2 = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const release = {name: 'Release', url: 'https://release.com'};
|
||||
const publish = stub().resolves(release);
|
||||
const success1 = stub().rejects();
|
||||
const success2 = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||
analyzeCommits,
|
||||
generateNotes,
|
||||
publish,
|
||||
success: [success1, success2],
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.is(success1.callCount, 1);
|
||||
t.deepEqual(success1.args[0][0], config);
|
||||
t.deepEqual(success1.args[0][1].releases, [
|
||||
{...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
||||
]);
|
||||
|
||||
t.is(success2.callCount, 1);
|
||||
t.deepEqual(success2.args[0][1].releases, [
|
||||
{...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
||||
]);
|
||||
});
|
||||
|
||||
test.serial('Log all "verifyConditions" errors', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
@@ -227,10 +290,12 @@ test.serial('Log all "verifyConditions" errors', async t => {
|
||||
const error1 = new Error('error 1');
|
||||
const error2 = new SemanticReleaseError('error 2', 'ERR2');
|
||||
const error3 = new SemanticReleaseError('error 3', 'ERR3');
|
||||
const fail = stub().resolves();
|
||||
const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
...config,
|
||||
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -247,6 +312,11 @@ test.serial('Log all "verifyConditions" errors', async t => {
|
||||
error1,
|
||||
]);
|
||||
t.true(t.context.error.calledAfter(t.context.log));
|
||||
t.is(fail.callCount, 1);
|
||||
t.deepEqual(fail.args[0][0], config);
|
||||
t.deepEqual(fail.args[0][1].options, options);
|
||||
t.deepEqual(fail.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(fail.args[0][1].errors, [error2, error3]);
|
||||
});
|
||||
|
||||
test.serial('Log all "verifyRelease" errors', async t => {
|
||||
@@ -261,12 +331,14 @@ test.serial('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 = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
...config,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits: stub().resolves('major'),
|
||||
verifyRelease: [stub().rejects(error1), stub().rejects(error2)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -278,9 +350,12 @@ test.serial('Log all "verifyRelease" errors', async t => {
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
|
||||
t.is(fail.callCount, 1);
|
||||
t.deepEqual(fail.args[0][0], config);
|
||||
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
|
||||
});
|
||||
|
||||
test.serial('Dry-run skips publish', async t => {
|
||||
test.serial('Dry-run skips publish and success', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
@@ -298,6 +373,7 @@ test.serial('Dry-run skips publish', async t => {
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const success = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: true,
|
||||
@@ -308,6 +384,7 @@ test.serial('Dry-run skips publish', async t => {
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -322,6 +399,41 @@ test.serial('Dry-run skips publish', async t => {
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 0);
|
||||
t.is(success.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Dry-run skips fail', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
await gitCommits(['Second']);
|
||||
|
||||
const error1 = new SemanticReleaseError('error 1', 'ERR1');
|
||||
const error2 = new SemanticReleaseError('error 2', 'ERR2');
|
||||
const fail = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: true,
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [stub().rejects(error1), stub().rejects(error2)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = await t.throws(semanticRelease(options));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
|
||||
t.is(fail.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t => {
|
||||
@@ -342,6 +454,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const success = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: false,
|
||||
@@ -352,6 +465,8 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -366,6 +481,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 0);
|
||||
t.is(success.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Allow local releases with "noCi" option', async t => {
|
||||
@@ -386,6 +502,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const success = stub().resolves();
|
||||
|
||||
const options = {
|
||||
noCi: true,
|
||||
@@ -396,6 +513,8 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -414,6 +533,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 1);
|
||||
t.is(success.callCount, 1);
|
||||
});
|
||||
|
||||
test.serial('Accept "undefined" value returned by the "generateNotes" plugins', async t => {
|
||||
@@ -428,7 +548,6 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
||||
|
||||
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
@@ -437,11 +556,13 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -464,18 +585,6 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
||||
t.falsy(publish.args[0][1].nextRelease.notes);
|
||||
});
|
||||
|
||||
test.serial('Returns falsy value if not running from a git repository', async t => {
|
||||
// Set the current working directory to a temp directory
|
||||
process.chdir(tempy.directory());
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'}));
|
||||
t.is(t.context.error.args[0][0], 'Semantic-release must run from a git repository.');
|
||||
});
|
||||
|
||||
test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
@@ -487,7 +596,7 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
|
||||
t.falsy(await semanticRelease({repositoryUrl}));
|
||||
t.is(
|
||||
t.context.log.args[6][0],
|
||||
t.context.log.args[8][0],
|
||||
"This run was triggered by a pull request and therefore a new version won't be published."
|
||||
);
|
||||
});
|
||||
@@ -495,21 +604,16 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
test.serial('Returns falsy value if not running from the configured branch', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
const publish = stub().resolves();
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits: stub().resolves(),
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -530,7 +634,6 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
@@ -539,11 +642,13 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
verifyConditions: [stub().resolves()],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -573,22 +678,17 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
|
||||
'Test commit\n\n commit body\n[skip release]',
|
||||
'Test commit\n\n commit body\n[release skip]',
|
||||
]);
|
||||
|
||||
const verifyConditions1 = stub().resolves();
|
||||
const verifyConditions2 = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||
verifyConditions: [stub().resolves(), stub().resolves()],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -623,12 +723,60 @@ test.serial('Hide sensitive environment variable values from the logs', async t
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.regex(t.context.stdout.args[6][0], /Console: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stdout.args[7][0], /Log: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stdout.args[8][0], /Console: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stdout.args[9][0], /Log: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stderr.args[0][0], /Error: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stderr.args[1][0], /Invalid token \[secure\]/);
|
||||
});
|
||||
|
||||
test.serial('Log both plugins errors and errors thrown by "fail" plugin', async t => {
|
||||
process.env.MY_TOKEN = 'secret token';
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const pluginError = new SemanticReleaseError('Plugin error', 'ERR');
|
||||
const failError1 = new Error('Fail error 1');
|
||||
const failError2 = new Error('Fail error 2');
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: stub().rejects(pluginError),
|
||||
fail: [stub().rejects(failError1), stub().rejects(failError2)],
|
||||
};
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError1);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s Plugin error', 'ERR']);
|
||||
});
|
||||
|
||||
test.serial('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
||||
process.env.MY_TOKEN = 'secret token';
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const pluginError = new Error('Plugin error');
|
||||
const fail = stub().resolves();
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: stub().rejects(pluginError),
|
||||
fail,
|
||||
};
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.true(fail.notCalled);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
|
||||
});
|
||||
|
||||
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
@@ -662,6 +810,8 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
analyzeCommits,
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -672,7 +822,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
|
||||
// Verify error message
|
||||
t.regex(error.message, new RegExp(DEFINITIONS.analyzeCommits.output.message));
|
||||
t.regex(error.message, /Received: 'string'/);
|
||||
t.regex(error.details, /string/);
|
||||
});
|
||||
|
||||
test.serial('Get all commits including the ones not in the shallow clone', async t => {
|
||||
@@ -685,20 +835,18 @@ test.serial('Get all commits including the ones not in the shallow clone', async
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(notes),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
|
||||
@@ -101,6 +101,7 @@ test.serial('Release patch, minor and major versions', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {success: false, fail: false},
|
||||
});
|
||||
// Create a npm-shrinkwrap.json file
|
||||
await execa('npm', ['shrinkwrap'], {env: testEnv});
|
||||
@@ -298,7 +299,7 @@ test.serial('Exit with 1 if a plugin is not found', async t => {
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: `git+https://github.com/${owner}/${packageName}`},
|
||||
release: {analyzeCommits: 'non-existing-path'},
|
||||
release: {analyzeCommits: 'non-existing-path', success: false, fail: false},
|
||||
});
|
||||
|
||||
const {code, stderr} = await t.throws(execa(cli, [], {env}));
|
||||
@@ -316,7 +317,7 @@ test.serial('Exit with 1 if a shareable config is not found', async t => {
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: `git+https://github.com/${owner}/${packageName}`},
|
||||
release: {extends: 'non-existing-path'},
|
||||
release: {extends: 'non-existing-path', success: false, fail: false},
|
||||
});
|
||||
|
||||
const {code, stderr} = await t.throws(execa(cli, [], {env}));
|
||||
@@ -336,7 +337,7 @@ test.serial('Exit with 1 if a shareable config reference a not found plugin', as
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: `git+https://github.com/${owner}/${packageName}`},
|
||||
release: {extends: './shareable.json'},
|
||||
release: {extends: './shareable.json', success: false, fail: false},
|
||||
});
|
||||
await writeJson('./shareable.json', shareable);
|
||||
|
||||
@@ -357,6 +358,7 @@ test.serial('Dry-run', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {success: false, fail: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -394,6 +396,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {success: false, fail: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -459,7 +462,17 @@ test.serial('Pass options via CLI arguments', async t => {
|
||||
t.log('$ semantic-release');
|
||||
const {stdout, code} = await execa(
|
||||
cli,
|
||||
['--verify-conditions', '@semantic-release/npm', '--publish', '@semantic-release/npm', '--debug'],
|
||||
[
|
||||
'--verify-conditions',
|
||||
'@semantic-release/npm',
|
||||
'--publish',
|
||||
'@semantic-release/npm',
|
||||
`--success`,
|
||||
false,
|
||||
`--fail`,
|
||||
false,
|
||||
'--debug',
|
||||
],
|
||||
{env}
|
||||
);
|
||||
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
|
||||
@@ -515,7 +528,7 @@ test.serial('Run via JS API', async t => {
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit']);
|
||||
t.log('$ Call semantic-release via API');
|
||||
await semanticRelease();
|
||||
await semanticRelease({fail: false, success: false});
|
||||
|
||||
// Verify package.json and has been updated
|
||||
t.is((await readJson('./package.json')).version, version);
|
||||
@@ -545,7 +558,7 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
release: {verifyConditions: pluginError},
|
||||
release: {verifyConditions: pluginError, fail: false, success: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -572,7 +585,7 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 1', async
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
release: {verifyConditions: pluginInheritedError},
|
||||
release: {verifyConditions: pluginInheritedError, fail: false, success: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import test from 'ava';
|
||||
import definitions from '../../lib/plugins/definitions';
|
||||
|
||||
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(definitions.verifyConditions.config.validator({}));
|
||||
t.false(definitions.verifyConditions.config.validator({path: null}));
|
||||
|
||||
t.true(definitions.verifyConditions.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.verifyConditions.config.validator());
|
||||
t.true(definitions.verifyConditions.config.validator('plugin-path.js'));
|
||||
t.true(definitions.verifyConditions.config.validator(() => {}));
|
||||
t.true(definitions.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
|
||||
t.false(definitions.analyzeCommits.config.validator({}));
|
||||
t.false(definitions.analyzeCommits.config.validator({path: null}));
|
||||
t.false(definitions.analyzeCommits.config.validator([]));
|
||||
t.false(definitions.analyzeCommits.config.validator());
|
||||
|
||||
t.true(definitions.analyzeCommits.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.analyzeCommits.config.validator('plugin-path.js'));
|
||||
t.true(definitions.analyzeCommits.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(definitions.verifyRelease.config.validator({}));
|
||||
t.false(definitions.verifyRelease.config.validator({path: null}));
|
||||
|
||||
t.true(definitions.verifyRelease.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.verifyRelease.config.validator());
|
||||
t.true(definitions.verifyRelease.config.validator('plugin-path.js'));
|
||||
t.true(definitions.verifyRelease.config.validator(() => {}));
|
||||
t.true(definitions.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
|
||||
t.false(definitions.generateNotes.config.validator({}));
|
||||
t.false(definitions.generateNotes.config.validator({path: null}));
|
||||
t.false(definitions.generateNotes.config.validator([]));
|
||||
|
||||
t.true(definitions.generateNotes.config.validator());
|
||||
t.true(definitions.generateNotes.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.generateNotes.config.validator('plugin-path.js'));
|
||||
t.true(definitions.generateNotes.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
|
||||
t.false(definitions.publish.config.validator({}));
|
||||
t.false(definitions.publish.config.validator({path: null}));
|
||||
t.false(definitions.publish.config.validator());
|
||||
|
||||
t.true(definitions.publish.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.publish.config.validator('plugin-path.js'));
|
||||
t.true(definitions.publish.config.validator(() => {}));
|
||||
t.true(definitions.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||
t.false(definitions.analyzeCommits.output.validator('invalid'));
|
||||
t.false(definitions.analyzeCommits.output.validator(1));
|
||||
t.false(definitions.analyzeCommits.output.validator({}));
|
||||
|
||||
t.true(definitions.analyzeCommits.output.validator());
|
||||
t.true(definitions.analyzeCommits.output.validator(null));
|
||||
t.true(definitions.analyzeCommits.output.validator('major'));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin output, if defined, must be a string', t => {
|
||||
t.false(definitions.generateNotes.output.validator(1));
|
||||
t.false(definitions.generateNotes.output.validator({}));
|
||||
|
||||
t.true(definitions.generateNotes.output.validator());
|
||||
t.true(definitions.generateNotes.output.validator(null));
|
||||
t.true(definitions.generateNotes.output.validator(''));
|
||||
t.true(definitions.generateNotes.output.validator('string'));
|
||||
});
|
||||
@@ -12,6 +12,7 @@ test.beforeEach(t => {
|
||||
test('Normalize and load plugin from string', t => {
|
||||
const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/plugin-noop', t.context.logger);
|
||||
|
||||
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
|
||||
});
|
||||
@@ -19,6 +20,7 @@ test('Normalize and load plugin from string', t => {
|
||||
test('Normalize and load plugin from object', t => {
|
||||
const plugin = normalize('publish', {}, {}, {path: './test/fixtures/plugin-noop'}, t.context.logger);
|
||||
|
||||
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']);
|
||||
});
|
||||
@@ -32,6 +34,7 @@ test('Normalize and load plugin from a base file path', t => {
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
t.is(plugin.pluginName, './plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], [
|
||||
'Load plugin %s from %s in shareable config %s',
|
||||
@@ -41,9 +44,40 @@ test('Normalize and load plugin from a base file path', t => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from function', t => {
|
||||
const plugin = normalize('', {}, {}, () => {}, t.context.logger);
|
||||
test('Wrap plugin in a function that add the "pluginName" to the error"', async t => {
|
||||
const plugin = normalize(
|
||||
'verifyConditions',
|
||||
{'./plugin-error': './test/fixtures'},
|
||||
{},
|
||||
'./plugin-error',
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
t.is(error.pluginName, './plugin-error');
|
||||
});
|
||||
|
||||
test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async t => {
|
||||
const plugin = normalize(
|
||||
'verifyConditions',
|
||||
{'./plugin-errors': './test/fixtures'},
|
||||
{},
|
||||
'./plugin-errors',
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
const errors = [...(await t.throws(plugin()))];
|
||||
for (const error of errors) {
|
||||
t.is(error.pluginName, './plugin-errors');
|
||||
}
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from function', t => {
|
||||
const pluginFunction = () => {};
|
||||
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger);
|
||||
|
||||
t.is(plugin.pluginName, '[Function: pluginFunction]');
|
||||
t.is(typeof plugin, 'function');
|
||||
});
|
||||
|
||||
@@ -54,18 +88,42 @@ test('Normalize and load plugin that retuns multiple functions', t => {
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
|
||||
});
|
||||
|
||||
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, {
|
||||
validator: output => output === 1,
|
||||
message: 'The output must be 1.',
|
||||
});
|
||||
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
|
||||
const analyzeCommits = stub().resolves(2);
|
||||
const plugin = normalize('analyzeCommits', {}, {}, analyzeCommits, t.context.logger);
|
||||
|
||||
await t.notThrows(plugin());
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
pluginFunction.resolves(2);
|
||||
const error = await t.throws(plugin(), Error);
|
||||
t.is(error.message, 'The output must be 1. Received: 2');
|
||||
t.is(error.code, 'EANALYZEOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
|
||||
const generateNotes = stub().resolves(2);
|
||||
const plugin = normalize('generateNotes', {}, {}, generateNotes, t.context.logger);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
t.is(error.code, 'ERELEASENOTESOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
|
||||
const plugin = normalize(
|
||||
'publish',
|
||||
{'./plugin-identity': './test/fixtures'},
|
||||
{},
|
||||
'./plugin-identity',
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin(2));
|
||||
|
||||
t.is(error.code, 'EPUBLISHOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
|
||||
@@ -127,12 +185,8 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async
|
||||
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));
|
||||
|
||||
t.is(error.code, 'EPLUGINCONF');
|
||||
t.is(error.code, 'EPLUGIN');
|
||||
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.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Throws an error if the plugin is not found', t => {
|
||||
|
||||
@@ -18,13 +18,32 @@ test('Execute each function in series passing the same input', async t => {
|
||||
t.true(step2.calledBefore(step3));
|
||||
});
|
||||
|
||||
test('Execute each function in series passing a transformed input', async t => {
|
||||
test('With one step, returns the step values rather than an Array ', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
|
||||
const result = await pipeline([step1])(0);
|
||||
|
||||
t.deepEqual(result, 1);
|
||||
t.true(step1.calledWith(0));
|
||||
});
|
||||
|
||||
test('With one step, throws the error rather than an AggregateError ', async t => {
|
||||
const error = new Error('test error 1');
|
||||
const step1 = stub().rejects(error);
|
||||
|
||||
const thrown = await t.throws(pipeline([step1])(0));
|
||||
|
||||
t.is(error, thrown);
|
||||
});
|
||||
|
||||
test('Execute each function in series passing a transformed input from "getNextInput"', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = (lastResult, result) => lastResult + result;
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => prevResult + result);
|
||||
const result = await pipeline([step1, step2, step3, step4])(0, {settleAll: false, getNextInput});
|
||||
|
||||
t.deepEqual(result, [1, 2, 3, 4]);
|
||||
t.true(step1.calledWith(0));
|
||||
@@ -36,22 +55,45 @@ test('Execute each function in series passing a transformed input', async t => {
|
||||
t.true(step3.calledBefore(step4));
|
||||
});
|
||||
|
||||
test('Execute each function in series passing the result of the previous one', async t => {
|
||||
test('Execute each function in series passing the "lastResult" and "result" to "getNextInput"', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = stub().returnsArg(0);
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => result);
|
||||
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: false, getNextInput});
|
||||
|
||||
t.deepEqual(result, [1, 2, 3, 4]);
|
||||
t.true(step1.calledWith(0));
|
||||
t.true(step2.calledWith(1));
|
||||
t.true(step3.calledWith(2));
|
||||
t.true(step4.calledWith(3));
|
||||
t.true(step1.calledBefore(step2));
|
||||
t.true(step2.calledBefore(step3));
|
||||
t.true(step3.calledBefore(step4));
|
||||
t.deepEqual(getNextInput.args, [[5, 1], [5, 2], [5, 3], [5, 4]]);
|
||||
});
|
||||
|
||||
test('Execute each function in series calling "transform" to modify the results', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = stub().returnsArg(0);
|
||||
const transform = stub().callsFake(result => result + 1);
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(5, {getNextInput, transform});
|
||||
|
||||
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
|
||||
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
|
||||
});
|
||||
|
||||
test('Execute each function in series calling "transform" to modify the results with "settleAll"', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = stub().returnsArg(0);
|
||||
const transform = stub().callsFake(result => result + 1);
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: true, getNextInput, transform});
|
||||
|
||||
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
|
||||
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
|
||||
});
|
||||
|
||||
test('Stop execution and throw error is a step rejects', async t => {
|
||||
@@ -89,7 +131,7 @@ test('Execute all even if a Promise rejects', async t => {
|
||||
const step2 = stub().rejects(error1);
|
||||
const step3 = stub().rejects(error2);
|
||||
|
||||
const errors = await t.throws(pipeline([step1, step2, step3])(0, true));
|
||||
const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true}));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.true(step1.calledWith(0));
|
||||
@@ -105,7 +147,7 @@ test('Throw all errors from all steps throwing an AggregateError', async t => {
|
||||
const step1 = stub().rejects(new AggregateError([error1, error2]));
|
||||
const step2 = stub().rejects(new AggregateError([error3, error4]));
|
||||
|
||||
const errors = await t.throws(pipeline([step1, step2])(0, true));
|
||||
const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true}));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2, error3, error4]);
|
||||
t.true(step1.calledWith(0));
|
||||
@@ -119,10 +161,9 @@ test('Execute each function in series passing a transformed input even if a step
|
||||
const step2 = stub().rejects(error2);
|
||||
const step3 = stub().rejects(error3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = (prevResult, result) => prevResult + result;
|
||||
|
||||
const errors = await t.throws(
|
||||
pipeline([step1, step2, step3, step4])(0, true, (prevResult, result) => prevResult + result)
|
||||
);
|
||||
const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput}));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error2, error3]);
|
||||
t.true(step1.calledWith(0));
|
||||
|
||||
@@ -28,6 +28,8 @@ test('Export default plugins', t => {
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Export plugins based on config', t => {
|
||||
@@ -48,6 +50,8 @@ test('Export plugins based on config', t => {
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test.serial('Export plugins loaded from the dependency of a shareable config module', async t => {
|
||||
@@ -76,6 +80,8 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test.serial('Export plugins loaded from the dependency of a shareable config file', async t => {
|
||||
@@ -101,6 +107,8 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Use default when only options are passed for a single plugin', t => {
|
||||
@@ -128,22 +136,10 @@ test('Merge global options with plugin options', async t => {
|
||||
});
|
||||
|
||||
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
|
||||
const errors = Array.from(
|
||||
t.throws(() => getPlugins({verifyConditions: {}, verifyRelease: {}}, {}, t.context.logger))
|
||||
);
|
||||
const errors = Array.from(t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[0].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.'
|
||||
);
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[1].message,
|
||||
'The "verifyRelease" 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.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
|
||||
@@ -153,8 +149,4 @@ test('Throw an error if an array of plugin configuration is missing a path for p
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[0].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.'
|
||||
);
|
||||
});
|
||||
|
||||
+10
-13
@@ -29,29 +29,29 @@ test.afterEach.always(() => {
|
||||
process.chdir(cwd);
|
||||
});
|
||||
|
||||
test.serial('Return "false" if does not run on a git repository', async t => {
|
||||
const dir = tempy.directory();
|
||||
process.chdir(dir);
|
||||
|
||||
t.false(await verify({}, 'master', t.context.logger));
|
||||
});
|
||||
|
||||
test.serial('Throw a AggregateError', async t => {
|
||||
await gitRepo();
|
||||
|
||||
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, 'The repositoryUrl option is required');
|
||||
t.is(errors[0].code, 'ENOREPOURL');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].message, 'The tagFormat template must compile to a valid Git tag format');
|
||||
t.is(errors[1].code, 'EINVALIDTAGFORMAT');
|
||||
t.is(errors[2].name, 'SemanticReleaseError');
|
||||
t.is(errors[2].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[2].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if does not run on a git repository', async t => {
|
||||
const dir = tempy.directory();
|
||||
process.chdir(dir);
|
||||
|
||||
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'ENOGITREPO');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: `?\${version}`};
|
||||
@@ -59,7 +59,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', asyn
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, 'The tagFormat template must compile to a valid Git tag format');
|
||||
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
|
||||
});
|
||||
|
||||
@@ -70,7 +69,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains t
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[0].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
@@ -81,7 +79,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[0].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user