diff --git a/lib/lifecycles/bump.js b/lib/lifecycles/bump.js
index da72199f6..270e66896 100644
--- a/lib/lifecycles/bump.js
+++ b/lib/lifecycles/bump.js
@@ -20,16 +20,46 @@ async function Bump (args, version) {
configsToUpdate = {}
if (args.skip.bump) return version
+
+ if (args.releaseAs && !(['major', 'minor', 'patch'].includes(args.releaseAs.toLowerCase()) || semver.valid(args.releaseAs))) {
+ throw new Error("releaseAs must be one of 'major', 'minor' or 'patch', or a valid semvar version.")
+ }
+
let newVersion = version
await runLifecycleScript(args, 'prerelease')
const stdout = await runLifecycleScript(args, 'prebump')
- if (stdout && stdout.trim().length) args.releaseAs = stdout.trim()
- const release = await bumpVersion(args.releaseAs, version, args)
+ if (stdout?.trim().length) {
+ const prebumpString = stdout.trim()
+ if (semver.valid(prebumpString)) args.releaseAs = prebumpString
+ }
if (!args.firstRelease) {
- const releaseType = getReleaseType(args.prerelease, release.releaseType, version)
- const releaseTypeAsVersion = releaseType === 'pre' + release.releaseType ? semver.valid(release.releaseType + '-' + args.prerelease + '.0') : semver.valid(releaseType)
+ if (semver.valid(args.releaseAs)) {
+ const releaseAs = new semver.SemVer(args.releaseAs)
+ if (isString(args.prerelease) && releaseAs.prerelease.length && releaseAs.prerelease.slice(0, -1).join('.') !== args.prerelease) {
+ // If both releaseAs and the prerelease identifier are supplied, they must match. The behavior
+ // for a mismatch is undefined, so error out instead.
+ throw new Error('releaseAs and prerelease have conflicting prerelease identifiers')
+ } else if (isString(args.prerelease) && releaseAs.prerelease.length) {
+ newVersion = releaseAs.version
+ } else if (isString(args.prerelease)) {
+ newVersion = `${releaseAs.major}.${releaseAs.minor}.${releaseAs.patch}-${args.prerelease}.0`
+ } else {
+ newVersion = releaseAs.version
+ }
+
+ // Check if the previous version is the same version and prerelease, and increment if so
+ if (isString(args.prerelease) && ['prerelease', null].includes(semver.diff(version, newVersion)) && semver.lte(newVersion, version)) {
+ newVersion = semver.inc(version, 'prerelease', args.prerelease)
+ }
- newVersion = releaseTypeAsVersion || semver.inc(version, releaseType, args.prerelease)
+ // Append any build info from releaseAs
+ newVersion = semvarToVersionStr(newVersion, releaseAs.build)
+ } else {
+ const release = await bumpVersion(args.releaseAs, version, args)
+ const releaseType = getReleaseType(args.prerelease, release.releaseType, version)
+
+ newVersion = semver.inc(version, releaseType, args.prerelease)
+ }
updateConfigs(args, newVersion)
} else {
checkpoint(args, 'skip version bump on first release', [], chalk.red(figures.cross))
@@ -42,6 +72,16 @@ Bump.getUpdatedConfigs = function () {
return configsToUpdate
}
+/**
+ * Convert a semver object to a full version string including build metadata
+ * @param {string} semverVersion The semvar version string
+ * @param {string[]} semverBuild An array of the build metadata elements, to be joined with '.'
+ * @returns {string}
+ */
+function semvarToVersionStr(semverVersion, semverBuild) {
+ return [semverVersion, semverBuild.join('.')].filter(Boolean).join('+')
+}
+
function getReleaseType (prerelease, expectedReleaseType, currentVersion) {
if (isString(prerelease)) {
if (isInPrerelease(currentVersion)) {
diff --git a/package-lock.json b/package-lock.json
index b58814d42..8b7ead2ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
},
"devDependencies": {
"chai": "^4.2.0",
+ "chai-as-promised": "^7.1.1",
"eslint": "^8.16.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
@@ -983,6 +984,18 @@
"node": ">=4"
}
},
+ "node_modules/chai-as-promised": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
+ "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
+ "dev": true,
+ "dependencies": {
+ "check-error": "^1.0.2"
+ },
+ "peerDependencies": {
+ "chai": ">= 2.1.2 < 5"
+ }
+ },
"node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -6565,6 +6578,15 @@
"type-detect": "^4.0.5"
}
},
+ "chai-as-promised": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
+ "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
+ "dev": true,
+ "requires": {
+ "check-error": "^1.0.2"
+ }
+ },
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
diff --git a/package.json b/package.json
index bd5038504..629a33113 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
},
"devDependencies": {
"chai": "^4.2.0",
+ "chai-as-promised": "^7.1.1",
"eslint": "^8.16.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
diff --git a/test/config-files.spec.js b/test/config-files.spec.js
index 8f39693f6..50bda4c62 100644
--- a/test/config-files.spec.js
+++ b/test/config-files.spec.js
@@ -8,7 +8,9 @@ const { Readable } = require('stream')
const mockery = require('mockery')
const stdMocks = require('std-mocks')
-require('chai').should()
+const chai = require('chai')
+const expect = chai.expect
+chai.use(require('chai-as-promised'))
function exec () {
const cli = require('../command')
@@ -168,12 +170,7 @@ describe('config files', () => {
it('throws an error when a non-object is returned from .versionrc.js', async function () {
mock({ bump: 'minor' })
fs.writeFileSync('.versionrc.js', 'module.exports = 3', 'utf-8')
- try {
- await exec()
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/Invalid configuration/)
- }
+
+ expect(exec).to.throw(/Invalid configuration/)
})
})
diff --git a/test/core.spec.js b/test/core.spec.js
index a6dfbf0fe..5ea3276bf 100644
--- a/test/core.spec.js
+++ b/test/core.spec.js
@@ -14,7 +14,10 @@ const stripAnsi = require('strip-ansi')
const cli = require('../command')
const formatCommitMessage = require('../lib/format-commit-message')
-const should = require('chai').should()
+const chai = require('chai')
+const should = chai.should()
+const expect = chai.expect
+chai.use(require('chai-as-promised'))
// set by mock()
let standardVersion
@@ -157,12 +160,7 @@ describe('cli', function () {
mock({ bump: 'minor', changelog: 'foo\n' })
await exec('--skip.changelog true')
getPackageVersion().should.equal('1.1.0')
- try {
- fs.readFileSync('CHANGELOG.md', 'utf-8')
- throw new Error('File should not exist')
- } catch (err) {
- err.code.should.equal('ENOENT')
- }
+ expect(() => fs.readFileSync('CHANGELOG.md', 'utf-8')).to.throw(/ENOENT/)
})
})
@@ -217,12 +215,7 @@ describe('cli', function () {
it('[DEPRECATED] (--changelogHeader) exits with error if changelog header matches last version search regex', async function () {
mock({ bump: 'minor', fs: { 'CHANGELOG.md': '' } })
- try {
- await exec('--changelogHeader="## 3.0.2"')
- throw new Error('That should not have worked')
- } catch (error) {
- error.message.should.match(/custom changelog header must not match/)
- }
+ expect(exec('--changelogHeader="## 3.0.2"')).to.be.rejectedWith(/custom changelog header must not match/)
})
})
@@ -249,17 +242,11 @@ describe('cli', function () {
fs: { 'CHANGELOG.md': 'legacy header format\n' }
})
- try {
- await exec({
- scripts: {
- prerelease: "node -e \"throw new Error('prerelease' + ' fail')\""
- }
- })
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/prerelease fail/)
- }
+ expect(exec({
+ scripts: {
+ prerelease: "node -e \"throw new Error('prerelease' + ' fail')\""
+ }
+ })).to.be.rejectedWith(/prerelease fail/)
})
})
@@ -277,6 +264,49 @@ describe('cli', function () {
})
const { stdout } = flush()
stdout.join('').should.match(/9\.9\.9/)
+ getPackageVersion().should.equal('9.9.9')
+ })
+
+ it('should not allow prebump hook to return a releaseAs command', async function () {
+ mock({
+ bump: 'minor',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec({
+ scripts: {
+ prebump: "node -e \"console.log('major')\""
+ }
+ })
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ it('should allow prebump hook to return an arbitrary string', async function () {
+ mock({
+ bump: 'minor',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec({
+ scripts: {
+ prebump: "node -e \"console.log('Hello World')\""
+ }
+ })
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ it('should allow prebump hook to return a version with build info', async function () {
+ mock({
+ bump: 'minor',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec({
+ scripts: {
+ prebump: "node -e \"console.log('9.9.9-test+build')\""
+ }
+ })
+ getPackageVersion().should.equal('9.9.9-test+build')
})
})
@@ -302,18 +332,12 @@ describe('cli', function () {
fs: { 'CHANGELOG.md': 'legacy header format\n' }
})
- try {
- await exec({
- scripts: {
- postbump: "node -e \"throw new Error('postbump' + ' fail')\""
- }
- })
- await exec('--patch')
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/postbump fail/)
- }
+ expect(exec({
+ scripts: {
+ postbump: "node -e \"throw new Error('postbump' + ' fail')\""
+ }
+ })).to.be.rejectedWith(/postbump fail/)
+ expect(exec('--patch')).to.be.rejectedWith(/postbump fail/)
})
})
})
@@ -345,6 +369,12 @@ describe('cli', function () {
getPackageVersion().should.equal(`${nextVersion[type]}-${type}.0`)
})
})
+
+ it('exits with error if an invalid release type is provided', async function () {
+ mock({ bump: 'minor', fs: { 'CHANGELOG.md': '' } })
+
+ expect(exec('--release-as invalid')).to.be.rejectedWith(/releaseAs must be one of/)
+ })
})
describe('release-as-exact', function () {
@@ -395,11 +425,59 @@ describe('cli', function () {
bump: 'patch',
fs: { 'CHANGELOG.md': 'legacy header format\n' },
pkg: {
- version: '100.0.0-amazing.0'
+ version: '100.0.0-amazing.1'
}
})
await exec('--release-as 100.0.0-amazing.0 --prerelease amazing')
- should.equal(getPackageVersion(), '100.0.0-amazing.1')
+ should.equal(getPackageVersion(), '100.0.0-amazing.2')
+ })
+
+ it('release 100.0.0 with prerelease amazing correctly sets version', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' },
+ pkg: {
+ version: '99.0.0-amazing.0'
+ }
+ })
+ await exec('--release-as 100.0.0 --prerelease amazing')
+ should.equal(getPackageVersion(), '100.0.0-amazing.0')
+ })
+
+ it('release 100.0.0-amazing.0 with prerelease amazing correctly sets version', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' },
+ pkg: {
+ version: '99.0.0-amazing.0'
+ }
+ })
+ await exec('--release-as 100.0.0-amazing.0 --prerelease amazing')
+ should.equal(getPackageVersion(), '100.0.0-amazing.0')
+ })
+
+ it('release 100.0.0-amazing.0 with prerelease amazing retains build metadata', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' },
+ pkg: {
+ version: '100.0.0-amazing.0'
+ }
+ })
+ await exec('--release-as 100.0.0-amazing.0+build.1234 --prerelease amazing')
+ should.equal(getPackageVersion(), '100.0.0-amazing.1+build.1234')
+ })
+
+ it('release 100.0.0-amazing.3 with prerelease amazing correctly sets prerelease version', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' },
+ pkg: {
+ version: '100.0.0-amazing.0'
+ }
+ })
+ await exec('--release-as 100.0.0-amazing.3 --prerelease amazing')
+ should.equal(getPackageVersion(), '100.0.0-amazing.3')
})
})
@@ -427,6 +505,19 @@ describe('cli', function () {
await exec('--prerelease dev')
getPackageVersion().should.equal('1.1.0-dev.2')
})
+
+ it('exits with error if an invalid release version is provided', async function () {
+ mock({ bump: 'minor', fs: { 'CHANGELOG.md': '' } })
+
+ expect(exec('--release-as 10.2')).to.be.rejectedWith(/releaseAs must be one of/)
+ })
+
+ it('exits with error if release version conflicts with prerelease', async function () {
+ mock({ bump: 'minor', fs: { 'CHANGELOG.md': '' } })
+
+ expect(exec('--release-as 1.2.3-amazing.2 --prerelease awesome')).to.be
+ .rejectedWith(/releaseAs and prerelease have conflicting prerelease identifiers/)
+ })
})
it('appends line feed at end of package.json', async function () {
@@ -478,35 +569,20 @@ describe('commit-and-tag-version', function () {
it('should exit on bump error', async function () {
mock({ bump: new Error('bump err') })
- try {
- await exec()
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (err) {
- err.message.should.match(/bump err/)
- }
+
+ expect(exec()).to.be.rejectedWith(/bump err/)
})
it('should exit on changelog error', async function () {
mock({ bump: 'minor', changelog: new Error('changelog err') })
- try {
- await exec()
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (err) {
- err.message.should.match(/changelog err/)
- }
+
+ expect(exec()).to.be.rejectedWith(/changelog err/)
})
it('should exit with error without a package file to bump', async function () {
mock({ bump: 'patch', pkg: false })
- try {
- await exec({ gitTagFallback: false })
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (err) {
- err.message.should.equal('no package file found')
- }
+
+ expect(exec({ gitTagFallback: false })).to.be.rejectedWith('no package file found')
})
it('bumps version # in bower.json', async function () {
@@ -880,24 +956,20 @@ describe('with mocked git', function () {
it('fails if git add fails', async function () {
const gitArgs = [['add', 'CHANGELOG.md', 'package.json']]
+ const gitError = new Error('Command failed: git\nfailed add')
const execFile = (_args, cmd, cmdArgs) => {
cmd.should.equal('git')
const expected = gitArgs.shift()
cmdArgs.should.deep.equal(expected)
+
if (expected[0] === 'add') {
- return Promise.reject(new Error('Command failed: git\nfailed add'))
+ return Promise.reject(gitError)
}
return Promise.resolve('')
}
mock({ bump: 'patch', changelog: 'foo\n', execFile })
- try {
- await exec({}, true)
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/failed add/)
- }
+ expect(exec({}, true)).to.be.rejectedWith(gitError)
})
it('fails if git commit fails', async function () {
@@ -905,24 +977,19 @@ describe('with mocked git', function () {
['add', 'CHANGELOG.md', 'package.json'],
['commit', 'CHANGELOG.md', 'package.json', '-m', 'chore(release): 1.0.1']
]
+ const gitError = new Error('Command failed: git\nfailed commit')
const execFile = (_args, cmd, cmdArgs) => {
cmd.should.equal('git')
const expected = gitArgs.shift()
cmdArgs.should.deep.equal(expected)
if (expected[0] === 'commit') {
- return Promise.reject(new Error('Command failed: git\nfailed commit'))
+ return Promise.reject(gitError)
}
return Promise.resolve('')
}
mock({ bump: 'patch', changelog: 'foo\n', execFile })
- try {
- await exec({}, true)
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/failed commit/)
- }
+ expect(exec({}, true)).to.be.rejectedWith(gitError)
})
it('fails if git tag fails', async function () {
@@ -931,23 +998,18 @@ describe('with mocked git', function () {
['commit', 'CHANGELOG.md', 'package.json', '-m', 'chore(release): 1.0.1'],
['tag', '-a', 'v1.0.1', '-m', 'chore(release): 1.0.1']
]
+ const gitError = new Error('Command failed: git\nfailed tag')
const execFile = (_args, cmd, cmdArgs) => {
cmd.should.equal('git')
const expected = gitArgs.shift()
cmdArgs.should.deep.equal(expected)
if (expected[0] === 'tag') {
- return Promise.reject(new Error('Command failed: git\nfailed tag'))
+ return Promise.reject(gitError)
}
return Promise.resolve('')
}
mock({ bump: 'patch', changelog: 'foo\n', execFile })
- try {
- await exec({}, true)
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/failed tag/)
- }
+ expect(exec({}, true)).to.be.rejectedWith(gitError)
})
})
diff --git a/test/git.spec.js b/test/git.spec.js
index 22b345a04..4f3652346 100644
--- a/test/git.spec.js
+++ b/test/git.spec.js
@@ -8,7 +8,9 @@ const { Readable } = require('stream')
const mockery = require('mockery')
const stdMocks = require('std-mocks')
-require('chai').should()
+const chai = require('chai')
+const expect = chai.expect
+chai.use(require('chai-as-promised'))
function exec (opt = '') {
if (typeof opt === 'string') {
@@ -356,13 +358,7 @@ describe('git', function () {
)
mock({ bump: 'minor' })
- try {
- await exec('--patch')
- /* istanbul ignore next */
- throw new Error('Unexpected success')
- } catch (error) {
- error.message.should.match(/precommit-failure/)
- }
+ expect(exec('--patch')).to.be.rejectedWith(/precommit-failure/)
})
it('should allow an alternate commit message to be provided by precommit script', async function () {