for #2543 BREAKING CHANGE: semantic-release is now ESM-only. since it is used through its own executable, the impact on consuming projects should be minimal BREAKING CHANGE: references to plugin files in configs need to include the file extension because of executing in an ESM context
278 lines
12 KiB
JavaScript
278 lines
12 KiB
JavaScript
import test from 'ava';
|
|
import {union} from 'lodash-es';
|
|
import semver from 'semver';
|
|
import * as td from 'testdouble';
|
|
|
|
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
|
|
);
|
|
};
|
|
const remoteBranches = [];
|
|
const repositoryUrl = 'repositoryUrl';
|
|
let expand, getTags, getBranches;
|
|
|
|
test.beforeEach(async (t) => {
|
|
getTags = (await td.replaceEsm('../../lib/branches/get-tags.js')).default;
|
|
expand = (await td.replaceEsm('../../lib/branches/expand.js')).default;
|
|
getBranches = (await import('../../lib/branches/index.js')).default;
|
|
})
|
|
|
|
test.afterEach.always((t) => {
|
|
td.reset();
|
|
});
|
|
|
|
test.serial('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 context = {options: {branches}};
|
|
td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches);
|
|
td.when(getTags(context, remoteBranches)).thenResolve(branches);
|
|
|
|
let result = (await getBranches(repositoryUrl, 'master', context)).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');
|
|
t.is(getBranch(result, 'next').range, '>=1.0.0');
|
|
t.is(getBranch(result, 'next-major').range, '>=1.0.0');
|
|
|
|
release(branches, 'master', '1.0.0');
|
|
result = (await getBranches('repositoryUrl', 'master', context)).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');
|
|
t.is(getBranch(result, 'next').range, '>=1.0.0');
|
|
t.is(getBranch(result, 'next-major').range, '>=1.0.0');
|
|
|
|
release(branches, 'master', '1.0.1');
|
|
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
t.is(getBranch(result, 'master').range, '>=1.0.1', 'Can release only > than 1.0.1 on master');
|
|
t.is(getBranch(result, 'next').range, '>=1.0.1', 'Can release only > than 1.0.1 on next');
|
|
t.is(getBranch(result, 'next-major').range, '>=1.0.1', 'Can release only > than 1.0.1 on next-major');
|
|
|
|
merge(branches, 'master', 'next');
|
|
merge(branches, 'master', 'next-major');
|
|
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
t.is(getBranch(result, 'master').range, '>=1.0.1', 'Can release only > than 1.0.1 on master');
|
|
t.is(getBranch(result, 'next').range, '>=1.0.1', 'Can release only > than 1.0.1 on next');
|
|
t.is(getBranch(result, 'next-major').range, '>=1.0.1', 'Can release only > than 1.0.1 on next-major');
|
|
|
|
release(branches, 'next', '1.1.0');
|
|
release(branches, 'next', '1.1.1');
|
|
result = (await getBranches('repositoryUrl', 'master', {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.1', 'Can release only > than 1.1.1 on next');
|
|
t.is(getBranch(result, 'next-major').range, '>=1.1.1', 'Can release > than 1.1.1 on next-major');
|
|
|
|
release(branches, 'next-major', '2.0.0');
|
|
release(branches, 'next-major', '2.0.1');
|
|
result = (await getBranches('repositoryUrl', 'master', {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.1 <2.0.0', 'Can release only patch or minor, > than 1.1.0 on next');
|
|
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('repositoryUrl', 'master', {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');
|
|
result = (await getBranches('repositoryUrl', 'master', {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('repositoryUrl', 'master', {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('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
|
|
t.is(getBranch(result, 'master').range, '>=1.1.1', 'Can release only > than 1.1.1 on master');
|
|
t.is(getBranch(result, 'next').range, '>=1.1.1 <2.0.0', 'Can release only patch or minor, > than 1.1.1 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('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
t.is(getBranch(result, 'master').range, '>=1.1.1', 'Can release only > 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('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
t.is(getBranch(result, 'master').range, '>=1.1.1', 'Can release only > 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('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
t.is(getBranch(result, 'master').range, '>=2.0.1', 'Can release only > than 2.0.1 on master');
|
|
t.is(getBranch(result, 'next').range, '>=2.0.1', 'Can release only > than 2.0.1 on next');
|
|
t.is(getBranch(result, 'next-major').range, '>=2.0.1', 'Can release only > than 2.0.1 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');
|
|
release(branches, 'master', '3.0.0');
|
|
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
|
name,
|
|
range,
|
|
}));
|
|
t.is(getBranch(result, 'master').range, '>=3.0.0', 'Can release only > than 3.0.0 on master');
|
|
t.is(getBranch(result, 'next').range, '>=3.0.0', 'Can release only > than 3.0.0 on next');
|
|
t.is(getBranch(result, 'next-major').range, '>=3.0.0', 'Can release only > than 3.0.0 on next-major');
|
|
|
|
branches.push({name: '1.1.x', tags: []});
|
|
merge(branches, '1.x', '1.1.x');
|
|
result = (await getBranches('repositoryUrl', 'master', {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.serial('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 context = {options: {branches}};
|
|
td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches);
|
|
td.when(getTags(context, remoteBranches)).thenResolve(branches);
|
|
|
|
const error = await t.throwsAsync(getBranches(repositoryUrl, 'master', context));
|
|
const errors = [...error.errors];
|
|
|
|
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.serial('Throw a SemanticReleaseError if there is duplicate branches', async (t) => {
|
|
const branches = [
|
|
{name: 'master', tags: []},
|
|
{name: 'master', tags: []},
|
|
];
|
|
const context = {options: {branches}};
|
|
td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches);
|
|
td.when(getTags(context, remoteBranches)).thenResolve(branches);
|
|
|
|
const errors = [...(await t.throwsAsync(getBranches(repositoryUrl, 'master', context))).errors];
|
|
|
|
t.is(errors[0].name, 'SemanticReleaseError');
|
|
t.is(errors[0].code, 'EDUPLICATEBRANCHES');
|
|
t.truthy(errors[0].message);
|
|
t.truthy(errors[0].details);
|
|
});
|
|
|
|
test.serial('Throw a SemanticReleaseError for each invalid branch name', async (t) => {
|
|
const branches = [
|
|
{name: '~master', tags: []},
|
|
{name: '^master', tags: []},
|
|
];
|
|
const context = {options: {branches}};
|
|
const remoteBranches = [];
|
|
td.when(expand(repositoryUrl, context, branches)).thenResolve(remoteBranches);
|
|
td.when(getTags(context, remoteBranches)).thenResolve(branches);
|
|
|
|
const errors = [...(await t.throwsAsync(getBranches(repositoryUrl, 'master', context))).errors];
|
|
|
|
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);
|
|
});
|