diff --git a/lib/updaters/index.js b/lib/updaters/index.js
index 84ed1a0fa..0c4293d93 100644
--- a/lib/updaters/index.js
+++ b/lib/updaters/index.js
@@ -1,13 +1,17 @@
const path = require('path')
const JSON_BUMP_FILES = require('../../defaults').bumpFiles
+const updatersByType = {
+ json: require('./types/json'),
+ 'plain-text': require('./types/plain-text')
+}
const PLAIN_TEXT_BUMP_FILES = ['VERSION.txt', 'version.txt']
function getUpdaterByType (type) {
- try {
- return require(`./types/${type}`)
- } catch (e) {
- throw Error(`Unable to locate updated for provided type (${type}).`)
+ const updater = updatersByType[type]
+ if (!updater) {
+ throw Error(`Unable to locate updater for provided type (${type}).`)
}
+ return updater
}
function getUpdaterByFilename (filename) {
diff --git a/package.json b/package.json
index fbae24546..0d8436aeb 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"fix": "eslint . --fix",
"posttest": "eslint .",
- "test": "nyc mocha --timeout=30000 test.js",
+ "test": "nyc mocha --timeout=30000",
+ "test:unit": "mocha --exclude test/git.spec.js",
"release": "bin/cli.js"
},
"nyc": {
@@ -61,9 +62,10 @@
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"mocha": "^8.0.0",
- "mock-git": "^2.0.0",
+ "mock-fs": "^4.12.0",
"mockery": "^2.1.0",
"nyc": "^14.1.1",
- "shelljs": "^0.8.4"
+ "shelljs": "^0.8.4",
+ "std-mocks": "^1.0.1"
}
}
diff --git a/test.js b/test.js
deleted file mode 100644
index 83627fa66..000000000
--- a/test.js
+++ /dev/null
@@ -1,1338 +0,0 @@
-/* global describe it beforeEach afterEach */
-
-'use strict'
-
-const shell = require('shelljs')
-const fs = require('fs')
-const path = require('path')
-const stream = require('stream')
-const mockGit = require('mock-git')
-const mockery = require('mockery')
-const semver = require('semver')
-const formatCommitMessage = require('./lib/format-commit-message')
-const cli = require('./command')
-const standardVersion = require('./index')
-
-const isWindows = process.platform === 'win32'
-
-require('chai').should()
-
-const cliPath = path.resolve(__dirname, './bin/cli.js')
-
-function branch (branch) {
- shell.exec('git branch ' + branch)
-}
-
-function checkout (branch) {
- shell.exec('git checkout ' + branch)
-}
-
-function commit (msg) {
- shell.exec('git commit --allow-empty -m"' + msg + '"')
-}
-
-function merge (msg, branch) {
- shell.exec('git merge --no-ff -m"' + msg + '" ' + branch)
-}
-
-function execCli (argString) {
- return shell.exec('node ' + cliPath + (argString != null ? ' ' + argString : ''))
-}
-
-function execCliAsync (argString) {
- return standardVersion(cli.parse('standard-version ' + argString + ' --silent'))
-}
-
-function writePackageJson (version, option) {
- option = option || {}
- const pkg = Object.assign(option, { version: version })
- fs.writeFileSync('package.json', JSON.stringify(pkg), 'utf-8')
-}
-
-function writeBowerJson (version, option) {
- option = option || {}
- const bower = Object.assign(option, { version: version })
- fs.writeFileSync('bower.json', JSON.stringify(bower), 'utf-8')
-}
-
-function writeManifestJson (version, option) {
- option = option || {}
- const manifest = Object.assign(option, { version: version })
- fs.writeFileSync('manifest.json', JSON.stringify(manifest), 'utf-8')
-}
-
-function writeNpmShrinkwrapJson (version, option) {
- option = option || {}
- const shrinkwrap = Object.assign(option, { version: version })
- fs.writeFileSync('npm-shrinkwrap.json', JSON.stringify(shrinkwrap), 'utf-8')
-}
-
-function writePackageLockJson (version, option) {
- option = option || {}
- const pkgLock = Object.assign(option, { version: version })
- fs.writeFileSync('package-lock.json', JSON.stringify(pkgLock), 'utf-8')
-}
-
-function writeGitPreCommitHook () {
- fs.writeFileSync('.git/hooks/pre-commit', '#!/bin/sh\necho "precommit ran"\nexit 1', 'utf-8')
- fs.chmodSync('.git/hooks/pre-commit', '755')
-}
-
-function writePostBumpHook (causeError) {
- writeHook('postbump', causeError)
-}
-
-function writeHook (hookName, causeError, script) {
- shell.mkdir('-p', 'scripts')
- let content = script || 'console.error("' + hookName + ' ran")'
- content += causeError ? '\nthrow new Error("' + hookName + '-failure")' : ''
- fs.writeFileSync('scripts/' + hookName + '.js', content, 'utf-8')
- fs.chmodSync('scripts/' + hookName + '.js', '755')
-}
-
-function initInTempFolder () {
- shell.rm('-rf', 'tmp')
- shell.config.silent = true
- shell.mkdir('tmp')
- shell.cd('tmp')
- shell.exec('git init')
- shell.exec('git config commit.gpgSign false')
- commit('root-commit')
- writePackageJson('1.0.0')
-}
-
-function finishTemp () {
- shell.cd('../')
- shell.rm('-rf', 'tmp')
-}
-
-function getPackageVersion () {
- return JSON.parse(fs.readFileSync('package.json', 'utf-8')).version
-}
-
-describe('format-commit-message', function () {
- it('works for no {{currentTag}}', function () {
- formatCommitMessage('chore(release): 1.0.0', '1.0.0').should.equal('chore(release): 1.0.0')
- })
- it('works for one {{currentTag}}', function () {
- formatCommitMessage('chore(release): {{currentTag}}', '1.0.0').should.equal('chore(release): 1.0.0')
- })
- it('works for two {{currentTag}}', function () {
- formatCommitMessage('chore(release): {{currentTag}} \n\n* CHANGELOG: https://github.com/conventional-changelog/standard-version/blob/v{{currentTag}}/CHANGELOG.md', '1.0.0').should.equal('chore(release): 1.0.0 \n\n* CHANGELOG: https://github.com/conventional-changelog/standard-version/blob/v1.0.0/CHANGELOG.md')
- })
-})
-
-describe('cli', function () {
- beforeEach(initInTempFolder)
- afterEach(finishTemp)
-
- describe('CHANGELOG.md does not exist', function () {
- it('populates changelog with commits since last tag by default', function () {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('fix: patch release')
-
- execCli().code.should.equal(0)
-
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/patch release/)
- content.should.not.match(/first commit/)
- })
-
- it('includes all commits if --first-release is true', function () {
- writePackageJson('1.0.1')
-
- commit('feat: first commit')
- commit('fix: patch release')
- execCli('--first-release').code.should.equal(0)
-
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/patch release/)
- content.should.match(/first commit/)
- shell.exec('git tag').stdout.should.match(/1\.0\.1/)
- })
-
- it('skipping changelog will not create a changelog file', function () {
- writePackageJson('1.0.0')
-
- commit('feat: first commit')
- return execCliAsync('--skip.changelog true')
- .then(function () {
- getPackageVersion().should.equal('1.1.0')
- let fileNotFound = false
- try {
- fs.readFileSync('CHANGELOG.md', 'utf-8')
- } catch (err) {
- fileNotFound = true
- }
-
- fileNotFound.should.equal(true)
- })
- })
- })
-
- describe('CHANGELOG.md exists', function () {
- it('appends the new release above the last release, removing the old header (legacy format)', function () {
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('fix: patch release')
-
- execCli().code.should.equal(0)
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/1\.0\.1/)
- content.should.not.match(/legacy header format/)
- })
-
- // TODO: we should use snapshots which are easier to update than large
- // string assertions; we should also consider not using the CLI which
- // is slower than calling standard-version directly.
- it('appends the new release above the last release, removing the old header (new format)', function () {
- // we don't create a package.json, so no {{host}} and {{repo}} tag
- // will be populated, let's use a compareUrlFormat without these.
- const cliArgs = '--compareUrlFormat=/compare/{{previousTag}}...{{currentTag}}'
-
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('fix: patch release')
-
- execCli(cliArgs).code.should.equal(0)
- let content = fs.readFileSync('CHANGELOG.md', 'utf-8')
-
- // remove commit hashes and dates to make testing against a static string easier:
- content = content.replace(/patch release [0-9a-f]{6,8}/g, 'patch release ABCDEFXY').replace(/\([0-9]{4}-[0-9]{2}-[0-9]{2}\)/g, '(YYYY-MM-DD)')
- content.should.equal('# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n\n### [1.0.1](/compare/v1.0.0...v1.0.1) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* patch release ABCDEFXY\n')
-
- commit('fix: another patch release')
- // we've populated no package.json, so no {{host}} and
- execCli(cliArgs).code.should.equal(0)
- content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content = content.replace(/patch release [0-9a-f]{6,8}/g, 'patch release ABCDEFXY').replace(/\([0-9]{4}-[0-9]{2}-[0-9]{2}\)/g, '(YYYY-MM-DD)')
- content.should.equal('# Changelog\n\nAll notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.\n\n### [1.0.2](/compare/v1.0.1...v1.0.2) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* another patch release ABCDEFXY\n\n### [1.0.1](/compare/v1.0.0...v1.0.1) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* patch release ABCDEFXY\n')
- })
-
- it('commits all staged files', function () {
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('fix: patch release')
-
- fs.writeFileSync('STUFF.md', 'stuff\n', 'utf-8')
-
- shell.exec('git add STUFF.md')
-
- execCli('--commit-all').code.should.equal(0)
-
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- const status = shell.exec('git status --porcelain') // see http://unix.stackexchange.com/questions/155046/determine-if-git-working-directory-is-clean-from-a-script
-
- status.should.equal('')
- status.should.not.match(/STUFF.md/)
-
- content.should.match(/1\.0\.1/)
- content.should.not.match(/legacy header format/)
- })
-
- it('[DEPRECATED] (--changelogHeader) allows for a custom changelog header', function () {
- fs.writeFileSync('CHANGELOG.md', '', 'utf-8')
- commit('feat: first commit')
- execCli('--changelogHeader="# Pork Chop Log"').code.should.equal(0)
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/# Pork Chop Log/)
- })
-
- it('[DEPRECATED] (--changelogHeader) exits with error if changelog header matches last version search regex', function () {
- fs.writeFileSync('CHANGELOG.md', '', 'utf-8')
- commit('feat: first commit')
- execCli('--changelogHeader="## 3.0.2"').code.should.equal(1)
- })
- })
-
- // TODO: investigate why mock-git does not play well with execFile on Windows.
- if (!isWindows) {
- describe('with mocked git', function () {
- it('--sign signs the commit and tag', function () {
- // mock git with file that writes args to gitcapture.log
- return mockGit('require("fs").appendFileSync("gitcapture.log", JSON.stringify(process.argv.splice(2)) + "\\n")')
- .then(function (unmock) {
- execCli('--sign').code.should.equal(0)
-
- const captured = shell.cat('gitcapture.log').stdout.split('\n').map(function (line) {
- return line ? JSON.parse(line) : line
- })
- /* eslint-disable no-useless-escape */
- captured[captured.length - 4].should.deep.equal(['commit', '-S', 'CHANGELOG.md', 'package.json', '-m', 'chore(release): 1.0.1'])
- captured[captured.length - 3].should.deep.equal(['tag', '-s', 'v1.0.1', '-m', 'chore(release): 1.0.1'])
- /* eslint-enable no-useless-escape */
- unmock()
- })
- })
-
- it('exits with error code if git commit fails', function () {
- // mock git by throwing on attempt to commit
- return mockGit('console.error("commit yourself"); process.exit(128);', 'commit')
- .then(function (unmock) {
- const result = execCli()
- result.code.should.equal(1)
- result.stderr.should.match(/commit yourself/)
-
- unmock()
- })
- })
-
- it('exits with error code if git add fails', function () {
- // mock git by throwing on attempt to add
- return mockGit('console.error("addition is hard"); process.exit(128);', 'add')
- .then(function (unmock) {
- const result = execCli()
- result.code.should.equal(1)
- result.stderr.should.match(/addition is hard/)
-
- unmock()
- })
- })
-
- it('exits with error code if git tag fails', function () {
- // mock git by throwing on attempt to commit
- return mockGit('console.error("tag, you\'re it"); process.exit(128);', 'tag')
- .then(function (unmock) {
- const result = execCli()
- result.code.should.equal(1)
- result.stderr.should.match(/tag, you're it/)
-
- unmock()
- })
- })
-
- it('doesn\'t fail fast on stderr output from git', function () {
- // mock git by throwing on attempt to commit
- return mockGit('console.error("haha, kidding, this is just a warning"); process.exit(0);', 'add')
- .then(function (unmock) {
- writePackageJson('1.0.0')
-
- const result = execCli()
- result.code.should.equal(1)
- result.stderr.should.match(/haha, kidding, this is just a warning/)
-
- unmock()
- })
- })
- })
- }
-
- describe('lifecycle scripts', () => {
- describe('prerelease hook', function () {
- it('should run the prerelease hook when provided', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- prerelease: 'node scripts/prerelease'
- }
- }
- })
- writeHook('prerelease')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(0)
- result.stderr.should.match(/prerelease ran/)
- })
-
- it('should abort if the hook returns a non-zero exit code', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- prerelease: 'node scripts/prerelease && exit 1'
- }
- }
- })
- writeHook('prerelease')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(1)
- result.stderr.should.match(/prerelease ran/)
- })
- })
-
- describe('prebump hook', function () {
- it('should allow prebump hook to return an alternate version #', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- prebump: 'node scripts/prebump'
- }
- }
- })
- writeHook('prebump', false, 'console.log("9.9.9")')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.stdout.should.match(/9\.9\.9/)
- result.code.should.equal(0)
- })
- })
-
- describe('postbump hook', function () {
- it('should run the postbump hook when provided', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- postbump: 'node scripts/postbump'
- }
- }
- })
- writePostBumpHook()
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(0)
- result.stderr.should.match(/postbump ran/)
- })
-
- it('should run the postbump and exit with error when postbump fails', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- postbump: 'node scripts/postbump'
- }
- }
- })
- writePostBumpHook(true)
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(1)
- result.stderr.should.match(/postbump-failure/)
- })
- })
-
- describe('precommit hook', function () {
- it('should run the precommit hook when provided via .versionrc.json (#371)', function () {
- fs.writeFileSync('.versionrc.json', JSON.stringify({
- scripts: {
- precommit: 'node scripts/precommit'
- }
- }), 'utf-8')
-
- writeHook('precommit')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
- commit('feat: first commit')
- const result = execCli()
- result.code.should.equal(0)
- result.stderr.should.match(/precommit ran/)
- })
-
- it('should run the precommit hook when provided', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- precommit: 'node scripts/precommit'
- }
- }
- })
- writeHook('precommit')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(0)
- result.stderr.should.match(/precommit ran/)
- })
-
- it('should run the precommit hook and exit with error when precommit fails', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- precommit: 'node scripts/precommit'
- }
- }
- })
- writeHook('precommit', true)
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(1)
- result.stderr.should.match(/precommit-failure/)
- })
-
- it('should allow an alternate commit message to be provided by precommit script', function () {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: {
- precommit: 'node scripts/precommit'
- }
- }
- })
- writeHook('precommit', false, 'console.log("releasing %s delivers #222")')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- const result = execCli('--patch')
- result.code.should.equal(0)
- shell.exec('git log --oneline -n1').should.match(/delivers #222/)
- })
- })
- })
-
- describe('pre-release', function () {
- it('works fine without specifying a tag id when prereleasing', function () {
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- return execCliAsync('--prerelease')
- .then(function () {
- // it's a feature commit, so it's minor type
- getPackageVersion().should.equal('1.1.0-0')
- })
- })
-
- it('advises use of --tag prerelease for publishing to npm', function () {
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- execCli('--prerelease').stdout.should.include('--tag prerelease')
- })
-
- it('advises use of --tag alpha for publishing to npm when tagging alpha', function () {
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- execCli('--prerelease alpha').stdout.should.include('--tag alpha')
- })
-
- it('does not advise use of --tag prerelease for private modules', function () {
- writePackageJson('1.0.0', { private: true })
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('feat: first commit')
- execCli('--prerelease').stdout.should.not.include('--tag prerelease')
- })
- })
-
- describe('manual-release', function () {
- it('throws error when not specifying a release type', function () {
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('fix: first commit')
- execCli('--release-as').code.should.above(0)
- })
-
- describe('release-types', function () {
- const regularTypes = ['major', 'minor', 'patch']
-
- regularTypes.forEach(function (type) {
- it('creates a ' + type + ' release', function () {
- const originVer = '1.0.0'
- writePackageJson(originVer)
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('fix: first commit')
-
- return execCliAsync('--release-as ' + type)
- .then(function () {
- const version = {
- major: semver.major(originVer),
- minor: semver.minor(originVer),
- patch: semver.patch(originVer)
- }
-
- version[type] += 1
-
- getPackageVersion().should.equal(version.major + '.' + version.minor + '.' + version.patch)
- })
- })
- })
-
- // this is for pre-releases
- regularTypes.forEach(function (type) {
- it('creates a pre' + type + ' release', function () {
- const originVer = '1.0.0'
- writePackageJson(originVer)
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('fix: first commit')
-
- return execCliAsync('--release-as ' + type + ' --prerelease ' + type)
- .then(function () {
- const version = {
- major: semver.major(originVer),
- minor: semver.minor(originVer),
- patch: semver.patch(originVer)
- }
-
- version[type] += 1
-
- getPackageVersion().should.equal(version.major + '.' + version.minor + '.' + version.patch + '-' + type + '.0')
- })
- })
- })
- })
-
- describe('release-as-exact', function () {
- it('releases as v100.0.0', function () {
- const originVer = '1.0.0'
- writePackageJson(originVer)
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('fix: first commit')
-
- return execCliAsync('--release-as v100.0.0')
- .then(function () {
- getPackageVersion().should.equal('100.0.0')
- })
- })
-
- it('releases as 200.0.0-amazing', function () {
- const originVer = '1.0.0'
- writePackageJson(originVer)
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('fix: first commit')
-
- return execCliAsync('--release-as 200.0.0-amazing')
- .then(function () {
- getPackageVersion().should.equal('200.0.0-amazing')
- })
- })
- })
-
- it('creates a prerelease with a new minor version after two prerelease patches', function () {
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
-
- commit('fix: first patch')
- return execCliAsync('--release-as patch --prerelease dev')
- .then(function () {
- getPackageVersion().should.equal('1.0.1-dev.0')
- })
-
- // second
- .then(function () {
- commit('fix: second patch')
- return execCliAsync('--prerelease dev')
- })
- .then(function () {
- getPackageVersion().should.equal('1.0.1-dev.1')
- })
-
- // third
- .then(function () {
- commit('feat: first new feat')
- return execCliAsync('--release-as minor --prerelease dev')
- })
- .then(function () {
- getPackageVersion().should.equal('1.1.0-dev.0')
- })
-
- .then(function () {
- commit('fix: third patch')
- return execCliAsync('--release-as minor --prerelease dev')
- })
- .then(function () {
- getPackageVersion().should.equal('1.1.0-dev.1')
- })
-
- .then(function () {
- commit('fix: forth patch')
- return execCliAsync('--prerelease dev')
- })
- .then(function () {
- getPackageVersion().should.equal('1.1.0-dev.2')
- })
- })
- })
-
- it('handles commit messages longer than 80 characters', function () {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('fix: this is my fairly long commit message which is testing whether or not we allow for long commit messages')
-
- execCli().code.should.equal(0)
-
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/this is my fairly long commit message which is testing whether or not we allow for long commit messages/)
- })
-
- it('formats the commit and tag messages appropriately', function () {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
-
- execCli().code.should.equal(0)
-
- // check last commit message
- shell.exec('git log --oneline -n1').stdout.should.match(/chore\(release\): 1\.1\.0/)
- // check annotated tag message
- shell.exec('git tag -l -n1 v1.1.0').stdout.should.match(/chore\(release\): 1\.1\.0/)
- })
-
- it('appends line feed at end of package.json', function () {
- execCli().code.should.equal(0)
-
- const pkgJson = fs.readFileSync('package.json', 'utf-8')
- pkgJson.should.equal(['{', ' "version": "1.0.1"', '}', ''].join('\n'))
- })
-
- it('preserves indentation of tabs in package.json', function () {
- const indentation = '\t'
- const newPkgJson = ['{', indentation + '"version": "1.0.0"', '}', ''].join('\n')
- fs.writeFileSync('package.json', newPkgJson, 'utf-8')
-
- execCli().code.should.equal(0)
- const pkgJson = fs.readFileSync('package.json', 'utf-8')
- pkgJson.should.equal(['{', indentation + '"version": "1.0.1"', '}', ''].join('\n'))
- })
-
- it('preserves indentation of spaces in package.json', function () {
- const indentation = ' '
- const newPkgJson = ['{', indentation + '"version": "1.0.0"', '}', ''].join('\n')
- fs.writeFileSync('package.json', newPkgJson, 'utf-8')
-
- execCli().code.should.equal(0)
- const pkgJson = fs.readFileSync('package.json', 'utf-8')
- pkgJson.should.equal(['{', indentation + '"version": "1.0.1"', '}', ''].join('\n'))
- })
-
- it('preserves line feed in package.json', function () {
- const newPkgJson = ['{', ' "version": "1.0.0"', '}', ''].join('\n')
- fs.writeFileSync('package.json', newPkgJson, 'utf-8')
-
- execCli().code.should.equal(0)
- const pkgJson = fs.readFileSync('package.json', 'utf-8')
- pkgJson.should.equal(['{', ' "version": "1.0.1"', '}', ''].join('\n'))
- })
-
- it('preserves carriage return + line feed in package.json', function () {
- const newPkgJson = ['{', ' "version": "1.0.0"', '}', ''].join('\r\n')
- fs.writeFileSync('package.json', newPkgJson, 'utf-8')
-
- execCli().code.should.equal(0)
- const pkgJson = fs.readFileSync('package.json', 'utf-8')
- pkgJson.should.equal(['{', ' "version": "1.0.1"', '}', ''].join('\r\n'))
- })
-
- it('does not run git hooks if the --no-verify flag is passed', function () {
- writeGitPreCommitHook()
-
- commit('feat: first commit')
- execCli('--no-verify').code.should.equal(0)
- commit('feat: second commit')
- execCli('-n').code.should.equal(0)
- })
-
- it('does not print output when the --silent flag is passed', function () {
- const result = execCli('--silent')
- result.code.should.equal(0)
- result.stdout.should.equal('')
- result.stderr.should.equal('')
- })
-
- it('does not display `npm publish` if the package is private', function () {
- writePackageJson('1.0.0', { private: true })
-
- const result = execCli()
- result.code.should.equal(0)
- result.stdout.should.not.match(/npm publish/)
- })
-
- it('does not display `all staged files` without the --commit-all flag', function () {
- const result = execCli()
- result.code.should.equal(0)
- result.stdout.should.not.match(/and all staged files/)
- })
-
- it('does display `all staged files` if the --commit-all flag is passed', function () {
- const result = execCli('--commit-all')
- result.code.should.equal(0)
- result.stdout.should.match(/and all staged files/)
- })
-
- it('includes merge commits', function () {
- const branchName = 'new-feature'
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- branch(branchName)
- checkout(branchName)
- commit('Implementing new feature')
- checkout('master')
- merge('feat: new feature from branch', branchName)
-
- execCli().code.should.equal(0)
-
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/new feature from branch/)
-
- const pkgJson = fs.readFileSync('package.json', 'utf-8')
- pkgJson.should.equal(['{', ' "version": "1.1.0"', '}', ''].join('\n'))
- })
-
- it('exits with error code if "scripts" is not an object', () => {
- writePackageJson('1.0.0', {
- 'standard-version': {
- scripts: 'echo hello'
- }
- })
-
- commit('feat: first commit')
- const result = execCli()
- result.code.should.equal(1)
- result.stderr.should.match(/scripts must be an object/)
- })
-
- it('exits with error code if "skip" is not an object', () => {
- writePackageJson('1.0.0', {
- 'standard-version': {
- skip: true
- }
- })
-
- commit('feat: first commit')
- const result = execCli()
- result.code.should.equal(1)
- result.stderr.should.match(/skip must be an object/)
- })
-})
-
-describe('standard-version', function () {
- beforeEach(initInTempFolder)
- afterEach(finishTemp)
-
- describe('with mocked conventionalRecommendedBump', function () {
- beforeEach(function () {
- mockery.enable({ warnOnUnregistered: false, useCleanCache: true })
- mockery.registerMock('conventional-recommended-bump', function (_, cb) {
- cb(new Error('bump err'))
- })
- })
-
- afterEach(function () {
- mockery.deregisterMock('conventional-recommended-bump')
- mockery.disable()
- })
-
- it('should exit on bump error', function (done) {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
-
- require('./index')({ silent: true })
- .catch((err) => {
- err.message.should.match(/bump err/)
- done()
- })
- })
- })
-
- describe('with mocked conventionalChangelog', function () {
- beforeEach(function () {
- mockery.enable({ warnOnUnregistered: false, useCleanCache: true })
- mockery.registerMock('conventional-changelog', function () {
- const readable = new stream.Readable({ objectMode: true })
- readable._read = function () {
- }
- setImmediate(readable.emit.bind(readable), 'error', new Error('changelog err'))
- return readable
- })
- })
-
- afterEach(function () {
- mockery.deregisterMock('conventional-changelog')
- mockery.disable()
- })
-
- it('should exit on changelog error', function (done) {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
-
- require('./index')({ silent: true })
- .catch((err) => {
- err.message.should.match(/changelog err/)
- return done()
- })
- })
- })
-
- it('formats the commit and tag messages appropriately', function (done) {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
-
- require('./index')({ silent: true })
- .then(() => {
- // check last commit message
- shell.exec('git log --oneline -n1').stdout.should.match(/chore\(release\): 1\.1\.0/)
- // check annotated tag message
- shell.exec('git tag -l -n1 v1.1.0').stdout.should.match(/chore\(release\): 1\.1\.0/)
- done()
- })
- })
-
- describe('without a package file to bump', function () {
- it('should exit with error', function () {
- shell.rm('package.json')
- return require('./index')({
- silent: true,
- gitTagFallback: false
- })
- .catch((err) => {
- err.message.should.equal('no package file found')
- })
- })
- })
-
- describe('bower.json support', function () {
- beforeEach(function () {
- writeBowerJson('1.0.0')
- })
-
- it('bumps version # in bower.json', function () {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- return require('./index')({ silent: true })
- .then(() => {
- JSON.parse(fs.readFileSync('bower.json', 'utf-8')).version.should.equal('1.1.0')
- getPackageVersion().should.equal('1.1.0')
- })
- })
- })
-
- describe('manifest.json support', function () {
- beforeEach(function () {
- writeManifestJson('1.0.0')
- })
-
- it('bumps version # in manifest.json', function () {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- return require('./index')({ silent: true })
- .then(() => {
- JSON.parse(fs.readFileSync('manifest.json', 'utf-8')).version.should.equal('1.1.0')
- getPackageVersion().should.equal('1.1.0')
- })
- })
- })
-
- describe('custom `bumpFiles` support', function () {
- it('mix.exs + version.txt', function () {
- // @todo This file path is relative to the `tmp` directory, which is a little confusing
- fs.copyFileSync('../test/mocks/mix.exs', 'mix.exs')
- fs.copyFileSync('../test/mocks/version.txt', 'version.txt')
- fs.copyFileSync('../test/mocks/updater/customer-updater.js', 'custom-updater.js')
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- return require('./index')({
- silent: true,
- bumpFiles: [
- 'version.txt',
- {
- filename: 'mix.exs',
- updater: 'custom-updater.js'
- }
- ]
- })
- .then(() => {
- fs.readFileSync('mix.exs', 'utf-8').should.contain('version: "1.1.0"')
- fs.readFileSync('version.txt', 'utf-8').should.equal('1.1.0')
- })
- })
-
- it('bumps a custom `plain-text` file', function () {
- fs.copyFileSync('../test/mocks/VERSION-1.0.0.txt', 'VERSION_TRACKER.txt')
- commit('feat: first commit')
- return require('./index')({
- silent: true,
- bumpFiles: [
- {
- filename: 'VERSION_TRACKER.txt',
- type: 'plain-text'
- }
- ]
- })
- .then(() => {
- fs.readFileSync('VERSION_TRACKER.txt', 'utf-8').should.equal('1.1.0')
- })
- })
- })
-
- describe('custom `packageFiles` support', function () {
- it('reads and writes to a custom `plain-text` file', function () {
- fs.copyFileSync('../test/mocks/VERSION-6.3.1.txt', 'VERSION_TRACKER.txt')
- commit('feat: yet another commit')
- return require('./index')({
- silent: true,
- packageFiles: [
- {
- filename: 'VERSION_TRACKER.txt',
- type: 'plain-text'
- }
- ],
- bumpFiles: [
- {
- filename: 'VERSION_TRACKER.txt',
- type: 'plain-text'
- }
- ]
- })
- .then(() => {
- fs.readFileSync('VERSION_TRACKER.txt', 'utf-8').should.equal('6.4.0')
- })
- })
- })
-
- describe('npm-shrinkwrap.json support', function () {
- beforeEach(function () {
- writeNpmShrinkwrapJson('1.0.0')
- })
-
- it('bumps version # in npm-shrinkwrap.json', function (done) {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- require('./index')({ silent: true })
- .then(() => {
- JSON.parse(fs.readFileSync('npm-shrinkwrap.json', 'utf-8')).version.should.equal('1.1.0')
- getPackageVersion().should.equal('1.1.0')
- return done()
- })
- })
- })
-
- describe('package-lock.json support', function () {
- beforeEach(function () {
- writePackageLockJson('1.0.0')
- fs.writeFileSync('.gitignore', '', 'utf-8')
- })
-
- it('bumps version # in package-lock.json', function () {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- return require('./index')({ silent: true })
- .then(() => {
- JSON.parse(fs.readFileSync('package-lock.json', 'utf-8')).version.should.equal('1.1.0')
- getPackageVersion().should.equal('1.1.0')
- })
- })
- })
-
- describe('dry-run', function () {
- it('skips all non-idempotent steps', function (done) {
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- execCli('--dry-run').stdout.should.match(/### Features/)
- shell.exec('git log --oneline -n1').stdout.should.match(/feat: new feature!/)
- shell.exec('git tag').stdout.should.match(/1\.0\.0/)
- getPackageVersion().should.equal('1.0.0')
- return done()
- })
- })
-
- describe('skip', () => {
- it('allows bump and changelog generation to be skipped', function () {
- const changelogContent = 'legacy header format\n'
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', changelogContent, 'utf-8')
-
- commit('feat: first commit')
- return execCliAsync('--skip.bump true --skip.changelog true')
- .then(function () {
- getPackageVersion().should.equal('1.0.0')
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.equal(changelogContent)
- })
- })
-
- it('allows the commit phase to be skipped', function () {
- const changelogContent = 'legacy header format\n'
- writePackageJson('1.0.0')
- fs.writeFileSync('CHANGELOG.md', changelogContent, 'utf-8')
-
- commit('feat: new feature from branch')
- return execCliAsync('--skip.commit true')
- .then(function () {
- getPackageVersion().should.equal('1.1.0')
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/new feature from branch/)
- // check last commit message
- shell.exec('git log --oneline -n1').stdout.should.match(/feat: new feature from branch/)
- })
- })
- })
-
- describe('.gitignore', () => {
- beforeEach(function () {
- writeBowerJson('1.0.0')
- })
-
- it('does not update files present in .gitignore', () => {
- fs.writeFileSync('.gitignore', 'bower.json', 'utf-8')
-
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- return require('./index')({ silent: true })
- .then(() => {
- JSON.parse(fs.readFileSync('bower.json', 'utf-8')).version.should.equal('1.0.0')
- getPackageVersion().should.equal('1.1.0')
- })
- })
- })
-
- describe('.gitignore', () => {
- beforeEach(function () {
- writeBowerJson('1.0.0')
- })
-
- it('does not update files present in .gitignore', () => {
- fs.writeFileSync('.gitignore', 'bower.json', 'utf-8')
-
- commit('feat: first commit')
- shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
- commit('feat: new feature!')
- return require('./index')({ silent: true })
- .then(() => {
- JSON.parse(fs.readFileSync('bower.json', 'utf-8')).version.should.equal('1.0.0')
- getPackageVersion().should.equal('1.1.0')
- })
- })
- })
-
- describe('gitTagFallback', () => {
- it('defaults to 1.0.0 if no tags in git history', () => {
- shell.rm('package.json')
- commit('feat: first commit')
- return require('./index')({ silent: true })
- .then(() => {
- const output = shell.exec('git tag')
- output.stdout.should.include('v1.1.0')
- })
- })
-
- it('bases version on last tag, if tags are found', () => {
- shell.rm('package.json')
- shell.exec('git tag -a v5.0.0 -m "a release"')
- shell.exec('git tag -a v3.0.0 -m "another release"')
- commit('feat: another commit')
- return require('./index')({ silent: true })
- .then(() => {
- const output = shell.exec('git tag')
- output.stdout.should.include('v5.1.0')
- })
- })
-
- it('does not display `npm publish` if there is no package.json', function () {
- shell.rm('package.json')
- const result = execCli()
- result.code.should.equal(0)
- result.stdout.should.not.match(/npm publish/)
- })
- })
-
- describe('configuration', () => {
- it('reads config from package.json', function () {
- writePackageJson('1.0.0', {
- repository: {
- url: 'git+https://company@scm.org/office/app.git'
- },
- 'standard-version': {
- issueUrlFormat: 'https://standard-version.company.net/browse/{{id}}'
- }
- })
- commit('feat: another commit addresses issue #1')
- execCli()
- // CHANGELOG should have the new issue URL format.
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('https://standard-version.company.net/browse/1')
- })
-
- it('reads config from .versionrc', function () {
- // write configuration that overrides default issue
- // URL format.
- fs.writeFileSync('.versionrc', JSON.stringify({
- issueUrlFormat: 'http://www.foo.com/{{id}}'
- }), 'utf-8')
- commit('feat: another commit addresses issue #1')
- execCli()
- // CHANGELOG should have the new issue URL format.
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('http://www.foo.com/1')
- })
-
- it('reads config from .versionrc.json', function () {
- // write configuration that overrides default issue
- // URL format.
- fs.writeFileSync('.versionrc.json', JSON.stringify({
- issueUrlFormat: 'http://www.foo.com/{{id}}'
- }), 'utf-8')
- commit('feat: another commit addresses issue #1')
- execCli()
- // CHANGELOG should have the new issue URL format.
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('http://www.foo.com/1')
- })
-
- it('evaluates a config-function from .versionrc.js', function () {
- // write configuration that overrides default issue
- // URL format.
- fs.writeFileSync(
- '.versionrc.js',
- `module.exports = function() {
- return {
- issueUrlFormat: 'http://www.versionrc.js/function/{{id}}'
- }
- }`,
- 'utf-8'
- )
- commit('feat: another commit addresses issue #1')
- execCli()
- // CHANGELOG should have the new issue URL format.
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('http://www.versionrc.js/function/1')
- })
-
- it('evaluates a config-object from .versionrc.js', function () {
- // write configuration that overrides default issue
- // URL format.
- fs.writeFileSync(
- '.versionrc.js',
- `module.exports = {
- issueUrlFormat: 'http://www.versionrc.js/object/{{id}}'
- }`,
- 'utf-8'
- )
- commit('feat: another commit addresses issue #1')
- execCli()
- // CHANGELOG should have the new issue URL format.
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('http://www.versionrc.js/object/1')
- })
-
- it('throws an error when a non-object is returned from .versionrc.js', function () {
- // write configuration that overrides default issue
- // URL format.
- fs.writeFileSync(
- '.versionrc.js',
- 'module.exports = 3',
- 'utf-8'
- )
- commit('feat: another commit addresses issue #1')
- execCli().code.should.equal(1)
- })
-
- it('.versionrc : releaseCommitMessageFormat', function () {
- // write configuration that overrides default issue
- // URL format.
- fs.writeFileSync('.versionrc', JSON.stringify({
- releaseCommitMessageFormat: 'This commit represents release: {{currentTag}}'
- }), 'utf-8')
- commit('feat: another commit addresses issue #1')
- execCli()
- shell.exec('git log --oneline -n1').should.include('This commit represents release: 1.1.0')
- })
-
- it('--releaseCommitMessageFormat', function () {
- commit('feat: another commit addresses issue #1')
- execCli('--releaseCommitMessageFormat="{{currentTag}} is the version."')
- shell.exec('git log --oneline -n1').should.include('1.1.0 is the version.')
- })
-
- it('.versionrc : issuePrefixes', function () {
- // write configuration that overrides default issuePrefixes
- // and reference prefix in issue URL format.
- fs.writeFileSync('.versionrc', JSON.stringify({
- issueUrlFormat: 'http://www.foo.com/{{prefix}}{{id}}',
- issuePrefixes: ['ABC-']
- }), 'utf-8')
- commit('feat: another commit addresses issue ABC-1')
- execCli()
- // CHANGELOG should have the new issue URL format.
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('http://www.foo.com/ABC-1')
- })
-
- it('--header', function () {
- fs.writeFileSync('CHANGELOG.md', '', 'utf-8')
- commit('feat: first commit')
- execCli('--header="# Welcome to our CHANGELOG.md"').code.should.equal(0)
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.match(/# Welcome to our CHANGELOG.md/)
- })
-
- it('--issuePrefixes and --issueUrlFormat', function () {
- commit('feat: another commit addresses issue ABC-1')
- execCli('--issuePrefixes="ABC-" --issueUrlFormat="http://www.foo.com/{{prefix}}{{id}}"')
- const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
- content.should.include('http://www.foo.com/ABC-1')
- })
-
- it('[LEGACY] supports --message (and single %s replacement)', function () {
- commit('feat: another commit addresses issue #1')
- execCli('--message="V:%s"')
- shell.exec('git log --oneline -n1').should.include('V:1.1.0')
- })
-
- it('[LEGACY] supports -m (and multiple %s replacements)', function () {
- commit('feat: another commit addresses issue #1')
- execCli('--message="V:%s is the %s."')
- shell.exec('git log --oneline -n1').should.include('V:1.1.0 is the 1.1.0.')
- })
- })
-
- describe('pre-major', () => {
- it('bumps the minor rather than major, if version < 1.0.0', function () {
- writePackageJson('0.5.0', {
- repository: {
- url: 'https://github.com/yargs/yargs.git'
- }
- })
- commit('feat!: this is a breaking change')
- execCli()
- getPackageVersion().should.equal('0.6.0')
- })
-
- it('bumps major if --release-as=major specified, if version < 1.0.0', function () {
- writePackageJson('0.5.0', {
- repository: {
- url: 'https://github.com/yargs/yargs.git'
- }
- })
- commit('feat!: this is a breaking change')
- execCli('-r major')
- getPackageVersion().should.equal('1.0.0')
- })
- })
-})
-
-describe('GHSL-2020-111', function () {
- beforeEach(initInTempFolder)
- afterEach(finishTemp)
- it('does not allow command injection via basic configuration', function () {
- return standardVersion({
- silent: true,
- noVerify: true,
- infile: 'foo.txt',
- releaseCommitMessageFormat: 'bla `touch exploit`'
- }).then(function () {
- const stat = shell.test('-f', './exploit')
- stat.should.equal(false)
- })
- })
-})
diff --git a/test/config-files.spec.js b/test/config-files.spec.js
new file mode 100644
index 000000000..68f718b7d
--- /dev/null
+++ b/test/config-files.spec.js
@@ -0,0 +1,174 @@
+/* global describe it beforeEach afterEach */
+
+'use strict'
+
+const shell = require('shelljs')
+const fs = require('fs')
+const { Readable } = require('stream')
+const mockery = require('mockery')
+const stdMocks = require('std-mocks')
+
+require('chai').should()
+
+function exec () {
+ const cli = require('../command')
+ const opt = cli.parse('standard-version')
+ opt.skip = { commit: true, tag: true }
+ return require('../index')(opt)
+}
+
+/**
+ * Mock external conventional-changelog modules
+ *
+ * Mocks should be unregistered in test cleanup by calling unmock()
+ *
+ * bump?: 'major' | 'minor' | 'patch' | Error | (opt, cb) => { cb(err) | cb(null, { releaseType }) }
+ * changelog?: string | Error | Array string | null>
+ * tags?: string[] | Error
+ */
+function mock ({ bump, changelog, tags } = {}) {
+ mockery.enable({ warnOnUnregistered: false, useCleanCache: true })
+
+ mockery.registerMock('conventional-recommended-bump', function (opt, cb) {
+ if (typeof bump === 'function') bump(opt, cb)
+ else if (bump instanceof Error) cb(bump)
+ else cb(null, bump ? { releaseType: bump } : {})
+ })
+
+ if (!Array.isArray(changelog)) changelog = [changelog]
+ mockery.registerMock(
+ 'conventional-changelog',
+ (opt) =>
+ new Readable({
+ read (_size) {
+ const next = changelog.shift()
+ if (next instanceof Error) {
+ this.destroy(next)
+ } else if (typeof next === 'function') {
+ this.push(next(opt))
+ } else {
+ this.push(next ? Buffer.from(next, 'utf8') : null)
+ }
+ }
+ })
+ )
+
+ mockery.registerMock('git-semver-tags', function (cb) {
+ if (tags instanceof Error) cb(tags)
+ else cb(null, tags | [])
+ })
+
+ stdMocks.use()
+ return () => stdMocks.flush()
+}
+
+describe('config files', () => {
+ beforeEach(function () {
+ shell.rm('-rf', 'tmp')
+ shell.config.silent = true
+ shell.mkdir('tmp')
+ shell.cd('tmp')
+ fs.writeFileSync(
+ 'package.json',
+ JSON.stringify({ version: '1.0.0' }),
+ 'utf-8'
+ )
+ })
+
+ afterEach(function () {
+ shell.cd('../')
+ shell.rm('-rf', 'tmp')
+
+ mockery.deregisterAll()
+ mockery.disable()
+ stdMocks.restore()
+
+ // push out prints from the Mocha reporter
+ const { stdout } = stdMocks.flush()
+ for (const str of stdout) {
+ if (str.startsWith(' ')) process.stdout.write(str)
+ }
+ })
+
+ it('reads config from package.json', async function () {
+ const issueUrlFormat = 'https://standard-version.company.net/browse/{{id}}'
+ mock({
+ bump: 'minor',
+ changelog: ({ preset }) => preset.issueUrlFormat
+ })
+ const pkg = {
+ version: '1.0.0',
+ repository: { url: 'git+https://company@scm.org/office/app.git' },
+ 'standard-version': { issueUrlFormat }
+ }
+ fs.writeFileSync('package.json', JSON.stringify(pkg), 'utf-8')
+
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.include(issueUrlFormat)
+ })
+
+ it('reads config from .versionrc', async function () {
+ const issueUrlFormat = 'http://www.foo.com/{{id}}'
+ const changelog = ({ preset }) => preset.issueUrlFormat
+ mock({ bump: 'minor', changelog })
+ fs.writeFileSync('.versionrc', JSON.stringify({ issueUrlFormat }), 'utf-8')
+
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.include(issueUrlFormat)
+ })
+
+ it('reads config from .versionrc.json', async function () {
+ const issueUrlFormat = 'http://www.foo.com/{{id}}'
+ const changelog = ({ preset }) => preset.issueUrlFormat
+ mock({ bump: 'minor', changelog })
+ fs.writeFileSync(
+ '.versionrc.json',
+ JSON.stringify({ issueUrlFormat }),
+ 'utf-8'
+ )
+
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.include(issueUrlFormat)
+ })
+
+ it('evaluates a config-function from .versionrc.js', async function () {
+ const issueUrlFormat = 'http://www.foo.com/{{id}}'
+ const src = `module.exports = function() { return ${JSON.stringify({
+ issueUrlFormat
+ })} }`
+ const changelog = ({ preset }) => preset.issueUrlFormat
+ mock({ bump: 'minor', changelog })
+ fs.writeFileSync('.versionrc.js', src, 'utf-8')
+
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.include(issueUrlFormat)
+ })
+
+ it('evaluates a config-object from .versionrc.js', async function () {
+ const issueUrlFormat = 'http://www.foo.com/{{id}}'
+ const src = `module.exports = ${JSON.stringify({ issueUrlFormat })}`
+ const changelog = ({ preset }) => preset.issueUrlFormat
+ mock({ bump: 'minor', changelog })
+ fs.writeFileSync('.versionrc.js', src, 'utf-8')
+
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.include(issueUrlFormat)
+ })
+
+ 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/)
+ }
+ })
+})
diff --git a/test/core.spec.js b/test/core.spec.js
new file mode 100644
index 000000000..99cb27eb2
--- /dev/null
+++ b/test/core.spec.js
@@ -0,0 +1,771 @@
+/* global describe it afterEach */
+
+'use strict'
+
+const shell = require('shelljs')
+const fs = require('fs')
+const { resolve } = require('path')
+const { Readable } = require('stream')
+const mockFS = require('mock-fs')
+const mockery = require('mockery')
+const stdMocks = require('std-mocks')
+
+const cli = require('../command')
+const formatCommitMessage = require('../lib/format-commit-message')
+
+require('chai').should()
+
+// set by mock()
+let standardVersion
+
+function exec (opt = '', git) {
+ if (typeof opt === 'string') {
+ opt = cli.parse(`standard-version ${opt}`)
+ }
+ if (!git) opt.skip = Object.assign({}, opt.skip, { commit: true, tag: true })
+ return standardVersion(opt)
+}
+
+function getPackageVersion () {
+ return JSON.parse(fs.readFileSync('package.json', 'utf-8')).version
+}
+
+/**
+ * Mock external conventional-changelog modules
+ *
+ * Mocks should be unregistered in test cleanup by calling unmock()
+ *
+ * bump?: 'major' | 'minor' | 'patch' | Error | (opt, cb) => { cb(err) | cb(null, { releaseType }) }
+ * changelog?: string | Error | Array string | null>
+ * execFile?: ({ dryRun, silent }, cmd, cmdArgs) => Promise
+ * fs?: { [string]: string | Buffer | any }
+ * pkg?: { [string]: any }
+ * tags?: string[] | Error
+ */
+function mock ({ bump, changelog, execFile, fs, pkg, tags } = {}) {
+ mockery.enable({ warnOnUnregistered: false, useCleanCache: true })
+
+ mockery.registerMock('conventional-recommended-bump', function (opt, cb) {
+ if (typeof bump === 'function') bump(opt, cb)
+ else if (bump instanceof Error) cb(bump)
+ else cb(null, bump ? { releaseType: bump } : {})
+ })
+
+ if (!Array.isArray(changelog)) changelog = [changelog]
+ mockery.registerMock(
+ 'conventional-changelog',
+ (opt) =>
+ new Readable({
+ read (_size) {
+ const next = changelog.shift()
+ if (next instanceof Error) {
+ this.destroy(next)
+ } else if (typeof next === 'function') {
+ this.push(next(opt))
+ } else {
+ this.push(next ? Buffer.from(next, 'utf8') : null)
+ }
+ }
+ })
+ )
+
+ mockery.registerMock('git-semver-tags', function (cb) {
+ if (tags instanceof Error) cb(tags)
+ else cb(null, tags | [])
+ })
+
+ if (typeof execFile === 'function') {
+ // called from commit & tag lifecycle methods
+ mockery.registerMock('../run-execFile', execFile)
+ }
+
+ // needs to be set after mockery, but before mock-fs
+ standardVersion = require('../index')
+
+ fs = Object.assign({}, fs)
+ if (pkg) {
+ fs['package.json'] = JSON.stringify(pkg)
+ } else if (pkg === undefined && !fs['package.json']) {
+ fs['package.json'] = JSON.stringify({ version: '1.0.0' })
+ }
+ mockFS(fs)
+
+ stdMocks.use()
+ return () => stdMocks.flush()
+}
+
+function unmock () {
+ mockery.deregisterAll()
+ mockery.disable()
+ mockFS.restore()
+ stdMocks.restore()
+ standardVersion = null
+
+ // push out prints from the Mocha reporter
+ const { stdout } = stdMocks.flush()
+ for (const str of stdout) {
+ if (str.startsWith(' ')) process.stdout.write(str)
+ }
+}
+
+describe('format-commit-message', function () {
+ it('works for no {{currentTag}}', function () {
+ formatCommitMessage('chore(release): 1.0.0', '1.0.0').should.equal(
+ 'chore(release): 1.0.0'
+ )
+ })
+ it('works for one {{currentTag}}', function () {
+ formatCommitMessage('chore(release): {{currentTag}}', '1.0.0').should.equal(
+ 'chore(release): 1.0.0'
+ )
+ })
+ it('works for two {{currentTag}}', function () {
+ formatCommitMessage(
+ 'chore(release): {{currentTag}} \n\n* CHANGELOG: https://github.com/conventional-changelog/standard-version/blob/v{{currentTag}}/CHANGELOG.md',
+ '1.0.0'
+ ).should.equal(
+ 'chore(release): 1.0.0 \n\n* CHANGELOG: https://github.com/conventional-changelog/standard-version/blob/v1.0.0/CHANGELOG.md'
+ )
+ })
+})
+
+describe('cli', function () {
+ afterEach(unmock)
+
+ describe('CHANGELOG.md does not exist', function () {
+ it('populates changelog with commits since last tag by default', async function () {
+ mock({ bump: 'patch', changelog: 'patch release\n', tags: ['v1.0.0'] })
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(/patch release/)
+ })
+
+ it('includes all commits if --first-release is true', async function () {
+ mock({
+ bump: 'minor',
+ changelog: 'first commit\npatch release\n',
+ pkg: { version: '1.0.1' }
+ })
+ await exec('--first-release')
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(/patch release/)
+ content.should.match(/first commit/)
+ })
+
+ it('skipping changelog will not create a changelog file', async 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')
+ }
+ })
+ })
+
+ describe('CHANGELOG.md exists', function () {
+ it('appends the new release above the last release, removing the old header (legacy format)', async function () {
+ mock({
+ bump: 'patch',
+ changelog: 'release 1.0.1\n',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' },
+ tags: ['v1.0.0']
+ })
+ await exec()
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(/1\.0\.1/)
+ content.should.not.match(/legacy header format/)
+ })
+
+ it('appends the new release above the last release, removing the old header (new format)', async function () {
+ const { header } = require('../defaults')
+ const changelog1 =
+ '### [1.0.1](/compare/v1.0.0...v1.0.1) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* patch release ABCDEFXY\n'
+ mock({ bump: 'patch', changelog: changelog1, tags: ['v1.0.0'] })
+ await exec()
+ let content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.equal(header + '\n' + changelog1)
+
+ const changelog2 =
+ '### [1.0.2](/compare/v1.0.1...v1.0.2) (YYYY-MM-DD)\n\n\n### Bug Fixes\n\n* another patch release ABCDEFXY\n'
+ unmock()
+ mock({
+ bump: 'patch',
+ changelog: changelog2,
+ fs: { 'CHANGELOG.md': content },
+ tags: ['v1.0.0', 'v1.0.1']
+ })
+ await exec()
+ content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.equal(header + '\n' + changelog2 + changelog1)
+ })
+
+ it('[DEPRECATED] (--changelogHeader) allows for a custom changelog header', async function () {
+ const header = '# Pork Chop Log'
+ mock({
+ bump: 'minor',
+ changelog: header + '\n',
+ fs: { 'CHANGELOG.md': '' }
+ })
+ await exec(`--changelogHeader="${header}"`)
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(new RegExp(header))
+ })
+
+ 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/)
+ }
+ })
+ })
+
+ describe('lifecycle scripts', () => {
+ describe('prerelease hook', function () {
+ it('should run the prerelease hook when provided', async function () {
+ const flush = mock({
+ bump: 'minor',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec({
+ scripts: { prerelease: 'node -e "console.error(\'prerelease\' + \' ran\')"' }
+ })
+ const { stderr } = flush()
+ stderr.join('\n').should.match(/prerelease ran/)
+ })
+
+ it('should abort if the hook returns a non-zero exit code', async function () {
+ mock({
+ bump: 'minor',
+ 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/)
+ }
+ })
+ })
+
+ describe('prebump hook', function () {
+ it('should allow prebump hook to return an alternate version #', async function () {
+ const flush = mock({
+ bump: 'minor',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec({ scripts: { prebump: 'node -e "console.log(Array.of(9, 9, 9).join(\'.\'))"' } })
+ const { stdout } = flush()
+ stdout.join('').should.match(/9\.9\.9/)
+ })
+ })
+
+ describe('postbump hook', function () {
+ it('should run the postbump hook when provided', async function () {
+ const flush = mock({
+ bump: 'minor',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec({
+ scripts: { postbump: 'node -e "console.error(\'postbump\' + \' ran\')"' }
+ })
+ const { stderr } = flush()
+ stderr.join('\n').should.match(/postbump ran/)
+ })
+
+ it('should run the postbump and exit with error when postbump fails', async function () {
+ mock({
+ bump: 'minor',
+ 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/)
+ }
+ })
+ })
+ })
+
+ describe('manual-release', function () {
+ describe('release-types', function () {
+ const regularTypes = ['major', 'minor', 'patch']
+ const nextVersion = { major: '2.0.0', minor: '1.1.0', patch: '1.0.1' }
+
+ regularTypes.forEach(function (type) {
+ it('creates a ' + type + ' release', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+ await exec('--release-as ' + type)
+ getPackageVersion().should.equal(nextVersion[type])
+ })
+ })
+
+ // this is for pre-releases
+ regularTypes.forEach(function (type) {
+ it('creates a pre' + type + ' release', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+ await exec('--release-as ' + type + ' --prerelease ' + type)
+ getPackageVersion().should.equal(`${nextVersion[type]}-${type}.0`)
+ })
+ })
+ })
+
+ describe('release-as-exact', function () {
+ it('releases as v100.0.0', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+ await exec('--release-as v100.0.0')
+ getPackageVersion().should.equal('100.0.0')
+ })
+
+ it('releases as 200.0.0-amazing', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+ await exec('--release-as 200.0.0-amazing')
+ getPackageVersion().should.equal('200.0.0-amazing')
+ })
+ })
+
+ it('creates a prerelease with a new minor version after two prerelease patches', async function () {
+ let releaseType = 'patch'
+ const bump = (_, cb) => cb(null, { releaseType })
+ mock({
+ bump,
+ fs: { 'CHANGELOG.md': 'legacy header format\n' }
+ })
+
+ await exec('--release-as patch --prerelease dev')
+ getPackageVersion().should.equal('1.0.1-dev.0')
+
+ await exec('--prerelease dev')
+ getPackageVersion().should.equal('1.0.1-dev.1')
+
+ releaseType = 'minor'
+ await exec('--release-as minor --prerelease dev')
+ getPackageVersion().should.equal('1.1.0-dev.0')
+
+ await exec('--release-as minor --prerelease dev')
+ getPackageVersion().should.equal('1.1.0-dev.1')
+
+ await exec('--prerelease dev')
+ getPackageVersion().should.equal('1.1.0-dev.2')
+ })
+ })
+
+ it('appends line feed at end of package.json', async function () {
+ mock({ bump: 'patch' })
+ await exec()
+ const pkgJson = fs.readFileSync('package.json', 'utf-8')
+ pkgJson.should.equal('{\n "version": "1.0.1"\n}\n')
+ })
+
+ it('preserves indentation of tabs in package.json', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'package.json': '{\n\t"version": "1.0.0"\n}\n' }
+ })
+ await exec()
+ const pkgJson = fs.readFileSync('package.json', 'utf-8')
+ pkgJson.should.equal('{\n\t"version": "1.0.1"\n}\n')
+ })
+
+ it('preserves indentation of spaces in package.json', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'package.json': '{\n "version": "1.0.0"\n}\n' }
+ })
+ await exec()
+ const pkgJson = fs.readFileSync('package.json', 'utf-8')
+ pkgJson.should.equal('{\n "version": "1.0.1"\n}\n')
+ })
+
+ it('preserves carriage return + line feed in package.json', async function () {
+ mock({
+ bump: 'patch',
+ fs: { 'package.json': '{\r\n "version": "1.0.0"\r\n}\r\n' }
+ })
+ await exec()
+ const pkgJson = fs.readFileSync('package.json', 'utf-8')
+ pkgJson.should.equal('{\r\n "version": "1.0.1"\r\n}\r\n')
+ })
+
+ it('does not print output when the --silent flag is passed', async function () {
+ const flush = mock()
+ await exec('--silent')
+ flush().should.eql({ stdout: [], stderr: [] })
+ })
+})
+
+describe('standard-version', function () {
+ afterEach(unmock)
+
+ 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/)
+ }
+ })
+
+ 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/)
+ }
+ })
+
+ 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')
+ }
+ })
+
+ it('bumps version # in bower.json', async function () {
+ mock({
+ bump: 'minor',
+ fs: { 'bower.json': JSON.stringify({ version: '1.0.0' }) },
+ tags: ['v1.0.0']
+ })
+ await exec()
+ JSON.parse(fs.readFileSync('bower.json', 'utf-8')).version.should.equal(
+ '1.1.0'
+ )
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ it('bumps version # in manifest.json', async function () {
+ mock({
+ bump: 'minor',
+ fs: { 'manifest.json': JSON.stringify({ version: '1.0.0' }) },
+ tags: ['v1.0.0']
+ })
+ await exec()
+ JSON.parse(fs.readFileSync('manifest.json', 'utf-8')).version.should.equal(
+ '1.1.0'
+ )
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ describe('custom `bumpFiles` support', function () {
+ it('mix.exs + version.txt', async function () {
+ const updater = 'custom-updater.js'
+ const updaterModule = require('./mocks/updater/customer-updater')
+ mock({
+ bump: 'minor',
+ fs: {
+ 'mix.exs': fs.readFileSync('./test/mocks/mix.exs'),
+ 'version.txt': fs.readFileSync('./test/mocks/version.txt')
+ },
+ tags: ['v1.0.0']
+ })
+ mockery.registerMock(resolve(process.cwd(), updater), updaterModule)
+
+ await exec({
+ bumpFiles: [
+ 'version.txt',
+ { filename: 'mix.exs', updater: 'custom-updater.js' }
+ ]
+ })
+ fs.readFileSync('mix.exs', 'utf-8').should.contain('version: "1.1.0"')
+ fs.readFileSync('version.txt', 'utf-8').should.equal('1.1.0')
+ })
+
+ it('bumps a custom `plain-text` file', async function () {
+ mock({
+ bump: 'minor',
+ fs: {
+ 'VERSION_TRACKER.txt': fs.readFileSync(
+ './test/mocks/VERSION-1.0.0.txt'
+ )
+ }
+ })
+ await exec({
+ bumpFiles: [{ filename: 'VERSION_TRACKER.txt', type: 'plain-text' }]
+ })
+ fs.readFileSync('VERSION_TRACKER.txt', 'utf-8').should.equal('1.1.0')
+ })
+ })
+
+ describe('custom `packageFiles` support', function () {
+ it('reads and writes to a custom `plain-text` file', async function () {
+ mock({
+ bump: 'minor',
+ fs: {
+ 'VERSION_TRACKER.txt': fs.readFileSync(
+ './test/mocks/VERSION-6.3.1.txt'
+ )
+ }
+ })
+ await exec({
+ packageFiles: [{ filename: 'VERSION_TRACKER.txt', type: 'plain-text' }],
+ bumpFiles: [{ filename: 'VERSION_TRACKER.txt', type: 'plain-text' }]
+ })
+ fs.readFileSync('VERSION_TRACKER.txt', 'utf-8').should.equal('6.4.0')
+ })
+ })
+
+ it('bumps version # in npm-shrinkwrap.json', async function () {
+ mock({
+ bump: 'minor',
+ fs: {
+ 'npm-shrinkwrap.json': JSON.stringify({ version: '1.0.0' })
+ },
+ tags: ['v1.0.0']
+ })
+ await exec()
+ JSON.parse(
+ fs.readFileSync('npm-shrinkwrap.json', 'utf-8')
+ ).version.should.equal('1.1.0')
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ it('bumps version # in package-lock.json', async function () {
+ mock({
+ bump: 'minor',
+ fs: {
+ '.gitignore': '',
+ 'package-lock.json': JSON.stringify({ version: '1.0.0' })
+ },
+ tags: ['v1.0.0']
+ })
+ await exec()
+ JSON.parse(
+ fs.readFileSync('package-lock.json', 'utf-8')
+ ).version.should.equal('1.1.0')
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ describe('skip', () => {
+ it('allows bump and changelog generation to be skipped', async function () {
+ const changelogContent = 'legacy header format\n'
+ mock({
+ bump: 'minor',
+ changelog: 'foo\n',
+ fs: { 'CHANGELOG.md': changelogContent }
+ })
+
+ await exec('--skip.bump true --skip.changelog true')
+ getPackageVersion().should.equal('1.0.0')
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.equal(changelogContent)
+ })
+ })
+
+ it('does not update files present in .gitignore', async () => {
+ mock({
+ bump: 'minor',
+ fs: {
+ '.gitignore': 'bower.json',
+ 'bower.json': JSON.stringify({ version: '1.0.0' })
+ },
+ tags: ['v1.0.0']
+ })
+ await exec()
+ JSON.parse(fs.readFileSync('bower.json', 'utf-8')).version.should.equal(
+ '1.0.0'
+ )
+ getPackageVersion().should.equal('1.1.0')
+ })
+
+ describe('configuration', () => {
+ it('--header', async function () {
+ mock({ bump: 'minor', fs: { 'CHANGELOG.md': '' } })
+ await exec('--header="# Welcome to our CHANGELOG.md"')
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(/# Welcome to our CHANGELOG.md/)
+ })
+
+ it('--issuePrefixes and --issueUrlFormat', async function () {
+ const format = 'http://www.foo.com/{{prefix}}{{id}}'
+ const prefix = 'ABC-'
+ const changelog = ({ preset }) =>
+ preset.issueUrlFormat + ':' + preset.issuePrefixes
+ mock({ bump: 'minor', changelog })
+ await exec(`--issuePrefixes="${prefix}" --issueUrlFormat="${format}"`)
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.include(`${format}:${prefix}`)
+ })
+ })
+
+ describe('pre-major', () => {
+ it('bumps the minor rather than major, if version < 1.0.0', async function () {
+ mock({
+ bump: 'minor',
+ pkg: {
+ version: '0.5.0',
+ repository: { url: 'https://github.com/yargs/yargs.git' }
+ }
+ })
+ await exec()
+ getPackageVersion().should.equal('0.6.0')
+ })
+
+ it('bumps major if --release-as=major specified, if version < 1.0.0', async function () {
+ mock({
+ bump: 'major',
+ pkg: {
+ version: '0.5.0',
+ repository: { url: 'https://github.com/yargs/yargs.git' }
+ }
+ })
+ await exec('-r major')
+ getPackageVersion().should.equal('1.0.0')
+ })
+ })
+})
+
+describe('GHSL-2020-111', function () {
+ afterEach(unmock)
+
+ it('does not allow command injection via basic configuration', async function () {
+ mock({ bump: 'patch' })
+ await exec({
+ noVerify: true,
+ infile: 'foo.txt',
+ releaseCommitMessageFormat: 'bla `touch exploit`'
+ })
+ const stat = shell.test('-f', './exploit')
+ stat.should.equal(false)
+ })
+})
+
+describe('with mocked git', function () {
+ afterEach(unmock)
+
+ it('--sign signs the commit and tag', async function () {
+ const gitArgs = [
+ ['add', 'CHANGELOG.md', 'package.json'],
+ ['commit', '-S', 'CHANGELOG.md', 'package.json', '-m', 'chore(release): 1.0.1'],
+ ['tag', '-s', 'v1.0.1', '-m', 'chore(release): 1.0.1'],
+ ['rev-parse', '--abbrev-ref', 'HEAD']
+ ]
+ const execFile = (_args, cmd, cmdArgs) => {
+ cmd.should.equal('git')
+ const expected = gitArgs.shift()
+ cmdArgs.should.deep.equal(expected)
+ if (expected[0] === 'rev-parse') return Promise.resolve('master')
+ return Promise.resolve('')
+ }
+ mock({ bump: 'patch', changelog: 'foo\n', execFile })
+
+ await exec('--sign', true)
+ gitArgs.should.have.lengthOf(0)
+ })
+
+ it('fails if git add fails', async function () {
+ const gitArgs = [
+ ['add', 'CHANGELOG.md', 'package.json']
+ ]
+ 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.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/)
+ }
+ })
+
+ it('fails if git commit fails', async function () {
+ const gitArgs = [
+ ['add', 'CHANGELOG.md', 'package.json'],
+ ['commit', 'CHANGELOG.md', 'package.json', '-m', 'chore(release): 1.0.1']
+ ]
+ 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.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/)
+ }
+ })
+
+ it('fails if git tag fails', async function () {
+ const gitArgs = [
+ ['add', 'CHANGELOG.md', 'package.json'],
+ ['commit', 'CHANGELOG.md', 'package.json', '-m', 'chore(release): 1.0.1'],
+ ['tag', '-a', 'v1.0.1', '-m', 'chore(release): 1.0.1']
+ ]
+ 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.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/)
+ }
+ })
+})
diff --git a/test/git.spec.js b/test/git.spec.js
new file mode 100644
index 000000000..448937286
--- /dev/null
+++ b/test/git.spec.js
@@ -0,0 +1,352 @@
+/* global describe it beforeEach afterEach */
+
+'use strict'
+
+const shell = require('shelljs')
+const fs = require('fs')
+const { Readable } = require('stream')
+const mockery = require('mockery')
+const stdMocks = require('std-mocks')
+
+require('chai').should()
+
+function exec (opt = '') {
+ if (typeof opt === 'string') {
+ const cli = require('../command')
+ opt = cli.parse(`standard-version ${opt}`)
+ }
+ return require('../index')(opt)
+}
+
+function writePackageJson (version, option) {
+ const pkg = Object.assign({}, option, { version })
+ fs.writeFileSync('package.json', JSON.stringify(pkg), 'utf-8')
+}
+
+function writeHook (hookName, causeError, script) {
+ shell.mkdir('-p', 'scripts')
+ let content = script || 'console.error("' + hookName + ' ran")'
+ content += causeError ? '\nthrow new Error("' + hookName + '-failure")' : ''
+ fs.writeFileSync('scripts/' + hookName + '.js', content, 'utf-8')
+ fs.chmodSync('scripts/' + hookName + '.js', '755')
+}
+
+function getPackageVersion () {
+ return JSON.parse(fs.readFileSync('package.json', 'utf-8')).version
+}
+
+/**
+ * Mock external conventional-changelog modules
+ *
+ * bump: 'major' | 'minor' | 'patch' | Error | (opt, cb) => { cb(err) | cb(null, { releaseType }) }
+ * changelog?: string | Error | Array string | null>
+ * tags?: string[] | Error
+ */
+function mock ({ bump, changelog, tags }) {
+ if (bump === undefined) throw new Error('bump must be defined for mock()')
+ mockery.enable({ warnOnUnregistered: false, useCleanCache: true })
+
+ mockery.registerMock('conventional-recommended-bump', function (opt, cb) {
+ if (typeof bump === 'function') bump(opt, cb)
+ else if (bump instanceof Error) cb(bump)
+ else cb(null, { releaseType: bump })
+ })
+
+ if (!Array.isArray(changelog)) changelog = [changelog]
+ mockery.registerMock('conventional-changelog', (opt) => new Readable({
+ read (_size) {
+ const next = changelog.shift()
+ if (next instanceof Error) {
+ this.destroy(next)
+ } else if (typeof next === 'function') {
+ this.push(next(opt))
+ } else {
+ this.push(next ? Buffer.from(next, 'utf8') : null)
+ }
+ }
+ }))
+
+ mockery.registerMock('git-semver-tags', function (cb) {
+ if (tags instanceof Error) cb(tags)
+ else cb(null, tags || [])
+ })
+
+ stdMocks.use()
+ return () => stdMocks.flush()
+}
+
+describe('git', function () {
+ beforeEach(function () {
+ shell.rm('-rf', 'tmp')
+ shell.config.silent = true
+ shell.mkdir('tmp')
+ shell.cd('tmp')
+ shell.exec('git init')
+ shell.exec('git config commit.gpgSign false')
+ shell.exec('git config core.autocrlf false')
+ shell.exec('git commit --allow-empty -m"root-commit"')
+ writePackageJson('1.0.0')
+ })
+
+ afterEach(function () {
+ shell.cd('../')
+ shell.rm('-rf', 'tmp')
+
+ mockery.deregisterAll()
+ mockery.disable()
+ stdMocks.restore()
+
+ // push out prints from the Mocha reporter
+ const { stdout } = stdMocks.flush()
+ for (const str of stdout) {
+ if (str.startsWith(' ')) process.stdout.write(str)
+ }
+ })
+
+ it('formats the commit and tag messages appropriately', async function () {
+ mock({ bump: 'minor', tags: ['v1.0.0'] })
+ await exec({})
+ // check last commit message
+ shell.exec('git log --oneline -n1').stdout.should.match(/chore\(release\): 1\.1\.0/)
+ // check annotated tag message
+ shell.exec('git tag -l -n1 v1.1.0').stdout.should.match(/chore\(release\): 1\.1\.0/)
+ })
+
+ it('formats the tag if --first-release is true', async function () {
+ writePackageJson('1.0.1')
+ mock({ bump: 'minor' })
+ await exec('--first-release')
+ shell.exec('git tag').stdout.should.match(/1\.0\.1/)
+ })
+
+ it('commits all staged files', async function () {
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+ fs.writeFileSync('STUFF.md', 'stuff\n', 'utf-8')
+ shell.exec('git add STUFF.md')
+
+ mock({ bump: 'patch', changelog: 'release 1.0.1\n', tags: ['v1.0.0'] })
+ await exec('--commit-all')
+ const status = shell.exec('git status --porcelain') // see http://unix.stackexchange.com/questions/155046/determine-if-git-working-directory-is-clean-from-a-script
+ status.should.equal('')
+ status.should.not.match(/STUFF.md/)
+
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(/1\.0\.1/)
+ content.should.not.match(/legacy header format/)
+ })
+
+ it('does not run git hooks if the --no-verify flag is passed', async function () {
+ fs.writeFileSync('.git/hooks/pre-commit', '#!/bin/sh\necho "precommit ran"\nexit 1', 'utf-8')
+ fs.chmodSync('.git/hooks/pre-commit', '755')
+
+ mock({ bump: 'minor' })
+ await exec('--no-verify')
+ await exec('-n')
+ })
+
+ it('allows the commit phase to be skipped', async function () {
+ const changelogContent = 'legacy header format\n'
+ writePackageJson('1.0.0')
+ fs.writeFileSync('CHANGELOG.md', changelogContent, 'utf-8')
+
+ mock({ bump: 'minor', changelog: 'new feature\n' })
+ await exec('--skip.commit true')
+ getPackageVersion().should.equal('1.1.0')
+ const content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.match(/new feature/)
+ shell.exec('git log --oneline -n1').stdout.should.match(/root-commit/)
+ })
+
+ it('dry-run skips all non-idempotent steps', async function () {
+ shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
+ const flush = mock({ bump: 'minor', changelog: '### Features\n', tags: ['v1.0.0'] })
+ await exec('--dry-run')
+ const { stdout } = flush()
+ stdout.join('').should.match(/### Features/)
+ shell.exec('git log --oneline -n1').stdout.should.match(/root-commit/)
+ shell.exec('git tag').stdout.should.match(/1\.0\.0/)
+ getPackageVersion().should.equal('1.0.0')
+ })
+
+ it('works fine without specifying a tag id when prereleasing', async function () {
+ writePackageJson('1.0.0')
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+ mock({ bump: 'minor' })
+ await exec('--prerelease')
+ getPackageVersion().should.equal('1.1.0-0')
+ })
+
+ describe('gitTagFallback', () => {
+ it('defaults to 1.0.0 if no tags in git history', async () => {
+ shell.rm('package.json')
+ mock({ bump: 'minor' })
+ await exec({})
+ const output = shell.exec('git tag')
+ output.stdout.should.include('v1.1.0')
+ })
+
+ it('bases version on greatest version tag, if tags are found', async () => {
+ shell.rm('package.json')
+ mock({ bump: 'minor', tags: ['v3.9.0', 'v5.0.0', 'v3.0.0'] })
+ await exec({})
+ const output = shell.exec('git tag')
+ output.stdout.should.include('v5.1.0')
+ })
+ })
+
+ describe('configuration', () => {
+ it('.versionrc : releaseCommitMessageFormat', async function () {
+ fs.writeFileSync('.versionrc', JSON.stringify({
+ releaseCommitMessageFormat: 'This commit represents release: {{currentTag}}'
+ }), 'utf-8')
+ mock({ bump: 'minor' })
+ await exec('')
+ shell.exec('git log --oneline -n1').should.include('This commit represents release: 1.1.0')
+ })
+
+ it('--releaseCommitMessageFormat', async function () {
+ mock({ bump: 'minor' })
+ await exec('--releaseCommitMessageFormat="{{currentTag}} is the version."')
+ shell.exec('git log --oneline -n1').should.include('1.1.0 is the version.')
+ })
+
+ it('[LEGACY] supports --message (and single %s replacement)', async function () {
+ mock({ bump: 'minor' })
+ await exec('--message="V:%s"')
+ shell.exec('git log --oneline -n1').should.include('V:1.1.0')
+ })
+
+ it('[LEGACY] supports -m (and multiple %s replacements)', async function () {
+ mock({ bump: 'minor' })
+ await exec('--message="V:%s is the %s."')
+ shell.exec('git log --oneline -n1').should.include('V:1.1.0 is the 1.1.0.')
+ })
+ })
+
+ describe('precommit hook', function () {
+ it('should run the precommit hook when provided via .versionrc.json (#371)', async function () {
+ fs.writeFileSync('.versionrc.json', JSON.stringify({
+ scripts: { precommit: 'node scripts/precommit' }
+ }), 'utf-8')
+
+ writeHook('precommit')
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+ const flush = mock({ bump: 'minor' })
+ await exec('')
+ const { stderr } = flush()
+ stderr[0].should.match(/precommit ran/)
+ })
+
+ it('should run the precommit hook when provided', async function () {
+ writePackageJson('1.0.0', {
+ 'standard-version': {
+ scripts: { precommit: 'node scripts/precommit' }
+ }
+ })
+ writeHook('precommit')
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+
+ const flush = mock({ bump: 'minor' })
+ await exec('--patch')
+ const { stderr } = flush()
+ stderr[0].should.match(/precommit ran/)
+ })
+
+ it('should run the precommit hook and exit with error when precommit fails', async function () {
+ writePackageJson('1.0.0', {
+ 'standard-version': {
+ scripts: { precommit: 'node scripts/precommit' }
+ }
+ })
+ writeHook('precommit', true)
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+
+ mock({ bump: 'minor' })
+ try {
+ await exec('--patch')
+ /* istanbul ignore next */
+ throw new Error('Unexpected success')
+ } catch (error) {
+ error.message.should.match(/precommit-failure/)
+ }
+ })
+
+ it('should allow an alternate commit message to be provided by precommit script', async function () {
+ writePackageJson('1.0.0', {
+ 'standard-version': {
+ scripts: { precommit: 'node scripts/precommit' }
+ }
+ })
+ writeHook('precommit', false, 'console.log("releasing %s delivers #222")')
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+
+ mock({ bump: 'minor' })
+ await exec('--patch')
+ shell.exec('git log --oneline -n1').should.match(/delivers #222/)
+ })
+ })
+
+ describe('Run ... to publish', function () {
+ it('does normally display `npm publish`', async function () {
+ const flush = mock({ bump: 'patch' })
+ await exec('')
+ flush().stdout.join('').should.match(/npm publish/)
+ })
+
+ it('does not display `npm publish` if the package is private', async function () {
+ writePackageJson('1.0.0', { private: true })
+ const flush = mock({ bump: 'patch' })
+ await exec('')
+ flush().stdout.join('').should.not.match(/npm publish/)
+ })
+
+ it('does not display `npm publish` if there is no package.json', async function () {
+ shell.rm('package.json')
+ const flush = mock({ bump: 'patch' })
+ await exec('')
+ flush().stdout.join('').should.not.match(/npm publish/)
+ })
+
+ it('does not display `all staged files` without the --commit-all flag', async function () {
+ const flush = mock({ bump: 'patch' })
+ await exec('')
+ flush().stdout.join('').should.not.match(/all staged files/)
+ })
+
+ it('does display `all staged files` if the --commit-all flag is passed', async function () {
+ const flush = mock({ bump: 'patch' })
+ await exec('--commit-all')
+ flush().stdout.join('').should.match(/all staged files/)
+ })
+
+ it('advises use of --tag prerelease for publishing to npm', async function () {
+ writePackageJson('1.0.0')
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+
+ const flush = mock({ bump: 'patch' })
+ await exec('--prerelease')
+ const { stdout } = flush()
+ stdout.join('').should.include('--tag prerelease')
+ })
+
+ it('advises use of --tag alpha for publishing to npm when tagging alpha', async function () {
+ writePackageJson('1.0.0')
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+
+ const flush = mock({ bump: 'patch' })
+ await exec('--prerelease alpha')
+ const { stdout } = flush()
+ stdout.join('').should.include('--tag alpha')
+ })
+
+ it('does not advise use of --tag prerelease for private modules', async function () {
+ writePackageJson('1.0.0', { private: true })
+ fs.writeFileSync('CHANGELOG.md', 'legacy header format\n', 'utf-8')
+
+ const flush = mock({ bump: 'minor' })
+ await exec('--prerelease')
+ const { stdout } = flush()
+ stdout.join('').should.not.include('--tag prerelease')
+ })
+ })
+})