Skip to content

Commit

Permalink
feat: add prebump, postbump, precommit, lifecycle scripts (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe authored Jun 5, 2017
1 parent 86af7fc commit dfd1d12
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 169 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -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%
Expand Down
7 changes: 3 additions & 4 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
})
}
12 changes: 12 additions & 0 deletions command.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
220 changes: 100 additions & 120 deletions index.js
Original file line number Diff line number Diff line change
@@ -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
})
})
}

/**
Expand Down Expand Up @@ -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 ' : ''
Expand All @@ -215,32 +215,31 @@ 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 '
} else {
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) {
Expand All @@ -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))
}
}
11 changes: 11 additions & 0 deletions lib/checkpoint.js
Original file line number Diff line number Diff line change
@@ -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)
}))))
}
}
12 changes: 12 additions & 0 deletions lib/print-error.js
Original file line number Diff line number Diff line change
@@ -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))
}
}
Loading

0 comments on commit dfd1d12

Please sign in to comment.