UNPKG

releasy-mono

Version:

CLI tool to release node applications with tag and auto semver bump

280 lines (276 loc) 9.73 kB
var { cat } = require('shelljs') var Q = require('q') var util = require('util') var exec = require('child_process').exec var spawn = require('child_process').spawn var fs = require('fs') var yaml = require('js-yaml') var providers = require('./providers.js') var Github = require('github-api') var steps = { getOptionsFile: () => { const possibleFiles = ['_releasy.yaml', '_releasy.yml', '_releasy.json'] for (let i = 0; i < possibleFiles.length; i++) { const name = possibleFiles[i] if (test('-e', name)) { return yaml.safeLoad(cat(name)) } } return {} }, pickVersionProvider: (fileName, overrideProviders) => { const currentProviders = overrideProviders || providers for (const i in currentProviders) { const provider = currentProviders[i] if (provider.supports(fileName)) { return new provider(fileName) } } throw new Error(util.format("Unable to find a provider that supports '%s' as a version file", fileName)) }, setup: (versionProvider, type, prerelease) => { let version = versionProvider.readVersion() // Support for "promote" predicate, which bumps prerelease to stable, without changing version number. if (type === 'promote') { // Promote only makes sense when there is a prerelease if (version.prerelease.length === 0) { throw new Error(`The version you are trying to promote to stable (${version.format()}) is already stable.\n`) } else { version.prerelease = [] prerelease = 'stable' } } // For other types, simply increment else { const incrementType = type === 'prerelease' || prerelease === 'stable' || version.prerelease.length === 0 ? type || 'patch' : (`pre${type}`) || 'prepatch' version = version.inc(incrementType) } if (prerelease && prerelease !== 'stable' && type !== 'prerelease') { version.prerelease = [prerelease] } return { versionProvider: versionProvider, newVersion: version.format(), oldVersion: versionProvider.readVersion().format(), } }, scripts: (msg, config, key) => { const pkg = /package\.json$/.test(config.versionProvider.filePath) const manifest = /manifest\.json$/.test(config.versionProvider.filePath) if (!(pkg || manifest)) return Q() try { var cmd = config.versionProvider.getScript(key) } catch (err) { return Q() } return cmd ? steps.spawn(cmd, msg, config.dryRun, config.quiet) : Q() }, preReleasy: config => { const msg = 'Pre releasy' return steps.status(config) .then(stdout => { if (!config.dryRun && stdout[0].indexOf('nothing to commit') === -1) { throw new Error('\nPlease commit your changes before proceeding.') } else { return Q() } }) .then(() => { return steps.scripts(msg, config, 'prereleasy') }) .then(() => { return steps.status(config) }) .then(stdout => { if (stdout[0].indexOf('nothing to commit') > -1) return Q() const cmd = config.versionProvider.getScript('prereleasy') const preCfg = { commitMessage: `Pre releasy commit\n\n ${cmd}`, dryRun: config.dryRun, versionProvider: { filePath: '.', }, quiet: true, } return steps.commit(preCfg) }) }, run: (cmd, successMessage, dryRun, quiet) => { const promise = dryRun ? Q() : Q.nfcall(exec, cmd) if (successMessage) { promise.then(stdout => { if (!quiet) console.log(successMessage + ' > '.blue + cmd.blue) return stdout }) } return promise }, spawn: (cmd, successMessage, dryRun, quiet) => { const deferred = Q.defer() deferred.promise.then(() => { if (!quiet) console.log(successMessage + ' > '.blue + cmd.blue) }) if (dryRun) { deferred.resolve() return deferred.promise } const childEnv = Object.create(process.env) const childIO = quiet ? null : 'inherit' let argsW32 = false if (/^(?:[A-Z]*_*)*[A-Z]*=/.test(cmd) && !dryRun) { var env = cmd.split('=') childEnv[env[0]] = env[1].split(' ')[0] } if (process.platform === 'win32') { const argsCmd = env ? env[1].substr(env[1].indexOf(' ') + 1) : cmd var args = ['/s', '/c', argsCmd] var command = 'cmd.exe' argsW32 = true } else { args = env ? env[1].split(' ').slice(1) : cmd.split(' ') command = args.shift() } const childProcess = spawn(command, args, { env: childEnv, stdio: childIO, windowsVerbatimArguments: argsW32 }) childProcess.on('close', code => { if (code === 0) deferred.resolve() else deferred.reject(`\nCommand exited with error code: ${code}`) }) return deferred.promise }, status: config => { return steps.run('git status', '', false, config.quiet).then(stdout => { return stdout }) }, bump: config => { // Update version on CHANGELOG.md if (fs.existsSync(config.changelogPath) && !config.dryRun) { const data = fs.readFileSync(config.changelogPath, err => { if (err)throw new Error(`Error reading file: ${err}`) }).toString() if (data.indexOf(config.unreleased) < 0 && !config.quiet) { console.log("I can't update your CHANGELOG. :( \ \nMake your CHANGELOG great again and follow the CHANGELOG format http://keepachangelog.com/en/1.0.0/".red.bold) } else { const position = data.indexOf(config.unreleased) + config.unreleased.length const bufferedText = Buffer.from(`${config.changelogVersion}${data.substring(position)}`) const file = fs.openSync(config.changelogPath, 'r+') fs.writeSync(file, bufferedText, 0, bufferedText.length, position, err => { if (err)throw new Error(`Error writing file: ${err}`) fs.close(file) }) } } const promise = config.dryRun ? Q() : Q(config.versionProvider.writeVersion(config.newVersion)) return promise.then(result => { if (!config.quiet) console.log(`Version bumped to ${config.newVersion.bold.green}`) return result }) }, add: config => { let gitAddCommand = `git add ${config.versionProvider.filePath.join(' ')}` let successMessage = `File(s) ${config.versionProvider.filePath} added` if (fs.existsSync(config.changelogPath)) { gitAddCommand += ` ${config.changelogPath}` successMessage = `Files ${config.versionProvider.filePath} ${config.changelogPath} added` } return steps.run(gitAddCommand, successMessage, config.dryRun, config.quiet) }, commit: config => { let successMessage = `File(s) ${config.versionProvider.filePath} commited` if (fs.existsSync(config.changelogPath)) { successMessage = `Files ${config.versionProvider.filePath} ${config.changelogPath} commited` } return steps.run(`git commit -m "${config.commitMessage}"`, successMessage, config.dryRun, config.quiet) }, tag: config => { return steps.run(`git tag ${config.tagName} -m "${config.tagMessage}"`, `Tag created: ${config.tagName}`, config.dryRun, config.quiet) }, push: config => { return steps.run(`git push && git push origin ${config.tagName}`, 'Pushed commit and tags', config.dryRun, config.quiet) }, releaseNotes: config => { // Post Release Notes at the Github const { changelogPath, githubAuth, githubInfo, release, dryRun } = config if (fs.existsSync(changelogPath) && githubAuth && !dryRun) { const client = new Github({ token: githubAuth }) client.getRepo(githubInfo[0], githubInfo[1]) .createRelease(release) .then(() => { if (!config.quiet) console.log('Release Notes submitted'.green) }) .catch(err => { throw new Error(`Error on request ${err}`) }) } else { console.log('You don\'t have a CHANGELOG to post Release Notes'.yellow) } }, postReleasy: config => { const msg = 'Post releasy' return steps.scripts(msg, config, 'postreleasy') }, publish: config => { let cmd = 'npm publish' let msg = `Published ${config.newVersion} to npm` if (config.npmTag) { cmd += ` --tag ${config.npmTag}` msg += `with a tag of "${config.npmTag}"` } if (config.npmFolder) { cmd += ` ${config.npmFolder}` } return steps.run(cmd, msg, config.dryRun, config.quiet) }, release: (config, options) => { if (!config.quiet) console.log('Starting release...') let promise = steps.preReleasy(config) promise.then(() => { return steps.bump(config) }) if (options.commit) { promise = promise.then(() => { return steps.add(config) }) promise = promise.then(() => { return steps.commit(config) }) } if (options.tag) { promise = promise.then(() => { return steps.tag(config) }) } if (options.push) { promise = promise.then(() => { return steps.push(config) }) } if (options.notes) { promise = promise.then(() => { return steps.releaseNotes(config) }) } if (options.npm) { promise = promise.then(() => { return steps.publish(config) }) } promise = promise .then(() => { return steps.postReleasy(config) }) .then(() => { if (!config.quiet) console.log('All steps finished successfully.'.green) }) promise.fail(reason => { if (!config.quiet) console.error('[ERROR] Failed to release.\n'.red.bold, reason) }) return promise }, } module.exports = steps