From d07335353ff78d01baa5a01dfde345ad4e4cad03 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Mon, 5 Jun 2017 15:23:29 -0700 Subject: [PATCH] feat: add dry-run mode (#187) --- README.md | 12 +++++++ command.js | 39 ++++++++++------------- defaults.json | 4 ++- index.js | 63 +++++++++++++++++++------------------ lib/checkpoint.js | 7 +++-- lib/print-error.js | 4 +-- lib/run-exec.js | 7 +++-- lib/run-lifecycle-hook.js | 8 ++--- lib/run-lifecycle-script.js | 11 ++++--- lib/write-file.js | 6 ++++ test.js | 13 ++++++++ 11 files changed, 104 insertions(+), 70 deletions(-) create mode 100644 lib/write-file.js diff --git a/README.md b/README.md index 1440cafa7..9dc47ae17 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,18 @@ If you want to commit generated artifacts in the release commit (e.g. [#96](http "release": "git add && 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 diff --git a/command.js b/command.js index 0c41c6beb..3c6522693 100755 --- a/command.js +++ b/command.js @@ -6,76 +6,71 @@ module.exports = require('yargs') alias: 'r', describe: 'Specify the release type manually (like npm version )', 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 } diff --git a/defaults.json b/defaults.json index 26ef43c73..b737c0c68 100644 --- a/defaults.json +++ b/defaults.json @@ -6,5 +6,7 @@ "noVerify": false, "commitAll": false, "silent": false, - "tagPrefix": "v" + "tagPrefix": "v", + "scripts": {}, + "dryRun": false } diff --git a/index.js b/index.js index 73e5dd143..5b56f3c42 100755 --- a/index.js +++ b/index.js @@ -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) @@ -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 @@ -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} */ @@ -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 @@ -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(' { - 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) + '"') }) } @@ -225,31 +228,31 @@ 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') } } } diff --git a/lib/checkpoint.js b/lib/checkpoint.js index ff7f5d1cb..516ab568f 100644 --- a/lib/checkpoint.js +++ b/lib/checkpoint.js @@ -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) })))) } diff --git a/lib/print-error.js b/lib/print-error.js index 983aeacc5..84aaa8257 100644 --- a/lib/print-error.js +++ b/lib/print-error.js @@ -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' diff --git a/lib/run-exec.js b/lib/run-exec.js index bfca9766b..4ce3cb09f 100644 --- a/lib/run-exec.js +++ b/lib/run-exec.js @@ -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) }) diff --git a/lib/run-lifecycle-hook.js b/lib/run-lifecycle-hook.js index caaeeba06..17efd336a 100644 --- a/lib/run-lifecycle-hook.js +++ b/lib/run-lifecycle-hook.js @@ -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) } diff --git a/lib/run-lifecycle-script.js b/lib/run-lifecycle-script.js index 2ad322b89..f4c2baa07 100644 --- a/lib/run-lifecycle-script.js +++ b/lib/run-lifecycle-script.js @@ -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) } diff --git a/lib/write-file.js b/lib/write-file.js new file mode 100644 index 000000000..b6aaa19f7 --- /dev/null +++ b/lib/write-file.js @@ -0,0 +1,6 @@ +const fs = require('fs') + +module.exports = function (args, filePath, content) { + if (args.dryRun) return + fs.writeFileSync(filePath, content, 'utf8') +} diff --git a/test.js b/test.js index 77db31d60..e1702fd3a 100644 --- a/test.js +++ b/test.js @@ -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() + }) + }) })