#!/usr/bin/env node /*global cat:true cd:true cp:true echo:true exec:true exit:true ls:true*/ "use strict"; var baseDir, repoDir, prevVersion, newVersion, nextVersion, tagTime, fs = require( "fs" ), path = require( "path" ), // support: node <0.8 existsSync = fs.existsSync || path.existsSync, rnewline = /\r?\n/, repo = "git@github.com:jquery/jquery-ui.git", branch = "master"; walk([ bootstrap, section( "setting up repo" ), cloneRepo, checkState, section( "calculating versions" ), getVersions, confirm, section( "building release" ), buildRelease, section( "pushing tag" ), confirmReview, pushRelease, section( "updating branch version" ), updateBranchVersion, section( "pushing " + branch ), confirmReview, pushBranch, section( "generating changelog" ), generateChangelog, section( "gathering contributors" ), gatherContributors, section( "generating quick download" ), generateQuickDownload, section( "updating trac" ), updateTrac, confirm // TODO: upload release zip to GitHub ]); function cloneRepo() { echo( "Cloning " + repo.cyan + "..." ); git( "clone " + repo + " " + repoDir, "Error cloning repo." ); cd( repoDir ); echo( "Checking out " + branch.cyan + " branch..." ); git( "checkout " + branch, "Error checking out branch." ); echo(); echo( "Installing dependencies..." ); if ( exec( "npm install" ).code !== 0 ) { abort( "Error installing dependencies." ); } if ( exec( "npm install download.jqueryui.com" ).code !== 0 ) { abort( "Error installing dependencies." ); } echo(); } function checkState() { echo( "Checking AUTHORS.txt..." ); var result, lastActualAuthor, lastListedAuthor = cat( "AUTHORS.txt" ).trim().split( rnewline ).pop(); result = exec( "grunt authors", { silent: true }); if ( result.code !== 0 ) { abort( "Error getting list of authors." ); } lastActualAuthor = result.output.split( rnewline ).splice( -4, 1 )[ 0 ]; if ( lastListedAuthor !== lastActualAuthor ) { echo( "Last listed author is " + lastListedAuthor.red + "." ); echo( "Last actual author is " + lastActualAuthor.green + "." ); abort( "Please update AUTHORS.txt." ); } echo( "Last listed author (" + lastListedAuthor.cyan + ") is correct." ); } function getVersions() { // prevVersion, newVersion, nextVersion are defined in the parent scope var parts, major, minor, patch, currentVersion = readPackage().version; echo( "Validating current version..." ); if ( currentVersion.substr( -3, 3 ) !== "pre" ) { echo( "The current version is " + currentVersion.red + "." ); abort( "The version must be a pre version." ); } newVersion = currentVersion.substr( 0, currentVersion.length - 3 ); parts = newVersion.split( "." ); major = parseInt( parts[ 0 ], 10 ); minor = parseInt( parts[ 1 ], 10 ); patch = parseInt( parts[ 2 ], 10 ); // TODO: handle 2.0.0 if ( minor === 0 ) { abort( "This script is not smart enough to handle the 2.0.0 release." ); } prevVersion = patch === 0 ? [ major, minor - 1, 0 ].join( "." ) : [ major, minor, patch - 1 ].join( "." ); // TODO: Remove version hack after 1.9.0 release if ( prevVersion === "1.8.0" ) { prevVersion = "1.8"; } nextVersion = [ major, minor, patch + 1 ].join( "." ) + "pre"; echo( "We are going from " + prevVersion.cyan + " to " + newVersion.cyan + "." ); echo( "After the release, the version will be " + nextVersion.cyan + "." ); } function buildRelease() { var pkg; echo( "Creating " + "release".cyan + " branch..." ); git( "checkout -b release", "Error creating release branch." ); echo(); echo( "Updating package.json..." ); pkg = readPackage(); pkg.version = newVersion; pkg.author.url = pkg.author.url.replace( "master", newVersion ); pkg.licenses.forEach(function( license ) { license.url = license.url.replace( "master", newVersion ); }); writePackage( pkg ); echo( "Generating manifest files..." ); if ( exec( "grunt manifest" ).code !== 0 ) { abort( "Error generating manifest files." ); } echo(); echo( "Building release..." ); if ( exec( "grunt release_cdn" ).code !== 0 ) { abort( "Error building release." ); } echo(); echo( "Committing release artifacts..." ); git( "add *.jquery.json", "Error adding manifest files to git." ); git( "commit -am 'Tagging the " + newVersion + " release.'", "Error committing release changes." ); echo(); echo( "Tagging release..." ); git( "tag " + newVersion, "Error tagging " + newVersion + "." ); tagTime = git( "log -1 --format='%ad'", "Error getting tag timestamp." ).trim(); } function pushRelease() { echo( "Pushing release to GitHub..." ); git( "push --tags", "Error pushing tags to GitHub." ); } function updateBranchVersion() { var pkg; echo( "Checking out " + branch.cyan + " branch..." ); git( "checkout " + branch, "Error checking out " + branch + " branch." ); echo( "Updating package.json..." ); pkg = readPackage(); pkg.version = nextVersion; writePackage( pkg ); echo( "Committing version update..." ); git( "commit -am 'Updating the " + branch + " version to " + nextVersion + ".'", "Error committing package.json." ); } function pushBranch() { echo( "Pushing " + branch.cyan + " to GitHub..." ); git( "push", "Error pushing to GitHub." ); } function generateChangelog() { var commits, changelogPath = baseDir + "/changelog", changelog = cat( "build/release/changelog-shell" ) + "\n", fullFormat = "* %s (TICKETREF, [%h](http://github.com/jquery/jquery-ui/commit/%H))"; changelog = changelog.replace( "{title}", "jQuery UI " + newVersion + " Changelog" ); echo ( "Adding commits..." ); commits = gitLog( fullFormat ); echo( "Adding links to tickets..." ); changelog += commits // Add ticket references .map(function( commit ) { var tickets = []; commit.replace( /Fixe[sd] #(\d+)/g, function( match, ticket ) { tickets.push( ticket ); }); return tickets.length ? commit.replace( "TICKETREF", tickets.map(function( ticket ) { return "[#" + ticket + "](http://bugs.jqueryui.com/ticket/" + ticket + ")"; }).join( ", " ) ) : // Leave TICKETREF token in place so it's easy to find commits without tickets commit; }) // Sort commits so that they're grouped by component .sort() .join( "\n" ) + "\n"; echo( "Adding Trac tickets..." ); changelog += trac( "/query?milestone=" + newVersion + "&resolution=fixed" + "&col=id&col=component&col=summary&order=component" ) + "\n"; fs.writeFileSync( changelogPath, changelog ); echo( "Stored changelog in " + changelogPath.cyan + "." ); } function gatherContributors() { var contributors, contributorsPath = baseDir + "/contributors"; echo( "Adding committers and authors..." ); contributors = gitLog( "%aN%n%cN" ); echo( "Adding reporters and commenters from Trac..." ); contributors = contributors.concat( trac( "/report/22?V=" + newVersion + "&max=-1" ) .split( rnewline ) // Remove header and trailing newline .slice( 1, -1 ) ); echo( "Sorting contributors..." ); contributors = unique( contributors ).sort(function( a, b ) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1; }); echo ( "Adding people thanked in commits..." ); contributors = contributors.concat( gitLog( "%b%n%s" ).filter(function( line ) { return (/thank/i).test( line ); })); fs.writeFileSync( contributorsPath, contributors.join( "\n" ) ); echo( "Stored contributors in " + contributorsPath.cyan + "." ); } function generateQuickDownload() { var config, downloadDir = repoDir + "/node_modules/download.jqueryui.com", filename = "jquery-ui-" + newVersion + ".custom.zip", destination = baseDir + "/" + filename; cd( downloadDir ); // Update jQuery UI version for download builder config = JSON.parse( cat( "config.json" ) ); config.jqueryUi = newVersion; JSON.stringify( config ).to( "config.json" ); // Generate quick download // TODO: Find a way to avoid having to clone jquery-ui inside download builder if ( exec( "grunt prepare build" ).code !== 0 ) { abort( "Error generating quick download." ); } cp( downloadDir + "/release/" + filename, destination ); // cp() doesn't have error handling, so check for the file if ( ls( destination ).length !== 1 ) { abort( "Error copying quick download." ); } // Go back to repo directory for consistency cd( repoDir ); } function updateTrac() { echo( newVersion.cyan + " was tagged at " + tagTime.cyan + "." ); echo( "Close the " + newVersion.cyan + " Milestone with the above date and time." ); echo( "Create the " + newVersion.cyan + " Version with the above date and time." ); echo( "Create a Milestone for the next minor release." ); } // ===== HELPER FUNCTIONS ====================================================== function git( command, errorMessage ) { var result = exec( "git " + command ); if ( result.code !== 0 ) { abort( errorMessage ); } return result.output; } function gitLog( format ) { var result = exec( "git log " + prevVersion + ".." + newVersion + " " + "--format='" + format + "'", { silent: true }); if ( result.code !== 0 ) { abort( "Error getting git log." ); } result = result.output.split( rnewline ); if ( result[ result.length - 1 ] === "" ) { result.pop(); } return result; } function trac( path ) { var result = exec( "curl -s 'http://bugs.jqueryui.com" + path + "&format=tab'", { silent: true }); if ( result.code !== 0 ) { abort( "Error getting Trac data." ); } return result.output; } function unique( arr ) { var obj = {}; arr.forEach(function( item ) { obj[ item ] = 1; }); return Object.keys( obj ); } function readPackage() { return JSON.parse( fs.readFileSync( repoDir + "/package.json" ) ); } function writePackage( pkg ) { fs.writeFileSync( repoDir + "/package.json", JSON.stringify( pkg, null, "\t" ) + "\n" ); } function bootstrap( fn ) { console.log( "Determining directories..." ); baseDir = process.cwd() + "/__release"; repoDir = baseDir + "/repo"; if ( existsSync( baseDir ) ) { console.log( "The directory '" + baseDir + "' already exists." ); console.log( "Aborting." ); process.exit( 1 ); } console.log( "Creating directory..." ); fs.mkdirSync( baseDir ); console.log( "Installing dependencies..." ); require( "child_process" ).exec( "npm install shelljs colors", { cwd: baseDir }, function( error ) { if ( error ) { console.log( error ); return process.exit( 1 ); } require( "shelljs/global" ); require( "colors" ); fn(); }); } function section( name ) { return function() { echo(); echo( "##" ); echo( "## " + name.toUpperCase().magenta ); echo( "##" ); echo(); }; } function prompt( fn ) { process.stdin.once( "data", function( chunk ) { process.stdin.pause(); fn( chunk.toString().trim() ); }); process.stdin.resume(); } function confirm( fn ) { echo( "Press enter to continue, or ctrl+c to cancel.".yellow ); prompt( fn ); } function confirmReview( fn ) { echo( "Please review the output and generated files as a sanity check.".yellow ); confirm( fn ); } function abort( msg ) { echo( msg.red ); echo( "Aborting.".red ); exit( 1 ); } function walk( methods ) { var method = methods.shift(); function next() { if ( methods.length ) { walk( methods ); } } if ( !method.length ) { method(); next(); } else { method( next ); } }