diff --git a/lib/release/release-please.js b/lib/release/release-please.js index d4b515da..95a7e270 100644 --- a/lib/release/release-please.js +++ b/lib/release/release-please.js @@ -1,6 +1,4 @@ const RP = require('release-please') -const { DefaultVersioningStrategy } = require('release-please/build/src/versioning-strategies/default.js') -const { PrereleaseVersioningStrategy } = require('release-please/build/src/versioning-strategies/prerelease.js') const { ROOT_PROJECT_PATH } = require('release-please/build/src/manifest.js') const { CheckpointLogger, logger } = require('release-please/build/src/util/logger.js') const assert = require('assert') @@ -9,6 +7,7 @@ const omit = require('just-omit') const ChangelogNotes = require('./changelog.js') const NodeWorkspaceFormat = require('./node-workspace-format.js') const { getPublishTag, noop } = require('./util.js') +const { SemverVersioningStrategy } = require('./semver-versioning-strategy.js') /* istanbul ignore next: TODO fix flaky tests and enable coverage */ class ReleasePlease { @@ -52,9 +51,7 @@ class ReleasePlease { async init() { RP.registerChangelogNotes('default', ({ github, ...o }) => new ChangelogNotes(github, o)) - RP.registerVersioningStrategy('default', o => - o.prerelease ? new PrereleaseVersioningStrategy(o) : new DefaultVersioningStrategy(o), - ) + RP.registerVersioningStrategy('default', o => new SemverVersioningStrategy(o)) RP.registerPlugin( 'node-workspace-format', ({ github, targetBranch, repositoryConfig, ...o }) => diff --git a/lib/release/semver-versioning-strategy.js b/lib/release/semver-versioning-strategy.js new file mode 100644 index 00000000..6844deb3 --- /dev/null +++ b/lib/release/semver-versioning-strategy.js @@ -0,0 +1,70 @@ +const semver = require('semver') +const { Version } = require('release-please/build/src/version.js') + +const inc = (version, release, _preid) => { + const parsed = new semver.SemVer(version) + const implicitPreid = parsed.prerelease.length > 1 ? parsed.prerelease[0]?.toString() : undefined + const preid = _preid || implicitPreid + const next = new semver.SemVer(version).inc(release, preid) + if (!parsed.prerelease.length) { + return next.format() + } + const isFreshMajor = parsed.minor === 0 && parsed.patch === 0 + const isFreshMinor = parsed.patch === 0 + const shouldPrerelease = + (release === 'premajor' && isFreshMajor) || (release === 'preminor' && isFreshMinor) || release === 'prepatch' + if (shouldPrerelease) { + return semver.inc(version, 'prerelease', preid) + } + return next.format() +} + +const parseCommits = commits => { + let release = 'patch' + for (const commit of commits) { + if (commit.breaking) { + release = 'major' + break + } else if (['feat', 'feature'].includes(commit.type)) { + release = 'minor' + } + } + return release +} + +class SemverVersioningStrategyNested { + constructor(options, version, commits) { + this.options = options + this.commits = commits + this.version = version + } + + bump() { + return new SemverVersioningStrategy(this.options).bump(this.version, this.commits) + } +} + +class SemverVersioningStrategy { + constructor(options) { + this.options = options + } + + determineReleaseType(version, commits) { + return new SemverVersioningStrategyNested(this.options, version, commits) + } + + bump(currentVersion, commits) { + const prerelease = this.options.prerelease + const tag = this.options.prereleaseType + const releaseType = parseCommits(commits) + const addPreIfNeeded = prerelease ? `pre${releaseType}` : releaseType + const version = inc(currentVersion.toString(), addPreIfNeeded, tag) + /* istanbul ignore next */ + if (!version) { + throw new Error('Could not bump version') + } + return Version.parse(version) + } +} + +module.exports = { SemverVersioningStrategy } diff --git a/test/release/version.js b/test/release/version.js new file mode 100644 index 00000000..8c4d42e9 --- /dev/null +++ b/test/release/version.js @@ -0,0 +1,90 @@ +const t = require('tap') +const { Version } = require('release-please/build/src/version.js') +const { SemverVersioningStrategy } = require('../../lib/release/semver-versioning-strategy') + +const commit = { + type: 'chore', + breaking: false, + notes: [], + references: [], + scope: '', + bareMessage: '', + sha: '', + message: '', +} + +const g = v => ({ ...commit, ...v }) + +const COMMITS = { + major: [{ type: 'feat' }, {}, {}, { breaking: true }].map(g), + minor: [{}, {}, { type: 'feat' }].map(g), + patch: [{}, { type: 'chore' }, { type: 'fix' }].map(g), +} + +const throws = 'THROWS' + +const checks = [ + // Normal releases + ['2.0.0', 'major', false, undefined, '3.0.0'], + ['2.0.0', 'minor', false, undefined, '2.1.0'], + ['2.0.0', 'patch', false, undefined, '2.0.1'], + // premajor -> normal + ['2.0.0-pre.1', 'major', false, undefined, '2.0.0'], + ['2.0.0-pre.5', 'minor', false, undefined, '2.0.0'], + ['2.0.0-pre.4', 'patch', false, undefined, '2.0.0'], + // preminor -> normal + ['2.1.0-pre.1', 'major', false, undefined, '3.0.0'], + ['2.1.0-pre.5', 'minor', false, undefined, '2.1.0'], + ['2.1.0-pre.4', 'patch', false, undefined, '2.1.0'], + // prepatch -> normal + ['2.0.1-pre.1', 'major', false, undefined, '3.0.0'], + ['2.0.1-pre.5', 'minor', false, undefined, '2.1.0'], + ['2.0.1-pre.4', 'patch', false, undefined, '2.0.1'], + // Prereleases + ['2.0.0', 'major', true, 'pre', '3.0.0-pre.0'], + ['2.0.0', 'minor', true, 'pre', '2.1.0-pre.0'], + ['2.0.0', 'patch', true, 'pre', '2.0.1-pre.0'], + // premajor - prereleases + ['2.0.0-pre.1', 'major', true, undefined, '2.0.0-pre.2'], + ['2.0.0-pre.1', 'minor', true, undefined, '2.0.0-pre.2'], + ['2.0.0-pre.1', 'patch', true, undefined, '2.0.0-pre.2'], + // preminor - prereleases + ['2.1.0-pre.1', 'major', true, undefined, '3.0.0-pre.0'], + ['2.1.0-pre.1', 'minor', true, undefined, '2.1.0-pre.2'], + ['2.1.0-pre.1', 'patch', true, undefined, '2.1.0-pre.2'], + // prepatch - prereleases + ['2.0.1-pre.1', 'major', true, undefined, '3.0.0-pre.0'], + ['2.0.1-pre.1', 'minor', true, undefined, '2.1.0-pre.0'], + ['2.0.1-pre.1', 'patch', true, undefined, '2.0.1-pre.2'], + // different prerelease identifiers + ['2.0.0-beta.1', 'major', true, undefined, '2.0.0-beta.2'], + ['2.0.0-alpha.1', 'major', true, undefined, '2.0.0-alpha.2'], + ['2.0.0-rc.1', 'major', true, undefined, '2.0.0-rc.2'], + ['2.0.0-0', 'major', true, undefined, '2.0.0-1'], + // leaves prerelease + ['2.0.0-beta.1', 'major', false, undefined, '2.0.0'], + ['2.0.0-alpha.1', 'major', false, undefined, '2.0.0'], + ['2.0.0-rc.1', 'major', false, undefined, '2.0.0'], + ['2.0.0-0', 'major', false, undefined, '2.0.0'], + ['xxxx', 'major', false, undefined, throws], +] + +t.test('SemverVersioningStrategy', async t => { + for (const [version, commits, prerelease, prereleaseType, expected] of checks) { + const name = [version, commits, prerelease, prereleaseType, expected] + const id = name.map(v => (typeof v === 'undefined' ? 'undefined' : v)).join(',') + const instance = new SemverVersioningStrategy({ prerelease, prereleaseType }) + + if (expected === throws) { + t.throws(() => instance.bump(Version.parse(version), COMMITS[commits]), id) + continue + } + + const bump = instance.bump(Version.parse(version), COMMITS[commits]) + + const determine = instance.determineReleaseType(Version.parse(version), COMMITS[commits]) + + t.equal(bump.toString(), expected, id) + t.equal(determine.bump().toString(), expected, id) + } +})