Skip to content

Commit

Permalink
fix: correctly determine release to add to a channel
Browse files Browse the repository at this point in the history
- Add only the most recent release to a channel (rather than adding all the one not added yet)
- Avoid attempting to ad the version twice in case that version is already present in multiple upper branches
  • Loading branch information
pvdlg committed Nov 27, 2019
1 parent 5744c5e commit aec96c7
Show file tree
Hide file tree
Showing 13 changed files with 440 additions and 503 deletions.
49 changes: 25 additions & 24 deletions index.js
Expand Up @@ -3,7 +3,6 @@ const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
const hookStd = require('hook-std');
const pEachSeries = require('p-each-series');
const semver = require('semver');
const AggregateError = require('aggregate-error');
const pkg = require('./package.json');
Expand All @@ -13,7 +12,7 @@ const verify = require('./lib/verify');
const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const getReleasesToAdd = require('./lib/get-releases-to-add');
const getReleaseToAdd = require('./lib/get-release-to-add');
const {extractErrors, makeTag} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const getBranches = require('./lib/branches');
Expand All @@ -24,7 +23,7 @@ const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');

marked.setOptions({renderer: new TerminalRenderer()});

/* eslint complexity: ["warn", 25] */
/* eslint complexity: off */
async function run(context, plugins) {
const {cwd, env, options, logger} = context;
const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
Expand Down Expand Up @@ -92,35 +91,37 @@ async function run(context, plugins) {

await plugins.verifyConditions(context);

const releasesToAdd = getReleasesToAdd(context);
const errors = [];
context.releases = [];
const releaseToAdd = getReleaseToAdd(context);

if (releaseToAdd) {
const {lastRelease, currentRelease, nextRelease} = releaseToAdd;

await pEachSeries(releasesToAdd, async ({lastRelease, currentRelease, nextRelease}) => {
nextRelease.gitHead = await getTagHead(nextRelease.gitHead, {cwd, env});
currentRelease.gitHead = await getTagHead(currentRelease.gitHead, {cwd, env});
if (context.branch.mergeRange && !semver.satisfies(nextRelease.version, context.branch.mergeRange)) {
errors.push(getError('EINVALIDMAINTENANCEMERGE', {...context, nextRelease}));
return;
} else {
const commits = await getCommits({...context, lastRelease, nextRelease});
nextRelease.notes = await plugins.generateNotes({...context, commits, lastRelease, nextRelease});

await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
logger.success(`Created tag ${nextRelease.gitTag}`);

context.branch.tags.push({
version: nextRelease.version,
channel: nextRelease.channel,
gitTag: nextRelease.gitTag,
gitHead: nextRelease.gitHead,
});

const releases = await plugins.addChannel({...context, commits, lastRelease, currentRelease, nextRelease});
context.releases.push(...releases);
await plugins.success({...context, lastRelease, commits, nextRelease, releases});
}

const commits = await getCommits({...context, lastRelease, nextRelease});
nextRelease.notes = await plugins.generateNotes({...context, commits, lastRelease, nextRelease});

logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag, nextRelease.gitHead, {cwd, env});
await push(options.repositoryUrl, {cwd, env});
context.branch.tags.push({
version: nextRelease.version,
channel: nextRelease.channel,
gitTag: nextRelease.gitTag,
gitHead: nextRelease.gitHead,
});

const releases = await plugins.addChannel({...context, commits, lastRelease, currentRelease, nextRelease});
context.releases.push(...releases);
await plugins.success({...context, lastRelease, commits, nextRelease, releases});
});
}

if (errors.length > 0) {
throw new AggregateError(errors);
Expand Down
19 changes: 15 additions & 4 deletions lib/branches/get-tags.js
@@ -1,4 +1,4 @@
const {template, escapeRegExp} = require('lodash');
const {template, escapeRegExp, flatMap} = require('lodash');
const semver = require('semver');
const pReduce = require('p-reduce');
const debug = require('debug')('semantic-release:get-tags');
Expand All @@ -14,10 +14,21 @@ module.exports = async ({cwd, env, options: {tagFormat}}, branches) => {
return pReduce(
branches,
async (branches, branch) => {
const branchTags = (await getTags(branch.name, {cwd, env})).reduce((tags, tag) => {
const versions = (await getTags(branch.name, {cwd, env})).reduce((versions, tag) => {
const [, version, channel] = tag.match(tagRegexp) || [];
return version && semver.valid(semver.clean(version)) ? [...tags, {gitTag: tag, version, channel}] : tags;
}, []);
if (version && semver.valid(semver.clean(version))) {
return {
...versions,
[version]: versions[version]
? {...versions[version], channels: [...versions[version].channels, channel]}
: {gitTag: tag, version, channels: [channel]},
};
}

return versions;
}, {});

const branchTags = flatMap(versions);

debug('found tags for branch %s: %o', branch.name, branchTags);
return [...branches, {...branch, tags: branchTags}];
Expand Down
7 changes: 4 additions & 3 deletions lib/get-last-release.js
Expand Up @@ -27,16 +27,17 @@ const {makeTag, isSameChannel} = require('./utils');
* @return {LastRelease} The last tagged release or empty object if none is found.
*/
module.exports = ({branch, options: {tagFormat}}, {before} = {}) => {
const [{version, gitTag, channel} = {}] = branch.tags
const [{version, gitTag, channels} = {}] = branch.tags
.filter(
tag =>
(branch.type === 'prerelease' && isSameChannel(branch.channel, tag.channel)) || !semver.prerelease(tag.version)
(branch.type === 'prerelease' && tag.channels.some(channel => isSameChannel(branch.channel, channel))) ||
!semver.prerelease(tag.version)
)
.filter(tag => isUndefined(before) || semver.lt(tag.version, before))
.sort((a, b) => semver.rcompare(a.version, b.version));

if (gitTag) {
return {version, gitTag, channel, gitHead: gitTag, name: makeTag(tagFormat, version)};
return {version, gitTag, channels, gitHead: gitTag, name: makeTag(tagFormat, version)};
}

return {};
Expand Down
3 changes: 2 additions & 1 deletion lib/get-next-version.js
Expand Up @@ -8,7 +8,8 @@ module.exports = ({branch, nextRelease: {type, channel}, lastRelease, logger}) =
const {major, minor, patch} = semver.parse(lastRelease.version);
version =
branch.type === 'prerelease'
? semver.prerelease(lastRelease.version) && isSameChannel(lastRelease.channel, channel)
? semver.prerelease(lastRelease.version) &&
lastRelease.channels.some(lastReleaseChannel => isSameChannel(lastReleaseChannel, channel))
? semver.inc(lastRelease.version, 'prerelease')
: `${semver.inc(`${major}.${minor}.${patch}`, type)}-${branch.prerelease}.${FIRSTPRERELEASE}`
: semver.inc(lastRelease.version, type);
Expand Down
60 changes: 60 additions & 0 deletions lib/get-release-to-add.js
@@ -0,0 +1,60 @@
const {uniqBy, intersection} = require('lodash');
const semver = require('semver');
const semverDiff = require('semver-diff');
const getLastRelease = require('./get-last-release');
const {makeTag, getLowerBound} = require('./utils');

/**
* Find releases that have been merged from from a higher branch but not added on the channel of the current branch.
*
* @param {Object} context semantic-release context.
*
* @return {Array<Object>} Last release and next release to be added on the channel of the current branch.
*/
module.exports = context => {
const {
branch,
branches,
options: {tagFormat},
} = context;

const higherChannels = branches
// Consider only releases of higher branches
.slice(branches.findIndex(({name}) => name === branch.name) + 1)
// Exclude prerelease branches
.filter(({type}) => type !== 'prerelease')
.map(({channel}) => channel);

const versiontoAdd = uniqBy(
branch.tags.filter(
({channels, version}) =>
!channels.includes(branch.channel) &&
intersection(channels, higherChannels).length > 0 &&
(branch.type !== 'maintenance' || semver.gte(version, getLowerBound(branch.mergeRange)))
),
'version'
).sort((a, b) => semver.compare(b.version, a.version))[0];

if (versiontoAdd) {
const {version, gitTag, channels} = versiontoAdd;
const lastRelease = getLastRelease(context, {before: version});
if (semver.gt(getLastRelease(context).version, version)) {
return;
}

const type = lastRelease.version ? semverDiff(lastRelease.version, version) : 'major';
const name = makeTag(tagFormat, version);
return {
lastRelease,
currentRelease: {type, version, channels, gitTag, name, gitHead: gitTag},
nextRelease: {
type,
version,
channel: branch.channel,
gitTag: makeTag(tagFormat, version, branch.channel),
name,
gitHead: gitTag,
},
};
}
};
71 changes: 0 additions & 71 deletions lib/get-releases-to-add.js

This file was deleted.

38 changes: 15 additions & 23 deletions test/branches/get-tags.test.js
Expand Up @@ -20,9 +20,9 @@ test('Get the valid tags', async t => {
{
name: 'master',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channel: undefined},
{gitTag: 'v2.0.0', version: '2.0.0', channel: undefined},
{gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channel: undefined},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined]},
{gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined]},
{gitTag: 'v3.0.0-beta.1', version: '3.0.0-beta.1', channels: [undefined]},
],
},
]);
Expand Down Expand Up @@ -55,23 +55,17 @@ test('Get the valid tags from multiple branches', async t => {
{
name: '1.x',
tags: [
{gitTag: 'v1.0.0', version: '1.0.0', channel: undefined},
{gitTag: 'v1.0.0@1.x', version: '1.0.0', channel: '1.x'},
{gitTag: 'v1.1.0', version: '1.1.0', channel: undefined},
{gitTag: 'v1.1.0@1.x', version: '1.1.0', channel: '1.x'},
{gitTag: 'v1.0.0', version: '1.0.0', channels: [undefined, '1.x']},
{gitTag: 'v1.1.0', version: '1.1.0', channels: [undefined, '1.x']},
],
},
{
name: 'master',
tags: [
...result[0].tags,
{gitTag: 'v2.0.0', version: '2.0.0', channel: undefined},
{gitTag: 'v2.0.0@next', version: '2.0.0', channel: 'next'},
],
tags: [...result[0].tags, {gitTag: 'v2.0.0', version: '2.0.0', channels: [undefined, 'next']}],
},
{
name: 'next',
tags: [...result[1].tags, {gitTag: 'v3.0.0@next', version: '3.0.0', channel: 'next'}],
tags: [...result[1].tags, {gitTag: 'v3.0.0@next', version: '3.0.0', channels: ['next']}],
},
]);
});
Expand All @@ -91,10 +85,8 @@ test('Match the tag name from the begining of the string and the channel from th
{
name: 'master',
tags: [
{gitTag: 'prefix@v1.0.0', version: '1.0.0', channel: undefined},
{gitTag: 'prefix@v1.0.0@next', version: '1.0.0', channel: 'next'},
{gitTag: 'prefix@v2.0.0', version: '2.0.0', channel: undefined},
{gitTag: 'prefix@v2.0.0@next', version: '2.0.0', channel: 'next'},
{gitTag: 'prefix@v1.0.0', version: '1.0.0', channels: [undefined, 'next']},
{gitTag: 'prefix@v2.0.0', version: '2.0.0', channels: [undefined, 'next']},
],
},
]);
Expand Down Expand Up @@ -141,40 +133,40 @@ test('Get the highest valid tag corresponding to the "tagFormat"', async t => {

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}]},
{name: 'master', tags: [{gitTag: '1.0.0', version: '1.0.0', channels: [undefined]}]},
]);

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}]},
{name: 'master', tags: [{gitTag: 'foo-1.0.0-bar', version: '1.0.0', channels: [undefined]}]},
]);

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}],
tags: [{gitTag: 'foo-v1.0.0-bar', version: '1.0.0', channels: [undefined]}],
},
]);

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}],
tags: [{gitTag: '(.+)/1.0.0/(a-z)', version: '1.0.0', channels: [undefined]}],
},
]);

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}],
tags: [{gitTag: '2.0.0-1.0.0-bar.1', version: '1.0.0', channels: [undefined]}],
},
]);

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}]},
{name: 'master', tags: [{gitTag: '3.0.0-bar.2', version: '3.0.0', channels: [undefined]}]},
]);
});

0 comments on commit aec96c7

Please sign in to comment.