Skip to content

Commit

Permalink
feat: add dry-run mode (conventional-changelog#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcoe authored Jun 5, 2017
1 parent dfd1d12 commit d073353
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 70 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ If you want to commit generated artifacts in the release commit (e.g. [#96](http
"release": "git add <file(s) to commit> && standard-version -a"
```

### Dry run mode

running `standard-version` with the flag `--dry-run` allows you to see what
commands would be run, without committing to git or updating files.

```sh
# npm run script
npm run release -- --dry-run
# or global bin
standard-version --dry-run
```

### CLI Help

```sh
Expand Down
39 changes: 17 additions & 22 deletions command.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,71 @@ module.exports = require('yargs')
alias: 'r',
describe: 'Specify the release type manually (like npm version <major|minor|patch>)',
requiresArg: true,
string: true,
global: true
string: true
})
.option('prerelease', {
alias: 'p',
describe: 'make a pre-release with optional option value to specify a tag id',
string: true,
global: true
string: true
})
.option('infile', {
alias: 'i',
describe: 'Read the CHANGELOG from this file',
default: defaults.infile,
global: true
default: defaults.infile
})
.option('message', {
alias: 'm',
describe: 'Commit message, replaces %s with new version',
type: 'string',
default: defaults.message,
global: true
default: defaults.message
})
.option('first-release', {
alias: 'f',
describe: 'Is this the first release?',
type: 'boolean',
default: defaults.firstRelease,
global: true
default: defaults.firstRelease
})
.option('sign', {
alias: 's',
describe: 'Should the git commit and tag be signed?',
type: 'boolean',
default: defaults.sign,
global: true
default: defaults.sign
})
.option('no-verify', {
alias: 'n',
describe: 'Bypass pre-commit or commit-msg git hooks during the commit phase',
type: 'boolean',
default: defaults.noVerify,
global: true
default: defaults.noVerify
})
.option('commit-all', {
alias: 'a',
describe: 'Commit all staged changes, not just files affected by standard-version',
type: 'boolean',
default: defaults.commitAll,
global: true
default: defaults.commitAll
})
.option('silent', {
describe: 'Don\'t print logs and errors',
type: 'boolean',
default: defaults.silent,
global: true
default: defaults.silent
})
.option('tag-prefix', {
alias: 't',
describe: 'Set a custom prefix for the git tag to be created',
type: 'string',
default: defaults.tagPrefix,
global: true
default: defaults.tagPrefix
})
.option('scripts', {
describe: 'Scripts to execute for lifecycle events (prebump, precommit, etc.,)',
default: {}
default: defaults.scripts
})
.option('dry-run', {
type: 'boolean',
default: defaults.dryRun,
describe: 'See the commands that running standard-version would run'
})
.check((argv) => {
if (typeof argv.scripts !== 'object' || Array.isArray(argv.scripts)) {
throw Error('hooks must be an object')
throw Error('scripts must be an object')
} else {
return true
}
Expand Down
4 changes: 3 additions & 1 deletion defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
"noVerify": false,
"commitAll": false,
"silent": false,
"tagPrefix": "v"
"tagPrefix": "v",
"scripts": {},
"dryRun": false
}
63 changes: 33 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ 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')

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 = Object.assign({}, defaults, argv)

return runLifecycleScript(args, 'prebump', null, scripts)
return runLifecycleScript(args, 'prebump', null)
.then((stdout) => {
if (stdout && stdout.trim().length) args.releaseAs = stdout.trim()
return bumpVersion(args.releaseAs)
Expand All @@ -36,13 +36,13 @@ module.exports = function standardVersion (argv) {
checkpoint(args, 'skip version bump on first release', [], chalk.red(figures.cross))
}

return runLifecycleScript(args, 'postbump', newVersion, scripts)
return runLifecycleScript(args, 'postbump', newVersion, args)
})
.then(() => {
return outputChangelog(args)
return outputChangelog(args, newVersion)
})
.then(() => {
return runLifecycleScript(args, 'precommit', newVersion, scripts)
return runLifecycleScript(args, 'precommit', newVersion, args)
})
.then((message) => {
if (message && message.length) args.message = message
Expand All @@ -61,7 +61,7 @@ module.exports = function standardVersion (argv) {
* attempt to update the version # in a collection of common config
* files, e.g., package.json, bower.json.
*
* @param argv config object
* @param args config object
* @param newVersion version # to update to.
* @return {string}
*/
Expand All @@ -78,7 +78,7 @@ function updateConfigs (args, newVersion) {
var filename = path.basename(configPath)
checkpoint(args, 'bumping version in ' + filename + ' from %s to %s', [config.version, newVersion])
config.version = newVersion
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
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
Expand Down Expand Up @@ -171,19 +171,21 @@ function bumpVersion (releaseAs, callback) {
})
}

function outputChangelog (argv) {
function outputChangelog (args, newVersion) {
return new Promise((resolve, reject) => {
createIfMissing(argv)
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 = fs.readFileSync(argv.infile, 'utf-8')
var oldContent = args.dryRun ? '' : fs.readFileSync(args.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 context
if (args.dryRun) context = {version: newVersion}
var changelogStream = conventionalChangelog({
preset: 'angular'
}, undefined, {merges: null})
}, context, {merges: null})
.on('error', function (err) {
return reject(err)
})
Expand All @@ -193,63 +195,64 @@ function outputChangelog (argv) {
})

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')
checkpoint(args, 'outputting changes to %s', [args.infile])
if (args.dryRun) console.info(`\n---\n${chalk.gray(content.trim())}\n---\n`)
else writeFile(args, args.infile, header + '\n' + (content + oldContent).replace(/\n+$/, '\n'))
return resolve()
})
})
}

function commit (argv, newVersion) {
function commit (args, newVersion) {
var msg = 'committing %s'
var args = [argv.infile]
var verify = argv.verify === false || argv.n ? '--no-verify ' : ''
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(configsToUpdate).forEach(function (p) {
if (configsToUpdate[p]) {
msg += ' and %s'
args.unshift(path.basename(p))
paths.unshift(path.basename(p))
toAdd += ' ' + path.relative(process.cwd(), p)
}
})
checkpoint(argv, msg, args)
return runExec(argv, 'git add' + toAdd + ' ' + argv.infile)
checkpoint(args, msg, paths)
return runExec(args, 'git add' + toAdd + ' ' + args.infile)
.then(() => {
return runExec(argv, 'git commit ' + verify + (argv.sign ? '-S ' : '') + (argv.commitAll ? '' : (argv.infile + toAdd)) + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"')
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, argv) {
function tag (newVersion, pkgPrivate, args) {
var tagOption
if (argv.sign) {
if (args.sign) {
tagOption = '-s '
} else {
tagOption = '-a '
}
checkpoint(argv, 'tagging release %s', [newVersion])
return runExec(argv, 'git tag ' + tagOption + argv.tagPrefix + newVersion + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"')
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(argv, 'Run `%s` to publish', [message], chalk.blue(figures.info))
checkpoint(args, 'Run `%s` to publish', [message], chalk.blue(figures.info))
})
}

function createIfMissing (argv) {
function createIfMissing (args) {
try {
accessSync(argv.infile, fs.F_OK)
accessSync(args.infile, fs.F_OK)
} catch (err) {
if (err.code === 'ENOENT') {
checkpoint(argv, 'created %s', [argv.infile])
argv.outputUnreleased = true
fs.writeFileSync(argv.infile, '\n', 'utf-8')
checkpoint(args, 'created %s', [args.infile])
args.outputUnreleased = true
writeFile(args, args.infile, '\n')
}
}
}
7 changes: 4 additions & 3 deletions lib/checkpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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) {
module.exports = function (args, msg, vars, 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) {
return chalk.bold(arg)
}))))
}
Expand Down
4 changes: 2 additions & 2 deletions lib/print-error.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const chalk = require('chalk')

module.exports = function (argv, msg, opts) {
if (!argv.silent) {
module.exports = function (args, msg, opts) {
if (!args.silent) {
opts = Object.assign({
level: 'error',
color: 'red'
Expand Down
7 changes: 4 additions & 3 deletions lib/run-exec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
const exec = require('child_process').exec
const printError = require('./print-error')

module.exports = function (argv, cmd) {
module.exports = function (args, cmd) {
if (args.dryRun) return Promise.resolve()
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)
printError(args, stderr || err.message)
return reject(err)
} else if (stderr) {
printError(argv, stderr, {level: 'warn', color: 'yellow'})
printError(args, stderr, {level: 'warn', color: 'yellow'})
}
return resolve(stdout)
})
Expand Down
8 changes: 4 additions & 4 deletions lib/run-lifecycle-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ const checkpoint = require('./checkpoint')
const figures = require('figures')
const runExec = require('./run-exec')

module.exports = function (argv, hookName, newVersion, hooks, cb) {
module.exports = function (args, 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)
checkpoint(args, 'Running lifecycle hook "%s"', [hookName])
checkpoint(args, '- hook command: "%s"', [command], chalk.blue(figures.info))
return runExec(args, command)
}
11 changes: 6 additions & 5 deletions lib/run-lifecycle-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ 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()
module.exports = function (args, hookName, newVersion) {
const scripts = args.scripts
if (!scripts || !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)
checkpoint(args, 'Running lifecycle script "%s"', [hookName])
checkpoint(args, '- execute command: "%s"', [command], chalk.blue(figures.info))
return runExec(args, command)
}
6 changes: 6 additions & 0 deletions lib/write-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const fs = require('fs')

module.exports = function (args, filePath, content) {
if (args.dryRun) return
fs.writeFileSync(filePath, content, 'utf8')
}
13 changes: 13 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -675,4 +675,17 @@ describe('standard-version', function () {
})
})
})

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()
})
})
})

0 comments on commit d073353

Please sign in to comment.