feat: support multiple branches and distribution channels

- Allow to configure multiple branches to release from
- Allow to define a distribution channel associated with each branch
- Manage the availability on distribution channels based on git merges
- Support regular releases, maintenance releases and pre-releases
- Add the `addChannel` plugin step to make an existing release available on a different distribution channel

BREAKING CHANGE: the `branch` option has been removed in favor of `branches`

The new `branches` option expect either an Array or a single branch definition. To migrate your configuration:
- If you want to publish package from multiple branches, please the configuration documentation
- If you use the default configuration and want to publish only from `master`: nothing to change
- If you use the `branch` configuration and want to publish only from one branch: replace `branch` by `branches` (`"branch": "my-release-branch"` => `"branches": "my-release-branch"`)
This commit is contained in:
Pierre Vanduynslager
2018-11-29 14:13:03 -05:00
parent 7a9922a492
commit 7b4052470b
50 changed files with 4069 additions and 516 deletions
+199
View File
@@ -0,0 +1,199 @@
import test from 'ava';
import {union} from 'lodash';
import semver from 'semver';
import proxyquire from 'proxyquire';
const getBranch = (branches, branch) => branches.find(({name}) => name === branch);
const release = (branches, name, version) => getBranch(branches, name).tags.push({version});
const merge = (branches, source, target, tag) => {
getBranch(branches, target).tags = union(
getBranch(branches, source).tags.filter(({version}) => !tag || semver.cmp(version, '<=', tag)),
getBranch(branches, target).tags
);
};
test('Enforce ranges with branching release workflow', async t => {
const branches = [
{name: '1.x', tags: []},
{name: '1.0.x', tags: []},
{name: 'master', tags: []},
{name: 'next', tags: []},
{name: 'next-major', tags: []},
{name: 'beta', prerelease: true, tags: []},
{name: 'alpha', prerelease: true, tags: []},
];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches});
let result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, '1.0.x').range, '>=1.0.0 <1.0.0', 'Cannot release on 1.0.x before a releasing on master');
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.0', 'Cannot release on 1.x before a releasing on master');
t.is(getBranch(result, 'master').range, '>=1.0.0 <1.1.0', 'Can release only patch on master');
t.is(getBranch(result, 'next').range, '>=1.1.0 <2.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.0', 'Can release only major on next-major');
release(branches, 'master', '1.0.0');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, '1.0.x').range, '>=1.0.0 <1.0.0', 'Cannot release on 1.0.x before a releasing on master');
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.0', 'Cannot release on 1.x before a releasing on master');
t.is(getBranch(result, 'master').range, '>=1.0.0 <1.1.0', 'Can release only patch on master');
release(branches, 'master', '1.0.1');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
merge(branches, 'master', 'next');
merge(branches, 'master', 'next-major');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
t.is(getBranch(result, 'next').range, '>=1.1.0 <2.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.0', 'Can release only major on next-major');
release(branches, 'next', '1.1.0');
release(branches, 'next', '1.1.1');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'next').range, '>=1.1.1 <2.0.0', 'Can release only patch or minor, > than 1.1.0 on next');
release(branches, 'next-major', '2.0.0');
release(branches, 'next-major', '2.0.1');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major');
merge(branches, 'next-major', 'beta');
release(branches, 'beta', '3.0.0-beta.1');
merge(branches, 'beta', 'alpha');
release(branches, 'alpha', '4.0.0-alpha.1');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major');
merge(branches, 'master', '1.0.x');
merge(branches, 'master', '1.x');
release(branches, 'master', '1.0.1');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.0.1 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
t.is(
getBranch(result, '1.0.x').range,
'>=1.0.1 <1.0.1',
'Cannot release on 1.0.x before >= 1.1.0 is released on master'
);
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.1', 'Cannot release on 1.x before >= 1.2.0 is released on master');
release(branches, 'master', '1.0.2');
release(branches, 'master', '1.0.3');
release(branches, 'master', '1.0.4');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.0.4 <1.1.0', 'Can release only patch, > than 1.0.4 on master');
t.is(
getBranch(result, '1.0.x').range,
'>=1.0.1 <1.0.2',
'Cannot release on 1.0.x before >= 1.1.0 is released on master'
);
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.2', 'Cannot release on 1.x before >= 1.2.0 is released on master');
merge(branches, 'next', 'master');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master');
t.is(getBranch(result, 'next').range, '>=1.2.0 <2.0.0', 'Can release only patch or minor, > than 1.2.0 on next');
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release any version, > than 2.0.1 on next-major');
t.is(
getBranch(result, '1.0.x').range,
'>=1.0.1 <1.0.2',
'Cannot release on 1.0.x before 1.0.x version from master are merged'
);
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.0.2', 'Cannot release on 1.x before >= 2.0.0 is released on master');
merge(branches, 'master', '1.0.x', '1.0.4');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master');
t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range');
t.is(getBranch(result, '1.x').range, '>=1.1.0 <1.1.0', 'Cannot release on 1.x before >= 2.0.0 is released on master');
merge(branches, 'master', '1.x');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=1.1.1 <1.2.0', 'Can release only patch, > than 1.1.1 on master');
t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range');
t.is(getBranch(result, '1.x').range, '>=1.1.1 <1.1.1', 'Cannot release on 1.x before >= 2.0.0 is released on master');
merge(branches, 'next-major', 'next');
merge(branches, 'next', 'master');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=2.0.1 <2.1.0', 'Can release only patch, > than 2.0.1 on master');
t.is(getBranch(result, 'next').range, '>=2.1.0 <3.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only major on next-major');
t.is(getBranch(result, '1.x').range, '>=1.1.1 <2.0.0', 'Can release on 1.x only within range');
merge(branches, 'beta', 'master');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, 'master').range, '>=2.0.1 <2.1.0', 'Can release only patch, > than 2.0.1 on master');
t.is(getBranch(result, 'next').range, '>=2.1.0 <3.0.0', 'Can release only minor on next');
t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only major on next-major');
branches.push({name: '1.1.x', tags: []});
merge(branches, '1.x', '1.1.x');
result = (await getBranches({options: {branches}})).map(({name, range}) => ({name, range}));
t.is(getBranch(result, '1.0.x').range, '>=1.0.4 <1.1.0', 'Can release on 1.0.x only within range');
t.is(getBranch(result, '1.1.x').range, '>=1.1.1 <1.2.0', 'Can release on 1.1.x only within range');
t.is(getBranch(result, '1.x').range, '>=1.2.0 <2.0.0', 'Can release on 1.x only within range');
});
test('Throw SemanticReleaseError for invalid configurations', async t => {
const branches = [
{name: '123', range: '123', tags: []},
{name: '1.x', tags: []},
{name: 'maintenance-1', range: '1.x', tags: []},
{name: '1.x.x', tags: []},
{name: 'beta', prerelease: '', tags: []},
{name: 'alpha', prerelease: 'alpha', tags: []},
{name: 'preview', prerelease: 'alpha', tags: []},
];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches});
const errors = [...(await t.throws(getBranches({options: {branches}})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EMAINTENANCEBRANCH');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EMAINTENANCEBRANCHES');
t.truthy(errors[1].message);
t.truthy(errors[1].details);
t.is(errors[2].name, 'SemanticReleaseError');
t.is(errors[2].code, 'EPRERELEASEBRANCH');
t.truthy(errors[2].message);
t.truthy(errors[2].details);
t.is(errors[3].name, 'SemanticReleaseError');
t.is(errors[3].code, 'EPRERELEASEBRANCHES');
t.truthy(errors[3].message);
t.truthy(errors[3].details);
t.is(errors[4].name, 'SemanticReleaseError');
t.is(errors[4].code, 'ERELEASEBRANCHES');
t.truthy(errors[4].message);
t.truthy(errors[4].details);
});
test('Throw a SemanticReleaseError if there is duplicate branches', async t => {
const branches = [{name: 'master', tags: []}, {name: 'master', tags: []}];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches});
const errors = [...(await t.throws(getBranches({options: {branches}})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EDUPLICATEBRANCHES');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError for each invalid branch name', async t => {
const branches = [{name: '~master', tags: []}, {name: '^master', tags: []}];
const getBranches = proxyquire('../../lib/branches', {'./get-tags': () => branches});
const errors = [...(await t.throws(getBranches({options: {branches}})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDBRANCHNAME');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EINVALIDBRANCHNAME');
t.truthy(errors[1].message);
t.truthy(errors[1].details);
});
+46
View File
@@ -0,0 +1,46 @@
import test from 'ava';
import expand from '../../lib/branches/expand';
import {gitRepo, gitCommits, gitCheckout} from '../helpers/git-utils';
test('Expand branches defined with globs', async t => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('1.1.x', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitCheckout('1.x.x', true, {cwd});
await gitCommits(['Third'], {cwd});
await gitCheckout('2.x', true, {cwd});
await gitCommits(['Fourth'], {cwd});
await gitCheckout('next', true, {cwd});
await gitCommits(['Fifth'], {cwd});
await gitCheckout('pre/foo', true, {cwd});
await gitCommits(['Sixth'], {cwd});
await gitCheckout('pre/bar', true, {cwd});
await gitCommits(['Seventh'], {cwd});
await gitCheckout('beta', true, {cwd});
await gitCommits(['Eighth'], {cwd});
const branches = [
// Should match all maintenance type branches
{name: '+([1-9])?(.{+([1-9]),x}).x'},
{name: 'master', channel: 'latest'},
{name: 'next'},
{name: 'pre/{foo,bar}', channel: `\${name.replace(/^pre\\//g, '')}`, prerelease: true},
// Should be ignored as there is no matching branches in the repo
{name: 'missing'},
// Should be ignored as the matching branch in the repo is already matched by `/^pre\\/(\\w+)$/gi`
{name: '*/foo', channel: 'foo', prerelease: 'foo'},
{name: 'beta', channel: `channel-\${name}`, prerelease: true},
];
t.deepEqual(await expand({cwd}, branches), [
{name: '1.1.x'},
{name: '1.x.x'},
{name: '2.x'},
{name: 'master', channel: 'latest'},
{name: 'next'},
{name: 'pre/bar', channel: 'bar', prerelease: true},
{name: 'pre/foo', channel: 'foo', prerelease: true},
{name: 'beta', channel: 'channel-beta', prerelease: true},
]);
});
+202
View File
@@ -0,0 +1,202 @@
import test from 'ava';
import getTags from '../../lib/branches/get-tags';
import {gitRepo, gitCommits, gitTagVersion, gitCheckout, merge, changeAuthor} from '../helpers/git-utils';
test('Get the valid tags', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
commits.push(...(await gitCommits(['Second'], {cwd})));
await gitTagVersion('v1.0.0', undefined, {cwd});
commits.push(...(await gitCommits(['Third'], {cwd})));
await gitTagVersion('v3.0', undefined, {cwd});
commits.push(...(await gitCommits(['Fourth'], {cwd})));
await gitTagVersion('v3.0.0-beta.1', undefined, {cwd});
const result = await getTags({cwd, options: {tagFormat: `v\${version}`}}, [{name: 'master'}]);
t.deepEqual(result, [
{
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[1].hash},
{gitTag: 'v2.0.0', version: '2.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channel: undefined, gitHead: commits[3].hash},
],
},
]);
});
test('Get the valid tags from multiple branches', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@1.x', undefined, {cwd});
commits.push(...(await gitCommits(['Second'], {cwd})));
await gitTagVersion('v1.1.0', undefined, {cwd});
await gitTagVersion('v1.1.0@1.x', undefined, {cwd});
await gitCheckout('1.x', true, {cwd});
await gitCheckout('master', false, {cwd});
commits.push(...(await gitCommits(['Third'], {cwd})));
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitTagVersion('v2.0.0@next', undefined, {cwd});
await gitCheckout('next', true, {cwd});
commits.push(...(await gitCommits(['Fourth'], {cwd})));
await gitTagVersion('v3.0.0@next', undefined, {cwd});
const result = await getTags({cwd, options: {tagFormat: `v\${version}`}}, [
{name: '1.x'},
{name: 'master'},
{name: 'next'},
]);
t.deepEqual(result, [
{
name: '1.x',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'v1.0.0@1.x', version: '1.0.0', channel: '1.x', gitHead: commits[0].hash},
{gitTag: 'v1.1.0', version: '1.1.0', channel: undefined, gitHead: commits[1].hash},
{gitTag: 'v1.1.0@1.x', version: '1.1.0', channel: '1.x', gitHead: commits[1].hash},
],
},
{
name: 'master',
tags: [
...result[0].tags,
{gitTag: 'v2.0.0', version: '2.0.0', channel: undefined, gitHead: commits[2].hash},
{gitTag: 'v2.0.0@next', version: '2.0.0', channel: 'next', gitHead: commits[2].hash},
],
},
{
name: 'next',
tags: [...result[1].tags, {gitTag: 'v3.0.0@next', version: '3.0.0', channel: 'next', gitHead: commits[3].hash}],
},
]);
});
test('Match the tag name from the begining of the string and the channel from the last "@"', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('prefix@v1.0.0', undefined, {cwd});
await gitTagVersion('prefix@v1.0.0@next', undefined, {cwd});
await gitTagVersion('prefix@v2.0.0', undefined, {cwd});
await gitTagVersion('prefix@v2.0.0@next', undefined, {cwd});
await gitTagVersion('other-prefix@v3.0.0', undefined, {cwd});
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}]);
t.deepEqual(result, [
{
name: 'master',
tags: [
{gitTag: 'prefix@v1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'prefix@v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: commits[0].hash},
{gitTag: 'prefix@v2.0.0', version: '2.0.0', channel: undefined, gitHead: commits[0].hash},
{gitTag: 'prefix@v2.0.0@next', version: '2.0.0', channel: 'next', gitHead: commits[0].hash},
],
},
]);
});
test('Return branches with and empty tags array if no valid tag is found', async t => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v2.0.x', undefined, {cwd});
await gitCommits(['Third'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
t.deepEqual(result, [{name: 'master', tags: []}, {name: 'next', tags: []}]);
});
test('Return branches with and empty tags array if no valid tag is found in history of configured branches', async t => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('other-branch', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitTagVersion('v2.0.0@next', undefined, {cwd});
await gitTagVersion('v3.0.0', undefined, {cwd});
await gitTagVersion('v3.0.0@next', undefined, {cwd});
await gitCheckout('master', false, {cwd});
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
t.deepEqual(result, [{name: 'master', tags: []}, {name: 'next', tags: []}]);
});
test('Get the highest valid tag corresponding to the "tagFormat"', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('1.0.0', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `\${version}`}}, [{name: 'master'}]), [
{name: 'master', tags: [{gitTag: '1.0.0', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}]},
]);
await gitTagVersion('foo-1.0.0-bar', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `foo-\${version}-bar`}}, [{name: 'master'}]), [
{name: 'master', tags: [{gitTag: 'foo-1.0.0-bar', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}]},
]);
await gitTagVersion('foo-v1.0.0-bar', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `foo-v\${version}-bar`}}, [{name: 'master'}]), [
{
name: 'master',
tags: [{gitTag: 'foo-v1.0.0-bar', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}],
},
]);
await gitTagVersion('(.+)/1.0.0/(a-z)', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `(.+)/\${version}/(a-z)`}}, [{name: 'master'}]), [
{
name: 'master',
tags: [{gitTag: '(.+)/1.0.0/(a-z)', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}],
},
]);
await gitTagVersion('2.0.0-1.0.0-bar.1', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `2.0.0-\${version}-bar.1`}}, [{name: 'master'}]), [
{
name: 'master',
tags: [{gitTag: '2.0.0-1.0.0-bar.1', version: '1.0.0', channel: undefined, gitHead: commits[0].hash}],
},
]);
await gitTagVersion('3.0.0-bar.2', undefined, {cwd});
t.deepEqual(await getTags({cwd, options: {tagFormat: `\${version}-bar.2`}}, [{name: 'master'}]), [
{name: 'master', tags: [{gitTag: '3.0.0-bar.2', version: '3.0.0', channel: undefined, gitHead: commits[0].hash}]},
]);
});
test('Get the tag on branch where commits have been rebased', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitCheckout('next', true, {cwd});
commits.push(...(await gitCommits(['Second/n/n/commit body'], {cwd})));
await gitTagVersion('v1.0.0@next', undefined, {cwd});
await gitCheckout('master', false, {cwd});
await merge('next', {cwd});
// Simulate GitHub "Rebase and Merge" by changing the committer info, which will result in a new commit sha and losing the tag
await changeAuthor(commits[1].hash, {cwd});
const result = await getTags({cwd, options: {tagFormat: `v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
t.deepEqual(result, [
{
name: 'master',
tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: commits[1].hash}],
},
{
name: 'next',
tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: commits[1].hash}],
},
]);
});
+307
View File
@@ -0,0 +1,307 @@
import test from 'ava';
import normalize from '../../lib/branches/normalize';
const toTags = versions => versions.map(version => ({version}));
test('Maintenance branches - initial state', t => {
const maintenance = [{name: '1.x', tags: []}, {name: '1.1.x', tags: []}, {name: '1.2.x', tags: []}];
const release = [{name: 'master', tags: []}];
t.deepEqual(
normalize
.maintenance({maintenance, release})
.map(({type, name, range, accept, channel, 'merge-range': maintenanceRange}) => ({
type,
name,
range,
accept,
channel,
'merge-range': maintenanceRange,
})),
[
{
type: 'maintenance',
name: '1.1.x',
range: '>=1.1.0 <1.0.0',
accept: [],
channel: '1.1.x',
'merge-range': '>=1.1.0 <1.2.0',
},
{
type: 'maintenance',
name: '1.2.x',
range: '>=1.2.0 <1.0.0',
accept: [],
channel: '1.2.x',
'merge-range': '>=1.2.0 <1.3.0',
},
{
type: 'maintenance',
name: '1.x',
range: '>=1.3.0 <1.0.0',
accept: [],
channel: '1.x',
'merge-range': '>=1.3.0 <2.0.0',
},
]
);
});
test('Maintenance branches - cap range to first release present on default branch and not in any Maintenance one', t => {
const maintenance = [
{name: '1.x', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '1.2.1', '1.3.0', '1.4.0', '1.5.0'])},
{name: 'name', range: '1.1.x', tags: toTags(['1.0.0', '1.0.1', '1.1.0', '1.1.1'])},
{name: '1.2.x', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '1.2.1'])},
{name: '2.x.x', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '1.2.1', '1.5.0'])},
];
const release = [
{
name: 'master',
tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '1.2.1', '1.3.0', '1.4.0', '1.5.0', '1.6.0', '2.0.0']),
},
];
t.deepEqual(
normalize
.maintenance({maintenance, release})
.map(({type, name, range, accept, channel, 'merge-range': maintenanceRange}) => ({
type,
name,
range,
accept,
channel,
'merge-range': maintenanceRange,
})),
[
{
type: 'maintenance',
name: 'name',
range: '>=1.1.1 <1.2.0',
accept: ['patch'],
channel: 'name',
'merge-range': '>=1.1.0 <1.2.0',
},
{
type: 'maintenance',
name: '1.2.x',
range: '>=1.2.1 <1.3.0',
accept: ['patch'],
channel: '1.2.x',
'merge-range': '>=1.2.0 <1.3.0',
},
{
type: 'maintenance',
name: '1.x',
range: '>=1.5.0 <1.6.0',
accept: ['patch'],
channel: '1.x',
'merge-range': '>=1.3.0 <2.0.0',
},
{
type: 'maintenance',
name: '2.x.x',
range: '>=2.0.0 <1.6.0',
accept: [],
channel: '2.x.x',
'merge-range': '>=2.0.0 <3.0.0',
},
]
);
});
test('Maintenance branches - cap range to default branch last release if all release are also present on maintenance branch', t => {
const maintenance = [
{name: '1.x', tags: toTags(['1.0.0', '1.2.0', '1.3.0'])},
{name: '2.x.x', tags: toTags(['1.0.0', '1.2.0', '1.3.0', '2.0.0'])},
];
const release = [{name: 'master', tags: toTags(['1.0.0', '1.2.0', '1.3.0', '2.0.0'])}];
t.deepEqual(
normalize
.maintenance({maintenance, release})
.map(({type, name, range, accept, channel, 'merge-range': maintenanceRange}) => ({
type,
name,
range,
accept,
channel,
'merge-range': maintenanceRange,
})),
[
{
type: 'maintenance',
name: '1.x',
range: '>=1.3.0 <2.0.0',
accept: ['patch', 'minor'],
channel: '1.x',
'merge-range': '>=1.0.0 <2.0.0',
},
{
type: 'maintenance',
name: '2.x.x',
range: '>=2.0.0 <2.0.0',
accept: [],
channel: '2.x.x',
'merge-range': '>=2.0.0 <3.0.0',
},
]
);
});
test('Release branches - initial state', t => {
const release = [{name: 'master', tags: []}, {name: 'next', tags: []}, {name: 'next-major', tags: []}];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=1.0.0 <1.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=1.1.0 <2.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'next-major',
range: '>=2.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
},
]
);
});
test('Release branches - 3 release branches', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.0.1', '1.0.2'])},
{name: 'next', tags: toTags(['1.0.0', '1.0.1', '1.0.2', '1.1.0', '1.2.0'])},
{name: 'next-major', tags: toTags(['1.0.0', '1.0.1', '1.0.2', '1.1.0', '1.2.0', '2.0.0', '2.0.1', '2.1.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=1.0.2 <1.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=1.2.0 <2.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'next-major',
range: '>=2.1.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
},
]
);
});
test('Release branches - 2 release branches', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.0.1', '1.1.0', '1.1.1', '1.2.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.0.1', '1.1.0', '1.1.1', '1.2.0', '2.0.0', '2.0.1', '2.1.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=1.2.0 <2.0.0', accept: ['patch', 'minor'], channel: undefined},
{type: 'release', name: 'next', range: '>=2.1.0', accept: ['patch', 'minor', 'major'], channel: 'next'},
]
);
});
test('Release branches - 1 release branches', t => {
const release = [{name: 'master', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0'])}];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[{type: 'release', name: 'master', range: '>=1.2.0', accept: ['patch', 'minor', 'major'], channel: undefined}]
);
});
test('Release branches - cap ranges to first release only present on following branch', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.1.0', '1.2.0', '2.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0', '1.2.0', '2.0.0', '2.1.0'])},
{name: 'next-major', tags: toTags(['1.0.0', '1.1.0', '1.2.0', '2.0.0', '2.1.0', '2.2.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=2.0.0 <2.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=2.1.0 <2.2.0', accept: ['patch'], channel: 'next'},
{
type: 'release',
name: 'next-major',
range: '>=2.2.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
},
]
);
});
test('Release branches - Handle missing previous tags in branch history', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '2.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0', '1.1.1', '1.2.0', '2.0.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=2.0.0 <3.0.0', accept: ['patch', 'minor'], channel: undefined},
{type: 'release', name: 'next', range: '>=3.0.0', accept: ['patch', 'minor', 'major'], channel: 'next'},
]
);
});
test('Release branches - enforce release gaps after downstream merge', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
{name: 'next-major', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=2.0.0 <2.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=2.1.0 <3.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'next-major',
range: '>=3.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
},
]
);
});
test('Release branches - limit releases on 2nd and 3rd branche based on 1st branch last release', t => {
const release = [
{name: 'master', tags: toTags(['1.0.0', '1.1.0', '2.0.0', '3.0.0'])},
{name: 'next', tags: toTags(['1.0.0', '1.1.0'])},
{name: 'next-major', tags: toTags(['1.0.0', '1.1.0', '2.0.0'])},
];
t.deepEqual(
normalize.release({release}).map(({type, name, range, accept, channel}) => ({type, name, range, accept, channel})),
[
{type: 'release', name: 'master', range: '>=3.0.0 <3.1.0', accept: ['patch'], channel: undefined},
{type: 'release', name: 'next', range: '>=3.1.0 <4.0.0', accept: ['patch', 'minor'], channel: 'next'},
{
type: 'release',
name: 'next-major',
range: '>=4.0.0',
accept: ['patch', 'minor', 'major'],
channel: 'next-major',
},
]
);
});
test('Prerelease branches', t => {
const prerelease = [{name: 'beta', prerelease: true, tags: []}, {name: 'alpha', prerelease: 'preview', tags: []}];
t.deepEqual(normalize.prerelease({prerelease}).map(({type, name, channel}) => ({type, name, channel})), [
{type: 'prerelease', name: 'beta', channel: 'beta'},
{type: 'prerelease', name: 'alpha', channel: 'alpha'},
]);
});
+4 -3
View File
@@ -29,6 +29,7 @@ test.serial('Pass options to semantic-release API', async t => {
'',
'-b',
'master',
'next',
'-r',
'https://github/com/owner/repo.git',
'-t',
@@ -68,7 +69,7 @@ test.serial('Pass options to semantic-release API', async t => {
const exitCode = await cli();
t.is(run.args[0][0].branch, 'master');
t.deepEqual(run.args[0][0].branches, ['master', 'next']);
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
@@ -92,7 +93,7 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
const argv = [
'',
'',
'--branch',
'--branches',
'master',
'--repository-url',
'https://github/com/owner/repo.git',
@@ -110,7 +111,7 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
const exitCode = await cli();
t.is(run.args[0][0].branch, 'master');
t.deepEqual(run.args[0][0].branches, ['master']);
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
+86
View File
@@ -0,0 +1,86 @@
import test from 'ava';
import {maintenance, prerelease, release} from '../../lib/definitions/branches';
test('A "maintenance" branch is identified by having a "range" property or a "name" formatted like "N.x", "N.x.x" or "N.N.x"', t => {
t.true(maintenance.filter({name: '1.x.x'}));
t.true(maintenance.filter({name: '1.0.x'}));
t.true(maintenance.filter({name: '1.x'}));
t.true(maintenance.filter({name: 'some-name', range: '1.x.x'}));
t.true(maintenance.filter({name: 'some-name', range: '1.1.x'}));
t.true(maintenance.filter({name: 'some-name', range: ''}));
t.true(maintenance.filter({name: 'some-name', range: null}));
t.true(maintenance.filter({name: 'some-name', range: false}));
t.false(maintenance.filter({name: 'some-name'}));
t.false(maintenance.filter({name: '1.0.0'}));
t.false(maintenance.filter({name: 'x.x.x'}));
});
test('A "maintenance" branches must have a "range" property formatted like "N.x", "N.x.x" or "N.N.x"', t => {
t.true(maintenance.branchValidator({name: 'some-name', range: '1.x.x'}));
t.true(maintenance.branchValidator({name: 'some-name', range: '1.1.x'}));
t.false(maintenance.branchValidator({name: 'some-name', range: '^1.0.0'}));
t.false(maintenance.branchValidator({name: 'some-name', range: '>=1.0.0 <2.0.0'}));
t.false(maintenance.branchValidator({name: 'some-name', range: '1.0.0'}));
t.false(maintenance.branchValidator({name: 'some-name', range: 'wrong-range'}));
t.false(maintenance.branchValidator({name: 'some-name', range: ''}));
t.false(maintenance.branchValidator({name: 'some-name', range: null}));
t.false(maintenance.branchValidator({name: 'some-name', range: false}));
});
test('The "maintenance" branches must have unique ranges', t => {
t.true(maintenance.branchesValidator([{range: '1.x.x'}, {range: '1.0.x'}]));
t.false(maintenance.branchesValidator([{range: '1.x.x'}, {range: '1.x.x'}]));
t.false(maintenance.branchesValidator([{range: '1.x.x'}, {range: '1.x'}]));
});
test('A "prerelease" branch is identified by having a range "prerelease" property', t => {
t.true(prerelease.filter({name: 'some-name', prerelease: true}));
t.true(prerelease.filter({name: 'some-name', prerelease: 'beta'}));
t.true(prerelease.filter({name: 'some-name', prerelease: ''}));
t.true(prerelease.filter({name: 'some-name', prerelease: null}));
t.true(prerelease.filter({name: 'some-name', prerelease: false}));
t.false(prerelease.filter({name: 'some-name'}));
});
test('A "prerelease" branch must have a valid prerelease detonation in "prerelease" property or in "name" if "prerelease" is "true"', t => {
t.true(prerelease.branchValidator({name: 'beta', prerelease: true}));
t.true(prerelease.branchValidator({name: 'some-name', prerelease: 'beta'}));
t.false(prerelease.branchValidator({name: 'some-name', prerelease: ''}));
t.false(prerelease.branchValidator({name: 'some-name', prerelease: null}));
t.false(prerelease.branchValidator({name: 'some-name', prerelease: false}));
t.false(prerelease.branchValidator({name: 'some-name', prerelease: '000'}));
t.false(prerelease.branchValidator({name: 'some-name', prerelease: '#beta'}));
t.false(prerelease.branchValidator({name: '000', prerelease: true}));
t.false(prerelease.branchValidator({name: '#beta', prerelease: true}));
});
test('The "prerelease" branches must have unique "prerelease" property', t => {
t.true(prerelease.branchesValidator([{prerelease: 'beta'}, {prerelease: 'alpha'}]));
t.false(prerelease.branchesValidator([{range: 'beta'}, {range: 'beta'}, {range: 'alpha'}]));
});
test('A "release" branch is identified by not havin a "range" or "prerelease" property or a "name" formatted like "N.x", "N.x.x" or "N.N.x"', t => {
t.true(release.filter({name: 'some-name'}));
t.false(release.filter({name: '1.x.x'}));
t.false(release.filter({name: '1.0.x'}));
t.false(release.filter({name: 'some-name', range: '1.x.x'}));
t.false(release.filter({name: 'some-name', range: '1.1.x'}));
t.false(release.filter({name: 'some-name', prerelease: true}));
t.false(release.filter({name: 'some-name', prerelease: 'beta'}));
});
test('There must be between 1 and 3 release branches', t => {
t.true(release.branchesValidator([{name: 'branch1'}]));
t.true(release.branchesValidator([{name: 'branch1'}, {name: 'branch2'}]));
t.true(release.branchesValidator([{name: 'branch1'}, {name: 'branch2'}, {name: 'branch3'}]));
t.false(release.branchesValidator([]));
t.false(release.branchesValidator([{name: 'branch1'}, {name: 'branch2'}, {name: 'branch3'}, {name: 'branch4'}]));
});
+10
View File
@@ -32,6 +32,16 @@ test('The "publish" plugin output, if defined, must be an object', t => {
t.true(plugins.publish.outputValidator(''));
});
test('The "addChannel" plugin output, if defined, must be an object', t => {
t.false(plugins.addChannel.outputValidator(1));
t.false(plugins.addChannel.outputValidator('string'));
t.true(plugins.addChannel.outputValidator({}));
t.true(plugins.addChannel.outputValidator());
t.true(plugins.addChannel.outputValidator(null));
t.true(plugins.addChannel.outputValidator(''));
});
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
const env = {MY_TOKEN: 'secret token'};
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
+19
View File
@@ -66,6 +66,25 @@ test('Get all commits since gitHead (from lastRelease) on a detached head repo',
t.truthy(result[0].committer.name);
});
test('Get all commits between lastRelease.gitHead and a shas', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First', 'Second', 'Third'], {cwd});
// Retrieve the commits with the commits module, between commit 'First' and 'Third'
const result = await getCommits({
cwd,
lastRelease: {gitHead: commits[commits.length - 1].hash},
nextRelease: {gitHead: commits[1].hash},
logger: t.context.logger,
});
// Verify the commits created and retrieved by the module are identical
t.is(result.length, 1);
t.deepEqual(result, commits.slice(1, commits.length - 1));
});
test('Return empty array if lastRelease.gitHead is the last commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
+94 -63
View File
@@ -6,7 +6,7 @@ import {omit} from 'lodash';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import yaml from 'js-yaml';
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
import {gitRepo, gitTagVersion, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
const DEFAULT_PLUGINS = [
'@semantic-release/commit-analyzer',
@@ -23,8 +23,10 @@ test.beforeEach(t => {
test('Default values, reading repositoryUrl from package.json', async t => {
const pkg = {repository: 'https://host.null/owner/package.git'};
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const {cwd} = await gitRepo(true);
await gitCommits(['First'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.1.0', undefined, {cwd});
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@host.null:owner/repo.git', {cwd});
// Create package.json in repository root
@@ -33,21 +35,35 @@ test('Default values, reading repositoryUrl from package.json', async t => {
const {options: result} = await t.context.getConfig({cwd});
// Verify the default options are set
t.is(result.branch, 'master');
t.deepEqual(result.branches, [
'+([1-9])?(.{+([1-9]),x}).x',
'master',
'next',
'next-major',
{name: 'beta', prerelease: true},
{name: 'alpha', prerelease: true},
]);
t.is(result.repositoryUrl, 'https://host.null/owner/package.git');
t.is(result.tagFormat, `v\${version}`);
});
test('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const {cwd} = await gitRepo(true);
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'https://host.null/owner/module.git', {cwd});
const {options: result} = await t.context.getConfig({cwd});
// Verify the default options are set
t.is(result.branch, 'master');
t.deepEqual(result.branches, [
'+([1-9])?(.{+([1-9]),x}).x',
'master',
'next',
'next-major',
{name: 'beta', prerelease: true},
{name: 'alpha', prerelease: true},
]);
t.is(result.repositoryUrl, 'https://host.null/owner/module.git');
t.is(result.tagFormat, `v\${version}`);
});
@@ -62,7 +78,14 @@ test('Default values, reading repositoryUrl (http url) from package.json if not
const {options: result} = await t.context.getConfig({cwd});
// Verify the default options are set
t.is(result.branch, 'master');
t.deepEqual(result.branches, [
'+([1-9])?(.{+([1-9]),x}).x',
'master',
'next',
'next-major',
{name: 'beta', prerelease: true},
{name: 'alpha', prerelease: true},
]);
t.is(result.repositoryUrl, 'https://host.null/owner/module.git');
t.is(result.tagFormat, `v\${version}`);
});
@@ -73,7 +96,7 @@ test('Read options from package.json', async t => {
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -83,10 +106,11 @@ test('Read options from package.json', async t => {
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from package.json
t.deepEqual(result, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from .releaserc.yml', async t => {
@@ -94,7 +118,7 @@ test('Read options from .releaserc.yml', async t => {
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -104,10 +128,11 @@ test('Read options from .releaserc.yml', async t => {
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from package.json
t.deepEqual(result, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from .releaserc.json', async t => {
@@ -115,7 +140,7 @@ test('Read options from .releaserc.json', async t => {
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -125,10 +150,11 @@ test('Read options from .releaserc.json', async t => {
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from package.json
t.deepEqual(result, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from .releaserc.js', async t => {
@@ -136,7 +162,7 @@ test('Read options from .releaserc.js', async t => {
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -146,10 +172,11 @@ test('Read options from .releaserc.js', async t => {
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from package.json
t.deepEqual(result, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read options from release.config.js', async t => {
@@ -157,7 +184,7 @@ test('Read options from release.config.js', async t => {
const {cwd} = await gitRepo();
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -167,10 +194,11 @@ test('Read options from release.config.js', async t => {
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from package.json
t.deepEqual(result, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Prioritise CLI/API parameters over file configuration and git repo', async t => {
@@ -181,11 +209,11 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async
cwd = await gitShallowClone(repositoryUrl);
const pkgOptions = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_pkg'},
branch: 'branch_pkg',
branches: ['branch_pkg'],
};
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_cli'},
branch: 'branch_cli',
branches: ['branch_cli'],
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
plugins: false,
@@ -196,10 +224,11 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async
const result = await t.context.getConfig({cwd}, options);
const expected = {...options, branches: ['branch_cli']};
// Verify the options contains the plugin config from CLI/API
t.deepEqual(result.options, options);
t.deepEqual(result.options, expected);
// Verify the plugins module is called with the plugin options from CLI/API
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Read configuration from file path in "extends"', async t => {
@@ -209,7 +238,7 @@ test('Read configuration from file path in "extends"', async t => {
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: ['plugin-1', ['plugin-2', {plugin2Opt: 'value'}]],
@@ -220,10 +249,11 @@ test('Read configuration from file path in "extends"', async t => {
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from shareable.json
t.deepEqual(result, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: './shareable.json',
generateNotes: './shareable.json',
@@ -239,7 +269,7 @@ test('Read configuration from module path in "extends"', async t => {
const options = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -248,12 +278,13 @@ test('Read configuration from module path in "extends"', async t => {
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'node_modules/shareable/index.json'), options);
const {options: results} = await t.context.getConfig({cwd});
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options, branches: ['test_branch']};
// Verify the options contains the plugin config from shareable.json
t.deepEqual(results, options);
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: 'shareable',
generateNotes: 'shareable',
@@ -267,14 +298,14 @@ test('Read configuration from an array of paths in "extends"', async t => {
const options1 = {
verifyRelease: 'verifyRelease1',
analyzeCommits: {path: 'analyzeCommits1', param: 'analyzeCommits_param1'},
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
};
const options2 = {
verifyRelease: 'verifyRelease2',
generateNotes: 'generateNotes2',
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
branch: 'test_branch',
branches: ['test_branch'],
tagFormat: `v\${version}`,
plugins: false,
};
@@ -283,12 +314,13 @@ test('Read configuration from an array of paths in "extends"', async t => {
await outputJson(path.resolve(cwd, 'shareable1.json'), options1);
await outputJson(path.resolve(cwd, 'shareable2.json'), options2);
const {options: results} = await t.context.getConfig({cwd});
const {options: result} = await t.context.getConfig({cwd});
const expected = {...options1, ...options2, branches: ['test_branch']};
// Verify the options contains the plugin config from shareable1.json and shareable2.json
t.deepEqual(results, {...options1, ...options2});
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from shareable1.json and shareable2.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options: {...options1, ...options2}});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
t.deepEqual(t.context.plugins.args[0][1], {
verifyRelease1: './shareable1.json',
verifyRelease2: './shareable2.json',
@@ -303,7 +335,7 @@ test('Prioritize configuration from config file over "extends"', async t => {
const {cwd} = await gitRepo();
const pkgOptions = {
extends: './shareable.json',
branch: 'test_pkg',
branches: ['test_pkg'],
generateNotes: 'generateNotes',
publish: [{path: 'publishPkg', param: 'publishPkg_param'}],
};
@@ -311,7 +343,7 @@ test('Prioritize configuration from config file over "extends"', async t => {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotesShareable',
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
plugins: false,
@@ -320,12 +352,13 @@ test('Prioritize configuration from config file over "extends"', async t => {
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
await outputJson(path.resolve(cwd, 'shareable.json'), options1);
const {options} = await t.context.getConfig({cwd});
const {options: result} = await t.context.getConfig({cwd});
const expected = omit({...options1, ...pkgOptions, branches: ['test_pkg']}, 'extends');
// Verify the options contains the plugin config from package.json and shareable.json
t.deepEqual(options, omit({...options1, ...pkgOptions}, 'extends'));
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json and shareable.json
t.deepEqual(t.context.plugins.args[0][0], {cwd, options: omit({...options, ...pkgOptions}, 'extends')});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
t.deepEqual(t.context.plugins.args[0][1], {
analyzeCommits: './shareable.json',
generateNotesShareable: './shareable.json',
@@ -338,13 +371,13 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
const {cwd} = await gitRepo();
const cliOptions = {
extends: './shareable2.json',
branch: 'branch_opts',
branches: ['branch_opts'],
publish: [{path: 'publishOpts', param: 'publishOpts_param'}],
repositoryUrl: 'https://host.null/owner/module.git',
};
const pkgOptions = {
extends: './shareable1.json',
branch: 'branch_pkg',
branches: ['branch_pkg'],
generateNotes: 'generateNotes',
publish: [{path: 'publishPkg', param: 'publishPkg_param'}],
};
@@ -352,13 +385,13 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
analyzeCommits: 'analyzeCommits1',
generateNotes: 'generateNotesShareable1',
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
branch: 'test_branch1',
branches: ['test_branch1'],
repositoryUrl: 'https://host.null/owner/module.git',
};
const options2 = {
analyzeCommits: 'analyzeCommits2',
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
branch: 'test_branch2',
branches: ['test_branch2'],
tagFormat: `v\${version}`,
plugins: false,
};
@@ -367,15 +400,13 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
await outputJson(path.resolve(cwd, 'shareable1.json'), options1);
await outputJson(path.resolve(cwd, 'shareable2.json'), options2);
const {options} = await t.context.getConfig({cwd}, cliOptions);
const {options: result} = await t.context.getConfig({cwd}, cliOptions);
const expected = omit({...options2, ...pkgOptions, ...cliOptions, branches: ['branch_opts']}, 'extends');
// Verify the options contains the plugin config from package.json and shareable2.json
t.deepEqual(options, omit({...options2, ...pkgOptions, ...cliOptions}, 'extends'));
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from package.json and shareable2.json
t.deepEqual(t.context.plugins.args[0][0], {
cwd,
options: omit({...options2, ...pkgOptions, ...cliOptions}, 'extends'),
});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
});
test('Allow to unset properties defined in shareable config with "null"', async t => {
@@ -384,7 +415,7 @@ test('Allow to unset properties defined in shareable config with "null"', async
const pkgOptions = {
extends: './shareable.json',
analyzeCommits: null,
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
plugins: null,
};
@@ -415,6 +446,7 @@ test('Allow to unset properties defined in shareable config with "null"', async
},
cwd,
});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
analyzeCommits: './shareable.json',
@@ -428,7 +460,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a
const pkgOptions = {
extends: './shareable.json',
analyzeCommits: undefined,
branch: 'test_branch',
branches: ['test_branch'],
repositoryUrl: 'https://host.null/owner/module.git',
};
const options1 = {
@@ -441,18 +473,17 @@ test('Allow to unset properties defined in shareable config with "undefined"', a
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`);
await outputJson(path.resolve(cwd, 'shareable.json'), options1);
const {options} = await t.context.getConfig({cwd});
const {options: result} = await t.context.getConfig({cwd});
const expected = {
...omit(options1, 'analyzeCommits'),
...omit(pkgOptions, ['extends', 'analyzeCommits']),
branches: ['test_branch'],
};
// Verify the options contains the plugin config from shareable.json
t.deepEqual(options, {...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits'])});
t.deepEqual(result, expected);
// Verify the plugins module is called with the plugin options from shareable.json
t.deepEqual(t.context.plugins.args[0][0], {
options: {
...omit(options1, 'analyzeCommits'),
...omit(pkgOptions, ['extends', 'analyzeCommits']),
},
cwd,
});
t.deepEqual(t.context.plugins.args[0][0], {options: expected, cwd});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
analyzeCommits: './shareable.json',
+58 -22
View File
@@ -8,7 +8,7 @@ test('Return the same "git" formatted URL if "gitCredentials" is not defined', a
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'}}),
await getAuthUrl({cwd, env, branch: {name: 'master'}, options: {repositoryUrl: 'git@host.null:owner/repo.git'}}),
'git@host.null:owner/repo.git'
);
});
@@ -17,7 +17,12 @@ test('Return the same "https" formatted URL if "gitCredentials" is not defined',
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'https://host.null/owner/repo.git'}}),
await getAuthUrl({
cwd,
env,
branch: {name: 'master'},
options: {repositoryUrl: 'https://host.null/owner/repo.git'},
}),
'https://host.null/owner/repo.git'
);
});
@@ -26,7 +31,12 @@ test('Return the "https" formatted URL if "gitCredentials" is not defined and re
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'git+https://host.null/owner/repo.git'}}),
await getAuthUrl({
cwd,
env,
branch: {name: 'master'},
options: {repositoryUrl: 'git+https://host.null/owner/repo.git'},
}),
'https://host.null/owner/repo.git'
);
});
@@ -35,7 +45,7 @@ test('Do not add trailing ".git" if not present in the origian URL', async t =>
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo'}}),
await getAuthUrl({cwd, env, vranch: {name: 'master'}, options: {repositoryUrl: 'git@host.null:owner/repo'}}),
'git@host.null:owner/repo'
);
});
@@ -47,7 +57,8 @@ test('Handle "https" URL with group and subgroup', async t => {
await getAuthUrl({
cwd,
env,
options: {branch: 'master', repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'},
}),
'https://host.null/group/subgroup/owner/repo.git'
);
@@ -60,7 +71,8 @@ test('Handle "git" URL with group and subgroup', async t => {
await getAuthUrl({
cwd,
env,
options: {branch: 'master', repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'},
}),
'git@host.null:group/subgroup/owner/repo.git'
);
@@ -70,7 +82,12 @@ test('Convert shorthand URL', async t => {
const {cwd} = await gitRepo();
t.is(
await getAuthUrl({cwd, env, options: {repositoryUrl: 'semanitc-release/semanitc-release'}}),
await getAuthUrl({
cwd,
env,
branch: {name: 'master'},
options: {repositoryUrl: 'semanitc-release/semanitc-release'},
}),
'https://github.com/semanitc-release/semanitc-release.git'
);
});
@@ -82,7 +99,8 @@ test('Convert GitLab shorthand URL', async t => {
await getAuthUrl({
cwd,
env,
options: {branch: 'master', repositoryUrl: 'gitlab:semanitc-release/semanitc-release'},
branch: {name: 'master'},
options: {repositoryUrl: 'gitlab:semanitc-release/semanitc-release'},
}),
'https://gitlab.com/semanitc-release/semanitc-release.git'
);
@@ -95,7 +113,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
@@ -121,7 +140,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'https://host.null/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'https://host.null/owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
@@ -134,7 +154,8 @@ test('Return the "http" formatted URL if "gitCredentials" is defined and reposit
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'http://host.null/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'http://host.null/owner/repo.git'},
}),
'http://user:pass@host.null/owner/repo.git'
);
@@ -147,7 +168,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git+https://host.null/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git+https://host.null/owner/repo.git'},
}),
'https://user:pass@host.null/owner/repo.git'
);
@@ -160,7 +182,8 @@ test('Return the "http" formatted URL if "gitCredentials" is defined and reposit
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git+http://host.null/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git+http://host.null/owner/repo.git'},
}),
'http://user:pass@host.null/owner/repo.git'
);
@@ -173,7 +196,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GH_T
await getAuthUrl({
cwd,
env: {...env, GH_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://token@host.null/owner/repo.git'
);
@@ -186,7 +210,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GITH
await getAuthUrl({
cwd,
env: {...env, GITHUB_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://token@host.null/owner/repo.git'
);
@@ -199,7 +224,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GL_T
await getAuthUrl({
cwd,
env: {...env, GL_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
@@ -212,7 +238,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "GITL
await getAuthUrl({
cwd,
env: {...env, GITLAB_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
@@ -225,7 +252,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "BB_T
await getAuthUrl({
cwd,
env: {...env, BB_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
@@ -238,7 +266,8 @@ test('Return the "https" formatted URL if "gitCredentials" is defined with "BITB
await getAuthUrl({
cwd,
env: {...env, BITBUCKET_TOKEN: 'token'},
options: {branch: 'master', repositoryUrl: 'git@host.null:owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:owner/repo.git'},
}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
@@ -251,7 +280,8 @@ test('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'},
}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
@@ -264,7 +294,8 @@ test('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
options: {branch: 'master', repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'},
branch: {name: 'master'},
options: {repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'},
}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
@@ -274,7 +305,12 @@ test('Do not add git credential to repositoryUrl if push is allowed', async t =>
const {cwd, repositoryUrl} = await gitRepo(true);
t.is(
await getAuthUrl({cwd, env: {...env, GIT_CREDENTIALS: 'user:pass'}, options: {branch: 'master', repositoryUrl}}),
await getAuthUrl({
cwd,
env: {...env, GIT_CREDENTIALS: 'user:pass'},
branch: {name: 'master'},
options: {repositoryUrl},
}),
repositoryUrl
);
});
+49 -132
View File
@@ -1,7 +1,6 @@
import test from 'ava';
import {stub} from 'sinon';
import getLastRelease from '../lib/get-last-release';
import {gitRepo, gitCommits, gitTagVersion, gitCheckout} from './helpers/git-utils';
test.beforeEach(t => {
// Stub the logger functions
@@ -9,143 +8,61 @@ test.beforeEach(t => {
t.context.logger = {log: t.context.log};
});
test('Get the highest non-prerelease valid tag', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Create some commits and tags
await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
const commits = await gitCommits(['Second'], {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitCommits(['Third'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitCommits(['Fourth'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});
await gitCommits(['Fifth'], {cwd});
await gitTagVersion('v3.0.0-beta.1', undefined, {cwd});
test('Get the highest non-prerelease valid tag', t => {
const result = getLastRelease({
branch: {
name: 'master',
tags: [
{version: '2.0.0', gitTag: 'v2.0.0', gitHead: '222'},
{version: '1.0.0', gitTag: 'v1.0.0', gitHead: '111'},
{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1@beta', gitHead: '333'},
],
type: 'release',
},
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
t.deepEqual(t.context.log.args[0], ['Found git tag v2.0.0 associated with version 2.0.0']);
t.deepEqual(result, {version: '2.0.0', gitTag: 'v2.0.0', name: 'v2.0.0', gitHead: '222', channel: undefined});
t.deepEqual(t.context.log.args[0][0], 'Found git tag v2.0.0 associated with version 2.0.0 on branch master');
});
test('Get the highest tag in the history of the current branch', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commit to the master branch
await gitCommits(['First'], {cwd});
// Create the tag corresponding to version 1.0.0
// Create the new branch 'other-branch' from master
await gitCheckout('other-branch', true, {cwd});
// Add commit to the 'other-branch' branch
await gitCommits(['Second'], {cwd});
// Create the tag corresponding to version 3.0.0
await gitTagVersion('v3.0.0', undefined, {cwd});
// Checkout master
await gitCheckout('master', false, {cwd});
// Add another commit to the master branch
const commits = await gitCommits(['Third'], {cwd});
// Create the tag corresponding to version 2.0.0
await gitTagVersion('v2.0.0', undefined, {cwd});
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
});
test('Match the tag name from the begining of the string', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
const commits = await gitCommits(['First'], {cwd});
await gitTagVersion('prefix/v1.0.0', undefined, {cwd});
await gitTagVersion('prefix/v2.0.0', undefined, {cwd});
await gitTagVersion('other-prefix/v3.0.0', undefined, {cwd});
const result = await getLastRelease({cwd, options: {tagFormat: `prefix/v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'prefix/v2.0.0', version: '2.0.0'});
});
test('Return empty object if no valid tag is found', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Create some commits and tags
await gitCommits(['First'], {cwd});
await gitTagVersion('foo', undefined, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v2.0.x', undefined, {cwd});
await gitCommits(['Third'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
test('Return empty object if no valid tag is found', t => {
const result = getLastRelease({
branch: {
name: 'master',
tags: [{version: '3.0.0-beta.1', gitTag: 'v3.0.0-beta.1@beta', gitHead: '111'}],
type: 'release',
},
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
t.deepEqual(t.context.log.args[0][0], 'No git tag version found on branch master');
});
test('Return empty object if no valid tag is found in history', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('other-branch', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v2.0.0', undefined, {cwd});
await gitTagVersion('v3.0.0', undefined, {cwd});
await gitCheckout('master', false, {cwd});
test('Get the highest non-prerelease valid tag before a certain version', t => {
const result = getLastRelease(
{
branch: {
name: 'master',
channel: undefined,
tags: [
{version: '2.0.0', gitTag: 'v2.0.0', gitHead: '333'},
{version: '1.0.0', gitTag: 'v1.0.0', gitHead: '111'},
{version: '2.0.0-beta.1', gitTag: 'v2.0.0-beta.1@beta', gitHead: '222'},
{version: '2.1.0', gitTag: 'v2.1.0', gitHead: '444'},
{version: '2.1.1', gitTag: 'v2.1.1', gitHead: '555'},
],
type: 'release',
},
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
},
{before: '2.1.0'}
);
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test('Get the highest valid tag corresponding to the "tagFormat"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Create some commits and tags
const [{hash: gitHead}] = await gitCommits(['First'], {cwd});
await gitTagVersion('1.0.0', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `\${version}`}, logger: t.context.logger}), {
gitHead,
gitTag: '1.0.0',
version: '1.0.0',
});
await gitTagVersion('foo-1.0.0-bar', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `foo-\${version}-bar`}, logger: t.context.logger}), {
gitHead,
gitTag: 'foo-1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('foo-v1.0.0-bar', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `foo-v\${version}-bar`}, logger: t.context.logger}), {
gitHead,
gitTag: 'foo-v1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('(.+)/1.0.0/(a-z)', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `(.+)/\${version}/(a-z)`}, logger: t.context.logger}), {
gitHead,
gitTag: '(.+)/1.0.0/(a-z)',
version: '1.0.0',
});
await gitTagVersion('2.0.0-1.0.0-bar.1', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `2.0.0-\${version}-bar.1`}, logger: t.context.logger}), {
gitHead,
gitTag: '2.0.0-1.0.0-bar.1',
version: '1.0.0',
});
await gitTagVersion('3.0.0-bar.1', undefined, {cwd});
t.deepEqual(await getLastRelease({cwd, options: {tagFormat: `\${version}-bar.1`}, logger: t.context.logger}), {
gitHead,
gitTag: '3.0.0-bar.1',
version: '3.0.0',
});
t.deepEqual(result, {version: '2.0.0', gitTag: 'v2.0.0', name: 'v2.0.0', gitHead: '333', channel: undefined});
t.deepEqual(t.context.log.args[0][0], 'Found git tag v2.0.0 associated with version 2.0.0 on branch master');
});
+114 -20
View File
@@ -9,33 +9,127 @@ test.beforeEach(t => {
});
test('Increase version for patch release', t => {
const version = getNextVersion({
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
});
t.is(version, '1.0.1');
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
}),
'1.0.1'
);
});
test('Increase version for minor release', t => {
const version = getNextVersion({
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
});
t.is(version, '1.1.0');
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
}),
'1.1.0'
);
});
test('Increase version for major release', t => {
const version = getNextVersion({
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
});
t.is(version, '2.0.0');
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
}),
'2.0.0'
);
});
test('Return 1.0.0 if there is no previous release', t => {
const version = getNextVersion({nextRelease: {type: 'minor'}, lastRelease: {}, logger: t.context.logger});
t.is(version, '1.0.0');
t.is(
getNextVersion({
branch: {name: 'master', type: 'release'},
nextRelease: {type: 'minor'},
lastRelease: {},
logger: t.context.logger,
}),
'1.0.0'
);
});
test('Increase version for patch release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
}),
'1.0.1-beta.1'
);
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'patch'},
lastRelease: {version: '1.0.0-beta.1'},
logger: t.context.logger,
}),
'1.0.0-beta.2'
);
});
test('Increase version for minor release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
}),
'1.1.0-beta.1'
);
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'minor'},
lastRelease: {version: '1.0.0-beta.1'},
logger: t.context.logger,
}),
'1.0.0-beta.2'
);
});
test('Increase version for major release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0'},
logger: t.context.logger,
}),
'2.0.0-beta.1'
);
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'major'},
lastRelease: {version: '1.0.0-beta.1'},
logger: t.context.logger,
}),
'1.0.0-beta.2'
);
});
test('Return 1.0.0 if there is no previous release on prerelease branch', t => {
t.is(
getNextVersion({
branch: {name: 'beta', type: 'prerelease', prerelease: 'beta'},
nextRelease: {type: 'minor'},
lastRelease: {},
logger: t.context.logger,
}),
'1.0.0-beta.1'
);
});
+258
View File
@@ -0,0 +1,258 @@
import test from 'ava';
import {stub} from 'sinon';
import getReleasesToAdd from '../lib/get-releases-to-add';
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test('Return versions merged from release to maintenance branch', t => {
const result = getReleasesToAdd({
branch: {
name: '1.x',
channel: '1.x',
tags: [
{gitTag: 'v1.0.0@1.x', version: '1.0.0', channel: '1.x', gitHead: '111'},
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.1.0', version: '1.1.0', gitHead: '222'},
{gitTag: 'v1.1.1', version: '1.1.1', gitHead: '333'},
],
},
branches: [{name: '1.x', channel: '1.x'}, {name: 'master'}],
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: '1.x', gitTag: 'v1.0.0@1.x', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: '1.x',
gitTag: 'v1.1.0@1.x',
name: 'v1.1.0',
gitHead: '222',
},
},
{
lastRelease: {version: '1.1.0', channel: undefined, gitTag: 'v1.1.0', name: 'v1.1.0', gitHead: '222'},
currentRelease: {
type: 'patch',
version: '1.1.1',
channel: undefined,
gitTag: 'v1.1.1',
name: 'v1.1.1',
gitHead: '333',
},
nextRelease: {
type: 'patch',
version: '1.1.1',
channel: '1.x',
gitTag: 'v1.1.1@1.x',
name: 'v1.1.1',
gitHead: '333',
},
},
]);
});
test('Return versions merged from future branch to release branch', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'},
{gitTag: 'v1.1.0@next', version: '1.1.0', channel: 'next', gitHead: '222'},
{gitTag: 'v2.0.0@next-major', version: '2.0.0', channel: 'next-major', gitHead: '333'},
],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}],
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: undefined, gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: 'next',
gitTag: 'v1.1.0@next',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
},
{
lastRelease: {version: '1.1.0', gitTag: 'v1.1.0@next', name: 'v1.1.0', gitHead: '222', channel: 'next'},
currentRelease: {
type: 'major',
version: '2.0.0',
channel: 'next-major',
gitTag: 'v2.0.0@next-major',
name: 'v2.0.0',
gitHead: '333',
},
nextRelease: {
type: 'major',
version: '2.0.0',
channel: undefined,
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: '333',
},
},
]);
});
test('Return releases sorted by ascending order', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v2.0.0@next-major', version: '2.0.0', channel: 'next-major', gitHead: '333'},
{gitTag: 'v1.1.0@next', version: '1.1.0', channel: 'next', gitHead: '222'},
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'},
],
},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}, {name: 'next-major', channel: 'next-major'}],
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: undefined, gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: 'next',
gitTag: 'v1.1.0@next',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
},
{
lastRelease: {version: '1.1.0', gitTag: 'v1.1.0@next', name: 'v1.1.0', gitHead: '222', channel: 'next'},
currentRelease: {
type: 'major',
version: '2.0.0',
channel: 'next-major',
gitTag: 'v2.0.0@next-major',
name: 'v2.0.0',
gitHead: '333',
},
nextRelease: {
type: 'major',
version: '2.0.0',
channel: undefined,
gitTag: 'v2.0.0',
name: 'v2.0.0',
gitHead: '333',
},
},
]);
});
test('no lastRelease', t => {
const result = getReleasesToAdd({
branch: {name: 'master', tags: [{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'}]},
branches: [{name: 'master'}, {name: 'next', channel: 'next'}],
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
t.deepEqual(result, [
{
lastRelease: {},
currentRelease: {
type: 'major',
version: '1.0.0',
channel: 'next',
gitTag: 'v1.0.0@next',
name: 'v1.0.0',
gitHead: '111',
},
nextRelease: {
type: 'major',
version: '1.0.0',
channel: undefined,
gitTag: 'v1.0.0',
name: 'v1.0.0',
gitHead: '111',
},
},
]);
});
test('Ignore pre-release versions', t => {
const result = getReleasesToAdd({
branch: {
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', gitHead: '111'},
{gitTag: 'v1.0.0@next', version: '1.0.0', channel: 'next', gitHead: '111'},
{gitTag: 'v1.1.0@next', version: '1.1.0', channel: 'next', gitHead: '222'},
{gitTag: 'v2.0.0-alpha.1@alpha', version: '2.0.0', channel: 'alpha', gitHead: '333'},
],
},
branches: [
{name: 'master'},
{name: 'next', channel: 'next'},
{name: 'alpha', type: 'prerelease', channel: 'alpha'},
],
options: {tagFormat: `v\${version}`},
logger: t.context.logger,
});
t.deepEqual(result, [
{
lastRelease: {version: '1.0.0', channel: undefined, gitTag: 'v1.0.0', name: 'v1.0.0', gitHead: '111'},
currentRelease: {
type: 'minor',
version: '1.1.0',
channel: 'next',
gitTag: 'v1.1.0@next',
name: 'v1.1.0',
gitHead: '222',
},
nextRelease: {
type: 'minor',
version: '1.1.0',
channel: undefined,
gitTag: 'v1.1.0',
name: 'v1.1.0',
gitHead: '222',
},
},
]);
});
+40 -11
View File
@@ -3,12 +3,14 @@ import tempy from 'tempy';
import {
getTagHead,
isRefInHistory,
isRefExists,
fetch,
getGitHead,
repoUrl,
tag,
push,
getTags,
getBranches,
isGitRepo,
verifyTagName,
isBranchUpToDate,
@@ -56,7 +58,7 @@ test('Unshallow and fetch repository', async t => {
// Verify the shallow clone contains only one commit
t.is((await gitGetCommits(undefined, {cwd})).length, 1);
await fetch(repositoryUrl, {cwd});
await fetch({cwd});
// Verify the shallow clone contains all the commits
t.is((await gitGetCommits(undefined, {cwd})).length, 2);
@@ -64,10 +66,10 @@ test('Unshallow and fetch repository', async t => {
test('Do not throw error when unshallow a complete repository', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo();
const {cwd} = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First'], {cwd});
await t.notThrows(fetch(repositoryUrl, {cwd}));
await t.notThrows(fetch({cwd}));
});
test('Fetch all tags on a detached head repository', async t => {
@@ -82,7 +84,7 @@ test('Fetch all tags on a detached head repository', async t => {
await gitPush(repositoryUrl, 'master', {cwd});
cwd = await gitDetachedHead(repositoryUrl, commit.hash);
await fetch(repositoryUrl, {cwd});
await fetch({cwd});
t.deepEqual((await getTags({cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
});
@@ -98,9 +100,36 @@ test('Verify if the commit `sha` is in the direct history of the current branch'
const otherCommits = await gitCommits(['Second'], {cwd});
await gitCheckout('master', false, {cwd});
t.true(await isRefInHistory(commits[0].hash, {cwd}));
t.falsy(await isRefInHistory(otherCommits[0].hash, {cwd}));
await t.throws(isRefInHistory('non-existant-sha', {cwd}));
t.true(await isRefInHistory(commits[0].hash, 'master', false, {cwd}));
t.falsy(await isRefInHistory(otherCommits[0].hash, 'master', false, {cwd}));
t.falsy(await isRefInHistory(otherCommits[0].hash, 'missing-branch', false, {cwd}));
await t.throws(isRefInHistory('non-existant-sha', 'master', false, {cwd}));
});
test('Verify if a branch exists', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd} = await gitRepo();
// Add commits to the master branch
await gitCommits(['First'], {cwd});
// Create the new branch 'other-branch' from master
await gitCheckout('other-branch', true, {cwd});
// Add commits to the 'other-branch' branch
await gitCommits(['Second'], {cwd});
t.true(await isRefExists('master', {cwd}));
t.true(await isRefExists('other-branch', {cwd}));
t.falsy(await isRefExists('next', {cwd}));
});
test('Get all branches', async t => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('second-branch', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitCheckout('third-branch', true, {cwd});
await gitCommits(['Third'], {cwd});
t.deepEqual((await getBranches({cwd})).sort(), ['master', 'second-branch', 'third-branch'].sort());
});
test('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
@@ -146,7 +175,7 @@ test('Add tag on head commit', async t => {
const {cwd} = await gitRepo();
const commits = await gitCommits(['Test commit'], {cwd});
await tag('tag_name', {cwd});
await tag('tag_name', 'HEAD', {cwd});
await t.is(await gitCommitTag(commits[0].hash, {cwd}), 'tag_name');
});
@@ -156,13 +185,13 @@ test('Push tag to remote repository', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['Test commit'], {cwd});
await tag('tag_name', {cwd});
await tag('tag_name', 'HEAD', {cwd});
await push(repositoryUrl, 'master', {cwd});
t.is(await gitRemoteTagHead(repositoryUrl, 'tag_name', {cwd}), commits[0].hash);
});
test('Push tag to remote repository with remote branch ahaed', async t => {
test('Push tag to remote repository with remote branch ahead', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const commits = await gitCommits(['First'], {cwd});
await gitPush(repositoryUrl, 'master', {cwd});
@@ -170,7 +199,7 @@ test('Push tag to remote repository with remote branch ahaed', async t => {
await gitCommits(['Second'], {cwd: tmpRepo});
await gitPush('origin', 'master', {cwd: tmpRepo});
await tag('tag_name', {cwd});
await tag('tag_name', 'HEAD', {cwd});
await push(repositoryUrl, 'master', {cwd});
t.is(await gitRemoteTagHead(repositoryUrl, 'tag_name', {cwd}), commits[0].hash);
+45 -2
View File
@@ -1,7 +1,7 @@
import tempy from 'tempy';
import execa from 'execa';
import fileUrl from 'file-url';
import pReduce from 'p-reduce';
import pEachSeries from 'p-each-series';
import gitLogParser from 'git-log-parser';
import getStream from 'get-stream';
@@ -69,7 +69,7 @@ export async function initBareRepo(repositoryUrl, branch = 'master') {
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
*/
export async function gitCommits(messages, execaOpts) {
await pReduce(messages, (_, message) =>
await pEachSeries(messages, message =>
execa.stdout('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)
);
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
@@ -225,3 +225,46 @@ export function gitCommitTag(gitHead, execaOpts) {
export async function gitPush(repositoryUrl = 'origin', branch = 'master', execaOpts) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`], execaOpts);
}
/**
* Merge a branch into the current one with `git merge`.
*
* @param {String} ref The ref to merge.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function merge(ref, execaOpts) {
await execa('git', ['merge', '--no-ff', ref], execaOpts);
}
/**
* Merge a branch into the current one with `git merge --ff`.
*
* @param {String} ref The ref to merge.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function mergeFf(ref, execaOpts) {
await execa('git', ['merge', '--ff', ref], execaOpts);
}
/**
* Merge a branch into the current one with `git rebase`.
*
* @param {String} ref The ref to merge.
* @param {Object} [execaOpts] Options to pass to `execa`.
*/
export async function rebase(ref, execaOpts) {
await execa('git', ['rebase', ref], execaOpts);
}
export async function changeAuthor(sha, execaOpts) {
await execa(
'git',
[
'filter-branch',
'-f',
'--env-filter',
`if [[ "$GIT_COMMIT" = "${sha}" ]]; then export GIT_COMMITTER_NAME="New Author" GIT_COMMITTER_EMAIL="author@test.com"; fi`,
],
execaOpts
);
}
+5
View File
@@ -0,0 +1,5 @@
import execa from 'execa';
export async function npmView(packageName, env) {
return JSON.parse(await execa.stdout('npm', ['view', packageName, '--json'], {env}));
}
+747 -93
View File
File diff suppressed because it is too large Load Diff
+95 -42
View File
@@ -5,8 +5,19 @@ import {escapeRegExp} from 'lodash';
import {writeJson, readJson} from 'fs-extra';
import execa from 'execa';
import {WritableStreamBuffer} from 'stream-buffers';
import delay from 'delay';
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
import {gitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
import {
gitHead,
gitTagHead,
gitRepo,
gitCommits,
gitRemoteTagHead,
gitPush,
gitCheckout,
merge,
} from './helpers/git-utils';
import {npmView} from './helpers/npm-utils';
import gitbox from './helpers/gitbox';
import mockServer from './helpers/mockserver';
import npmRegistry from './helpers/npm-registry';
@@ -58,7 +69,7 @@ test('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},
release: {branches: ['master', 'next'], success: false, fail: false},
});
// Create a npm-shrinkwrap.json file
await execa('npm', ['shrinkwrap'], {env: testEnv, cwd});
@@ -86,7 +97,7 @@ test('Release patch, minor and major versions', async t => {
let createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
body: {tag_name: `v${version}`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
@@ -105,15 +116,14 @@ test('Release patch, minor and major versions', async t => {
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
let {
'dist-tags': {latest: releasedVersion},
} = await npmView(packageName, testEnv);
let head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
@@ -128,7 +138,7 @@ test('Release patch, minor and major versions', async t => {
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
body: {tag_name: `v${version}`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
@@ -147,15 +157,14 @@ test('Release patch, minor and major versions', async t => {
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
({
'dist-tags': {latest: releasedVersion},
} = await npmView(packageName, testEnv));
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
@@ -170,7 +179,7 @@ test('Release patch, minor and major versions', async t => {
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
body: {tag_name: `v${version}`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
@@ -189,20 +198,19 @@ test('Release patch, minor and major versions', async t => {
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
({
'dist-tags': {latest: releasedVersion},
} = await npmView(packageName, testEnv));
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
/* Major release */
/* Major release on next */
version = '2.0.0';
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -212,16 +220,18 @@ test('Release patch, minor and major versions', async t => {
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
body: {tag_name: `v${version}@next`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
);
t.log('Commit a breaking change');
t.log('Commit a breaking change on next');
await gitCheckout('next', true, {cwd});
await gitPush('origin', 'next', {cwd});
await gitCommits(['feat: foo\n\n BREAKING CHANGE: bar'], {cwd});
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env, cwd}));
({stdout, code} = await execa(cli, [], {env: {...env, TRAVIS_BRANCH: 'next'}, cwd}));
t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
@@ -231,18 +241,67 @@ test('Release patch, minor and major versions', async t => {
t.is((await readJson(path.resolve(cwd, 'npm-shrinkwrap.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
({
'dist-tags': {next: releasedVersion},
} = await npmView(packageName, testEnv));
head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, head);
t.is(await gitTagHead(`v${version}`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}`, {cwd}), head);
t.log(`+ released ${releasedVersion} with head ${releasedGitHead}`);
t.is(await gitTagHead(`v${version}@next`, {cwd}), head);
t.is(await gitRemoteTagHead(authUrl, `v${version}@next`, {cwd}), head);
t.log(`+ released ${releasedVersion} on @next`);
await mockServer.verify(verifyMock);
await mockServer.verify(createReleaseMock);
/* Merge next into master */
version = '2.0.0';
const releaseId = 1;
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
const getReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases/tags/v2.0.0@next`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {id: releaseId}, method: 'GET'}
);
const updateReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases/${releaseId}`,
{
body: {tag_name: `v${version}`, name: `v${version}`, prerelease: false},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}, method: 'PATCH'}
);
t.log('Merge next into master');
await gitCheckout('master', false, {cwd});
await merge('next', {cwd});
await gitPush('origin', 'master', {cwd});
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env, cwd}));
t.regex(stdout, new RegExp(`Updated GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Adding version ${version} to npm registry on dist-tag latest`));
t.is(code, 0);
// Wait for 3s as the change of dist-tag takes time to be reflected in the registry
await delay(3000);
// Retrieve the published package from the registry and check version and gitHead
({
'dist-tags': {latest: releasedVersion},
} = await npmView(packageName, testEnv));
t.is(releasedVersion, version);
t.is(await gitTagHead(`v${version}`, {cwd}), await gitTagHead(`v${version}@next`, {cwd}));
t.is(
await gitRemoteTagHead(authUrl, `v${version}`, {cwd}),
await gitRemoteTagHead(authUrl, `v${version}@next`, {cwd})
);
t.log(`+ added ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getReleaseMock);
await mockServer.verify(updateReleaseMock);
});
test('Exit with 1 if a plugin is not found', async t => {
@@ -366,7 +425,7 @@ test('Allow local releases with "noCi" option', async t => {
const createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
body: {tag_name: `v${version}`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
@@ -384,9 +443,7 @@ test('Allow local releases with "noCi" option', async t => {
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
const {version: releasedVersion, gitHead: releasedGitHead} = await npmView(packageName, testEnv);
const head = await gitHead({cwd});
t.is(releasedVersion, version);
@@ -439,9 +496,7 @@ test('Pass options via CLI arguments', async t => {
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
const {version: releasedVersion, gitHead: releasedGitHead} = await npmView(packageName, testEnv);
const head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, head);
@@ -482,7 +537,7 @@ test('Run via JS API', async t => {
const createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
body: {tag_name: `v${version}`, name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
},
{body: {html_url: `release-url/${version}`}}
@@ -497,9 +552,7 @@ test('Run via JS API', async t => {
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv, cwd})).stdout
);
const {version: releasedVersion, gitHead: releasedGitHead} = await npmView(packageName, testEnv);
const head = await gitHead({cwd});
t.is(releasedVersion, version);
t.is(releasedGitHead, head);
+18
View File
@@ -152,6 +152,24 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin
t.regex(error.details, /2/);
});
test('Wrap "addChannel" plugin in a function that validate the output of the plugin', async t => {
const addChannel = stub().resolves(2);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'addChannel',
addChannel,
{}
);
const error = await t.throws(plugin({options: {}}));
t.is(error.code, 'EADDCHANNELOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
test('Plugin is called with "pluginConfig" (with object definition) and input', async t => {
const pluginFunction = stub().resolves();
const pluginConf = {path: pluginFunction, conf: 'confValue'};
+153
View File
@@ -0,0 +1,153 @@
import test from 'ava';
import AggregateError from 'aggregate-error';
import {
extractErrors,
tagsToVersions,
isMajorRange,
isMaintenanceRange,
getUpperBound,
getLowerBound,
highest,
lowest,
getLatestVersion,
getEarliestVersion,
getFirstVersion,
getRange,
makeTag,
} from '../lib/utils';
test('extractErrors', t => {
const errors = [new Error('Error 1'), new Error('Error 2')];
t.deepEqual(extractErrors(new AggregateError(errors)), errors);
t.deepEqual(extractErrors(errors[0]), [errors[0]]);
});
test('tagsToVersions', t => {
t.deepEqual(tagsToVersions([{version: '1.0.0'}, {version: '1.1.0'}, {version: '1.2.0'}]), [
'1.0.0',
'1.1.0',
'1.2.0',
]);
});
test('isMajorRange', t => {
t.false(isMajorRange('1.1.x'));
t.false(isMajorRange('1.1.X'));
t.false(isMajorRange('1.1.0'));
t.true(isMajorRange('1.x.x'));
t.true(isMajorRange('1.X.X'));
t.true(isMajorRange('1.x'));
t.true(isMajorRange('1.X'));
});
test('isMaintenanceRange', t => {
t.true(isMaintenanceRange('1.1.x'));
t.true(isMaintenanceRange('1.x.x'));
t.true(isMaintenanceRange('1.x'));
t.true(isMaintenanceRange('1.1.X'));
t.true(isMaintenanceRange('1.X.X'));
t.true(isMaintenanceRange('1.X'));
t.false(isMaintenanceRange('1.1.0'));
t.false(isMaintenanceRange('~1.0.0'));
t.false(isMaintenanceRange('^1.0.0'));
});
test('getUpperBound', t => {
t.is(getUpperBound('1.x.x'), '2.0.0');
t.is(getUpperBound('1.x'), '2.0.0');
t.is(getUpperBound('1.0.x'), '1.1.0');
t.is(getUpperBound('1.0.0'), '1.0.0');
t.is(getUpperBound('foo'), undefined);
});
test('getLowerBound', t => {
t.is(getLowerBound('1.x.x'), '1.0.0');
t.is(getLowerBound('1.x'), '1.0.0');
t.is(getLowerBound('1.0.x'), '1.0.0');
t.is(getLowerBound('1.0.0'), '1.0.0');
t.is(getLowerBound('foo'), undefined);
});
test('highest', t => {
t.is(highest('1.0.0', '2.0.0'), '2.0.0');
t.is(highest('1.1.1', '1.1.0'), '1.1.1');
t.is(highest(null, '1.0.0'), '1.0.0');
t.is(highest('1.0.0'), '1.0.0');
t.is(highest(), undefined);
});
test('lowest', t => {
t.is(lowest('1.0.0', '2.0.0'), '1.0.0');
t.is(lowest('1.1.1', '1.1.0'), '1.1.0');
t.is(lowest(null, '1.0.0'), '1.0.0');
t.is(lowest(), undefined);
});
test.serial('getLatestVersion', t => {
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.1', '1.0.0-alpha.1']), '1.2.0');
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2']), undefined);
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.1', '1.0.0-alpha.1']), '1.2.0');
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2']), undefined);
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.1', '1.0.0-alpha.1'], {withPrerelease: true}), '1.2.3-alpha.3');
t.is(getLatestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2'], {withPrerelease: true}), '1.2.3-alpha.3');
t.is(getLatestVersion([]), undefined);
});
test.serial('getEarliestVersion', t => {
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.0', '1.0.1-alpha.1']), '1.0.0');
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2']), undefined);
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.0', '1.0.1-alpha.1']), '1.0.0');
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2']), undefined);
t.is(
getEarliestVersion(['1.2.3-alpha.3', '1.2.0', '1.0.1', '1.0.0-alpha.1'], {withPrerelease: true}),
'1.0.0-alpha.1'
);
t.is(getEarliestVersion(['1.2.3-alpha.3', '1.2.3-alpha.2'], {withPrerelease: true}), '1.2.3-alpha.2');
t.is(getEarliestVersion([]), undefined);
});
test('getFirstVersion', t => {
t.is(getFirstVersion(['1.2.0', '1.0.0', '1.3.0', '1.1.0', '1.4.0'], []), '1.0.0');
t.is(
getFirstVersion(
['1.2.0', '1.0.0', '1.3.0', '1.1.0', '1.4.0'],
[
{name: 'master', tags: [{version: '1.0.0'}, {version: '1.1.0'}]},
{name: 'next', tags: [{version: '1.0.0'}, {version: '1.1.0'}, {version: '1.2.0'}]},
]
),
'1.3.0'
);
t.is(
getFirstVersion(
['1.2.0', '1.0.0', '1.1.0'],
[
{name: 'master', tags: [{version: '1.0.0'}, {version: '1.1.0'}]},
{name: 'next', tags: [{version: '1.0.0'}, {version: '1.1.0'}, {version: '1.2.0'}]},
]
),
undefined
);
});
test('getRange', t => {
t.is(getRange('1.0.0', '1.1.0'), '>=1.0.0 <1.1.0');
t.is(getRange('1.0.0'), '>=1.0.0');
});
test('makeTag', t => {
t.is(makeTag(`v\${version}`, '1.0.0'), 'v1.0.0');
t.is(makeTag(`v\${version}`, '1.0.0', 'next'), 'v1.0.0@next');
t.is(makeTag(`v\${version}@test`, '1.0.0', 'next'), 'v1.0.0@next@test');
});
+44 -6
View File
@@ -5,7 +5,7 @@ import {gitRepo} from './helpers/git-utils';
test('Throw a AggregateError', async t => {
const {cwd} = await gitRepo();
const options = {};
const options = {branches: [{name: 'master'}, {name: ''}]};
const errors = [...(await t.throws(verify({cwd, options})))];
@@ -21,11 +21,15 @@ test('Throw a AggregateError', async t => {
t.is(errors[2].code, 'ETAGNOVERSION');
t.truthy(errors[2].message);
t.truthy(errors[2].details);
t.is(errors[3].name, 'SemanticReleaseError');
t.is(errors[3].code, 'EINVALIDBRANCH');
t.truthy(errors[3].message);
t.truthy(errors[3].details);
});
test('Throw a SemanticReleaseError if does not run on a git repository', async t => {
const cwd = tempy.directory();
const options = {};
const options = {branches: []};
const errors = [...(await t.throws(verify({cwd, options})))];
@@ -37,7 +41,7 @@ test('Throw a SemanticReleaseError if does not run on a git repository', async t
test('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `?\${version}`};
const options = {repositoryUrl, tagFormat: `?\${version}`, branches: []};
const errors = [...(await t.throws(verify({cwd, options})))];
@@ -49,7 +53,7 @@ test('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t =>
test('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: 'test'};
const options = {repositoryUrl, tagFormat: 'test', branches: []};
const errors = [...(await t.throws(verify({cwd, options})))];
@@ -61,7 +65,7 @@ test('Throw a SemanticReleaseError if the "tagFormat" does not contains the "ver
test('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`};
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`, branches: []};
const errors = [...(await t.throws(verify({cwd, options})))];
@@ -71,9 +75,43 @@ test('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version
t.truthy(errors[0].details);
});
test('Throw a SemanticReleaseError for each invalid branch', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {
repositoryUrl,
tagFormat: `v\${version}`,
branches: [{name: ''}, {name: ' '}, {name: 1}, {}, {name: ''}, 1, 'master'],
};
const errors = [...(await t.throws(verify({cwd, options})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDBRANCH');
t.truthy(errors[0].message);
t.truthy(errors[0].details);
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EINVALIDBRANCH');
t.truthy(errors[1].message);
t.truthy(errors[1].details);
t.is(errors[2].name, 'SemanticReleaseError');
t.is(errors[2].code, 'EINVALIDBRANCH');
t.truthy(errors[2].message);
t.truthy(errors[2].details);
t.is(errors[3].name, 'SemanticReleaseError');
t.is(errors[3].code, 'EINVALIDBRANCH');
t.truthy(errors[3].message);
t.truthy(errors[3].details);
t.is(errors[4].code, 'EINVALIDBRANCH');
t.truthy(errors[4].message);
t.truthy(errors[4].details);
t.is(errors[5].code, 'EINVALIDBRANCH');
t.truthy(errors[5].message);
t.truthy(errors[5].details);
});
test('Return "true" if all verification pass', async t => {
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`};
const options = {repositoryUrl, tagFormat: `v\${version}`, branches: [{name: 'master'}]};
await t.notThrows(verify({cwd, options}));
});