blob: 1a5bc16d2d0cfc017e64dad73aadf2c234a1ea01 [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
var co = require('co');
var Q = require('q');
var glob = require('glob');
var path = require('path');
var fs = require('fs');
var util = require('util');
var optimist = require('optimist');
var shelljs = require('shelljs');
var apputil = require('./apputil');
var audit_license = require('./audit-license-headers');
var update_release_notes = require('./update-release-notes');
var create_archive = require('./create-verify-archive');
var executil = require('./executil');
var flagutil = require('./flagutil');
var gitutil = require('./gitutil');
var svnutil = require('./svnutil');
var repoutil = require('./repoutil');
var versionutil = require('./versionutil');
var repoupdate = require('./repo-update');
var repoclone = require('./repo-clone');
var reporeset = require('./repo-reset');
var jira_client = require('jira-client');
var inquirer = require('inquirer');
var semver = require('semver');
var linkify = require('jira-linkify');
var print = apputil.print;
/*
* Pseudo code for plugin automation:
* 1. Who are you? --> this is the release manager. Can we use JIRA for this?
*/
var jira; // issues.apache.org jira client object
var you; // store label for the user here
var jira_user; // store ref to jira project user
var cordova_project; // store ref to jira project for Cordova
var plugins_release_issue; // store ref to jira issue tracking release.
var jira_issue_types; // store ref to all issue types supported by our JIRA instance
var all_plugins_component; // store the specific component associated to the plugin issue.
var jira_task_issue; // store ref to the "task" issue type
var plugin_base; // parent directory holding all cordova plugins
var plugin_repos; // which plugins are we messing with? initially gets set to all plugin repos, later on gets filtered to only those we will release. an array of objects in a special coho-accepted format.
var dist_dev_svn; // cordova dist/dev repo
var dist_svn; // cordova dist repo
var svn_repos; // cordova dist and dist/dev svn repos
var plugin_data = {}; // massive object containing plugin release-relevant information
var plugins_to_release = []; // array of plugin names that need releasing
var plugins_ommitted = []; // array of plugin names that DO NOT need releasing
var plugins_to_merge_manually = []; // array of plugin names that RM will need to merge master into release branch manually.
var svn_user; // username for apache svn
var svn_password; // password for apache svn
var updated_repos; // sentinel variable for if we did repo updates
var created_distdev_dir; // sentinel var for if a new dir was created in cordova-dist-dev
function *updateDesiredRepos(repos) {
if (!updated_repos) {
updated_repos = true;
yield repoclone.cloneRepos(repos, /*silent*/true, null);
yield reporeset.resetRepos(repos, ['master']);
yield repoupdate.updateRepos(repos, ['master'], /*noFetch*/false);
yield repoclone.cloneRepos(svn_repos, /*silent*/true, null);
yield findChangesInPluginRepos(repos);
}
}
function *findChangesInPluginRepos(repos) {
yield repoutil.forEachRepo(repos, function*(repo) {
if (repo.repoName == 'cordova-plugins') return;
var last_release = (yield gitutil.findMostRecentTag())[0];
plugin_data[repo.repoName] = {
last_release: last_release
};
var changes = yield gitutil.summaryOfChanges(last_release);
changes = changes.split('\n').filter(function(line) {
return (line.toLowerCase().indexOf('incremented plugin version') == -1);
});
if (changes.length > 0) {
plugin_data[repo.repoName].needs_release = true;
plugin_data[repo.repoName].changes = changes.join('\n');
plugins_to_release.push(repo.repoName);
} else {
plugin_data[repo.repoName].needs_release = false;
plugins_ommitted.push(repo.repoName);
}
});
}
function manualPluginSelection() {
return inquirer.prompt({
type: 'checkbox',
name: 'plugins_list',
message: 'Select the plugins you want to release:',
choices: plugin_repos.map(function(p) { return p.repoName; }).filter(function(p) { return p != 'cordova-plugins'; })
}).then(function(answer) {
return answer.plugins_list;
});
}
function *interactive_plugins_release() {
console.log('Hi! So you want to do a plugins release, do you?');
// sanity check for tooling that will be needed during releaser.
if (!shelljs.which('gpg')) {
console.warn("You'll need gpg installed and have your Apache GPG keys all set up to do a plugins release!");
console.error('I did not find gpg on your PATH!');
console.error('Refer to ' + create_archive.GPG_DOCS + ' for instructions on how to get that set up as a first step.');
process.exit(1);
}
if (!shelljs.which('svn')) {
console.warn("You'll need svn installed to do a plugins release!");
console.error('I did not find `svn` on your PATH!');
process.exit(1);
}
return Q.fcall(function() {
if (process.env.JIRA_USER && process.env.JIRA_PASSWORD) {
return {
username: process.env.JIRA_USER,
password: process.env.JIRA_PASSWORD
}
} else {
console.log('Let\'s start with your JIRA credentials - this system will be interacting with Apache\'s JIRA instance (issues.apache.org) often.');
console.log('(Note that you can export environment variables `JIRA_USER` and `JIRA_PASSWORD` so I won\'t ask you next time.)');
return inquirer.prompt([{
type: 'input',
name: 'username',
message: 'Please enter your JIRA username'
},{
type: 'password',
name: 'password',
message: 'Please enter your JIRA password'
}])
}
}).then(function(answers) {
var username = answers.username;
you = username;
var password = answers.password;
jira = new jira_client({
protocol: 'https',
host: 'issues.apache.org',
base: 'jira',
apiVersion: '2',
strictSSL: true,
username: username,
password: password
});
return jira.getCurrentUser();
}).then(function(user) {
jira_user = user;
you = user.displayName || you; // either use JIRA display name, or username
console.log('Hey', you, '!');
console.log('Welcome. Let me pull some data from JIRA first...');
return jira.listProjects();
}).then(function(projects) {
// Find the Apache Cordova (CB) project in Apache's JIRA
for (var i = 0; i < projects.length; i++) {
var project = projects[i];
if (project.key == 'CB') {
cordova_project = project;
break;
}
}
return jira.listComponents('CB');
}).then(function(components) {
// Find the ALlPlugins component in Cordova'a JIRA components
for (var i = 0; i < components.length; i++) {
var component = components[i];
if (component.name == 'AllPlugins') {
all_plugins_component = component;
break;
}
}
return jira.listIssueTypes();
}).then(function(issue_types) {
jira_issue_types = issue_types;
for (var i = 0; i < issue_types.length; i++) {
var it = issue_types[i];
if (it.name == 'Task') {
jira_task_issue = it;
}
}
/*
* 2. Are your gpg keys in place? maybe basic validation
*/
inquirer.prompt({
type: 'confirm',
name: 'gpg',
message: 'Are your GPG keys in place?'
}).then(function(answer) {
if (answer.gpg) {
console.log('Great! Let\'s keep going.');
return inquirer.prompt({
type: 'confirm',
name: 'discuss',
message: 'Did you start a "[DISCUSS] Plugins release" thread on the Cordova mailing list?'
});
} else {
console.error('You should get your GPG keys sorted out first!');
console.warn('Follow the instructions here, then come back and try again: ' + create_archive.GPG_DOCS);
process.exit(2);
}
}).then(function(answer) {
/* 3. Y/N did you start a "[DISCUSS] Plugins release" thread on the mailing list?
* - Bonus: Can we parse the mailing list for this?
*/
if (answer.discuss) {
console.log('Nice! Way to keep everyone in the loop!');
return inquirer.prompt({
type: 'confirm',
name: 'jira',
message: 'Is there a JIRA issue filed for the plugins release? (i.e. "Plugins Release, <recent date>") - if not, I will create one for you.'
});
} else {
console.error('You definitely need to have a discussion about the plugins release on the mailing list first. Go do that!');
process.exit(3);
}
}).then(function(answer) {
/* 4. Ask for JIRA issue, or, Create JIRA issue; check docs/plugins-release-process.md for details
* - lets refer to this JIRA issue as $JIRA from here on out.
* - TODO: BONUS: COMMENT to this JIRA issue for each "top-level" step below that is completed.
*/
if (answer.jira) {
return inquirer.prompt({
type: 'input',
name: 'jira',
message: 'What is the JIRA issue number for your plugins release? Please provide only the numerical part of the issue key (i.e. CB-XXXXX)'
}).then(function(answers) {
var cb_issue = 'CB-' + answers.jira;
console.log('Looking for ' + cb_issue + '...');
return jira.findIssue(cb_issue).then(function(issue) {
return issue;
}, function(err) {
console.error('Error finding issue ' + cb_issue + '!');
process.exit(4);
});
});
} else {
console.warn('OK, no problem. I will create one for you now! Hang tight...');
var date = (new Date()).toDateString();
var new_issue = {
"fields": {
"project": {
"id": cordova_project.id
},
"summary": "Plugins Release, " + date,
"description": "Following steps at https://github.com/apache/cordova-coho/blob/master/docs/plugins-release-process.md\nGenerated automatically using cordova-coho.",
"assignee": {
"name": jira_user.name
},
"issuetype": {
"id": jira_task_issue.id
},
"components": [
{
"id": all_plugins_component.id
}
]
}
}
return jira.addNewIssue(new_issue).then(function(issue) {
return issue;
}, function(err) {
console.error('There was a problem creating the JIRA issue!', err);
process.exit(5);
});
}
}).then(function(jira_issue) {
console.log('Sweet, our Plugins Release JIRA issue is ' + jira_issue.key + ' (https://issues.apache.org/jira/browse/' + jira_issue.key + ')!');
plugins_release_issue = jira_issue;
}).then(function() {
/* 5: update the repos. ask the RM if they want to create a new set of repos, or use an existing directory */
return inquirer.prompt([{
type: 'confirm',
name: 'use_existing_plugins',
message: 'Do you want to use an existing set of plugin repositories? WARNING: If no, I will ask for a directory where I will clone all the needed repositories.'
}, {
type: 'input',
name: 'cwd',
default: apputil.getBaseDir(),
message: function(answers) {
if (answers.use_existing_plugins) {
return 'We need to update the plugin and apache SVN repositories. Enter the directory containing all of your Apache Cordova source code repositories (absolute or relative paths work here)';
} else {
return 'Please enter the directory you want to house the plugin repos we will work with. This directory will be created if it does not exist.';
}
}
}, {
type: 'confirm',
name: 'auto_detect',
message: 'Do you want me to try to auto-detect which plugins could use releases? If not, you will need to manually select which plugins you want to release. WARNING: if you use auto-detection along with the "clone-all-repos" option, you will be cloning a loooooong time.'
}, {
type: 'confirm',
name: 'ok',
message: function(answers) {
return 'WARNING! Are you sure the following directory is where you want the plugin repositories we will be working with to be located? We will be cloning/updating repos here: ' + path.resolve(path.normalize(answers.cwd));
}
}]);
}).then(function(answers) {
if (answers.ok) {
plugin_base = path.resolve(path.normalize(answers.cwd));
shelljs.mkdir('-p', plugin_base);
process.chdir(plugin_base);
plugin_repos = flagutil.computeReposFromFlag('plugins', {includeSvn:true});
dist_svn = flagutil.computeReposFromFlag('dist', {includeSvn:true});
dist_dev_svn = flagutil.computeReposFromFlag('dist/dev', {includeSvn:true});
svn_repos = dist_svn.concat(dist_dev_svn);
dist_svn = dist_svn[0];
dist_dev_svn = dist_dev_svn[0];
if (answers.auto_detect) {
return co.wrap(function *() {
yield updateDesiredRepos(plugin_repos);
return inquirer.prompt({
type: 'confirm',
name: 'plugins_ok',
message: 'I\'ve detected ' + plugins_to_release.length + ' plugin' + (plugins_to_release.length==1?'':'s') + ' to release: ' + plugins_to_release.join(', ') + '\nThat means we\'re skipping ' + plugins_ommitted.length + ' plugin' + (plugins_ommitted.length==1?'':'s') + ': ' + plugins_ommitted.join(', ') + '\nDo you want to proceed with the release process around the specified plugins above (and ommitting the ones specified as well)?'
}).then(function(answers) {
if (answers.plugins_ok) {
return plugins_to_release;
} else {
return manualPluginSelection();
}
});
})();
} else {
// No auto-detection, manually specified list of plugins.
return manualPluginSelection();
}
} else {
console.warn('Womp womp, try again? Probably this flow should be better eh?');
process.exit(6);
}
}).then(function(plugins_list) {
// at this point we either have a verified, or manually-specified, list of plugins to release.
plugins_to_release = plugins_list;
// modify the coho-formatted list of plugin repos to filter out to only the plugins we want to release.
plugin_repos = plugin_repos.filter(function(plugin) {
return plugins_to_release.indexOf(plugin.repoName) > -1;
});
return co.wrap(function *() {
yield updateDesiredRepos(plugin_repos);
// and remove all the data we collected for plugins we no longer care about.
var data_keys = Object.keys(plugin_data);
data_keys.forEach(function(key) {
if (plugins_to_release.indexOf(key) == -1) {
delete plugin_data[key];
}
});
/* 7. ensure license headers are present everywhere.*/
console.log('Checking license headers for specified plugin repos...');
var unknown_licenses = [];
yield audit_license.scrubRepos(plugin_repos, /*silent*/true, /*allowError*/false, function(repo, stdout) {
var unknown = stdout.split('\n').filter(function(line) {
return line.indexOf('Unknown Licenses') > -1;
})[0];
if (unknown[0] != '0') {
// There are some unknown licenses!
unknown_licenses.push({repo:repo.repoName,unknown:unknown});
}
});
return yield Promise.resolve(unknown_licenses);
})();
}).then(function(unknowns) {
if (unknowns.length) {
console.warn('We identified some unknown licenses in plugin repos!');
console.warn(unknowns);
return inquirer.prompt({
type: 'confirm',
name: 'proceed',
message: 'Do you want to proceed even though there are license problems?'
}).then(function(answer) {
if (!answer.proceed) {
console.error('License audit failed - good idea to abort. Fix it up and come back!');
process.exit(7);
}
});
} else {
console.log('No license issues found - continuing.');
}
}).then(function() {
return inquirer.prompt({
type: 'confirm',
name: 'proceed',
message: 'We are now ready to start making changes to the selected plugin repo, which will make changes to the master and release branches. Shall we proceed?'
}).then(function(answers) {
if (!answers.proceed) {
console.error('Bailing!');
process.exit(99);
}
});
}).then(function() {
// TODO: step 8 apparently is "rarely" done based on fil's experience running through the plugin release steps manually.
// soooo.... what do?
/* 8. ensure all dependencies and subdependencies have apache-compatible licenses.
* 9. update plugin versions + release notes.
* - for each plugin, remove the `-dev` suffix in plugin.xml, package.json, and plugin.xml of `tests/` subdirectory (if exists)*/
console.log('Removing the "-dev" suffix from versions in the master branch...');
return co.wrap(function *() {
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
yield gitutil.gitCheckout('master');
var current_version = yield versionutil.getRepoVersion(repo);
console.log(repo.repoName, '\'s current version is', current_version);
var devless_version = versionutil.removeDev(current_version);
plugin_data[repo.repoName].current_release = devless_version;
yield versionutil.updateRepoVersion(repo, devless_version, {commitChanges:false});
});
})();
}).then(function() {
/* - each plugin may need a version bump.
* - how to determine if patch, minor or major? show changes to each plugin and then prompt Release Manager for a decision?
* - reuse coho 'update release notes' command */
return co.wrap(function *() {
var plugs = Object.keys(plugin_data);
var release_note_prompts = [];
yield plugs.map(function*(plugin) {
var data = plugin_data[plugin];
var changes = data.changes;
var final_notes = yield update_release_notes.createNotes(plugin, data.current_release, changes);
release_note_prompts.push({
type: 'editor',
name: plugin,
message: 'Please tweak and compile ' + plugin + ' release notes',
default: final_notes
});
/* - what's the average case? just a patch bump? perhaps, for each plugin, show release notes and let RM override version beyond patch bump if RM believes it is necessary? */
release_note_prompts.push({
type: 'input',
name: plugin + '-version',
message: function(answers) {
var new_changes = answers[plugin];
var first_heading = new_changes.indexOf('###');
var second_heading = new_changes.indexOf('###', first_heading + 3);
var first_change = new_changes.indexOf('\n', first_heading + 3);
var len = second_heading - first_change;
var change_summary = new_changes.substr(first_change, len);
return 'Please enter a semver-compatible version number for this release of ' + plugin + ', based on the changes below:\n' + change_summary;
},
default: data.current_release,
validate: function(input) {
if (semver.valid(input)) {
return true;
} else {
return 'That\'s not a valid semver version!';
}
}
});
});
return inquirer.prompt(release_note_prompts);
})();
}).then(function(release_notes) {
return co.wrap(function *() {
console.log('Writing out new release notes and plugin versions (if applicable)...');
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
if (plugin_data[plugin_name].current_release != release_notes[plugin_name + '-version']) {
// If, after release notes review, RM decided on a different version...
// Overwrite plugin version
var previous_assumed_version = plugin_data[plugin_name].current_release;
plugin_data[plugin_name].current_release = release_notes[plugin_name + '-version'];
yield versionutil.updateRepoVersion(repo, plugin_data[plugin_name].current_release, {commitChanges:false});
// also overwrite the version originally specified in the release notes file, since we changed it now!
var rn = release_notes[plugin_name];
var new_rn = rn.replace(new RegExp('### ' + previous_assumed_version, 'g'), '### ' + plugin_data[plugin_name].current_release);
release_notes[plugin_name] = new_rn;
}
fs.writeFileSync(update_release_notes.FILE, release_notes[plugin_name], {encoding: 'utf8'});
/* - commit changes to versions and release notes together with description '$JIRA Updated version and release notes for release $v'
* - tag each plugin repo with $v*/
if (yield gitutil.pendingChangesExist()) {
yield gitutil.commitChanges(plugins_release_issue.key + ' Updated version and RELEASENOTES.md for release ' + plugin_data[plugin_name].current_release + ' (via coho)');
yield gitutil.tagRepo(plugin_data[plugin_name].current_release);
} else {
console.warn('No pending changes detected for ' + plugin_name + '; that\'s probably not good eh?');
}
});
})();
}).then(function() {
/* 10. Create release branch. Check if release branch, which would be named in the form "major.minor.x" (i.e. 2.3.x) already exists */
return co.wrap(function *() {
var repos_with_existing_release_branch = [];
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
var plugin_version = plugin_data[plugin_name].current_release;
var release_branch_name = versionutil.getReleaseBranchNameFromVersion(plugin_version);
if (yield gitutil.remoteBranchExists(repo, release_branch_name)) {
repos_with_existing_release_branch.push(repo);
// also store HEAD of release branch, so later on we can show a diff of the branch before pushing
plugin_data[plugin_name].previous_release_branch_head = gitutil.hashForRef(release_branch_name);
console.log('Release branch', release_branch_name, 'already exists, time to update it. We will be making changes to this existing branch.');
yield repoupdate.updateRepos([repo], [release_branch_name], /*noFetch*/false);
} else {
yield gitutil.createNewBranch(release_branch_name);
console.log('Created branch', release_branch_name, 'in repo', plugin_name);
}
});
return repos_with_existing_release_branch;
})();
}).then(function(repos_with_existing_release_branch) {
// Here we are passed an array of repos that already had release branches created prior to starting the release process.
// Our mission in this clause, should we choose to accept it, is to merge master back into the branch. But, this can be dangerous!
// Ask the RM if they want us to handle the merge automatically.
// If the RM says no, we will prompt them to handle it manually later.
var prompts = [];
repos_with_existing_release_branch.forEach(function(repo) {
var plugin_name = repo.repoName;
var rb = versionutil.getReleaseBranchNameFromVersion(plugin_data[plugin_name].current_release)
prompts.push({
type: 'confirm',
name: 'rb_automerge_proceed_' + plugin_name,
message: plugin_name + ' already has an existing release branch "' + rb + '". Do you want me to automatically merge master into this branch for you? If no, I will prompt you to modify the release branch yourself at a later time in this session.\nWARNING: this will run `git merge master -s recursive -X theirs` from the release branch, essentially favouring master branch changes. Only proceed with the auto-merge if you understand the repercussions of doing so.'
});
});
return inquirer.prompt(prompts);
}).then(function(answers) {
return co.wrap(function *() {
var prompts = [];
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
if (answers['rb_automerge_proceed_' + plugin_name]) {
// Auto-merge master into the release branch.
var rb = versionutil.getReleaseBranchNameFromVersion(plugin_data[plugin_name].current_release);
console.log('Checking out "' + rb + '" branch of', plugin_name, 'and merging master in...');
yield executil.execHelper(executil.ARGS('git merge -s recursive -X theirs', 'master'), false, false, function() {
console.log('Merge was fine, continuing.');
}, function(e) {
//yield gitutil.merge('master', function() { console.log('merge was fine, continuing.'); }, function(e) {
plugins_to_merge_manually.push(plugin_name);
});
} else {
plugins_to_merge_manually.push(plugin_name);
}
});
return inquirer.prompt(prompts);
})();
}).then(function() {
// prompt the RM about the plugins with manual merging work needed here.
var prompts = [];
plugins_to_merge_manually.forEach(function(plugin_name) {
var rb = versionutil.getReleaseBranchNameFromVersion(plugin_data[plugin_name].current_release)
prompts.push({
type: 'confirm',
name: 'rb_manualmerge_proceed_' + plugin_name,
message: plugin_name + ' already has an existing release branch "' + rb + '", and it needs a manual merge of master into it (either because you specified that, or because there was a merge conflict during auto-merge. Now is your chance to manually merge / cherry-pick / resolve conflicts on the "' + rb + '" branch. Once you have done this (probably in a separate shell or command prompt), hit Enter to continue.'
});
});
return inquirer.prompt(prompts);
}).then(function() {
/* 11. Increment plugin versions back on the master branch to include -dev*/
// Also increment the patch version
// So, check out master branch and do the thing.
return co.wrap(function *() {
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
var newest_version = semver.inc(plugin_data[plugin_name].current_release, 'patch') + '-dev';
console.log('Checking out master branch of', plugin_name, 'and setting version to', newest_version, ', then committing that change to master branch...');
yield gitutil.gitCheckout('master');
// store previous master HEAD, for later comparison/showing of diff
plugin_data[plugin_name].previous_master_head = gitutil.hashForRef('master');
yield versionutil.updateRepoVersion(repo, newest_version, {commitChanges:true});
});
})();
}).then(function() {
/* 12. Push tags, release branch, and master branch changes.
* start with pushing tag, then compile diffs for master branch push and ask user if they approve before pushing master */
return co.wrap(function *() {
var master_prompts = [];
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
var tag = plugin_data[plugin_name].current_release;
console.log(plugin_name, ': pushing tag ', tag);
yield gitutil.pushToOrigin(tag);
/* - show diff of last master commit for user confirmation*/
var diff = yield gitutil.diff(plugin_data[plugin_name].previous_master_head, 'master');
master_prompts.push({
type: 'confirm',
name: 'master_' + plugin_name,
message: 'About to push the following changes to the master branch of ' + plugin_name + ': ' + diff + '\nDo you wish to continue?'
});
});
return inquirer.prompt(master_prompts);
})();
}).then(function(answers) {
/*check confirmations and exit if RM bailed*/
return co.wrap(function *() {
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
if (!answers['master_' + plugin_name]) {
console.error('Aborting as master branch changes for ' + plugin_name + ' were not approved!');
process.exit(8);
}
});
})();
}).then(function() {
// at this point RM is cool pushing master branch changes up.
return co.wrap(function *() {
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
// at this point still have master branch checked out
yield gitutil.pushToOrigin('master');
});
})();
}).then(function() {
/* - show diff of release branch:
* - if release branch did not exist before, show diff (simple, just master..branch), confirm, then push
* - if release branch did exist before, show diff (last branch commit..HEAD), confirm, then push*/
return co.wrap(function *() {
var rb_prompts = [];
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
var plugin_version = plugin_data[plugin_name].current_release;
var release_branch_name = versionutil.getReleaseBranchNameFromVersion(plugin_version);
var previous_release_branch_head = plugin_data[plugin_name].previous_release_branch_head;
yield gitutil.gitCheckout(release_branch_name);
if (previous_release_branch_head) {
// release branch previously existed.
var diff = yield gitutil.diff(previous_release_branch_head, 'HEAD');
rb_prompts.push({
type: 'confirm',
name: 'rb_' + plugin_name,
message: 'About to push the following changes to the EXISTING release branch (' + release_branch_name + ') of ' + plugin_name + ': ' + diff + '\nDo you wish to continue?'
});
} else {
// release branch did NOT exist previously, this is a new release branch.
var diff = yield gitutil.diff('master', release_branch_name);
rb_prompts.push({
type: 'confirm',
name: 'rb_' + plugin_name,
message: 'About to push the following changes (compared to master) to the NEW release branch (' + release_branch_name + ') of ' + plugin_name + ': ' + diff + '\nDo you wish to continue?'
});
}
});
return inquirer.prompt(rb_prompts);
})();
}).then(function(answers) {
/*check confirmations and exit if RM bailed*/
return co.wrap(function *() {
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
if (!answers['rb_' + plugin_name]) {
console.error('Aborting as release branch changes for ' + plugin_name + ' were not approved!');
process.exit(8);
}
});
})();
}).then(function() {
// at this point RM is cool pushing master branch changes up.
return co.wrap(function *() {
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
var plugin_version = plugin_data[plugin_name].current_release;
var release_branch_name = versionutil.getReleaseBranchNameFromVersion(plugin_version);
// at this point have release branch checked out
yield gitutil.pushToOrigin(release_branch_name);
});
})();
}).then(function() {
// 13. Publish to apache svn:
// - first update dist-dev repo
return co.wrap(function *() {
var orig_dir = process.cwd();
var dist_dev_repo = path.join(plugin_base, dist_dev_svn.repoName);
process.chdir(dist_dev_repo);
console.log('Updating dist-dev svn repo...');
yield svnutil.update();
process.chdir(orig_dir);
})();
}).then(function() {
// - create-archive -r $ACTIVE --dest cordova-dist-dev/$JIRA
return co.wrap(function *() {
// location to store the archives in.
var dist_dev_dir = path.join(plugin_base, dist_dev_svn.repoName, plugins_release_issue.key);
if (!(fs.existsSync(dist_dev_dir))) {
shelljs.mkdir('-p', dist_dev_dir);
created_distdev_dir = true;
}
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
var plugin_name = repo.repoName;
var tag = plugin_data[plugin_name].current_release;
yield gitutil.gitCheckout(tag);
var archive = yield create_archive.createArchive(repo, tag, dist_dev_dir, true/*sign*/);
// - verify-archive cordova-dist-dev/$JIRA/*.tgz
yield create_archive.verifyArchive(archive);
yield gitutil.gitCheckout('master');
});
})();
}).then(function() {
if (process.env.SVN_USER && process.env.SVN_PASSWORD) {
return {
username: process.env.SVN_USER,
password: process.env.SVN_PASSWORD
};
} else {
console.log('We are about to push changes up to Apache SVN! Let me get your SVN credentials.');
console.log('(For next time, you can export the `SVN_USER` and `SVN_PASSWORD` environment variables to skip me asking you.)');
return inquirer.prompt([{
type: 'input',
name: 'username',
message: 'Please enter your svn username'
},{
type: 'password',
name: 'password',
message: 'Please enter your svn password'
}]);
}
}).then(function(answers) {
svn_user = answers.username;
svn_password = answers.password;
// - upload by running `svn` add and commit commands.
return co.wrap(function *() {
var orig_dir = process.cwd();
var dist_dev_repo = path.join(plugin_base, dist_dev_svn.repoName);
if (created_distdev_dir) {
// if we created the dir containing the archives, then we can
// just svn add the entire dir.
process.chdir(dist_dev_repo);
yield svnutil.add(plugins_release_issue.key);
yield svnutil.commit(svn_user, svn_password, plugins_release_issue.key + ': Uploading release candidates for plugins release');
} else {
// if it already existed, then we need to painstakingly add
// each individual archive file cause svn is cool
var archives_for_plugins = [];
yield repoutil.forEachRepo(plugin_repos, function*(repo) {
process.chdir(dist_dev_repo);
var plugin_name = repo.repoName;
var tag = plugin_data[plugin_name].current_release;
var fileref = plugin_name + '-' + tag;
archives_for_plugins.push(fileref);
var files_to_add = glob.sync(path.join(plugins_release_issue.key, fileref + '*'));
for (var i = 0; i < files_to_add.length; i++) {
yield svnutil.add(files_to_add[i]);
}
});
process.chdir(dist_dev_repo);
yield svnutil.commit(svn_user, svn_password, plugins_release_issue.key + ': Uploading release candidates for plugins ' + archives_for_plugins.join(', '));
}
process.chdir(orig_dir);
})();
}).then(function() {
console.log('Nicely done! Last few things you should do:');
/* 14. Dump instructions only? Prepare blog post - perhaps can dump out release notes-based blog content.
* - TODO: this apparently ends up as a .md file in cordova-docs. perhaps can dump this as a shell of a file into the cordova-docs repo? maybe even auto-branch the docs repo in prep for a PR?*/
console.log('1. Prepare a blog post: https://github.com/apache/cordova-coho/blob/master/docs/plugins-release-process.md#prepare-blog-post');
console.log('2. Start a vote thread! https://github.com/apache/cordova-coho/blob/master/docs/plugins-release-process.md#start-vote-thread');
console.log('3. You should test these plugins out! Check out cordova-mobile-spec, and in particular, the `createmobilespec` script that comes with it - it\'s a quick way to create a test project with all plugins included.');
process.exit(0);
});
}, function(auth_err) {
var keys = Object.keys(auth_err);
console.error('ERROR! There was a problem connecting to JIRA, received a', auth_err.statusCode, 'status code.');
process.exit(1);
});
/* 16. Bonus: separate script to 'approve' a plugins release, which would:
* - publish the artifacts to dist/ in apache
* - "tell apache about the release" which has a TODO to write a helper to POST the request appropriately.
* - publish to npm
* - push 'permanent release tags' (for apache) to git
* - issue cordova-docs blog post PR (only if we auto-branch in cordova-docs repo, as per step 14)
* - dump instructions only? post an ANNOUNCE thread to ML.
* - close $JIRA issue
*/
/*
* TODO: Need ability to serialize process of plugins release - save state of the process at any point.
*/
}
module.exports.interactive = interactive_plugins_release;
/*
* A function that handles version if it is defined or undefined
*
* @param {String} repo current repo
* @param {String|undefined} ver current version that can be defined or undefined
* @param {String|undefined} validate current version that can be defined or undefined
*
* @return {String} version Returns the calculated version
*
*/
// TODO: if using this function only to retrieve repo version, use the new
// versionutil.getRepoVersion method instead.
function *handleVersion(repo, ver, validate) {
var platform = repo.id;
var version = ver || undefined;
if (version === undefined) {
yield repoutil.forEachRepo([repo], function*() {
// Grabbing version from platformPackageJson
var platformPackage = path.join(process.cwd(), 'package.json');
var platformPackageJson = require(platformPackage);
if(validate === true) {
version = flagutil.validateVersionString(platformPackageJson.version);
} else {
version = platformPackageJson.version;
}
});
}
return version;
}