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:
@@ -0,0 +1,18 @@
|
||||
const {isString, remove, omit, mapValues, template} = require('lodash');
|
||||
const micromatch = require('micromatch');
|
||||
const {getBranches} = require('../git');
|
||||
|
||||
module.exports = async ({cwd}, branches) => {
|
||||
const gitBranches = await getBranches({cwd});
|
||||
|
||||
return branches.reduce(
|
||||
(branches, branch) => [
|
||||
...branches,
|
||||
...remove(gitBranches, name => micromatch(gitBranches, branch.name).includes(name)).map(name => ({
|
||||
name,
|
||||
...mapValues(omit(branch, 'name'), value => (isString(value) ? template(value)({name}) : value)),
|
||||
})),
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
const {template, escapeRegExp} = require('lodash');
|
||||
const semver = require('semver');
|
||||
const pReduce = require('p-reduce');
|
||||
const debug = require('debug')('semantic-release:get-tags');
|
||||
const {getTags, isRefInHistory, getTagHead} = require('../../lib/git');
|
||||
|
||||
module.exports = async ({cwd, env, options: {tagFormat}}, branches) => {
|
||||
// Generate a regex to parse tags formatted with `tagFormat`
|
||||
// by replacing the `version` variable in the template by `(.+)`.
|
||||
// The `tagFormat` is compiled with space as the `version` as it's an invalid tag character,
|
||||
// so it's guaranteed to no be present in the `tagFormat`.
|
||||
const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.[^@]+)@?(.+)?')}`;
|
||||
const tags = (await getTags({cwd, env}))
|
||||
.map(tag => {
|
||||
const [, version, channel] = tag.match(tagRegexp) || [];
|
||||
return {gitTag: tag, version, channel};
|
||||
})
|
||||
.filter(({version}) => version && semver.valid(semver.clean(version)));
|
||||
|
||||
debug('found tags: %o', tags);
|
||||
|
||||
return pReduce(
|
||||
branches,
|
||||
async (branches, branch) => {
|
||||
const branchTags = await pReduce(
|
||||
tags,
|
||||
async (tags, {gitTag, ...rest}) =>
|
||||
(await isRefInHistory(gitTag, branch.name, true, {cwd, env}))
|
||||
? [...tags, {...rest, gitTag, gitHead: await getTagHead(gitTag, {cwd, env})}]
|
||||
: tags,
|
||||
[]
|
||||
);
|
||||
|
||||
debug('found tags for branch %s: %o', branch.name, branchTags);
|
||||
return [...branches, {...branch, tags: branchTags}];
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
const {isString, isRegExp} = require('lodash');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const pEachSeries = require('p-each-series');
|
||||
const DEFINITIONS = require('../definitions/branches');
|
||||
const getError = require('../get-error');
|
||||
const {verifyBranchName} = require('../git');
|
||||
const expand = require('./expand');
|
||||
const getTags = require('./get-tags');
|
||||
const normalize = require('./normalize');
|
||||
|
||||
module.exports = async context => {
|
||||
const branches = await getTags(
|
||||
context,
|
||||
await expand(
|
||||
context,
|
||||
context.options.branches.map(branch => (isString(branch) || isRegExp(branch) ? {name: branch} : branch))
|
||||
)
|
||||
);
|
||||
|
||||
const errors = [];
|
||||
const branchesByType = Object.entries(DEFINITIONS).reduce(
|
||||
(branchesByType, [type, {filter}]) => ({[type]: branches.filter(filter), ...branchesByType}),
|
||||
{}
|
||||
);
|
||||
|
||||
const result = Object.entries(DEFINITIONS).reduce((result, [type, {branchesValidator, branchValidator}]) => {
|
||||
branchesByType[type].forEach(branch => {
|
||||
if (branchValidator && !branchValidator(branch)) {
|
||||
errors.push(getError(`E${type.toUpperCase()}BRANCH`, {branch}));
|
||||
}
|
||||
});
|
||||
|
||||
const branchesOfType = normalize[type](branchesByType);
|
||||
|
||||
if (!branchesValidator(branchesOfType)) {
|
||||
errors.push(getError(`E${type.toUpperCase()}BRANCHES`, {branches: branchesOfType}));
|
||||
}
|
||||
|
||||
return {...result, [type]: branchesOfType};
|
||||
}, {});
|
||||
|
||||
const duplicates = [...branches]
|
||||
.map(branch => branch.name)
|
||||
.sort()
|
||||
.filter((val, idx, arr) => arr[idx] === arr[idx + 1] && arr[idx] !== arr[idx - 1]);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
errors.push(getError('EDUPLICATEBRANCHES', {duplicates}));
|
||||
}
|
||||
|
||||
await pEachSeries(branches, async branch => {
|
||||
if (!(await verifyBranchName(branch.name))) {
|
||||
errors.push(getError('EINVALIDBRANCHNAME', branch));
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
|
||||
return [...result.maintenance, ...result.release, ...result.prerelease];
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
const {sortBy} = require('lodash');
|
||||
const semver = require('semver');
|
||||
const semverDiff = require('semver-diff');
|
||||
const {FIRST_RELEASE, RELEASE_TYPE} = require('../definitions/constants');
|
||||
const {
|
||||
tagsToVersions,
|
||||
isMajorRange,
|
||||
getUpperBound,
|
||||
getLowerBound,
|
||||
highest,
|
||||
lowest,
|
||||
getLatestVersion,
|
||||
getFirstVersion,
|
||||
getRange,
|
||||
} = require('../utils');
|
||||
|
||||
function maintenance({maintenance, release}) {
|
||||
return sortBy(
|
||||
maintenance.map(({name, range, channel, ...rest}) => ({
|
||||
...rest,
|
||||
name,
|
||||
range: range || name,
|
||||
channel: channel || name,
|
||||
})),
|
||||
'range'
|
||||
).map(({name, range, tags, ...rest}, idx, branches) => {
|
||||
const versions = tagsToVersions(tags);
|
||||
// Find the lower bound based on Maintenance branches
|
||||
const maintenanceMin =
|
||||
// If the current branch has a major range (1.x or 1.x.x) and the previous doesn't
|
||||
isMajorRange(range) && branches[idx - 1] && !isMajorRange(branches[idx - 1].range)
|
||||
? // Then the lowest bound is the upper bound of the previous branch range
|
||||
getUpperBound(branches[idx - 1].range)
|
||||
: // Otherwise the lowest bound is the lowest bound of the current branch range
|
||||
getLowerBound(range);
|
||||
// The actual lower bound is the highest version between the current branch last release and `maintenanceMin`
|
||||
const min = highest(getLatestVersion(versions) || FIRST_RELEASE, maintenanceMin);
|
||||
// Determine the first release of the default branch not present in any maintenance branch
|
||||
const base =
|
||||
(release[0] &&
|
||||
(getFirstVersion(tagsToVersions(release[0].tags), branches) ||
|
||||
getLatestVersion(tagsToVersions(release[0].tags)))) ||
|
||||
FIRST_RELEASE;
|
||||
// The upper bound is the lowest version between the `base` version and the upper bound of the current branch range
|
||||
const max = lowest(base, getUpperBound(range));
|
||||
const diff = semverDiff(min, max);
|
||||
return {
|
||||
...rest,
|
||||
type: 'maintenance',
|
||||
name,
|
||||
tags,
|
||||
range: getRange(min, max),
|
||||
accept: diff ? RELEASE_TYPE.slice(0, RELEASE_TYPE.indexOf(diff)) : [],
|
||||
'merge-range': getRange(maintenanceMin, getUpperBound(range)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function release({release}) {
|
||||
if (release.length === 0) {
|
||||
return release;
|
||||
}
|
||||
const breakpoints = release.length > 2 ? ['minor', 'major'] : ['major'];
|
||||
|
||||
// The intial bound is the last release from the base branch of `FIRST_RELEASE` (1.0.0)
|
||||
let bound = getLatestVersion(tagsToVersions(release[0].tags)) || FIRST_RELEASE;
|
||||
|
||||
return release.map(({name, tags, channel, ...rest}, idx) => {
|
||||
const versions = tagsToVersions(tags);
|
||||
// The lower bound is the highest version between the current branch last release and the previous branch upper bound (`bound`)
|
||||
const min = highest(getLatestVersion(versions), bound);
|
||||
if (release.length - 1 === idx) {
|
||||
// If the current branch is the last one of the release branch, there is no upper bound
|
||||
bound = undefined;
|
||||
} else {
|
||||
// The default upper bound is the lower bound increment with the release type of the current branch position
|
||||
const upperBound = semver.inc(min, breakpoints[idx]);
|
||||
// Find the lowest version that is present on the current branch but none of the previous ones
|
||||
const nextFirstVersion = getFirstVersion(tagsToVersions(release[idx + 1].tags), release.slice(0, idx + 1));
|
||||
// The upper bound is the lowest version between `nextFirstVersion` and the default upper bound
|
||||
bound = lowest(nextFirstVersion, upperBound);
|
||||
}
|
||||
const diff = bound ? semverDiff(min, bound) : null;
|
||||
return {
|
||||
...rest,
|
||||
channel: idx === 0 ? channel : channel || name,
|
||||
tags,
|
||||
type: 'release',
|
||||
name,
|
||||
range: getRange(min, bound),
|
||||
accept: bound ? RELEASE_TYPE.slice(0, RELEASE_TYPE.indexOf(diff)) : RELEASE_TYPE,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function prerelease({prerelease}) {
|
||||
return prerelease.map(({name, prerelease, channel, tags, ...rest}) => {
|
||||
const preid = prerelease === true ? name : prerelease;
|
||||
return {
|
||||
...rest,
|
||||
channel: channel || name,
|
||||
type: 'prerelease',
|
||||
name,
|
||||
prerelease: preid,
|
||||
tags,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {maintenance, release, prerelease};
|
||||
Reference in New Issue
Block a user