diff --git a/README.md b/README.md
index 9dc47ae17..5e57f4614 100644
--- a/README.md
+++ b/README.md
@@ -156,18 +156,16 @@ If you have your GPG key set up, add the `--sign` or `-s` flag to your `standard
`standard-version` supports lifecycle scripts. These allow you to execute your
own supplementary commands during the release. The following
-hooks are available:
+hooks are available and execute in the order documented:
-* `prebump`: executed before the version bump is calculated. If the `prebump`
+* `prebump`/`postbump`: executed before and after the version is bumped. 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.
+* `prechangelog`/`postchangelog`: executes before and after the CHANGELOG is generated.
+* `precommit`/`postcommit`: called before and after the commit step.
+* `pretag`/`posttag`: called before and after the tagging step.
-Simply add the following to your package.json, to enable lifecycle scripts:
+Simply add the following to your package.json to configure lifecycle scripts:
```json
{
@@ -179,6 +177,21 @@ Simply add the following to your package.json, to enable lifecycle scripts:
}
```
+### Skipping lifecycle steps
+
+You can skip any of the lifecycle steps (`bump`, `changelog`, `commit`, `tag`),
+by adding the following to your package.json:
+
+```json
+{
+ "standard-version": {
+ "skip": {
+ "changelog": true
+ }
+ }
+}
+```
+
### 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/command.js b/command.js
index 3c6522693..0fa0442ca 100755
--- a/command.js
+++ b/command.js
@@ -60,7 +60,11 @@ module.exports = require('yargs')
default: defaults.tagPrefix
})
.option('scripts', {
- describe: 'Scripts to execute for lifecycle events (prebump, precommit, etc.,)',
+ describe: 'Provide scripts to execute for lifecycle events (prebump, precommit, etc.,)',
+ default: defaults.scripts
+ })
+ .option('skip', {
+ describe: 'Map of steps in the release process that should be skipped',
default: defaults.scripts
})
.option('dry-run', {
@@ -71,6 +75,8 @@ module.exports = require('yargs')
.check((argv) => {
if (typeof argv.scripts !== 'object' || Array.isArray(argv.scripts)) {
throw Error('scripts must be an object')
+ } else if (typeof argv.skip !== 'object' || Array.isArray(argv.skip)) {
+ throw Error('skip must be an object')
} else {
return true
}
diff --git a/defaults.json b/defaults.json
index b737c0c68..2834fb345 100644
--- a/defaults.json
+++ b/defaults.json
@@ -8,5 +8,6 @@
"silent": false,
"tagPrefix": "v",
"scripts": {},
+ "skip": {},
"dryRun": false
}
diff --git a/index.js b/index.js
index 5b56f3c42..b5936eeb2 100755
--- a/index.js
+++ b/index.js
@@ -1,19 +1,10 @@
-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')
-const writeFile = require('./lib/write-file')
+
+const bump = require('./lib/lifecycles/bump')
+const changelog = require('./lib/lifecycles/changelog')
+const commit = require('./lib/lifecycles/commit')
+const tag = require('./lib/lifecycles/tag')
module.exports = function standardVersion (argv) {
var pkgPath = path.resolve(process.cwd(), './package.json')
@@ -22,30 +13,17 @@ module.exports = function standardVersion (argv) {
var defaults = require('./defaults')
var args = Object.assign({}, defaults, argv)
- return runLifecycleScript(args, 'prebump', null)
- .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))
- }
-
- return runLifecycleScript(args, 'postbump', newVersion, args)
- })
+ return Promise.resolve()
.then(() => {
- return outputChangelog(args, newVersion)
+ return bump(args, pkg)
})
- .then(() => {
- return runLifecycleScript(args, 'precommit', newVersion, args)
+ .then((_newVersion) => {
+ // if bump runs, it calculaes the new version that we
+ // should release at.
+ if (_newVersion) newVersion = _newVersion
+ return changelog(args, newVersion)
})
- .then((message) => {
- if (message && message.length) args.message = message
+ .then(() => {
return commit(args, newVersion)
})
.then(() => {
@@ -56,203 +34,3 @@ module.exports = function standardVersion (argv) {
throw err
})
}
-
-/**
- * attempt to update the version # in a collection of common config
- * files, e.g., package.json, bower.json.
- *
- * @param args config object
- * @param newVersion version # to update to.
- * @return {string}
- */
-var configsToUpdate = {}
-function updateConfigs (args, newVersion) {
- configsToUpdate[path.resolve(process.cwd(), './package.json')] = false
- configsToUpdate[path.resolve(process.cwd(), './npm-shrinkwrap.json')] = false
- configsToUpdate[path.resolve(process.cwd(), './bower.json')] = false
- Object.keys(configsToUpdate).forEach(function (configPath) {
- try {
- var stat = fs.lstatSync(configPath)
- if (stat.isFile()) {
- var config = require(configPath)
- var filename = path.basename(configPath)
- checkpoint(args, 'bumping version in ' + filename + ' from %s to %s', [config.version, newVersion])
- config.version = newVersion
- writeFile(args, configPath, JSON.stringify(config, null, 2) + '\n')
- // flag any config files that we modify the version # for
- // as having been updated.
- configsToUpdate[configPath] = true
- }
- } catch (err) {
- if (err.code !== 'ENOENT') console.warn(err.message)
- }
- })
-}
-
-function getReleaseType (prerelease, expectedReleaseType, currentVersion) {
- if (isString(prerelease)) {
- if (isInPrerelease(currentVersion)) {
- if (shouldContinuePrerelease(currentVersion, expectedReleaseType) ||
- getTypePriority(getCurrentActiveType(currentVersion)) > getTypePriority(expectedReleaseType)
- ) {
- return 'prerelease'
- }
- }
-
- return 'pre' + expectedReleaseType
- } else {
- return expectedReleaseType
- }
-}
-
-function isString (val) {
- return typeof val === 'string'
-}
-
-/**
- * if a version is currently in pre-release state,
- * and if it current in-pre-release type is same as expect type,
- * it should continue the pre-release with the same type
- *
- * @param version
- * @param expectType
- * @return {boolean}
- */
-function shouldContinuePrerelease (version, expectType) {
- return getCurrentActiveType(version) === expectType
-}
-
-function isInPrerelease (version) {
- return Array.isArray(semver.prerelease(version))
-}
-
-var TypeList = ['major', 'minor', 'patch'].reverse()
-
-/**
- * extract the in-pre-release type in target version
- *
- * @param version
- * @return {string}
- */
-function getCurrentActiveType (version) {
- var typelist = TypeList
- for (var i = 0; i < typelist.length; i++) {
- if (semver[typelist[i]](version)) {
- return typelist[i]
- }
- }
-}
-
-/**
- * calculate the priority of release type,
- * major - 2, minor - 1, patch - 0
- *
- * @param type
- * @return {number}
- */
-function getTypePriority (type) {
- return TypeList.indexOf(type)
-}
-
-function bumpVersion (releaseAs, callback) {
- 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 outputChangelog (args, newVersion) {
- return new Promise((resolve, reject) => {
- createIfMissing(args)
- 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 = args.dryRun ? '' : fs.readFileSync(args.infile, 'utf-8')
- // find the position of the last release and remove header:
- if (oldContent.indexOf(' {
- return runExec(args, 'git commit ' + verify + (args.sign ? '-S ' : '') + (args.commitAll ? '' : (args.infile + toAdd)) + ' -m "' + formatCommitMessage(args.message, newVersion) + '"')
- })
-}
-
-function formatCommitMessage (msg, newVersion) {
- return String(msg).indexOf('%s') !== -1 ? util.format(msg, newVersion) : msg
-}
-
-function tag (newVersion, pkgPrivate, args) {
- var tagOption
- if (args.sign) {
- tagOption = '-s '
- } else {
- tagOption = '-a '
- }
- checkpoint(args, 'tagging release %s', [newVersion])
- return runExec(args, 'git tag ' + tagOption + args.tagPrefix + newVersion + ' -m "' + formatCommitMessage(args.message, newVersion) + '"')
- .then(() => {
- var message = 'git push --follow-tags origin master'
- if (pkgPrivate !== true) message += '; npm publish'
-
- checkpoint(args, 'Run `%s` to publish', [message], chalk.blue(figures.info))
- })
-}
-
-function createIfMissing (args) {
- try {
- accessSync(args.infile, fs.F_OK)
- } catch (err) {
- if (err.code === 'ENOENT') {
- checkpoint(args, 'created %s', [args.infile])
- args.outputUnreleased = true
- writeFile(args, args.infile, '\n')
- }
- }
-}
diff --git a/lib/checkpoint.js b/lib/checkpoint.js
index 516ab568f..634c8b5f7 100644
--- a/lib/checkpoint.js
+++ b/lib/checkpoint.js
@@ -2,10 +2,10 @@ const chalk = require('chalk')
const figures = require('figures')
const util = require('util')
-module.exports = function (args, msg, vars, figure) {
+module.exports = function (argv, msg, args, figure) {
const defaultFigure = args.dryRun ? chalk.yellow(figures.tick) : chalk.green(figures.tick)
- if (!args.silent) {
- console.info((figure || defaultFigure) + ' ' + util.format.apply(util, [msg].concat(vars.map(function (arg) {
+ if (!argv.silent) {
+ console.info((figure || defaultFigure) + ' ' + util.format.apply(util, [msg].concat(args.map(function (arg) {
return chalk.bold(arg)
}))))
}
diff --git a/lib/format-commit-message.js b/lib/format-commit-message.js
new file mode 100644
index 000000000..06b8702fe
--- /dev/null
+++ b/lib/format-commit-message.js
@@ -0,0 +1,5 @@
+const util = require('util')
+
+module.exports = function (msg, newVersion) {
+ return String(msg).indexOf('%s') !== -1 ? util.format(msg, newVersion) : msg
+}
diff --git a/lib/lifecycles/bump.js b/lib/lifecycles/bump.js
new file mode 100644
index 000000000..2508b69a8
--- /dev/null
+++ b/lib/lifecycles/bump.js
@@ -0,0 +1,159 @@
+const chalk = require('chalk')
+const checkpoint = require('../checkpoint')
+const conventionalRecommendedBump = require('conventional-recommended-bump')
+const figures = require('figures')
+const fs = require('fs')
+const path = require('path')
+const runLifecycleScript = require('../run-lifecycle-script')
+const semver = require('semver')
+const writeFile = require('../write-file')
+
+var configsToUpdate = {}
+
+function Bump (args, pkg) {
+ // reset the cache of updated config files each
+ // time we perform the version bump step.
+ configsToUpdate = {}
+
+ if (args.skip.bump) return Promise.resolve()
+ var newVersion = pkg.version
+ return runLifecycleScript(args, 'prebump')
+ .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))
+ }
+ })
+ .then(() => {
+ return runLifecycleScript(args, 'postbump')
+ })
+ .then(() => {
+ return newVersion
+ })
+}
+
+Bump.getUpdatedConfigs = function () {
+ return configsToUpdate
+}
+
+function getReleaseType (prerelease, expectedReleaseType, currentVersion) {
+ if (isString(prerelease)) {
+ if (isInPrerelease(currentVersion)) {
+ if (shouldContinuePrerelease(currentVersion, expectedReleaseType) ||
+ getTypePriority(getCurrentActiveType(currentVersion)) > getTypePriority(expectedReleaseType)
+ ) {
+ return 'prerelease'
+ }
+ }
+
+ return 'pre' + expectedReleaseType
+ } else {
+ return expectedReleaseType
+ }
+}
+
+function isString (val) {
+ return typeof val === 'string'
+}
+
+/**
+ * if a version is currently in pre-release state,
+ * and if it current in-pre-release type is same as expect type,
+ * it should continue the pre-release with the same type
+ *
+ * @param version
+ * @param expectType
+ * @return {boolean}
+ */
+function shouldContinuePrerelease (version, expectType) {
+ return getCurrentActiveType(version) === expectType
+}
+
+function isInPrerelease (version) {
+ return Array.isArray(semver.prerelease(version))
+}
+
+var TypeList = ['major', 'minor', 'patch'].reverse()
+
+/**
+ * extract the in-pre-release type in target version
+ *
+ * @param version
+ * @return {string}
+ */
+function getCurrentActiveType (version) {
+ var typelist = TypeList
+ for (var i = 0; i < typelist.length; i++) {
+ if (semver[typelist[i]](version)) {
+ return typelist[i]
+ }
+ }
+}
+
+/**
+ * calculate the priority of release type,
+ * major - 2, minor - 1, patch - 0
+ *
+ * @param type
+ * @return {number}
+ */
+function getTypePriority (type) {
+ return TypeList.indexOf(type)
+}
+
+function bumpVersion (releaseAs, callback) {
+ 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)
+ })
+ }
+ })
+}
+
+/**
+ * attempt to update the version # in a collection of common config
+ * files, e.g., package.json, bower.json.
+ *
+ * @param args config object
+ * @param newVersion version # to update to.
+ * @return {string}
+ */
+function updateConfigs (args, newVersion) {
+ configsToUpdate[path.resolve(process.cwd(), './package.json')] = false
+ configsToUpdate[path.resolve(process.cwd(), './npm-shrinkwrap.json')] = false
+ configsToUpdate[path.resolve(process.cwd(), './bower.json')] = false
+ Object.keys(configsToUpdate).forEach(function (configPath) {
+ try {
+ var stat = fs.lstatSync(configPath)
+ if (stat.isFile()) {
+ var config = require(configPath)
+ var filename = path.basename(configPath)
+ checkpoint(args, 'bumping version in ' + filename + ' from %s to %s', [config.version, newVersion])
+ config.version = newVersion
+ writeFile(args, configPath, JSON.stringify(config, null, 2) + '\n')
+ // flag any config files that we modify the version # for
+ // as having been updated.
+ configsToUpdate[configPath] = true
+ }
+ } catch (err) {
+ if (err.code !== 'ENOENT') console.warn(err.message)
+ }
+ })
+}
+
+module.exports = Bump
diff --git a/lib/lifecycles/changelog.js b/lib/lifecycles/changelog.js
new file mode 100644
index 000000000..1eb16178e
--- /dev/null
+++ b/lib/lifecycles/changelog.js
@@ -0,0 +1,62 @@
+const accessSync = require('fs-access').sync
+const chalk = require('chalk')
+const checkpoint = require('../checkpoint')
+const conventionalChangelog = require('conventional-changelog')
+const fs = require('fs')
+const runLifecycleScript = require('../run-lifecycle-script')
+const writeFile = require('../write-file')
+
+module.exports = function (args, newVersion) {
+ if (args.skip.changelog) return Promise.resolve()
+ return runLifecycleScript(args, 'prechangelog')
+ .then(() => {
+ return outputChangelog(args, newVersion)
+ })
+ .then(() => {
+ return runLifecycleScript(args, 'postchangelog')
+ })
+}
+
+function outputChangelog (args, newVersion) {
+ return new Promise((resolve, reject) => {
+ createIfMissing(args)
+ 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 = args.dryRun ? '' : fs.readFileSync(args.infile, 'utf-8')
+ // find the position of the last release and remove header:
+ if (oldContent.indexOf(' {
+ if (message && message.length) args.message = message
+ return execCommit(args, newVersion)
+ })
+ .then(() => {
+ return runLifecycleScript(args, 'postcommit')
+ })
+}
+
+function execCommit (args, newVersion) {
+ var msg = 'committing %s'
+ var paths = [args.infile]
+ var verify = args.verify === false || args.n ? '--no-verify ' : ''
+ var toAdd = ''
+ // commit any of the config files that we've updated
+ // the version # for.
+ Object.keys(bump.getUpdatedConfigs()).forEach(function (p) {
+ if (bump.getUpdatedConfigs()[p]) {
+ msg += ' and %s'
+ paths.unshift(path.basename(p))
+ toAdd += ' ' + path.relative(process.cwd(), p)
+ }
+ })
+ checkpoint(args, msg, paths)
+ return runExec(args, 'git add' + toAdd + ' ' + args.infile)
+ .then(() => {
+ return runExec(args, 'git commit ' + verify + (args.sign ? '-S ' : '') + (args.commitAll ? '' : (args.infile + toAdd)) + ' -m "' + formatCommitMessage(args.message, newVersion) + '"')
+ })
+}
diff --git a/lib/lifecycles/tag.js b/lib/lifecycles/tag.js
new file mode 100644
index 000000000..b5bf7aefc
--- /dev/null
+++ b/lib/lifecycles/tag.js
@@ -0,0 +1,34 @@
+const chalk = require('chalk')
+const checkpoint = require('../checkpoint')
+const figures = require('figures')
+const formatCommitMessage = require('../format-commit-message')
+const runExec = require('../run-exec')
+const runLifecycleScript = require('../run-lifecycle-script')
+
+module.exports = function (newVersion, pkgPrivate, args) {
+ if (args.skip.tag) return Promise.resolve()
+ return runLifecycleScript(args, 'pretag')
+ .then(() => {
+ return execTag(newVersion, pkgPrivate, args)
+ })
+ .then(() => {
+ return runLifecycleScript(args, 'posttag')
+ })
+}
+
+function execTag (newVersion, pkgPrivate, args) {
+ var tagOption
+ if (args.sign) {
+ tagOption = '-s '
+ } else {
+ tagOption = '-a '
+ }
+ checkpoint(args, 'tagging release %s', [newVersion])
+ return runExec(args, 'git tag ' + tagOption + args.tagPrefix + newVersion + ' -m "' + formatCommitMessage(args.message, newVersion) + '"')
+ .then(() => {
+ var message = 'git push --follow-tags origin master'
+ if (pkgPrivate !== true) message += '; npm publish'
+
+ checkpoint(args, 'Run `%s` to publish', [message], chalk.blue(figures.info))
+ })
+}
diff --git a/lib/run-lifecycle-script.js b/lib/run-lifecycle-script.js
index f4c2baa07..a5014482b 100644
--- a/lib/run-lifecycle-script.js
+++ b/lib/run-lifecycle-script.js
@@ -3,11 +3,10 @@ const checkpoint = require('./checkpoint')
const figures = require('figures')
const runExec = require('./run-exec')
-module.exports = function (args, hookName, newVersion) {
+module.exports = function (args, hookName) {
const scripts = args.scripts
if (!scripts || !scripts[hookName]) return Promise.resolve()
var command = scripts[hookName]
- if (newVersion) command += ' --new-version="' + newVersion + '"'
checkpoint(args, 'Running lifecycle script "%s"', [hookName])
checkpoint(args, '- execute command: "%s"', [command], chalk.blue(figures.info))
return runExec(args, command)
diff --git a/test.js b/test.js
index e1702fd3a..d5fc9bbbe 100644
--- a/test.js
+++ b/test.js
@@ -688,4 +688,36 @@ describe('standard-version', function () {
return done()
})
})
+
+ describe('skip', () => {
+ it('allows bump and changelog generation to be skipped', function () {
+ let 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')
+ var content = fs.readFileSync('CHANGELOG.md', 'utf-8')
+ content.should.equal(changelogContent)
+ })
+ })
+
+ it('allows the commit phase to be skipped', function () {
+ let 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')
+ var 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/)
+ })
+ })
+ })
})