UNPKG

release-script

Version:

Release tools for projects. From github repo to npm and bower packages

466 lines (386 loc) 17.8 kB
#!/usr/bin/env node 'use strict'; var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); /* globals cat, config, cp, ls, popd, pushd, pwd, rm, test, exec, exit, which */ /* eslint curly: 0 */ require('colors'); require('shelljs/global'); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _semver = require('semver'); var _semver2 = _interopRequireDefault(_semver); var _yargs = require('yargs'); var _yargs2 = _interopRequireDefault(_yargs); var _request = require('request'); var _request2 = _interopRequireDefault(_request); var _githubUrlToObject = require('github-url-to-object'); var _githubUrlToObject2 = _interopRequireDefault(_githubUrlToObject); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // do not die on errors config.fatal = false; //------------------------------------------------------------------------------ // constants var repoRoot = pwd(); var packagePath = _path2.default.join(repoRoot, 'package.json'); var bowerjsonPath = _path2.default.join(repoRoot, 'bower.json'); var npmjson = JSON.parse(cat(packagePath)); var bowerjson = test('-f', bowerjsonPath) ? JSON.parse(cat(bowerjsonPath)) : null; var isPrivate = npmjson.private; var devDepsNode = npmjson.devDependencies; //------------------------------------------------------------------------------ // check if one of 'rf-changelog' or 'mt-changelog' is used by project var isCommitsChangelogUsed = devDepsNode && (devDepsNode['rf-changelog'] || devDepsNode['mt-changelog']); if (isCommitsChangelogUsed && !which('changelog')) { printErrorAndExit('The "[rf|mt]-changelog" package is present in "devDependencies", but it is not installed.'); } var isWithinMtChangelog = npmjson.name === 'mt-changelog'; isCommitsChangelogUsed = isCommitsChangelogUsed || isWithinMtChangelog; //------------------------------------------------------------------------------ // options var configOptions = npmjson['release-script'] || {}; var bowerRoot = _path2.default.join(repoRoot, configOptions.bowerRoot || 'amd/'); var tmpBowerRepo = _path2.default.join(repoRoot, configOptions.tmpBowerRepo || 'tmp-bower-repo'); var bowerRepo = configOptions.bowerRepo; // if it is not set, then there is no bower repo var docsRoot = _path2.default.join(repoRoot, configOptions.docsRoot || 'docs-built/'); var tmpDocsRepo = _path2.default.join(repoRoot, configOptions.tmpDocsRepo || 'tmp-docs-repo'); var docsRepo = configOptions.docsRepo; // if it is not set, then there is no docs/site repo var docsBuild = npmjson.scripts && npmjson.scripts['docs-build']; var githubToken = process.env.GITHUB_TOKEN; var altPkgRootFolder = configOptions.altPkgRootFolder; var skipBuildStep = configOptions.skipBuildStep; //------------------------------------------------------------------------------ // command line options var yargsConf = _yargs2.default.usage('Usage: $0 <version> [--preid <identifier>]\nor\nUsage: $0 --only-docs').example('$0 minor --preid beta', 'Release with a minor version bump and a pre-release tag. (npm tag `beta`)').example('$0 major', 'Release with a major version bump').example('$0 major --notes "a custom message text"', 'Add a custom message to the release').example('$0 --preid alpha', 'Release the same version with a pre-release tag. (npm tag `alpha`)').example('$0 0.101.0 --preid rc --tag canary', 'Release pre-release version `v0.101.0-rc.0` with npm tag `canary`').example('$0 ... --skip-test(s)', 'Use this flag if you need to skip `npm run test` step.').command('patch', 'Release patch').command('minor', 'Release minor').command('major', 'Release major').command('<version>', 'Release arbitrary version number').option('preid', { describe: 'pre-release identifier', type: 'string' }).option('tag', { describe: 'Npm tag name for pre-release version.\nIf it is not provided, then `preid` value is used', type: 'string' }).option('only-docs', { alias: 'docs', describe: 'Publish only documents' }).option('dry-run', { alias: 'n', describe: 'This option toggles "dry run" mode' }).option('run', { describe: 'You need this when "defaultDryRun": "true"' }).option('verbose', { describe: 'Increased debug output' }).option('skip-tests', { alias: 'skip-test', describe: 'Skip `npm run test` step' }).option('skip-version-bumping', { describe: 'Skip version bumping step' }).option('notes', { describe: 'A custom message for the release.\nOverrides [rf|mt]changelog message' }); var argv = yargsConf.argv; config.silent = !argv.verbose; var versionBumpOptions = { type: argv._[0], preid: argv.onlyDocs ? 'docs' : argv.preid, npmTagName: argv.tag || argv.preid }; if (!argv.skipVersionBumping && versionBumpOptions.type === undefined && versionBumpOptions.preid === undefined) { console.log('Must provide either a version bump type, "preid" or "--only-docs"'.red); console.log(yargsConf.help()); exit(1); } var notesForRelease = argv.notes; var isDefaultDryRunOptionSetTrue = configOptions.defaultDryRun === true || configOptions.defaultDryRun === 'true'; var dryRunMode = undefined; if (argv.run) { dryRunMode = false; } else { dryRunMode = argv.dryRun || isDefaultDryRunOptionSetTrue; } if (dryRunMode) { console.log('DRY RUN'.magenta); if (isDefaultDryRunOptionSetTrue) { console.log('------------------------------------------------------'); console.log('To actually run your command please add "--run" option'.yellow); console.log('------------------------------------------------------'); } } if (argv.preid) console.log('"--preid" detected. Documents will not be published'.yellow); if (argv.onlyDocs && !argv.preid) console.log('Publish only documents'.yellow); //------------------------------------------------------------------------------ // functions function printErrorAndExit(error) { console.error(error.red); exit(1); } function run(command, skipError) { var _exec = exec(command); var code = _exec.code; var output = _exec.output; if (code !== 0 && !skipError) printErrorAndExit(output); return output; } function safeRun(command, skipError) { if (dryRunMode) { console.log(('[' + command + ']').grey, 'DRY RUN'.magenta); } else { return run(command, skipError); } } function safeRm() { for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } if (dryRunMode) console.log(('[rm ' + args.join(' ') + ']').grey, 'DRY RUN'.magenta);else rm(args); } /** * Npm's `package.json` 'repository.url' could be set to one of three forms: * git@github.com:<author>/<repo-name>.git * git+https://github.com/<author>/<repo-name>.git * or just <author>/<repo-name> * @returns [<author>, <repo-name>] array */ function getOwnerAndRepo(url) { var match = url.match(/^git@github\.com:(.*)\.git$/); match = match || url.match(/^git\+https:\/\/github\.com\/(.*)\.git$/); var gitUrlBase = match && match[1]; return (gitUrlBase || url).split('/'); } function runAndGitRevertOnError(cmd) { var res = exec(cmd); if (res.code !== 0) { // if error, then revert and exit console.log(('"' + cmd + '" command failed, reverting version bump').red); run('git reset HEAD .'); run('git checkout package.json'); console.log('Version bump reverted'.red); printErrorAndExit(res.output); } } function releaseAdRepo(repo, srcFolder, tmpFolder, vVersion) { if (!repo || !srcFolder || !tmpFolder || !vVersion) { printErrorAndExit('Bug error. Create github issue: releaseAdRepo - One of parameters is not set.'); } rm('-rf', tmpFolder); run('git clone ' + repo + ' ' + tmpFolder); pushd(tmpFolder); rm('-rf', ls(tmpFolder).filter(function (file) { return file !== '.git'; })); // delete all but `.git` dir cp('-R', srcFolder, tmpFolder); safeRun('git add -A .'); safeRun('git commit -m "Release ' + vVersion + '"'); safeRun('git tag -a --message=' + vVersion + ' ' + vVersion); safeRun('git push --follow-tags'); popd(); safeRm('-rf', tmpFolder); } function release(_ref) { var type = _ref.type; var preid = _ref.preid; var npmTagName = _ref.npmTagName; // ensure git repo has no pending changes if (exec('git diff-index --name-only HEAD --').output.length) { printErrorAndExit('Git repository must be clean'); } console.info('No pending changes'.cyan); // ensure git repo last version is fetched exec('git fetch'); if (/behind (.*)\]/.test(exec('git status -sb').output)) { printErrorAndExit('Your repo is behind by ' + RegExp.$1 + ' commits'); } console.info('Current with latest changes from remote'.cyan); // version bumping var oldVersion = npmjson.version; var newVersion = undefined; if (argv.skipVersionBumping) { newVersion = oldVersion; console.log('The version remains the same '.yellow + oldVersion.green); } else { if (type === undefined) { newVersion = oldVersion; // --preid } else if (['major', 'minor', 'patch'].indexOf(type) >= 0) { newVersion = _semver2.default.inc(oldVersion, type); } else { newVersion = type; // '<version>', 'Release specific version' } if (preid) { newVersion = _semver2.default.inc(newVersion, 'pre', preid); } npmjson.version = newVersion; (JSON.stringify(npmjson, null, 2) + '\n').to(packagePath); console.log('The version changed from '.cyan + oldVersion.green + ' to '.cyan + newVersion.green); safeRun('git add package.json'); } if (npmjson.scripts) { // do not throw if there are no 'scripts' at all if (npmjson.scripts.test && !argv.skipTests) { // npm run test // this step is placed after version bumping // for the case when documents are been built in "npm run test" script console.log('Running: '.cyan + '"npm run test"'.green); config.silent = !skipBuildStep; runAndGitRevertOnError('npm run test'); config.silent = !argv.verbose; console.log('Completed: '.cyan + '"npm run test"'.green); } // npm run build if (argv.onlyDocs && docsBuild) { console.log('Running: '.cyan + 'docs-build'.green); runAndGitRevertOnError('npm run docs-build'); console.log('Completed: '.cyan + 'docs-build'.green); } else { if (npmjson.scripts.build && !skipBuildStep) { console.log('Running: '.cyan + 'build'.green); runAndGitRevertOnError('npm run build'); console.log('Completed: '.cyan + 'build'.green); } else { console.log('Skipping "npm run build" step.'.yellow); } } } // if (npmjson.scripts) var vVersion = 'v' + newVersion; var versionAndNotes = notesForRelease = notesForRelease ? vVersion + ' ' + notesForRelease : vVersion; // generate changelog // within mt-changelog at this stage `./bin/changelog` is already built and tested var changelogCmd = isWithinMtChangelog ? './bin/changelog' : 'changelog'; var changelog = _path2.default.join(repoRoot, 'CHANGELOG.md'); var changelogAlpha = _path2.default.join(repoRoot, 'CHANGELOG-alpha.md'); var changelogOutput = undefined, changelogArgs = undefined; if (preid) { changelogOutput = changelogAlpha; changelogArgs = ''; } else { changelogOutput = changelog; changelogArgs = '--exclude-pre-releases'; } if (isCommitsChangelogUsed) { var changelogAlphaRemovedFlag = false; if (test('-e', changelogAlpha)) { rm('-rf', changelogAlpha); changelogAlphaRemovedFlag = true; } run(changelogCmd + ' --title="' + versionAndNotes + '" --out ' + changelogOutput + ' ' + changelogArgs); safeRun('git add ' + changelog); if (preid || changelogAlphaRemovedFlag) { safeRun('git add -A ' + changelogAlpha); } console.log('The changelog has been generated'.cyan); } safeRun('git commit -m "Release ' + vVersion + '"'); // tag and release console.log('Tagging: '.cyan + vVersion.green); if (isCommitsChangelogUsed) { notesForRelease = run(changelogCmd + ' --title="' + versionAndNotes + '" -s'); safeRun('changelog --title="' + versionAndNotes + '" ' + changelogArgs + ' -s | git tag -a -F - ' + vVersion); } else { safeRun('git tag -a --message="' + versionAndNotes + '" ' + vVersion); } safeRun('git push --follow-tags'); console.log('Tagged: '.cyan + vVersion.green); if (!argv.onlyDocs) { var repo = npmjson.repository.url || npmjson.repository; // publish to GitHub if (githubToken) { console.log(('GitHub token found ' + githubToken).green); console.log('Publishing to GitHub: '.cyan + vVersion.green); if (dryRunMode) { console.log('[publishing to GitHub]'.grey, 'DRY RUN'.magenta); } else { var _getOwnerAndRepo = getOwnerAndRepo(repo); var _getOwnerAndRepo2 = _slicedToArray(_getOwnerAndRepo, 2); var githubOwner = _getOwnerAndRepo2[0]; var githubRepo = _getOwnerAndRepo2[1]; (0, _request2.default)({ uri: 'https://api.github.com/repos/' + githubOwner + '/' + githubRepo + '/releases', method: 'POST', json: true, body: { tag_name: vVersion, // eslint-disable-line camelcase name: githubRepo + ' ' + vVersion, body: notesForRelease, draft: false, prerelease: !!preid }, headers: { Authorization: 'token ' + githubToken, 'User-Agent': 'release-script (https://github.com/alexkval/release-script)' } }, function (err, res, body) { if (err) { console.log('API request to GitHub, error has occured:'.red); console.log(err); console.log('Skipping GitHub releasing'.yellow); } else if (res.statusMessage === 'Unauthorized') { console.log(('GitHub token ' + githubToken + ' is wrong').red); console.log('Skipping GitHub releasing'.yellow); } else { console.log(('Published at ' + body.html_url).green); } }); } } // npm if (isPrivate) { console.log('Package is private, skipping npm release'.yellow); } else { console.log('Releasing: '.cyan + 'npm package'.green); var npmPublishCmd = preid ? 'npm publish --tag ' + npmTagName : 'npm publish'; // publishing just /altPkgRootFolder content if (altPkgRootFolder) { // prepare custom `package.json` without `scripts` and `devDependencies` // because it already has been saved, we safely can use the same object delete npmjson.files; // because otherwise it would be wrong delete npmjson.scripts; delete npmjson.devDependencies; delete npmjson['release-script']; // this also doesn't belong to output var regexp = new RegExp(altPkgRootFolder + '\\/?'); npmjson.main = npmjson.main.replace(regexp, ''); // remove folder part from path (JSON.stringify(npmjson, null, 2) + '\n').to(_path2.default.join(altPkgRootFolder, 'package.json')); pushd(altPkgRootFolder); safeRun(npmPublishCmd); popd(); } else { safeRun(npmPublishCmd); } console.log('Released: '.cyan + 'npm package'.green); } // bower (separate repo) if (isPrivate) { console.log('Package is private, skipping bower release'.yellow); } else if (bowerRepo) { console.log('Releasing: '.cyan + 'bower package'.green); releaseAdRepo(bowerRepo, bowerRoot, tmpBowerRepo, vVersion); console.log('Released: '.cyan + 'bower package'.green); } else { console.log('The "bowerRepo" is not set in package.json. Skipping Bower package publishing.'.yellow); } // bower (register package if bower.json is located in this repo) if (bowerjson) { if (bowerjson.private) { console.log('Package is private, skipping bower registration'.yellow); } else if (!which('bower')) { console.log('Bower is not installed globally, skipping bower registration'.yellow); } else { console.log('Registering: '.cyan + 'bower package'.green); var output = safeRun('bower register ' + bowerjson.name + ' ' + (0, _githubUrlToObject2.default)(repo).clone_url, true); if (output.indexOf('EDUPLICATE') > -1) { console.log('Package already registered'.yellow); } else if (output.indexOf('registered successfully') < 0) { console.log('Error registering package, details:'.red); console.log(output.red); } else { console.log('Registered: '.cyan + 'bower package'.green); } } } } // documents site if (!isPrivate && docsRepo && (!preid || argv.onlyDocs)) { console.log('Releasing: '.cyan + 'documents site'.green); releaseAdRepo(docsRepo, docsRoot, tmpDocsRepo, vVersion); console.log('Documents site has been released'.green); } console.log('Version '.cyan + ('v' + newVersion).green + ' released!'.cyan); } //------------------------------------------------------------------------------ // release(versionBumpOptions);