Repository: conventional-changelog/standard-version
Pull request #186 (2017-06-04 21:03:17)

feat: adding support for lifecycle hooks
I'm putting the finishing touches on https://github.com/conventional-changelog/standard-version/pull/167

```md
`standard-version` supports lifecycle scripts. These allow you to execute your
own supplementary commands during the release. The following
hooks are available:

* `prebump`: executed before the version bump is calculated. If the `prebump`
  script returns a version #, it will be used rather than
  the version calculated by `standard-version`.
* `postbump`: executed after the version has been bumped and written to
  package.json. The flag `--new-version` is populated with the version that is
  being released.
* `precommit`: called after CHANGELOG.md and package.json have been updated,
  but before changes have been committed to git.

Simply add the following to your package.json, to enable lifecycle scripts:

```json
{
  "standard-version": {
    "scripts": {
      "prebump": "echo 9.9.9"
    }
  }
}
```
```

thanks for getting us most of the way there @lancecaraccioli.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11823686/badge)](https://coveralls.io/builds/11823686)

Coverage decreased (-0.6%) to 99.401% when pulling **31ce3ebdcca559faa0efbe8e8297b7a6323a7cda on issue-157** into **13eb9cd47d780f90938c1c447a31149d2f02baea on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11823879/badge)](https://coveralls.io/builds/11823879)

Coverage decreased (-0.9%) to 99.095% when pulling **d8e83460936157a85d8a3b1ff70e9c377ba5a27b on issue-157** into **13eb9cd47d780f90938c1c447a31149d2f02baea on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11823897/badge)](https://coveralls.io/builds/11823897)

Coverage decreased (-1.2%) to 98.83% when pulling **d8e83460936157a85d8a3b1ff70e9c377ba5a27b on issue-157** into **13eb9cd47d780f90938c1c447a31149d2f02baea on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11823932/badge)](https://coveralls.io/builds/11823932)

Coverage decreased (-1.2%) to 98.83% when pulling **d32a739a80d037b2178aca0fd1b88014d93528fb on issue-157** into **13eb9cd47d780f90938c1c447a31149d2f02baea on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11824096/badge)](https://coveralls.io/builds/11824096)

Coverage decreased (-0.4%) to 99.583% when pulling **e37092a916c5570c2b08b1d2fe4a821c7f417c41 on issue-157** into **13eb9cd47d780f90938c1c447a31149d2f02baea on master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/11824514/badge)](https://coveralls.io/builds/11824514)

Coverage decreased (-0.4%) to 99.613% when pulling **41fbf71ec1f9bb693a316511003143a86fd8a458 on issue-157** into **86af7fcc5a35a50cacc00f39d7f1fa1989172139 on master**.


Patch:
diff --git a/.travis.yml b/.travis.yml
index ed30357..bab1008 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: node_js
 node_js:
   - "4"
   - "5"
-  - "node"
+  - "stable"
 before_script:
   - git config --global user.name 'Travis-CI'
   - git config --global user.email 'dummy@example.org'
diff --git a/README.md b/README.md
index 396ff45..1440caf 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,33 @@ standard-version --no-verify
 
 If you have your GPG key set up, add the `--sign` or `-s` flag to your `standard-version` command.
 
+### Lifecycle scripts
+
+`standard-version` supports lifecycle scripts. These allow you to execute your
+own supplementary commands during the release. The following
+hooks are available:
+
+* `prebump`: executed before the version bump is calculated. If the `prebump`
+  script returns a version #, it will be used rather than
+  the version calculated by `standard-version`.
+* `postbump`: executed after the version has been bumped and written to
+  package.json. The flag `--new-version` is populated with the version that is
+  being released.
+* `precommit`: called after CHANGELOG.md and package.json have been updated,
+  but before changes have been committed to git.
+
+Simply add the following to your package.json, to enable lifecycle scripts:
+
+```json
+{
+  "standard-version": {
+    "scripts": {
+      "prebump": "echo 9.9.9"
+    }
+  }
+}
+```
+
 ### Committing generated artifacts in the release commit
 
 If you want to commit generated artifacts in the release commit (e.g. [#96](https://github.com/conventional-changelog/standard-version/issues/96)), you can use the `--commit-all` or `-a` flag. You will need to stage the artifacts you want to commit, so your `release` command could look like this:
diff --git a/appveyor.yml b/appveyor.yml
index 538cdfb..ec5ba45 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,10 +1,10 @@
 environment:
   matrix:
     - nodejs_version: '6'
-    - nodejs_version: '5'
-    - nodejs_version: '4'
 install:
   - ps: Install-Product node $env:nodejs_version
+  - git config --global user.name 'Appveyor'
+  - git config --global user.email 'dummy@example.org'
   - set CI=true
   - npm -g install npm@latest
   - set PATH=%APPDATA%\npm;%PATH%
diff --git a/bin/cli.js b/bin/cli.js
index ceafbcc..70ef2d6 100755
--- a/bin/cli.js
+++ b/bin/cli.js
@@ -6,9 +6,8 @@ var cmdParser = require('../command')
 if (process.version.match(/v(\d+)\./)[1] < 4) {
   console.error('standard-version: Node v4 or greater is required. `standard-version` did not run.')
 } else {
-  standardVersion(cmdParser.argv, function (err) {
-    if (err) {
+  standardVersion(cmdParser.argv)
+    .catch(() => {
       process.exit(1)
-    }
-  })
+    })
 }
diff --git a/command.js b/command.js
index cbbf45d..0c41c6b 100755
--- a/command.js
+++ b/command.js
@@ -69,10 +69,22 @@ module.exports = require('yargs')
     default: defaults.tagPrefix,
     global: true
   })
+  .option('scripts', {
+    describe: 'Scripts to execute for lifecycle events (prebump, precommit, etc.,)',
+    default: {}
+  })
+  .check((argv) => {
+    if (typeof argv.scripts !== 'object' || Array.isArray(argv.scripts)) {
+      throw Error('hooks must be an object')
+    } else {
+      return true
+    }
+  })
   .version()
   .alias('version', 'v')
   .help()
   .alias('help', 'h')
   .example('$0', 'Update changelog and tag release')
   .example('$0 -m "%s: see changelog for details"', 'Update changelog and tag release with custom commit message')
+  .pkgConf('standard-version')
   .wrap(97)
diff --git a/index.js b/index.js
index b4b698e..73e5dd1 100755
--- a/index.js
+++ b/index.js
@@ -1,50 +1,60 @@
-var conventionalRecommendedBump = require('conventional-recommended-bump')
-var conventionalChangelog = require('conventional-changelog')
-var path = require('path')
-
-var chalk = require('chalk')
-var figures = require('figures')
-var exec = require('child_process').exec
-var fs = require('fs')
-var accessSync = require('fs-access').sync
-var semver = require('semver')
-var util = require('util')
-var objectAssign = require('object-assign')
-
-module.exports = function standardVersion (argv, done) {
+const conventionalRecommendedBump = require('conventional-recommended-bump')
+const conventionalChangelog = require('conventional-changelog')
+const path = require('path')
+
+const chalk = require('chalk')
+const figures = require('figures')
+const fs = require('fs')
+const accessSync = require('fs-access').sync
+const semver = require('semver')
+const util = require('util')
+
+const checkpoint = require('./lib/checkpoint')
+const printError = require('./lib/print-error')
+const runExec = require('./lib/run-exec')
+const runLifecycleScript = require('./lib/run-lifecycle-script')
+
+module.exports = function standardVersion (argv) {
   var pkgPath = path.resolve(process.cwd(), './package.json')
   var pkg = require(pkgPath)
+  var newVersion = pkg.version
+  var scripts = argv.scripts || {}
   var defaults = require('./defaults')
-  var args = objectAssign({}, defaults, argv)
+  var args = Object.assign({}, defaults, argv)
 
-  bumpVersion(args.releaseAs, function (err, release) {
-    if (err) {
-      printError(args, err.message)
-      return done(err)
-    }
-
-    var newVersion = pkg.version
-
-    if (!args.firstRelease) {
-      var releaseType = getReleaseType(args.prerelease, release.releaseType, pkg.version)
-      newVersion = semver.valid(releaseType) || semver.inc(pkg.version, releaseType, args.prerelease)
-      updateConfigs(args, newVersion)
-    } else {
-      checkpoint(args, 'skip version bump on first release', [], chalk.red(figures.cross))
-    }
-
-    outputChangelog(args, function (err) {
-      if (err) {
-        return done(err)
+  return runLifecycleScript(args, 'prebump', null, scripts)
+    .then((stdout) => {
+      if (stdout && stdout.trim().length) args.releaseAs = stdout.trim()
+      return bumpVersion(args.releaseAs)
+    })
+    .then((release) => {
+      if (!args.firstRelease) {
+        var releaseType = getReleaseType(args.prerelease, release.releaseType, pkg.version)
+        newVersion = semver.valid(releaseType) || semver.inc(pkg.version, releaseType, args.prerelease)
+        updateConfigs(args, newVersion)
+      } else {
+        checkpoint(args, 'skip version bump on first release', [], chalk.red(figures.cross))
       }
-      commit(args, newVersion, function (err) {
-        if (err) {
-          return done(err)
-        }
-        return tag(newVersion, pkg.private, args, done)
-      })
+
+      return runLifecycleScript(args, 'postbump', newVersion, scripts)
+    })
+    .then(() => {
+      return outputChangelog(args)
+    })
+    .then(() => {
+      return runLifecycleScript(args, 'precommit', newVersion, scripts)
+    })
+    .then((message) => {
+      if (message && message.length) args.message = message
+      return commit(args, newVersion)
+    })
+    .then(() => {
+      return tag(newVersion, pkg.private, args)
+    })
+    .catch((err) => {
+      printError(args, err.message)
+      throw err
     })
-  })
 }
 
 /**
@@ -145,62 +155,52 @@ function getTypePriority (type) {
 }
 
 function bumpVersion (releaseAs, callback) {
-  if (releaseAs) {
-    callback(null, {
-      releaseType: releaseAs
-    })
-  } else {
-    conventionalRecommendedBump({
-      preset: 'angular'
-    }, function (err, release) {
-      callback(err, release)
-    })
-  }
-}
-
-function outputChangelog (argv, cb) {
-  createIfMissing(argv)
-  var header = '# Change Log\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'
-  var oldContent = fs.readFileSync(argv.infile, 'utf-8')
-  // find the position of the last release and remove header:
-  if (oldContent.indexOf('<a name=') !== -1) {
-    oldContent = oldContent.substring(oldContent.indexOf('<a name='))
-  }
-  var content = ''
-  var changelogStream = conventionalChangelog({
-    preset: 'angular'
-  }, undefined, {merges: null})
-    .on('error', function (err) {
-      return cb(err)
-    })
-
-  changelogStream.on('data', function (buffer) {
-    content += buffer.toString()
-  })
-
-  changelogStream.on('end', function () {
-    checkpoint(argv, 'outputting changes to %s', [argv.infile])
-    fs.writeFileSync(argv.infile, header + '\n' + (content + oldContent).replace(/\n+$/, '\n'), 'utf-8')
-    return cb()
+  return new Promise((resolve, reject) => {
+    if (releaseAs) {
+      return resolve({
+        releaseType: releaseAs
+      })
+    } else {
+      conventionalRecommendedBump({
+        preset: 'angular'
+      }, function (err, release) {
+        if (err) return reject(err)
+        else return resolve(release)
+      })
+    }
   })
 }
 
-function handledExec (argv, cmd, errorCb, successCb) {
-  // Exec given cmd and handle possible errors
-  exec(cmd, function (err, stdout, stderr) {
-    // If exec returns content in stderr, but no error, print it as a warning
-    // If exec returns an error, print it and exit with return code 1
-    if (err) {
-      printError(argv, stderr || err.message)
-      return errorCb(err)
-    } else if (stderr) {
-      printError(argv, stderr, {level: 'warn', color: 'yellow'})
+function outputChangelog (argv) {
+  return new Promise((resolve, reject) => {
+    createIfMissing(argv)
+    var header = '# Change Log\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'
+    var oldContent = fs.readFileSync(argv.infile, 'utf-8')
+    // find the position of the last release and remove header:
+    if (oldContent.indexOf('<a name=') !== -1) {
+      oldContent = oldContent.substring(oldContent.indexOf('<a name='))
     }
-    successCb()
+    var content = ''
+    var changelogStream = conventionalChangelog({
+      preset: 'angular'
+    }, undefined, {merges: null})
+      .on('error', function (err) {
+        return reject(err)
+      })
+
+    changelogStream.on('data', function (buffer) {
+      content += buffer.toString()
+    })
+
+    changelogStream.on('end', function () {
+      checkpoint(argv, 'outputting changes to %s', [argv.infile])
+      fs.writeFileSync(argv.infile, header + '\n' + (content + oldContent).replace(/\n+$/, '\n'), 'utf-8')
+      return resolve()
+    })
   })
 }
 
-function commit (argv, newVersion, cb) {
+function commit (argv, newVersion) {
   var msg = 'committing %s'
   var args = [argv.infile]
   var verify = argv.verify === false || argv.n ? '--no-verify ' : ''
@@ -215,18 +215,17 @@ function commit (argv, newVersion, cb) {
     }
   })
   checkpoint(argv, msg, args)
-  handledExec(argv, 'git add' + toAdd + ' ' + argv.infile, cb, function () {
-    handledExec(argv, 'git commit ' + verify + (argv.sign ? '-S ' : '') + (argv.commitAll ? '' : (argv.infile + toAdd)) + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"', cb, function () {
-      cb()
+  return runExec(argv, 'git add' + toAdd + ' ' + argv.infile)
+    .then(() => {
+      return runExec(argv, 'git commit ' + verify + (argv.sign ? '-S ' : '') + (argv.commitAll ? '' : (argv.infile + toAdd)) + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"')
     })
-  })
 }
 
 function formatCommitMessage (msg, newVersion) {
   return String(msg).indexOf('%s') !== -1 ? util.format(msg, newVersion) : msg
 }
 
-function tag (newVersion, pkgPrivate, argv, cb) {
+function tag (newVersion, pkgPrivate, argv) {
   var tagOption
   if (argv.sign) {
     tagOption = '-s '
@@ -234,13 +233,13 @@ function tag (newVersion, pkgPrivate, argv, cb) {
     tagOption = '-a '
   }
   checkpoint(argv, 'tagging release %s', [newVersion])
-  handledExec(argv, 'git tag ' + tagOption + argv.tagPrefix + newVersion + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"', cb, function () {
-    var message = 'git push --follow-tags origin master'
-    if (pkgPrivate !== true) message += '; npm publish'
+  return runExec(argv, 'git tag ' + tagOption + argv.tagPrefix + newVersion + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"')
+    .then(() => {
+      var message = 'git push --follow-tags origin master'
+      if (pkgPrivate !== true) message += '; npm publish'
 
-    checkpoint(argv, 'Run `%s` to publish', [message], chalk.blue(figures.info))
-    cb()
-  })
+      checkpoint(argv, 'Run `%s` to publish', [message], chalk.blue(figures.info))
+    })
 }
 
 function createIfMissing (argv) {
@@ -254,22 +253,3 @@ function createIfMissing (argv) {
     }
   }
 }
-
-function checkpoint (argv, msg, args, figure) {
-  if (!argv.silent) {
-    console.info((figure || chalk.green(figures.tick)) + ' ' + util.format.apply(util, [msg].concat(args.map(function (arg) {
-      return chalk.bold(arg)
-    }))))
-  }
-}
-
-function printError (argv, msg, opts) {
-  if (!argv.silent) {
-    opts = objectAssign({
-      level: 'error',
-      color: 'red'
-    }, opts)
-
-    console[opts.level](chalk[opts.color](msg))
-  }
-}
diff --git a/lib/checkpoint.js b/lib/checkpoint.js
new file mode 100644
index 0000000..ff7f5d1
--- /dev/null
+++ b/lib/checkpoint.js
@@ -0,0 +1,11 @@
+const chalk = require('chalk')
+const figures = require('figures')
+const util = require('util')
+
+module.exports = function (argv, msg, args, figure) {
+  if (!argv.silent) {
+    console.info((figure || chalk.green(figures.tick)) + ' ' + util.format.apply(util, [msg].concat(args.map(function (arg) {
+      return chalk.bold(arg)
+    }))))
+  }
+}
diff --git a/lib/print-error.js b/lib/print-error.js
new file mode 100644
index 0000000..983aeac
--- /dev/null
+++ b/lib/print-error.js
@@ -0,0 +1,12 @@
+const chalk = require('chalk')
+
+module.exports = function (argv, msg, opts) {
+  if (!argv.silent) {
+    opts = Object.assign({
+      level: 'error',
+      color: 'red'
+    }, opts)
+
+    console[opts.level](chalk[opts.color](msg))
+  }
+}
diff --git a/lib/run-exec.js b/lib/run-exec.js
new file mode 100644
index 0000000..bfca976
--- /dev/null
+++ b/lib/run-exec.js
@@ -0,0 +1,19 @@
+const exec = require('child_process').exec
+const printError = require('./print-error')
+
+module.exports = function (argv, cmd) {
+  return new Promise((resolve, reject) => {
+    // Exec given cmd and handle possible errors
+    exec(cmd, function (err, stdout, stderr) {
+      // If exec returns content in stderr, but no error, print it as a warning
+      // If exec returns an error, print it and exit with return code 1
+      if (err) {
+        printError(argv, stderr || err.message)
+        return reject(err)
+      } else if (stderr) {
+        printError(argv, stderr, {level: 'warn', color: 'yellow'})
+      }
+      return resolve(stdout)
+    })
+  })
+}
diff --git a/lib/run-lifecycle-hook.js b/lib/run-lifecycle-hook.js
new file mode 100644
index 0000000..caaeeba
--- /dev/null
+++ b/lib/run-lifecycle-hook.js
@@ -0,0 +1,12 @@
+const chalk = require('chalk')
+const checkpoint = require('./checkpoint')
+const figures = require('figures')
+const runExec = require('./run-exec')
+
+module.exports = function (argv, hookName, newVersion, hooks, cb) {
+  if (!hooks[hookName]) return Promise.resolve()
+  var command = hooks[hookName] + ' --new-version="' + newVersion + '"'
+  checkpoint(argv, 'Running lifecycle hook "%s"', [hookName])
+  checkpoint(argv, '- hook command: "%s"', [command], chalk.blue(figures.info))
+  return runExec(argv, command)
+}
diff --git a/lib/run-lifecycle-script.js b/lib/run-lifecycle-script.js
new file mode 100644
index 0000000..2ad322b
--- /dev/null
+++ b/lib/run-lifecycle-script.js
@@ -0,0 +1,13 @@
+const chalk = require('chalk')
+const checkpoint = require('./checkpoint')
+const figures = require('figures')
+const runExec = require('./run-exec')
+
+module.exports = function (argv, hookName, newVersion, scripts, cb) {
+  if (!scripts[hookName]) return Promise.resolve()
+  var command = scripts[hookName]
+  if (newVersion) command += ' --new-version="' + newVersion + '"'
+  checkpoint(argv, 'Running lifecycle script "%s"', [hookName])
+  checkpoint(argv, '- execute command: "%s"', [command], chalk.blue(figures.info))
+  return runExec(argv, command)
+}
diff --git a/package.json b/package.json
index d0222da..7f21bc8 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
     "test": "nyc mocha --timeout=20000 test.js",
     "release": "bin/cli.js"
   },
+  "nyc": {
+    "exclude": ["tmp/**"]
+  },
   "repository": {
     "type": "git",
     "url": "git+https://github.com/conventional-changelog/standard-version.git"
@@ -38,18 +41,16 @@
     "conventional-recommended-bump": "^1.0.0",
     "figures": "^1.5.0",
     "fs-access": "^1.0.0",
-    "object-assign": "^4.1.0",
     "semver": "^5.1.0",
-    "yargs": "^7.0.1"
+    "yargs": "^8.0.1"
   },
   "devDependencies": {
-    "bluebird": "^3.4.6",
     "chai": "^3.5.0",
     "coveralls": "^2.11.9",
     "mocha": "^3.1.0",
     "mock-git": "^1.0.2",
     "mockery": "^2.0.0",
-    "nyc": "^10.0.0",
+    "nyc": "^11.0.2",
     "shelljs": "^0.7.3",
     "standard": "^10.0.0"
   }


Test patch:
diff --git a/test.js b/test.js
index 9547d58..77db31d 100644
--- a/test.js
+++ b/test.js
@@ -2,7 +2,6 @@
 
 'use strict'
 
-var objectAssign = require('object-assign')
 var shell = require('shelljs')
 var fs = require('fs')
 var path = require('path')
@@ -10,11 +9,10 @@ var stream = require('stream')
 var mockGit = require('mock-git')
 var mockery = require('mockery')
 var semver = require('semver')
-var Promise = require('bluebird')
 var cli = require('./command')
 var standardVersion = require('./index')
 
-var should = require('chai').should()
+require('chai').should()
 
 var cliPath = path.resolve(__dirname, './bin/cli.js')
 
@@ -39,25 +37,25 @@ function execCli (argString) {
 }
 
 function execCliAsync (argString) {
-  return Promise.promisify(standardVersion)(cli.parse('standard-version ' + argString + ' --silent'))
+  return standardVersion(cli.parse('standard-version ' + argString + ' --silent'))
 }
 
 function writePackageJson (version, option) {
   option = option || {}
-  var pkg = objectAssign(option, {version: version})
+  var pkg = Object.assign(option, {version: version})
   fs.writeFileSync('package.json', JSON.stringify(pkg), 'utf-8')
   delete require.cache[require.resolve(path.join(process.cwd(), 'package.json'))]
 }
 
 function writeBowerJson (version, option) {
   option = option || {}
-  var bower = objectAssign(option, { version: version })
+  var bower = Object.assign(option, {version: version})
   fs.writeFileSync('bower.json', JSON.stringify(bower), 'utf-8')
 }
 
 function writeNpmShrinkwrapJson (version, option) {
   option = option || {}
-  var shrinkwrap = objectAssign(option, { version: version })
+  var shrinkwrap = Object.assign(option, { version: version })
   fs.writeFileSync('npm-shrinkwrap.json', JSON.stringify(shrinkwrap), 'utf-8')
 }
 
@@ -66,6 +64,18 @@ function writeGitPreCommitHook () {
   fs.chmodSync('.git/hooks/pre-commit', '755')
 }
 
+function writePostBumpHook (causeError) {
+  writeHook('postbump', causeError)
+}
+
+function writeHook (hookName, causeError, script) {
+  shell.mkdir('-p', 'scripts')
+  var 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
@@ -222,6 +232,116 @@ describe('cli', function () {
     })
   })
 
+  describe('lifecycle scripts', () => {
+    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<a name="1.0.0">\n', 'utf-8')
+
+        commit('feat: first commit')
+        var 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<a name="1.0.0">\n', 'utf-8')
+
+        commit('feat: first commit')
+        var 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<a name="1.0.0">\n', 'utf-8')
+
+        commit('feat: first commit')
+        var 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', function () {
+        writePackageJson('1.0.0', {
+          'standard-version': {
+            'scripts': {
+              'precommit': 'node scripts/precommit'
+            }
+          }
+        })
+        writeHook('precommit')
+        fs.writeFileSync('CHANGELOG.md', 'legacy header format<a name="1.0.0">\n', 'utf-8')
+
+        commit('feat: first commit')
+        var 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<a name="1.0.0">\n', 'utf-8')
+
+        commit('feat: first commit')
+        var 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<a name="1.0.0">\n', 'utf-8')
+
+        commit('feat: first commit')
+        var 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')
@@ -467,11 +587,11 @@ describe('standard-version', function () {
       shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
       commit('feat: new feature!')
 
-      require('./index')({silent: true}, function (err) {
-        should.exist(err)
-        err.message.should.match(/bump err/)
-        done()
-      })
+      require('./index')({silent: true})
+        .catch((err) => {
+          err.message.should.match(/bump err/)
+          done()
+        })
     })
   })
 
@@ -497,11 +617,11 @@ describe('standard-version', function () {
       shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
       commit('feat: new feature!')
 
-      require('./index')({silent: true}, function (err) {
-        should.exist(err)
-        err.message.should.match(/changelog err/)
-        done()
-      })
+      require('./index')({silent: true})
+        .catch((err) => {
+          err.message.should.match(/changelog err/)
+          return done()
+        })
     })
   })
 
@@ -510,15 +630,14 @@ describe('standard-version', function () {
     shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
     commit('feat: new feature!')
 
-    require('./index')({silent: true}, function (err) {
-      should.not.exist(err)
-
-      // 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()
-    })
+    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('bower.json support', function () {
@@ -530,12 +649,12 @@ describe('standard-version', function () {
       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}, function (err) {
-        if (err) return done(err)
-        JSON.parse(fs.readFileSync('bower.json', 'utf-8')).version.should.equal('1.1.0')
-        getPackageVersion().should.equal('1.1.0')
-        done()
-      })
+      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')
+          return done()
+        })
     })
   })
 
@@ -548,12 +667,12 @@ describe('standard-version', function () {
       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}, function (err) {
-        if (err) return done(err)
-        JSON.parse(fs.readFileSync('npm-shrinkwrap.json', 'utf-8')).version.should.equal('1.1.0')
-        getPackageVersion().should.equal('1.1.0')
-        done()
-      })
+      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()
+        })
     })
   })
 })