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);
 | |
| });
 |