@atomist/sample-sdm
Version:
Sample Atomist automation for software delivery
186 lines • 8.6 kB
JavaScript
;
/*
* Copyright © 2018 Atomist, Inc.
*
* Licensed 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const automation_client_1 = require("@atomist/automation-client");
const ghub_1 = require("@atomist/sdm/util/github/ghub");
const slack = require("@atomist/slack-messages");
const axios_1 = require("axios");
const stringify = require("json-stringify-safe");
const _ = require("lodash");
/**
* Manage a single issue for a subset of review problems
* @param commentFilter filter for relevant review comments
* @param title title of the issue to manage
* @param bodyFormatter function to create body from comments
* @return {Promise<void>}
* @constructor
*/
function singleIssueManagingReviewListener(commentFilter, title, bodyFormatter) {
return (ri) => __awaiter(this, void 0, void 0, function* () {
if (ri.push.branch !== ri.push.repo.defaultBranch) {
// We only care about pushes to the default branch
return;
}
const relevantComments = ri.review.comments.filter(commentFilter);
const existingIssue = yield findIssue(ri.credentials, ri.id, title);
if (relevantComments.length === 0) {
if (existingIssue) {
automation_client_1.logger.info("Closing issue %d because all comments have been addressed", existingIssue.number);
const congrats = `The last review problem was fixed by ${who(ri.push)} when they pushed ${linkToSha(ri.id)}`;
yield updateIssue(ri.credentials, ri.id, Object.assign({}, existingIssue, { state: "closed", body: congrats }));
}
return;
}
// there are some comments
if (!existingIssue) {
const issue = {
title,
body: bodyFormatter(relevantComments, ri.id),
};
automation_client_1.logger.info("Creating issue %j from review comments", issue);
yield createIssue(ri.credentials, ri.id, issue);
}
else {
// Update the issue if necessary, reopening it if need be
const body = bodyFormatter(relevantComments, ri.id);
if (body !== existingIssue.body) {
automation_client_1.logger.info("Updating issue %d with the latest comments", existingIssue.number);
yield updateIssue(ri.credentials, ri.id, Object.assign({}, existingIssue, { state: "open", body }));
}
else {
automation_client_1.logger.info("Not updating issue %d as body has not changed", existingIssue.number);
}
}
// Should we catch exceptions and not fail the Goal if this doesn't work?
});
}
exports.singleIssueManagingReviewListener = singleIssueManagingReviewListener;
/**
* Take this subset of issues and maintain an issue for each
* @param {CommentFilter} commentFilter
* @param {CommentsFormatter} bodyFormatter
* @return {ReviewListener}
*/
function multiIssueManagingReviewListener(commentFilter, bodyFormatter) {
return (ri) => __awaiter(this, void 0, void 0, function* () {
if (ri.push.branch !== ri.push.repo.defaultBranch) {
// We only care about pushes to the default branch
return;
}
const relevantComments = ri.review.comments.filter(commentFilter);
for (const comment of relevantComments) {
// TODO disambiguate
const title = comment.detail;
const existingIssue = yield findIssue(ri.credentials, ri.id, title);
// there are some comments
if (!existingIssue) {
const issue = {
title,
body: bodyFormatter(comment, ri.id),
};
automation_client_1.logger.info("Creating issue %j from review comment", issue);
yield createIssue(ri.credentials, ri.id, issue);
}
else {
// Update the issue if necessary, reopening it if need be
const body = bodyFormatter(comment, ri.id);
if (body !== existingIssue.body) {
automation_client_1.logger.info("Updating issue %d with the latest ", existingIssue.number);
yield updateIssue(ri.credentials, ri.id, Object.assign({}, existingIssue, { state: "open", body }));
}
else {
automation_client_1.logger.info("Not updating issue %d as body has not changed", existingIssue.number);
}
}
// Should we catch exceptions and not fail the Goal if this doesn't work?
}
});
}
exports.multiIssueManagingReviewListener = multiIssueManagingReviewListener;
function who(push) {
const screenName = _.get(push, "after.committer.person.chatId.screenName");
if (screenName) {
return slack.user(screenName);
}
return _.get(push, "after.committer.token", "someone");
}
function linkToSha(id) {
return slack.url(id.url + "/tree/" + id.sha, id.sha.substr(0, 7));
}
// update the state and body of an issue.
function updateIssue(credentials, rr, issue) {
return __awaiter(this, void 0, void 0, function* () {
const safeIssue = {
state: issue.state,
body: issue.body,
};
const token = credentials.token;
const grr = rr;
const url = encodeURI(`${grr.scheme}${grr.apiBase}/repos/${rr.owner}/${rr.repo}/issues/${issue.number}`);
automation_client_1.logger.info(`Request to '${url}' to update issue`);
yield axios_1.default.patch(url, safeIssue, ghub_1.authHeaders(token)).catch(err => {
automation_client_1.logger.error("Failure updating issue. response: %s", stringify(err.response.data));
throw err;
});
});
}
function createIssue(credentials, rr, issue) {
return __awaiter(this, void 0, void 0, function* () {
const token = credentials.token;
const grr = rr;
const url = `${grr.scheme}${grr.apiBase}/repos/${rr.owner}/${rr.repo}/issues`;
automation_client_1.logger.info(`Request to '${url}' to create issue`);
yield axios_1.default.post(url, issue, ghub_1.authHeaders(token));
});
}
// find the most recent open (or closed, if none are open) issue with precisely this title
function findIssue(credentials, rr, title) {
return __awaiter(this, void 0, void 0, function* () {
const token = credentials.token;
const grr = rr;
const url = encodeURI(`${grr.scheme}${grr.apiBase}/search/issues?q=is:issue+user:${rr.owner}+repo:${rr.repo}+"${title}"`);
automation_client_1.logger.info(`Request to '${url}' to get issues`);
const returnedIssues = yield axios_1.default.get(url, ghub_1.authHeaders(token)).then(r => r.data.items);
return returnedIssues.filter(i => i.title === title
&& i.url.includes(`/${rr.owner}/${rr.repo}/issues/`))
.sort(openFirst)[0];
});
}
/**
* Compare giving open issues a lower sort order
* @param {KnownIssue} a
* @param {KnownIssue} b
* @return {number}
*/
function openFirst(a, b) {
if (a.state === "open" && b.state === "closed") {
return -1;
}
if (b.state === "open" && a.state === "closed") {
return 1;
}
return b.number - a.number; // if same state, most recent one first.
}
//# sourceMappingURL=issueManagingReviewListeners.js.map