release-script
Version:
Release tools for projects. From github repo to npm and bower packages
466 lines (386 loc) • 17.8 kB
JavaScript
;
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);