rally-tools
Version:
The rally tools cli interface
341 lines (295 loc) • 13.3 kB
JavaScript
import {RallyBase, lib, AbortError, Collection, sleep, zip} from "./rally-tools.js";
import {configObject} from "./config.js";
import {spawn, runGit, runCommand} from "./decorators.js";
import rp from "request-promise";
import Stage from "./stage.js";
import fetch from "node-fetch";
import {Octokit} from "@octokit/rest";
import { config } from "chai";
let okit = null;
export const prodReadyLabel = "Ready For Release";
export const prodManualLabel = "Ready For Release (manual)";
export const prodMergedLabel = "Release Merged";
export const prodHotfixLabel = "hotfix";
/* The deployment process is separated into two different parts:
* `rally deploy prep` Links jira tickets to PRs and assigns labels based on their status
* `rally deploy merge` Takes all the labeled PRs, changes their base branch to the release, and merges them
*/
let Deploy = {
async test() {
//await this.makeRelease();
await this.gh();
},
get octokit(){
if(okit) return okit;
return okit = new Octokit({
auth: configObject.deploy.github,
userAgent: `rally-tools deploy ${configObject.appName}`,
});
},
getOctokitConfig() {
return {
owner: configObject.deploy.org,
repo: configObject.deploy.repo,
};
},
async getIssues(needsJira){
let base = this.getOctokitConfig();
let pullList = await this.octokit.paginate("GET /repos/{owner}/{repo}/issues", base);
let s = [];
for(let issue of pullList) {
s.push(await this.assembleIssue(issue, needsJira));
}
return s;
},
async gh(){
let issues = await this.getIssues(true);
for(let issue of issues){
//await this.printIssue(issue);
await this.checkStatus(issue);
}
},
cardRegex: /\[(?:\w+\s*\-\s*)?(\w+)\s*\-\s*(\d+)\]/,
async assembleIssue(issue, needsJira){
let parsedTitle = issue.parsedTitle = this.cardRegex.exec(issue.title);
if(configObject.verbose) {
write(chalk`Found github issue: {blue ${issue.title}}... `);
}
if(!parsedTitle || !needsJira) {
if(configObject.verbose) {
log(`No jira issue found in title`);
}
return issue;
}
let cardLink = `${configObject.deploy.board}/issue/${parsedTitle[1]}-${parsedTitle[2]}`;
let requestOptions = {
method: "GET",
headers: {
"Authorization": `Basic ${(Buffer.from(configObject.deploy.jira)).toString("base64")}`,
}
};
if(configObject.verbose) {
log(chalk`Checking jira board: {green ${this.printJiraTicket(issue)}}.`);
}
let response = await fetch(cardLink, requestOptions);
let jiraInfo = await response.json();
if(configObject.vvverbose){
log();
log(cardLink);
log(jiraInfo);
}
if(jiraInfo.errorMessages) {
log(cardLink);
log(jiraInfo.errorMessages);
return issue;
}
let parsedInfo = {
assignee_dev: jiraInfo.fields.assignee,
reporter: jiraInfo.fields.reporter,
labels: jiraInfo.fields.labels,
creator: jiraInfo.fields.creator,
status: jiraInfo.fields.status
};
issue.jiraInfoFull = jiraInfo;
issue.jira = parsedInfo;
if(configObject.verbose) {
log(chalk`Status of {green ${this.printJiraTicket(issue)}} is {red ${parsedInfo.status.name}}.`);
}
return issue;
},
name(j){
if(!j) return "(None)";
return j.displayName;
},
async printIssue(issue){
if(!issue.jira) return;
let j = issue.jira;
let f = issue.jiraInfoFull;
let format = chalk`PR #${issue.number}: ${issue.title}
Dev: ${this.name(j.assignee_dev)}
Status: ${j.status.name}
URL: ${issue.pull_request.html_url}
`;
log(format);
},
async setBase(issue, newBase) {
let config = this.getOctokitConfig();
config.pull_number = issue.number;
config.base = newBase;
return await this.octokit.request("PATCH /repos/{owner}/{repo}/pulls/{pull_number}", config);
},
async modifyLabel(issue, label, shouldHave){
let labels = new Set(issue.labels.map(x => x.name));
let oldSize = labels.size;
let verb;
if(shouldHave){
verb = "Adding";
labels.add(label);
}else{
verb = "Removing";
labels.delete(label);
}
if(labels.size != oldSize){
let config = this.getOctokitConfig();
config.pull_number = issue.number;
config.labels = Array.from(labels);
log(chalk`${verb} label {green ${label}} on {blue PR #${issue.number}}`)
return await this.octokit.request("PATCH /repos/{owner}/{repo}/issues/{pull_number}", config);
}
return [null, null];
},
async checkStatus(issue) {
if(configObject.vvverbose){
log(issue);
}
let labels = new Set(issue.labels.map(x => x.name));
if(!issue.jira){
if(labels.has(prodReadyLabel)){
log(chalk`{yellow Warning:} PR #${issue.number} has prod label but no linked jira card`);
}
return;
}
let board = issue?.parsedTitle?.[1];
let requiredProdStatus = configObject.deploy.boardMappings[board];
if(requiredProdStatus){
await this.modifyLabel(issue, prodReadyLabel, issue.jira.status.name == requiredProdStatus);
}
},
printJiraTicket(issue){
if(issue.parsedTitle){
return `${issue.parsedTitle[1]}-${issue.parsedTitle[2]}`;
}else{
return `(No Jira Ticket)`;
}
},
async makeRelease(args){
let releaseBranchName = ""
if(args.branch) {
releaseBranchName = args.branch;
} else {
let dateCommand = await spawn({"noecho": true}, "date", ["+release-%y-%b-%d"]);
releaseBranchName = dateCommand.stdout.trim();
}
let makeBranch = await runGit([0, 128], "checkout", "-b", releaseBranchName);
if(makeBranch[1].includes("already exists")){
await runGit([0], "checkout", releaseBranchName);
await runGit([0], "pull", "origin", releaseBranchName);
}else{
await runGit([0], "push", "-u", "origin", "HEAD");
}
let pull_request_descriptions = [];
let pull_request_dev_comments = [];
let issues = await this.getIssues();
for(let issue of issues){
let labels = new Set(issue.labels.map(x => x.name));
if(!labels.has(prodReadyLabel) && !labels.has(prodManualLabel)) continue;
await this.setBase(issue, releaseBranchName);
write(chalk`Changed base of ${issue.number} (${this.printJiraTicket(issue)}) to ${releaseBranchName}... `);
if(!issue.parsedTitle){
log();
write(chalk`Full title ^^: ${issue.title}...`);
}
let config = this.getOctokitConfig();
config.pull_number = issue.number;
let pull_request = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", config);
let pull_request_description = pull_request.data.body.replace("Description (user facing release note):","").replace(/Dev comments:[\s\S]*/,"").trim()
let pull_request_dev_comment = pull_request.data.body.replace(/[\s\S]*Dev comments:/,"").trim()
pull_request_descriptions.push(pull_request_description);
pull_request_dev_comments.push(pull_request_dev_comment);
config.merge_method = "squash";
await this.octokit.request("PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge", config);
log(chalk`Merged.`);
}
let config = this.getOctokitConfig();
config.title = releaseBranchName.split("-").join(" ");
config.head = releaseBranchName;
config.base = "staging";
config.body = "Description:\n"+pull_request_descriptions.filter(d => d.length != 0).map(d => `• ${d}`).join("\n") + "\n\nDev comments:\n" + pull_request_dev_comments.filter(d => d.length != 0).map(d => `• ${d}`).join("\n")
await this.octokit.request("POST /repos/{owner}/{repo}/pulls", config);
await runGit([0], "pull");
},
async sendSlackMsg(msgItems,slackChannel) {
let blocks = []
for (let item of msgItems) {
if (Array.isArray(item.content)) {
let characterCount = 0;
let subMsg = [];
for (let subItem of item.content) {
subMsg.push(subItem);
characterCount += subItem.length;
if (characterCount > 2000) {
characterCount = 0;
let block = {"type": "section","text": {"type": "mrkdwn","text": " "}};
block.text.text = item.type == "code" ? ("```"+subMsg.join("\n")+"```") : subMsg.join("\n");
if ((subMsg.join("\n")).length != 0) {
blocks.push(block)
}
subMsg = [];
}
}
if (subMsg.length != 0){
let block = {"type": "section","text": {"type": "mrkdwn","text": " "}};
block.text.text = item.type == "code" ? ("```"+subMsg.join("\n")+"```") : subMsg.join("\n");
if ((subMsg.join("\n")).length != 0) {
blocks.push(block)
}
}
}
else {
let block = {"type": "section","text": {"type": "mrkdwn","text": " "}};
block.text.text = item.type == "code" ? ("```"+item.content+"```") : item.content;
if (item.content.length != 0) {
blocks.push(block)
}
}
}
for (let block of blocks) {
response = await rp({method: "POST", body: JSON.stringify({"blocks": [block]}), headers: {"Content-Type": "application/json"}, uri: slackChannel});
}
},
async stageSlackMsg(args){
Stage.env = args.env || "UAT";
Stage.skipLoadMsg = true;
if (!args.branch) {
log(chalk`{red Error:} Please provide a branch`); return
}
await runCommand(`git checkout ${args.branch}`);
let requiredPresetsRules = await runCommand(`git diff staging...HEAD --name-only | rally @`);
await runCommand(`git checkout staging`);
requiredPresetsRules = requiredPresetsRules.replace("Reading from stdin\n","");
if(await Stage.downloadStage()){
log(chalk`{red Error:} Could not load stage`); return
}
let stagedBranchesMsg = [`Currently Staged Presets: ${Stage.stageData.stage.length}`].concat(Stage.stageData.stage.map(d=>` ${d.branch} ${d.commit}`));
let msgItems = [
{type: "normal", content: `@here The release branch has been staged by ${configObject.slackId ? `<@${configObject.slackId}>` : configObject.ownerName}`},
{type: "code", content: stagedBranchesMsg},
{type: "code", content: requiredPresetsRules.split("\n")}
]
await this.sendSlackMsg(msgItems,configObject.deploy.slackWebhooks.air_supply_release_staging)
},
async deploySlackMessage(args){
if (!args.pr) {
log(chalk`{red Error:} Please provide a pr number`); return
}
let today = new Date();
today = String(today.getMonth() + 1).padStart(2, '0') + '/' + String(today.getDate()).padStart(2, '0') + '/' + today.getFullYear();
let config = this.getOctokitConfig();
config.pull_number = args.pr;
let pull_request = await this.octokit.request("GET /repos/{owner}/{repo}/pulls/{pull_number}", config);
let branch = pull_request.data.head.ref;
let pull_request_descriptions = pull_request.data.body.replace("Description (user facing release note):","").replace("Description:","").replace(/Dev comments:[\s\S]*/,"").trim();
await runCommand(`git checkout ${branch}`);
let requiredPresetsRules = await runCommand(`git diff staging...HEAD --name-only | rally @`);
await runCommand(`git checkout staging`);
requiredPresetsRules = requiredPresetsRules.replace("Reading from stdin\n","");
let msgItems = [
{type: "normal", content: `@here ${args.hotfix ? "*HOTFIX*" :`*DEPLOY ${today}*`}`},
{type: "normal", content: `Deployer: ${configObject.slackId ? `<@${configObject.slackId}>` : configObject.ownerName}`},
{type: "normal", content: pull_request_descriptions.split("\n")},
{type: "code", content: requiredPresetsRules.split("\n")}
]
await this.sendSlackMsg(msgItems,configObject.deploy.slackWebhooks.rally_deployments)
}
};
export default Deploy;