standard-version
Advanced tools
Comparing version 4.1.0-candidate.0 to 4.1.0-candidate.1
@@ -9,4 +9,3 @@ var defaults = require('./defaults') | ||
requiresArg: true, | ||
string: true, | ||
global: true | ||
string: true | ||
}) | ||
@@ -16,4 +15,3 @@ .option('prerelease', { | ||
describe: 'make a pre-release with optional option value to specify a tag id', | ||
string: true, | ||
global: true | ||
string: true | ||
}) | ||
@@ -23,4 +21,3 @@ .option('infile', { | ||
describe: 'Read the CHANGELOG from this file', | ||
default: defaults.infile, | ||
global: true | ||
default: defaults.infile | ||
}) | ||
@@ -31,4 +28,3 @@ .option('message', { | ||
type: 'string', | ||
default: defaults.message, | ||
global: true | ||
default: defaults.message | ||
}) | ||
@@ -39,4 +35,3 @@ .option('first-release', { | ||
type: 'boolean', | ||
default: defaults.firstRelease, | ||
global: true | ||
default: defaults.firstRelease | ||
}) | ||
@@ -47,4 +42,3 @@ .option('sign', { | ||
type: 'boolean', | ||
default: defaults.sign, | ||
global: true | ||
default: defaults.sign | ||
}) | ||
@@ -55,4 +49,3 @@ .option('no-verify', { | ||
type: 'boolean', | ||
default: defaults.noVerify, | ||
global: true | ||
default: defaults.noVerify | ||
}) | ||
@@ -63,4 +56,3 @@ .option('commit-all', { | ||
type: 'boolean', | ||
default: defaults.commitAll, | ||
global: true | ||
default: defaults.commitAll | ||
}) | ||
@@ -70,4 +62,3 @@ .option('silent', { | ||
type: 'boolean', | ||
default: defaults.silent, | ||
global: true | ||
default: defaults.silent | ||
}) | ||
@@ -78,12 +69,22 @@ .option('tag-prefix', { | ||
type: 'string', | ||
default: defaults.tagPrefix, | ||
global: true | ||
default: defaults.tagPrefix | ||
}) | ||
.option('scripts', { | ||
describe: 'Scripts to execute for lifecycle events (prebump, precommit, etc.,)', | ||
default: {} | ||
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', { | ||
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 if (typeof argv.skip !== 'object' || Array.isArray(argv.skip)) { | ||
throw Error('skip must be an object') | ||
} else { | ||
@@ -90,0 +91,0 @@ return true |
@@ -9,3 +9,6 @@ { | ||
"silent": false, | ||
"tagPrefix": "v" | ||
"tagPrefix": "v", | ||
"scripts": {}, | ||
"skip": {}, | ||
"dryRun": false | ||
} |
245
index.js
@@ -1,17 +0,9 @@ | ||
const conventionalRecommendedBump = require('conventional-recommended-bump') | ||
const conventionalChangelog = require('conventional-changelog') | ||
const path = require('path') | ||
const printError = require('./lib/print-error') | ||
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 bump = require('./lib/lifecycles/bump') | ||
const changelog = require('./lib/lifecycles/changelog') | ||
const commit = require('./lib/lifecycles/commit') | ||
const tag = require('./lib/lifecycles/tag') | ||
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) { | ||
@@ -21,30 +13,16 @@ var pkgPath = path.resolve(process.cwd(), './package.json') | ||
var newVersion = pkg.version | ||
var scripts = argv.scripts || {} | ||
var defaults = require('./defaults') | ||
var args = Object.assign({}, defaults, argv) | ||
return runLifecycleScript(args, 'prebump', null, scripts) | ||
.then((stdout) => { | ||
if (stdout && stdout.trim().length) args.releaseAs = stdout.trim() | ||
return bumpVersion(args.releaseAs) | ||
return Promise.resolve() | ||
.then(() => { | ||
return bump(args, pkg) | ||
}) | ||
.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, scripts) | ||
.then((_newVersion) => { | ||
// if bump runs, it calculaes the new version that we | ||
// should release at. | ||
if (_newVersion) newVersion = _newVersion | ||
return changelog(args, newVersion) | ||
}) | ||
.then(() => { | ||
return outputChangelog(args) | ||
}) | ||
.then(() => { | ||
return runLifecycleScript(args, 'precommit', newVersion, scripts) | ||
}) | ||
.then((message) => { | ||
if (message && message.length) args.message = message | ||
return commit(args, newVersion) | ||
@@ -60,198 +38,1 @@ }) | ||
} | ||
/** | ||
* attempt to update the version # in a collection of common config | ||
* files, e.g., package.json, bower.json. | ||
* | ||
* @param argv 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 | ||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8') | ||
// 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 (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=')) | ||
} | ||
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) { | ||
var msg = 'committing %s' | ||
var args = [argv.infile] | ||
var verify = argv.verify === false || argv.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)) | ||
toAdd += ' ' + path.relative(process.cwd(), p) | ||
} | ||
}) | ||
checkpoint(argv, msg, args) | ||
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) { | ||
var tagOption | ||
if (argv.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) + '"') | ||
.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)) | ||
}) | ||
} | ||
function createIfMissing (argv) { | ||
try { | ||
accessSync(argv.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') | ||
} | ||
} | ||
} |
@@ -6,4 +6,5 @@ const chalk = require('chalk') | ||
module.exports = function (argv, msg, args, figure) { | ||
const defaultFigure = args.dryRun ? chalk.yellow(figures.tick) : chalk.green(figures.tick) | ||
if (!argv.silent) { | ||
console.info((figure || chalk.green(figures.tick)) + ' ' + util.format.apply(util, [msg].concat(args.map(function (arg) { | ||
console.info((figure || defaultFigure) + ' ' + util.format.apply(util, [msg].concat(args.map(function (arg) { | ||
return chalk.bold(arg) | ||
@@ -10,0 +11,0 @@ })))) |
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({ | ||
@@ -6,0 +6,0 @@ level: 'error', |
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) => { | ||
@@ -11,6 +12,6 @@ // Exec given cmd and handle possible errors | ||
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'}) | ||
} | ||
@@ -17,0 +18,0 @@ return resolve(stdout) |
@@ -6,8 +6,8 @@ const chalk = require('chalk') | ||
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) | ||
} |
@@ -6,9 +6,10 @@ const chalk = require('chalk') | ||
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) | ||
} |
{ | ||
"name": "standard-version", | ||
"version": "4.1.0-candidate.0", | ||
"version": "4.1.0-candidate.1", | ||
"description": "replacement for `npm version` with automatic CHANGELOG generation", | ||
@@ -5,0 +5,0 @@ "bin": "bin/cli.js", |
@@ -159,14 +159,12 @@ # Standard Version | ||
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: | ||
@@ -183,2 +181,17 @@ ```json | ||
### 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 | ||
@@ -193,2 +206,14 @@ | ||
### 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 | ||
@@ -195,0 +220,0 @@ |
45
test.js
@@ -678,2 +678,47 @@ /* global describe it beforeEach afterEach */ | ||
}) | ||
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() | ||
}) | ||
}) | ||
describe('skip', () => { | ||
it('allows bump and changelog generation to be skipped', function () { | ||
let changelogContent = 'legacy header format<a name="1.0.0">\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<a name="1.0.0">\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/) | ||
}) | ||
}) | ||
}) | ||
}) |
63013
24
1088
313
8