/* eslint-disable no-fallthrough */ (function () { 'use strict'; const colors = require('colors'); const strip = require('cli-color/strip'); const fs = require('fs'); const path = require('path'); const prompt = require('prompt-sync')({sigint: true}); const child_process = require('child_process'); const pkg = require('./package.json'); let oldVersion = pkg.version; const abortCmds = ['git reset --hard', 'git checkout staging', 'rm abort push']; const pushCmds = ['rm abort push']; const cleanupCmds = []; const defaultOptions = { encoding: 'utf-8' }; const origin = 'git@github.com:angular/material.git'; const lineWidth = 80; const lastMajorVer = JSON.parse(exec('curl https://material.angularjs.org/docs.json')).latest; let newVersion; try { child_process.execSync('gulp --version', defaultOptions); } catch (error) { throw new Error('Please install gulp globally via "npm i -g gulp@^3.9.1".'); } header(); const dryRun = prompt(`Is this a dry-run? [${"yes".cyan}/no] `, 'yes') !== 'no'; if (dryRun) { oldVersion = prompt(`What would you like the old version to be? (default: ${oldVersion.cyan}) `, oldVersion); build(); } else if (validate()) { build(); } function build () { newVersion = getNewVersion(); line(); checkoutVersionBranch(); updateVersion(); createChangelog(); commitChanges(); tagRelease(); cloneRepo('bower-material'); updateBowerVersion(); cloneRepo('code.material.angularjs.org'); updateSite(); updateMaster(); writeScript('abort', abortCmds.concat(cleanupCmds)); if (!dryRun) writeScript('push', pushCmds.concat(cleanupCmds)); line(); log('Your repo is ready to be pushed.'); log(`Please look over ${"CHANGELOG.md".cyan} and make any changes.`); log(`When you are ready, please run "${"./push".cyan}" to finish the process.`); log('If you would like to cancel this release, please run "./abort"'); } // utility methods /** confirms that you will be able to perform the release before attempting */ function validate () { if (exec('npm whoami') !== 'angular') { err('You must be authenticated with npm as "angular" to perform a release.'); } else if (exec('git rev-parse --abbrev-ref HEAD') !== 'staging') { err('Releases can only performed from "staging" at this time.'); } else { return true; } function err (msg) { const str = 'Error: ' + msg; log(str.red); } } /** creates the version branch and adds abort steps */ function checkoutVersionBranch () { exec(`git branch -q -D release/${newVersion}`); exec(`git checkout -q -b release/${newVersion}`); abortCmds.push('git checkout master'); abortCmds.push(`git branch -D release/${newVersion}`); } /** writes the new version to package.json */ function updateVersion () { start(`Updating ${"package.json".cyan} version from ${oldVersion.cyan} to ${newVersion.cyan}...`); pkg.version = newVersion; fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2)); done(); abortCmds.push('git checkout package.json'); pushCmds.push('git add package.json'); } /** generates the changelog from the commits since the last release */ function createChangelog () { start(`Generating changelog from ${oldVersion.cyan} to ${newVersion.cyan}...`); exec(`git fetch --tags ${origin}`); exec(`git checkout CHANGELOG.md`); exec(`gulp changelog --sha=$(git merge-base v${lastMajorVer} HEAD)`); done(); abortCmds.push('git checkout CHANGELOG.md'); pushCmds.push('git add CHANGELOG.md'); } /** utility method for clearing the terminal */ function clear () { write("\u001b[2J\u001b[0;0H"); } /** prompts the user for the new version */ function getNewVersion () { header(); const options = getVersionOptions(oldVersion); let key, version; log(`The current version is ${oldVersion.cyan}.`); log(''); log('What should the next version be?'); for (key in options) { log((+key + 1) + ') ' + options[ key ].cyan); } log(''); const type = prompt('Please select a new version: '); if (options[ type - 1 ]) version = options[ type - 1 ]; else if (type.match(/^\d+\.\d+\.\d+(-rc\.?\d+)?$/)) version = type; else throw new Error('Your entry was invalid.'); log(''); log('The new version will be ' + version.cyan + '.'); return prompt(`Is this correct? [${"yes".cyan}/no] `, 'yes') === 'yes' ? version : getNewVersion(); function getVersionOptions (version) { return version.match(/-rc\.?\d+$/) ? [increment(version, 'rc'), increment(version, 'minor')] : [increment(version, 'patch'), addRC(increment(version, 'minor'))]; function increment (versionString, type) { const version = parseVersion(versionString); if (version.rc) { switch (type) { case 'minor': version.rc = 0; break; case 'rc': version.rc++; break; } } else { version[ type ]++; // reset any version numbers lower than the one changed switch (type) { case 'minor': version.patch = 0; case 'patch': version.rc = 0; } } return getVersionString(version); function parseVersion (version) { const parts = version.split(/-rc\.|\./g); return { string: version, major: parts[ 0 ], minor: parts[ 1 ], patch: parts[ 2 ], rc: parts[ 3 ] || 0 }; } function getVersionString (version) { let str = version.major + '.' + version.minor + '.' + version.patch; if (version.rc) str += '-rc.' + version.rc; return str; } } function addRC (str) { return str + '-rc.1'; } } } /** adds git tag for release and pushes to github */ function tagRelease () { pushCmds.push( `git tag v${newVersion} -f`, `git push ${origin} HEAD`, `git push --tags ${origin}` ); } /** amends the commit to include local changes (ie. changelog) */ function commitChanges () { start('Committing changes...'); exec(`git commit -am "release: version ${newVersion}"`); done(); pushCmds.push('git commit --amend --no-edit'); } /** utility method for cloning github repos */ function cloneRepo (repo) { start(`Cloning ${repo.cyan} from Github...`); exec(`rm -rf ${repo}`); exec(`git clone git@github.com:angular/${repo}.git --depth=1`); done(); cleanupCmds.push(`rm -rf ${repo}`); } /** writes an array of commands to a bash script */ function writeScript (name, cmds) { fs.writeFileSync(name, '#!/usr/bin/env bash\n\n' + cmds.join('\n')); exec('chmod +x ' + name); } /** updates the version for bower-material in package.json and bower.json */ function updateBowerVersion () { start('Updating bower version...'); const options = { cwd: './bower-material' }; const bower = require(options.cwd + '/bower.json'), pkg = require(options.cwd + '/package.json'); // update versions in config files bower.version = pkg.version = newVersion; fs.writeFileSync(options.cwd + '/package.json', JSON.stringify(pkg, null, 2)); fs.writeFileSync(options.cwd + '/bower.json', JSON.stringify(bower, null, 2)); done(); start('Building bower files...'); // build files for bower exec([ 'rm -rf dist', 'gulp build', 'gulp build-all-modules --mode=default', 'gulp build-all-modules --mode=closure', 'rm -rf dist/demos' ]); done(); start('Copy files into bower repo...'); // copy files over to bower repo exec([ 'cp -Rf ../dist/* ./', 'git add -A', `git commit -m "release: version ${newVersion}"`, 'rm -rf ../dist' ], options); done(); // add steps to push script pushCmds.push( comment('push to bower (master and tag) and publish to npm'), 'cd ' + options.cwd, 'cp ../CHANGELOG.md .', 'git add CHANGELOG.md', 'git commit --amend --no-edit', `git tag -f v${newVersion}`, 'git pull --rebase --strategy=ours', 'git push', 'git push --tags', 'rm -rf .git/', 'npm publish', 'cd ..' ); } /** builds the website for the new version */ function updateSite () { start('Adding new version of the docs site...'); const options = { cwd: './code.material.angularjs.org' }; writeDocsJson(); // build files for bower exec([ 'rm -rf dist', 'gulp docs' ]); replaceFilePaths(); // copy files over to site repo exec([ `cp -Rf ../dist/docs ${newVersion}`, 'rm -rf latest && cp -Rf ../dist/docs latest', 'git add -A', `git commit -m "release: version ${newVersion}"`, 'rm -rf ../dist' ], options); replaceBaseHref(newVersion); replaceBaseHref('latest'); // update firebase.json file updateFirebaseJson(); exec(['git commit --amend --no-edit -a'], options); done(); // add steps to push script pushCmds.push( comment('push the site'), 'cd ' + options.cwd, 'git pull --rebase --strategy=ours', 'git push', 'cd ..' ); function updateFirebaseJson () { fs.writeFileSync(options.cwd + '/firebase.json', getFirebaseJson()); function getFirebaseJson () { const json = require(options.cwd + '/firebase.json'); json.hosting.rewrites = json.hosting.rewrites || []; const rewrites = json.hosting.rewrites; switch (rewrites.length) { case 0: rewrites.push(getRewrite('HEAD')); case 1: rewrites.push(getRewrite('latest')); default: rewrites.push(getRewrite(newVersion)); } return JSON.stringify(json, null, 2); function getRewrite (str) { return { source: '/' + str + '/**/!(*.@(js|html|css|json|svg|png|jpg|jpeg))', destination: '/' + str + '/index.html' }; } } } function writeDocsJson () { const config = require(options.cwd + '/docs.json'); config.versions.unshift(newVersion); // only set to default if not a release candidate config.latest = newVersion; fs.writeFileSync(options.cwd + '/docs.json', JSON.stringify(config, null, 2)); } } /** replaces localhost file paths with public URLs */ function replaceFilePaths () { // handle docs.js const filePath = path.join(__dirname, '/dist/docs/docs.js'); const file = fs.readFileSync(filePath); const contents = file.toString() .replace(/http:\/\/localhost:8080\/angular-material/g, 'https://gitcdn.xyz/cdn/angular/bower-material/v' + newVersion + '/angular-material') .replace(/http:\/\/localhost:8080\/docs.css/g, 'https://material.angularjs.org/' + newVersion + '/docs.css'); fs.writeFileSync(filePath, contents); } /** replaces base href in index.html for new version as well as latest */ function replaceBaseHref (folder) { // handle index.html const filePath = path.join(__dirname, '/code.material.angularjs.org/', folder, '/index.html'); const file = fs.readFileSync(filePath); const contents = file.toString().replace(/base href="\//g, 'base href="/' + folder + '/'); fs.writeFileSync(filePath, contents); } /** copies the changelog back over to master branch */ function updateMaster () { pushCmds.push( comment('update package.json in master'), 'git checkout master', `git pull --rebase ${origin} master --strategy=theirs`, `git checkout release/${newVersion} -- CHANGELOG.md`, `node -e "const newVersion = '${newVersion}'; ${stringifyFunction(buildCommand)}"`, 'git add CHANGELOG.md', 'git add package.json', `git commit -m "update version number in package.json to ${newVersion}"`, `git push ${origin} master` ); function buildCommand () { require('fs').writeFileSync('package.json', JSON.stringify(getUpdatedJson(), null, 2)); function getUpdatedJson () { const json = require('./package.json'); json.version = newVersion; return json; } } function stringifyFunction (method) { return method .toString() .split('\n') .slice(1, -1) .map(function (line) { return line.trim(); }) .join(' ') .replace(/"/g, '\\"'); } } /** utility method to output header */ function header () { clear(); line(); log(center('AngularJS Material Release')); line(); } /** outputs a centered message in the terminal */ function center (msg) { msg = ' ' + msg.trim() + ' '; const length = msg.length; const spaces = Math.floor((lineWidth - length) / 2); return Array(spaces + 1).join('-') + msg.green + Array(lineWidth - msg.length - spaces + 1).join('-'); } /** outputs done text when a task is completed */ function done () { log('done'.green); } /** * utility method for executing terminal commands while ignoring stderr * @param {string|Array} cmd * @param {Object=} userOptions * @return */ function exec (cmd, userOptions) { if (cmd instanceof Array) { return cmd.map(function (cmd) { return exec(cmd, userOptions); }); } try { const options = Object.create(defaultOptions); for (const key in userOptions) options[ key ] = userOptions[ key ]; return child_process.execSync(cmd + ' 2> /dev/null', options).toString().trim(); } catch (err) { return err; } } /** returns a commented message for use in bash scripts */ function comment (msg) { return '\n# ' + msg + '\n'; } /** * prints the left side of a task while it is being performed */ function start (msg) { const msgLength = strip(msg).length, diff = lineWidth - 4 - msgLength; write(msg + Array(diff + 1).join(' ')); } /** outputs to the terminal with string variable replacement */ function log (msg) { msg = msg || ''; console.log(msg); } /** writes a message without a newline */ function write (msg) { process.stdout.write(msg); } /** prints a horizontal line to the terminal */ function line () { log(Array(lineWidth + 1).join('-')); } })();