diff options
-rw-r--r-- | doc/cli/npm-version.md | 39 | ||||
-rw-r--r-- | lib/version.js | 58 | ||||
-rw-r--r-- | test/tap/version-git-not-clean.js | 38 | ||||
-rw-r--r-- | test/tap/version-lifecycle.js | 103 |
4 files changed, 198 insertions, 40 deletions
diff --git a/doc/cli/npm-version.md b/doc/cli/npm-version.md index 21295027f..0527a4d71 100644 --- a/doc/cli/npm-version.md +++ b/doc/cli/npm-version.md @@ -19,10 +19,11 @@ valid second argument to semver.inc (one of `patch`, `minor`, `major`, `prepatch`, `preminor`, `premajor`, `prerelease`). In the second case, the existing version will be incremented by 1 in the specified field. -If run in a git repo, it will also create a version commit and tag, and fail if -the repo is not clean. This behavior is controlled by `git-tag-version` (see -below), and can be disabled on the command line by running `npm ---no-git-tag-version version` +If run in a git repo, it will also create a version commit and tag. +This behavior is controlled by `git-tag-version` (see below), and can +be disabled on the command line by running `npm --no-git-tag-version version`. +It will fail if the working directory is not clean, unless the `--force` +flag is set. If supplied with `--message` (shorthand: `-m`) config option, npm will use it as a commit message when creating a version commit. If the @@ -46,11 +47,33 @@ in your git config for this to work properly. For example: If `preversion`, `version`, or `postversion` are in the `scripts` property of the package.json, they will be executed as part of running `npm version`. -`preversion` and `version` are executed before bumping the package version, and -`postversion` is executed afterwards. For example, to run `npm version` only if -all tests pass: - "scripts": { "preversion": "npm test" } +The exact order of execution is as follows: + 1. Check to make sure the git working directory is clean before we get started. + Your scripts may add files to the commit in future steps. + This step is skipped if the `--force` flag is set. + 2. Run the `preversion` script. These scripts have access to the old `version` in package.json. + A typical use would be running your full test suite before deploying. + Any files you want added to the commit should be explicitly added using `git add`. + 3. Bump `version` in `package.json` as requested (`patch`, `minor`, `major`, etc). + 4. Run the `version` script. These scripts have access to the new `version` in package.json + (so they can incorporate it into file headers in generated files for example). + Again, scripts should explicitly add generated files to the commit using `git add`. + 5. Commit and tag. + 6. Run the `postversion` script. Use it to clean up the file system or automatically push + the commit and/or tag. + +Take the following example: + + "scripts": { + "preversion": "npm test", + "version": "npm run build && git add -A dist", + "postversion": "git push && git push --tags && rm -rf build/temp" + } + +This runs all your tests, and proceeds only if they pass. Then runs your `build` script, and +adds everything in the `dist` directory to the commit. After the commit, it pushes the new commit +and tag up to the server, and deletes the `build/temp` directory. ## CONFIGURATION diff --git a/lib/version.js b/lib/version.js index c93e3b850..b33392488 100644 --- a/lib/version.js +++ b/lib/version.js @@ -52,34 +52,52 @@ function version (args, silent, cb_) { data.version = newVersion var lifecycleData = Object.create(data) lifecycleData._id = data.name + '@' + newVersion + var localData = {} var where = npm.prefix chain([ - [lifecycle, lifecycleData, 'preversion', where], - [version_, data, silent], - [lifecycle, lifecycleData, 'version', where], - [lifecycle, lifecycleData, 'postversion', where] ], - cb_) + [checkGit, localData], + [lifecycle, lifecycleData, 'preversion', where], + [updatePackage, newVersion, silent], + [lifecycle, lifecycleData, 'version', where], + [commit, localData, newVersion], + [lifecycle, lifecycleData, 'postversion', where] ], + cb_) }) } -function version_ (data, silent, cb_) { +function readPackage (cb) { + var packagePath = path.join(npm.localPrefix, 'package.json') + fs.readFile(packagePath, function (er, data) { + if (er) return cb(new Error(er)) + if (data) data = data.toString() + try { + data = JSON.parse(data) + } catch (e) { + er = e + data = null + } + cb(er, data) + }) +} + +function updatePackage (newVersion, silent, cb_) { function cb (er) { - if (!er && !silent) console.log('v' + data.version) + if (!er && !silent) console.log('v' + newVersion) cb_(er) } - checkGit(function (er, hasGit) { + readPackage(function (er, data) { if (er) return cb(new Error(er)) + data.version = newVersion + write(data, 'package.json', cb) + }) +} - write(data, 'package.json', function (er) { - if (er) return cb(new Error(er)) - - updateShrinkwrap(data.version, function (er, hasShrinkwrap) { - if (er || !hasGit) return cb(er) - commit(data.version, hasShrinkwrap, cb) - }) - }) +function commit (localData, newVersion, cb) { + updateShrinkwrap(newVersion, function (er, hasShrinkwrap) { + if (er || !localData.hasGit) return cb(er) + _commit(newVersion, hasShrinkwrap, cb) }) } @@ -121,7 +139,7 @@ function dump (data, cb) { cb() } -function checkGit (cb) { +function checkGit (localData, cb) { fs.stat(path.join(npm.localPrefix, '.git'), function (er, s) { var doGit = !er && s.isDirectory() && npm.config.get('git-tag-version') if (!doGit) { @@ -149,19 +167,19 @@ function checkGit (cb) { }).map(function (line) { return line.trim() }) - if (lines.length) { + if (lines.length && !npm.config.get('force')) { return cb(new Error( 'Git working directory not clean.\n' + lines.join('\n') )) } - + localData.hasGit = true cb(null, true) } ) }) } -function commit (version, hasShrinkwrap, cb) { +function _commit (version, hasShrinkwrap, cb) { var options = { env: process.env } var message = npm.config.get('message').replace(/%s/g, version) var sign = npm.config.get('sign-git-tag') diff --git a/test/tap/version-git-not-clean.js b/test/tap/version-git-not-clean.js index d770a86e6..a942be3e8 100644 --- a/test/tap/version-git-not-clean.js +++ b/test/tap/version-git-not-clean.js @@ -18,17 +18,6 @@ test('npm version <semver> with working directory not clean', function (t) { which('git', function (err, git) { t.ifError(err, 'git found') - function gitInit (_cb) { - var child = spawn(git, ['init']) - var out = '' - child.stdout.on('data', function (d) { - out += d.toString() - }) - child.on('exit', function () { - return _cb(out) - }) - } - function addPackageJSON (_cb) { var data = JSON.stringify({ name: 'blah', version: '0.1.2' }) fs.writeFile('package.json', data, function () { @@ -46,7 +35,7 @@ test('npm version <semver> with working directory not clean', function (t) { }) } - gitInit(function () { + common.makeGitRepo({path: pkg}, function () { addPackageJSON(function () { var data = JSON.stringify({ name: 'blah', version: '0.1.3' }) fs.writeFile('package.json', data, function () { @@ -66,6 +55,31 @@ test('npm version <semver> with working directory not clean', function (t) { }) }) +test('npm version <semver> --force with working directory not clean', function (t) { + npm.load({ cache: cache, registry: common.registry, prefix: pkg }, function () { + common.npm( + [ + '--force', + 'version', + 'patch' + ], + {cwd: pkg, env: {PATH: process.env.PATH}}, + function (err, code, stdout, stderr) { + t.ifError(err, 'npm version ran without issue') + t.notOk(code, 'exited with a non-error code') + var errorLines = stderr.trim().split('\n') + .map(function (line) { + return line.trim() + }) + .filter(function (line) { + return !line.indexOf('using --force') + }) + t.notOk(errorLines.length, 'no error output') + t.end() + }) + }) +}) + test('cleanup', function (t) { // windows fix for locked files process.chdir(osenv.tmpdir()) diff --git a/test/tap/version-lifecycle.js b/test/tap/version-lifecycle.js index da0af1086..7cf719c4f 100644 --- a/test/tap/version-lifecycle.js +++ b/test/tap/version-lifecycle.js @@ -10,6 +10,8 @@ var common = require('../common-tap.js') var npm = require('../../') var pkg = path.resolve(__dirname, 'version-lifecycle') var cache = path.resolve(pkg, 'cache') +var npmrc = path.resolve(pkg, './.npmrc') +var configContents = 'sign-git-tag=false\n' test('npm version <semver> with failing preversion lifecycle script', function (t) { setup() @@ -34,6 +36,29 @@ test('npm version <semver> with failing preversion lifecycle script', function ( }) }) +test('npm version <semver> with failing version lifecycle script', function (t) { + setup() + fs.writeFileSync(path.resolve(pkg, 'package.json'), JSON.stringify({ + author: 'Alex Wolfe', + name: 'version-lifecycle', + version: '0.0.0', + description: 'Test for npm version if postversion script fails', + scripts: { + version: './fail.sh' + } + }), 'utf8') + fs.writeFileSync(path.resolve(pkg, 'fail.sh'), 'exit 50', 'utf8') + fs.chmodSync(path.resolve(pkg, 'fail.sh'), 448) + npm.load({cache: cache, registry: common.registry}, function () { + var version = require('../../lib/version') + version(['patch'], function (err) { + t.ok(err) + t.ok(err.message.match(/Exit status 50/)) + t.end() + }) + }) +}) + test('npm version <semver> with failing postversion lifecycle script', function (t) { setup() fs.writeFileSync(path.resolve(pkg, 'package.json'), JSON.stringify({ @@ -57,6 +82,52 @@ test('npm version <semver> with failing postversion lifecycle script', function }) }) +test('npm version <semver> execution order', function (t) { + setup() + fs.writeFileSync(path.resolve(pkg, 'package.json'), JSON.stringify({ + author: 'Alex Wolfe', + name: 'version-lifecycle', + version: '0.0.0', + description: 'Test for npm version if postversion script fails', + scripts: { + preversion: './preversion.sh', + version: './version.sh', + postversion: './postversion.sh' + } + }), 'utf8') + makeScript('preversion') + makeScript('version') + makeScript('postversion') + npm.load({cache: cache, registry: common.registry}, function () { + common.makeGitRepo({path: pkg}, function (err, git) { + t.ifError(err, 'git bootstrap ran without error') + + var version = require('../../lib/version') + version(['patch'], function (err) { + t.ifError(err, 'version command complete') + + t.equal('0.0.0', readPackage('preversion').version, 'preversion') + t.deepEqual(readStatus('preversion', t), { + 'preversion-package.json': 'A' + }) + + t.equal('0.0.1', readPackage('version').version, 'version') + t.deepEqual(readStatus('version', t), { + 'package.json': 'M', + 'preversion-package.json': 'A', + 'version-package.json': 'A' + }) + + t.equal('0.0.1', readPackage('postversion').version, 'postversion') + t.deepEqual(readStatus('postversion', t), { + 'postversion-package.json': 'A' + }) + t.end() + }) + }) + }) +}) + test('cleanup', function (t) { process.chdir(osenv.tmpdir()) rimraf.sync(pkg) @@ -67,5 +138,37 @@ function setup () { mkdirp.sync(pkg) mkdirp.sync(path.join(pkg, 'node_modules')) mkdirp.sync(cache) + fs.writeFileSync(npmrc, configContents, 'ascii') process.chdir(pkg) } + +function makeScript (lifecycle) { + var contents = [ + 'cp package.json ' + lifecycle + '-package.json', + 'git add ' + lifecycle + '-package.json', + 'git status --porcelain > ' + lifecycle + '-git.txt' + ].join('\n') + var scriptPath = path.join(pkg, lifecycle + '.sh') + fs.writeFileSync(scriptPath, contents, 'utf-8') + fs.chmodSync(scriptPath, 448) +} + +function readPackage (lifecycle) { + return JSON.parse(fs.readFileSync(path.join(pkg, lifecycle + '-package.json'), 'utf-8')) +} + +function readStatus (lifecycle, t) { + var status = {} + fs.readFileSync(path.join(pkg, lifecycle + '-git.txt'), 'utf-8') + .trim() + .split('\n') + .forEach(function (line) { + line = line.trim() + if (line && !line.match(/^\?\? /)) { + var parts = line.split(/\s+/) + t.equal(parts.length, 2, lifecycle + ' : git status has too many words : ' + line) + status[parts[1].trim()] = parts[0].trim() + } + }) + return status +} |