Skip to content

Commit d073353

Browse files
authored
feat: add dry-run mode (#187)
1 parent dfd1d12 commit d073353

11 files changed

+104
-70
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ If you want to commit generated artifacts in the release commit (e.g. [#96](http
188188
"release": "git add <file(s) to commit> && standard-version -a"
189189
```
190190

191+
### Dry run mode
192+
193+
running `standard-version` with the flag `--dry-run` allows you to see what
194+
commands would be run, without committing to git or updating files.
195+
196+
```sh
197+
# npm run script
198+
npm run release -- --dry-run
199+
# or global bin
200+
standard-version --dry-run
201+
```
202+
191203
### CLI Help
192204

193205
```sh

command.js

+17-22
Original file line numberDiff line numberDiff line change
@@ -6,76 +6,71 @@ module.exports = require('yargs')
66
alias: 'r',
77
describe: 'Specify the release type manually (like npm version <major|minor|patch>)',
88
requiresArg: true,
9-
string: true,
10-
global: true
9+
string: true
1110
})
1211
.option('prerelease', {
1312
alias: 'p',
1413
describe: 'make a pre-release with optional option value to specify a tag id',
15-
string: true,
16-
global: true
14+
string: true
1715
})
1816
.option('infile', {
1917
alias: 'i',
2018
describe: 'Read the CHANGELOG from this file',
21-
default: defaults.infile,
22-
global: true
19+
default: defaults.infile
2320
})
2421
.option('message', {
2522
alias: 'm',
2623
describe: 'Commit message, replaces %s with new version',
2724
type: 'string',
28-
default: defaults.message,
29-
global: true
25+
default: defaults.message
3026
})
3127
.option('first-release', {
3228
alias: 'f',
3329
describe: 'Is this the first release?',
3430
type: 'boolean',
35-
default: defaults.firstRelease,
36-
global: true
31+
default: defaults.firstRelease
3732
})
3833
.option('sign', {
3934
alias: 's',
4035
describe: 'Should the git commit and tag be signed?',
4136
type: 'boolean',
42-
default: defaults.sign,
43-
global: true
37+
default: defaults.sign
4438
})
4539
.option('no-verify', {
4640
alias: 'n',
4741
describe: 'Bypass pre-commit or commit-msg git hooks during the commit phase',
4842
type: 'boolean',
49-
default: defaults.noVerify,
50-
global: true
43+
default: defaults.noVerify
5144
})
5245
.option('commit-all', {
5346
alias: 'a',
5447
describe: 'Commit all staged changes, not just files affected by standard-version',
5548
type: 'boolean',
56-
default: defaults.commitAll,
57-
global: true
49+
default: defaults.commitAll
5850
})
5951
.option('silent', {
6052
describe: 'Don\'t print logs and errors',
6153
type: 'boolean',
62-
default: defaults.silent,
63-
global: true
54+
default: defaults.silent
6455
})
6556
.option('tag-prefix', {
6657
alias: 't',
6758
describe: 'Set a custom prefix for the git tag to be created',
6859
type: 'string',
69-
default: defaults.tagPrefix,
70-
global: true
60+
default: defaults.tagPrefix
7161
})
7262
.option('scripts', {
7363
describe: 'Scripts to execute for lifecycle events (prebump, precommit, etc.,)',
74-
default: {}
64+
default: defaults.scripts
65+
})
66+
.option('dry-run', {
67+
type: 'boolean',
68+
default: defaults.dryRun,
69+
describe: 'See the commands that running standard-version would run'
7570
})
7671
.check((argv) => {
7772
if (typeof argv.scripts !== 'object' || Array.isArray(argv.scripts)) {
78-
throw Error('hooks must be an object')
73+
throw Error('scripts must be an object')
7974
} else {
8075
return true
8176
}

defaults.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
"noVerify": false,
77
"commitAll": false,
88
"silent": false,
9-
"tagPrefix": "v"
9+
"tagPrefix": "v",
10+
"scripts": {},
11+
"dryRun": false
1012
}

index.js

+33-30
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ const checkpoint = require('./lib/checkpoint')
1313
const printError = require('./lib/print-error')
1414
const runExec = require('./lib/run-exec')
1515
const runLifecycleScript = require('./lib/run-lifecycle-script')
16+
const writeFile = require('./lib/write-file')
1617

1718
module.exports = function standardVersion (argv) {
1819
var pkgPath = path.resolve(process.cwd(), './package.json')
1920
var pkg = require(pkgPath)
2021
var newVersion = pkg.version
21-
var scripts = argv.scripts || {}
2222
var defaults = require('./defaults')
2323
var args = Object.assign({}, defaults, argv)
2424

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

39-
return runLifecycleScript(args, 'postbump', newVersion, scripts)
39+
return runLifecycleScript(args, 'postbump', newVersion, args)
4040
})
4141
.then(() => {
42-
return outputChangelog(args)
42+
return outputChangelog(args, newVersion)
4343
})
4444
.then(() => {
45-
return runLifecycleScript(args, 'precommit', newVersion, scripts)
45+
return runLifecycleScript(args, 'precommit', newVersion, args)
4646
})
4747
.then((message) => {
4848
if (message && message.length) args.message = message
@@ -61,7 +61,7 @@ module.exports = function standardVersion (argv) {
6161
* attempt to update the version # in a collection of common config
6262
* files, e.g., package.json, bower.json.
6363
*
64-
* @param argv config object
64+
* @param args config object
6565
* @param newVersion version # to update to.
6666
* @return {string}
6767
*/
@@ -78,7 +78,7 @@ function updateConfigs (args, newVersion) {
7878
var filename = path.basename(configPath)
7979
checkpoint(args, 'bumping version in ' + filename + ' from %s to %s', [config.version, newVersion])
8080
config.version = newVersion
81-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
81+
writeFile(args, configPath, JSON.stringify(config, null, 2) + '\n')
8282
// flag any config files that we modify the version # for
8383
// as having been updated.
8484
configsToUpdate[configPath] = true
@@ -171,19 +171,21 @@ function bumpVersion (releaseAs, callback) {
171171
})
172172
}
173173

174-
function outputChangelog (argv) {
174+
function outputChangelog (args, newVersion) {
175175
return new Promise((resolve, reject) => {
176-
createIfMissing(argv)
176+
createIfMissing(args)
177177
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'
178-
var oldContent = fs.readFileSync(argv.infile, 'utf-8')
178+
var oldContent = args.dryRun ? '' : fs.readFileSync(args.infile, 'utf-8')
179179
// find the position of the last release and remove header:
180180
if (oldContent.indexOf('<a name=') !== -1) {
181181
oldContent = oldContent.substring(oldContent.indexOf('<a name='))
182182
}
183183
var content = ''
184+
var context
185+
if (args.dryRun) context = {version: newVersion}
184186
var changelogStream = conventionalChangelog({
185187
preset: 'angular'
186-
}, undefined, {merges: null})
188+
}, context, {merges: null})
187189
.on('error', function (err) {
188190
return reject(err)
189191
})
@@ -193,63 +195,64 @@ function outputChangelog (argv) {
193195
})
194196

195197
changelogStream.on('end', function () {
196-
checkpoint(argv, 'outputting changes to %s', [argv.infile])
197-
fs.writeFileSync(argv.infile, header + '\n' + (content + oldContent).replace(/\n+$/, '\n'), 'utf-8')
198+
checkpoint(args, 'outputting changes to %s', [args.infile])
199+
if (args.dryRun) console.info(`\n---\n${chalk.gray(content.trim())}\n---\n`)
200+
else writeFile(args, args.infile, header + '\n' + (content + oldContent).replace(/\n+$/, '\n'))
198201
return resolve()
199202
})
200203
})
201204
}
202205

203-
function commit (argv, newVersion) {
206+
function commit (args, newVersion) {
204207
var msg = 'committing %s'
205-
var args = [argv.infile]
206-
var verify = argv.verify === false || argv.n ? '--no-verify ' : ''
208+
var paths = [args.infile]
209+
var verify = args.verify === false || args.n ? '--no-verify ' : ''
207210
var toAdd = ''
208211
// commit any of the config files that we've updated
209212
// the version # for.
210213
Object.keys(configsToUpdate).forEach(function (p) {
211214
if (configsToUpdate[p]) {
212215
msg += ' and %s'
213-
args.unshift(path.basename(p))
216+
paths.unshift(path.basename(p))
214217
toAdd += ' ' + path.relative(process.cwd(), p)
215218
}
216219
})
217-
checkpoint(argv, msg, args)
218-
return runExec(argv, 'git add' + toAdd + ' ' + argv.infile)
220+
checkpoint(args, msg, paths)
221+
return runExec(args, 'git add' + toAdd + ' ' + args.infile)
219222
.then(() => {
220-
return runExec(argv, 'git commit ' + verify + (argv.sign ? '-S ' : '') + (argv.commitAll ? '' : (argv.infile + toAdd)) + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"')
223+
return runExec(args, 'git commit ' + verify + (args.sign ? '-S ' : '') + (args.commitAll ? '' : (args.infile + toAdd)) + ' -m "' + formatCommitMessage(args.message, newVersion) + '"')
221224
})
222225
}
223226

224227
function formatCommitMessage (msg, newVersion) {
225228
return String(msg).indexOf('%s') !== -1 ? util.format(msg, newVersion) : msg
226229
}
227230

228-
function tag (newVersion, pkgPrivate, argv) {
231+
function tag (newVersion, pkgPrivate, args) {
229232
var tagOption
230-
if (argv.sign) {
233+
if (args.sign) {
231234
tagOption = '-s '
232235
} else {
233236
tagOption = '-a '
234237
}
235-
checkpoint(argv, 'tagging release %s', [newVersion])
236-
return runExec(argv, 'git tag ' + tagOption + argv.tagPrefix + newVersion + ' -m "' + formatCommitMessage(argv.message, newVersion) + '"')
238+
checkpoint(args, 'tagging release %s', [newVersion])
239+
return runExec(args, 'git tag ' + tagOption + args.tagPrefix + newVersion + ' -m "' + formatCommitMessage(args.message, newVersion) + '"')
237240
.then(() => {
238241
var message = 'git push --follow-tags origin master'
239242
if (pkgPrivate !== true) message += '; npm publish'
240243

241-
checkpoint(argv, 'Run `%s` to publish', [message], chalk.blue(figures.info))
244+
checkpoint(args, 'Run `%s` to publish', [message], chalk.blue(figures.info))
242245
})
243246
}
244247

245-
function createIfMissing (argv) {
248+
function createIfMissing (args) {
246249
try {
247-
accessSync(argv.infile, fs.F_OK)
250+
accessSync(args.infile, fs.F_OK)
248251
} catch (err) {
249252
if (err.code === 'ENOENT') {
250-
checkpoint(argv, 'created %s', [argv.infile])
251-
argv.outputUnreleased = true
252-
fs.writeFileSync(argv.infile, '\n', 'utf-8')
253+
checkpoint(args, 'created %s', [args.infile])
254+
args.outputUnreleased = true
255+
writeFile(args, args.infile, '\n')
253256
}
254257
}
255258
}

lib/checkpoint.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ const chalk = require('chalk')
22
const figures = require('figures')
33
const util = require('util')
44

5-
module.exports = function (argv, msg, args, figure) {
6-
if (!argv.silent) {
7-
console.info((figure || chalk.green(figures.tick)) + ' ' + util.format.apply(util, [msg].concat(args.map(function (arg) {
5+
module.exports = function (args, msg, vars, figure) {
6+
const defaultFigure = args.dryRun ? chalk.yellow(figures.tick) : chalk.green(figures.tick)
7+
if (!args.silent) {
8+
console.info((figure || defaultFigure) + ' ' + util.format.apply(util, [msg].concat(vars.map(function (arg) {
89
return chalk.bold(arg)
910
}))))
1011
}

lib/print-error.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const chalk = require('chalk')
22

3-
module.exports = function (argv, msg, opts) {
4-
if (!argv.silent) {
3+
module.exports = function (args, msg, opts) {
4+
if (!args.silent) {
55
opts = Object.assign({
66
level: 'error',
77
color: 'red'

lib/run-exec.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
const exec = require('child_process').exec
22
const printError = require('./print-error')
33

4-
module.exports = function (argv, cmd) {
4+
module.exports = function (args, cmd) {
5+
if (args.dryRun) return Promise.resolve()
56
return new Promise((resolve, reject) => {
67
// Exec given cmd and handle possible errors
78
exec(cmd, function (err, stdout, stderr) {
89
// If exec returns content in stderr, but no error, print it as a warning
910
// If exec returns an error, print it and exit with return code 1
1011
if (err) {
11-
printError(argv, stderr || err.message)
12+
printError(args, stderr || err.message)
1213
return reject(err)
1314
} else if (stderr) {
14-
printError(argv, stderr, {level: 'warn', color: 'yellow'})
15+
printError(args, stderr, {level: 'warn', color: 'yellow'})
1516
}
1617
return resolve(stdout)
1718
})

lib/run-lifecycle-hook.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ const checkpoint = require('./checkpoint')
33
const figures = require('figures')
44
const runExec = require('./run-exec')
55

6-
module.exports = function (argv, hookName, newVersion, hooks, cb) {
6+
module.exports = function (args, hookName, newVersion, hooks, cb) {
77
if (!hooks[hookName]) return Promise.resolve()
88
var command = hooks[hookName] + ' --new-version="' + newVersion + '"'
9-
checkpoint(argv, 'Running lifecycle hook "%s"', [hookName])
10-
checkpoint(argv, '- hook command: "%s"', [command], chalk.blue(figures.info))
11-
return runExec(argv, command)
9+
checkpoint(args, 'Running lifecycle hook "%s"', [hookName])
10+
checkpoint(args, '- hook command: "%s"', [command], chalk.blue(figures.info))
11+
return runExec(args, command)
1212
}

lib/run-lifecycle-script.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ const checkpoint = require('./checkpoint')
33
const figures = require('figures')
44
const runExec = require('./run-exec')
55

6-
module.exports = function (argv, hookName, newVersion, scripts, cb) {
7-
if (!scripts[hookName]) return Promise.resolve()
6+
module.exports = function (args, hookName, newVersion) {
7+
const scripts = args.scripts
8+
if (!scripts || !scripts[hookName]) return Promise.resolve()
89
var command = scripts[hookName]
910
if (newVersion) command += ' --new-version="' + newVersion + '"'
10-
checkpoint(argv, 'Running lifecycle script "%s"', [hookName])
11-
checkpoint(argv, '- execute command: "%s"', [command], chalk.blue(figures.info))
12-
return runExec(argv, command)
11+
checkpoint(args, 'Running lifecycle script "%s"', [hookName])
12+
checkpoint(args, '- execute command: "%s"', [command], chalk.blue(figures.info))
13+
return runExec(args, command)
1314
}

lib/write-file.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const fs = require('fs')
2+
3+
module.exports = function (args, filePath, content) {
4+
if (args.dryRun) return
5+
fs.writeFileSync(filePath, content, 'utf8')
6+
}

test.js

+13
Original file line numberDiff line numberDiff line change
@@ -675,4 +675,17 @@ describe('standard-version', function () {
675675
})
676676
})
677677
})
678+
679+
describe('dry-run', function () {
680+
it('skips all non-idempotent steps', function (done) {
681+
commit('feat: first commit')
682+
shell.exec('git tag -a v1.0.0 -m "my awesome first release"')
683+
commit('feat: new feature!')
684+
execCli('--dry-run').stdout.should.match(/### Features/)
685+
shell.exec('git log --oneline -n1').stdout.should.match(/feat: new feature!/)
686+
shell.exec('git tag').stdout.should.match(/1\.0\.0/)
687+
getPackageVersion().should.equal('1.0.0')
688+
return done()
689+
})
690+
})
678691
})

0 commit comments

Comments
 (0)