feat: allow to release any version on a branch if up to date with next branch
This commit is contained in:
parent
534c0dbc89
commit
916c2685c5
@ -100,7 +100,7 @@ For example the configuration `['master', {name: 'pre/rc', prerelease: '${name.r
|
||||
|
||||
### Release branches
|
||||
|
||||
A release branch is the base type of branch used by **semantic-release** that allows to publish releases with a [semantic version](https://semver.org), optionally on a specific distribution channel. Distribution channels (for example [npm dist-tags](https://docs.npmjs.com/cli/dist-tag) or [Chrome release channels](https://www.chromium.org/getting-involved/dev-channel)) are a way to distribute new releases only to a subset of users in order to get early feedback. Later on those releases can be added to the general distribution channel to be made available to all users.
|
||||
A release branch is the base type of branch used by **semantic-release** that allows to publish releases with a [semantic version](https://semver.org), optionally on a specific distribution channel. Distribution channels (for example [npm dist-tags](https://docs.npmjs.com/cli/dist-tag) or [Chrome release channels](https://www.chromium.org/getting-involved/dev-channel)) are a way to distribute new releases only to a subset of users in order to get early feedback. Later on, those releases can be added to the general distribution channel to be made available to all users.
|
||||
|
||||
**semantic-release** will automatically add releases to the corresponding distribution channel when code is [merged from a release branch to another](#merging-into-a-release-branch).
|
||||
|
||||
@ -112,20 +112,17 @@ See [publishing on distribution channels recipe](../recipes/distribution-channel
|
||||
|
||||
#### Pushing to a release branch
|
||||
|
||||
With the configuration `"branches": ["master", "next"]`, if the last release published from `master` is `1.0.0` then:
|
||||
With the configuration `"branches": ["master", "next"]`, if the last release published from `master` is `1.0.0` and the last one from `next` is `2.0.0` then:
|
||||
- Only versions in range `1.x.x` can be published from `master`, so only `fix` and `feat` commits can be pushed to `master`
|
||||
- Only versions in range `>=2.0.0` release can be published from `next`, so a `BREAKING CHANGE` commit must be pushed first on `next` and can be followed by any type of commits
|
||||
- Once `next` get merged into `master` the release `2.0.0` will be made available on the channel associated with `master` and both `master` and `next` will accept any commit type
|
||||
|
||||
With the configuration `"branches": ["master", "next", "next-major"]`, if the last release published from `master` is `1.0.0` then:
|
||||
- Only versions in range `1.0.x` can be published from `master`, so only `fix` commits can be pushed to `master`
|
||||
- Only versions in range `>=1.1.0` can be published from `next`, so a `feat` commit must be pushed first on `next` and can be followed by `fix` and `feat` commits
|
||||
- Only versions in range `>=2.0.0` release can be published from `next`, so a `BREAKING CHANGE` commit must be pushed first on `next-major` and can be followed by any type of commits
|
||||
|
||||
Those verifications prevent situations such as:
|
||||
1. Create a `feat` commit on `next` which triggers the release of version `1.0.0` on channel `next`
|
||||
This verification prevent scenario such as:
|
||||
1. Create a `feat` commit on `next` which triggers the release of version `1.0.0` on the `next` channel
|
||||
2. Merge `next` into `master` which adds `1.0.0` on the default channel
|
||||
3. Push a `fix` commit to `master` which triggers the release of version `1.0.1` on the default channel
|
||||
4. Push a `fix` commit to `next` which would attempt to release the version `1.0.1` on channel `next` and fails as this version already exists
|
||||
3. Create a `feat` commit on `next` which triggers the release of version `1.1.0` on the `next` channel
|
||||
4. Create a `feat` commit on `master` which would attempt to release the version `1.1.0` on the default channel
|
||||
|
||||
In step 4 **semantic-release** will throw an `EINVALIDNEXTVERSION` error to prevent the attempt at releasing version `1.1.0` which was already released on step 3 with a different codebase. The error will indicate that the commit should be created on `next` instead. Alternatively if the `next` branch is merged into `master`, the version `1.1.0` will be made available on the default channel and the `feat` commit would be allowed on `master` to release `1.2.0`.
|
||||
|
||||
#### Merging into a release branch
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
const {sortBy, isNil} = require('lodash');
|
||||
const semver = require('semver');
|
||||
const semverDiff = require('semver-diff');
|
||||
const {FIRST_RELEASE, RELEASE_TYPE} = require('../definitions/constants');
|
||||
const {
|
||||
@ -61,35 +60,29 @@ function release({release}) {
|
||||
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;
|
||||
// The intial lastVersion is the last release from the base branch of `FIRST_RELEASE` (1.0.0)
|
||||
let lastVersion = 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);
|
||||
}
|
||||
// The new lastVersion is the highest version between the current branch last release and the previous branch lastVersion
|
||||
lastVersion = highest(getLatestVersion(versions), lastVersion);
|
||||
// The upper bound is:
|
||||
// - None if the current branch is the last one of the release branches
|
||||
// - Otherwise, The upper bound is the lowest version that is present on the current branch but none of the previous ones
|
||||
const bound =
|
||||
release.length - 1 === idx
|
||||
? undefined
|
||||
: getFirstVersion(tagsToVersions(release[idx + 1].tags), release.slice(0, idx + 1));
|
||||
|
||||
const diff = bound ? semverDiff(min, bound) : null;
|
||||
const diff = bound ? semverDiff(lastVersion, bound) : null;
|
||||
return {
|
||||
...rest,
|
||||
channel: idx === 0 ? channel : isNil(channel) ? name : channel,
|
||||
tags,
|
||||
type: 'release',
|
||||
name,
|
||||
range: getRange(min, bound),
|
||||
range: getRange(lastVersion, bound),
|
||||
accept: bound ? RELEASE_TYPE.slice(0, RELEASE_TYPE.indexOf(diff)) : RELEASE_TYPE,
|
||||
main: idx === 0,
|
||||
};
|
||||
|
@ -30,9 +30,9 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
}));
|
||||
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');
|
||||
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', {options: {branches}})).map(({name, range}) => ({
|
||||
@ -41,14 +41,18 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
}));
|
||||
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, '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 <1.1.0', 'Can release only patch, > than 1.0.1 on master');
|
||||
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');
|
||||
@ -56,9 +60,9 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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');
|
||||
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');
|
||||
@ -66,7 +70,9 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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');
|
||||
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');
|
||||
@ -74,6 +80,8 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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');
|
||||
@ -88,7 +96,6 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
|
||||
merge(branches, 'master', '1.0.x');
|
||||
merge(branches, 'master', '1.x');
|
||||
release(branches, 'master', '1.0.1');
|
||||
result = (await getBranches('repositoryUrl', 'master', {options: {branches}})).map(({name, range}) => ({
|
||||
name,
|
||||
range,
|
||||
@ -121,8 +128,9 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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, '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,
|
||||
@ -136,7 +144,7 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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, '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');
|
||||
|
||||
@ -145,7 +153,7 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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, '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');
|
||||
|
||||
@ -155,19 +163,20 @@ test('Enforce ranges with branching release workflow', async t => {
|
||||
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, '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, '>=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, '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');
|
||||
|
@ -159,19 +159,26 @@ test('Release branches - initial state', t => {
|
||||
.release({release})
|
||||
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
|
||||
[
|
||||
{type: 'release', name: 'master', range: '>=1.0.0 <1.1.0', accept: ['patch'], channel: undefined, main: true},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'master',
|
||||
range: '>=1.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: undefined,
|
||||
main: true,
|
||||
},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next',
|
||||
range: '>=1.1.0 <2.0.0',
|
||||
accept: ['patch', 'minor'],
|
||||
range: '>=1.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: 'next',
|
||||
main: false,
|
||||
},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next-major',
|
||||
range: '>=2.0.0',
|
||||
range: '>=1.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: 'next-major',
|
||||
main: false,
|
||||
@ -293,15 +300,15 @@ test('Release branches - Handle missing previous tags in branch history', t => {
|
||||
{
|
||||
type: 'release',
|
||||
name: 'master',
|
||||
range: '>=2.0.0 <3.0.0',
|
||||
accept: ['patch', 'minor'],
|
||||
range: '>=2.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: undefined,
|
||||
main: true,
|
||||
},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next',
|
||||
range: '>=3.0.0',
|
||||
range: '>=2.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: 'next',
|
||||
main: false,
|
||||
@ -310,40 +317,7 @@ test('Release branches - Handle missing previous tags in branch history', t => {
|
||||
);
|
||||
});
|
||||
|
||||
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, main}) => ({type, name, range, accept, channel, main})),
|
||||
[
|
||||
{type: 'release', name: 'master', range: '>=2.0.0 <2.1.0', accept: ['patch'], channel: undefined, main: true},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next',
|
||||
range: '>=2.1.0 <3.0.0',
|
||||
accept: ['patch', 'minor'],
|
||||
channel: 'next',
|
||||
main: false,
|
||||
},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next-major',
|
||||
range: '>=3.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: 'next-major',
|
||||
main: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('Release branches - limit releases on 2nd and 3rd branche based on 1st branch last release', t => {
|
||||
test('Release branches - limit releases on 2nd and 3rd branch 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'])},
|
||||
@ -355,19 +329,26 @@ test('Release branches - limit releases on 2nd and 3rd branche based on 1st bran
|
||||
.release({release})
|
||||
.map(({type, name, range, accept, channel, main}) => ({type, name, range, accept, channel, main})),
|
||||
[
|
||||
{type: 'release', name: 'master', range: '>=3.0.0 <3.1.0', accept: ['patch'], channel: undefined, main: true},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'master',
|
||||
range: '>=3.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: undefined,
|
||||
main: true,
|
||||
},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next',
|
||||
range: '>=3.1.0 <4.0.0',
|
||||
accept: ['patch', 'minor'],
|
||||
range: '>=3.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: 'next',
|
||||
main: false,
|
||||
},
|
||||
{
|
||||
type: 'release',
|
||||
name: 'next-major',
|
||||
range: '>=4.0.0',
|
||||
range: '>=3.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
channel: 'next-major',
|
||||
main: false,
|
||||
|
@ -97,8 +97,8 @@ test('Plugins are called with expected values', async t => {
|
||||
{
|
||||
channel: undefined,
|
||||
name: 'master',
|
||||
range: '>=1.0.0 <2.0.0',
|
||||
accept: ['patch', 'minor'],
|
||||
range: '>=1.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
tags: [{channels: ['next'], gitTag: 'v1.0.0', version: '1.0.0'}],
|
||||
type: 'release',
|
||||
main: true,
|
||||
@ -106,7 +106,7 @@ test('Plugins are called with expected values', async t => {
|
||||
{
|
||||
channel: 'next',
|
||||
name: 'next',
|
||||
range: '>=2.0.0',
|
||||
range: '>=1.0.0',
|
||||
accept: ['patch', 'minor', 'major'],
|
||||
tags: [{channels: ['next'], gitTag: 'v1.0.0', version: '1.0.0'}],
|
||||
type: 'release',
|
||||
|
Loading…
x
Reference in New Issue
Block a user