Build server prototype (integration with GitHub / NuGet / etc)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
micro-build-server/BuildServer/lib/commenter.ts

181 lines
6.7 KiB

"use strict";
import * as _ from "underscore";
import { createGithub, IGithub } from "./github-wrapper";
import { getStatusMessageFromRelease } from "./report-processor";
import { Settings } from "./types";
interface ICommentOnPullRequestOptions {
readonly action: string;
readonly baseRepoOptions: any;
readonly headRepoOptions: any;
}
interface ICheckPullRequestOptions extends ICommentOnPullRequestOptions {
readonly github: IGithub;
readonly onTenthAttempt: () => void;
}
const featureNamePattern = /^feature-(\d+)(?:-[a-zA-Z0-9]+)+$/;
const versionNamePattern = /^v\d+(\.\d+)*$/;
const masterNamePattern = /^master$/;
const httpNotFound = 404;
const maxCommentLength = 64000;
const writeComment = (options, message, callback) => options.github.issues.createComment({
body: message,
number: options.pullRequestNumber,
owner: options.baseRepoOptions.owner,
repo: options.baseRepoOptions.reponame,
}, callback);
const closePullRequest = (options, message, callback) => writeComment(options, message, (err) => {
if (err) {
return callback(err);
}
return options.github.issues.edit({
number: options.pullRequestNumber,
owner: options.baseRepoOptions.owner,
repo: options.baseRepoOptions.reponame,
state: "closed",
}, callback);
});
const checkHasIssue = (options: ICheckPullRequestOptions, issueNumber, callback) => options.github.issues.get({
number: issueNumber,
owner: options.baseRepoOptions.owner,
repo: options.baseRepoOptions.reponame,
}, (getIssueErr, result) => {
if (getIssueErr) {
if (getIssueErr.code && getIssueErr.code !== httpNotFound) {
return callback(getIssueErr.message);
}
return callback(null, false);
}
if (!result) {
return callback("Result is empty");
}
if (result.data.number.toString() !== issueNumber) {
return callback(null, false);
}
if (result.data.pull_request && result.data.pull_request.url) {
return callback(null, false);
}
return callback(null, true, result.data.title);
});
const checkHasReleases = (options: ICheckPullRequestOptions, callback) => options.github.repos.getReleases({
owner: options.baseRepoOptions.owner,
per_page: 1,
repo: options.baseRepoOptions.reponame,
}, (getReleasesErr, result) => {
if (getReleasesErr) {
return callback(getReleasesErr);
}
return callback(null, result && result.data && result.data.length);
});
const checkPullRequest = (options: ICheckPullRequestOptions, callback) => {
const head = options.headRepoOptions;
const base = options.baseRepoOptions;
if (head.reponame !== base.reponame) {
return closePullRequest(options, "Base and head repository names should match", callback);
}
if (head.owner === base.owner) {
if (!versionNamePattern.test(head.branchname) || !masterNamePattern.test(base.branchname)) {
return closePullRequest(options, "Only merging from version to master is allowed", callback);
}
return checkHasReleases(options, (hasReleasesErr, hasReleases) => {
if (hasReleasesErr) {
return writeComment(options, "Unable to check for releases", callback);
}
if (!hasReleases) {
return closePullRequest(options, "Merging from version to master is only allowed for repositories with releases", callback);
}
if (options.action === "opened") {
return writeComment(options, `Switching master branch to ${head.branchname} release`, callback);
}
return process.nextTick(callback);
});
}
if (!featureNamePattern.test(head.branchname)) {
return closePullRequest(options, `Only merging from feature branch is allowed (pattern: \`${featureNamePattern}\`)`, callback);
}
if (!versionNamePattern.test(base.branchname) && !masterNamePattern.test(base.branchname)) {
return closePullRequest(options, `Only merging to master or version branch is allowed; merging to '${base.branchname}' is not supported`, callback);
}
const execResult = featureNamePattern.exec(head.branchname);
const issueNumber = execResult && execResult[1];
return checkHasIssue(options, issueNumber, (hasIssueErr, hasIssue, issueTitle) => {
if (hasIssueErr) {
return writeComment(options, `Unable to check for issue:\r\n\r\n${hasIssueErr}`, callback);
}
if (!hasIssue) {
return closePullRequest(options, `Unable to find issue #${issueNumber}`, callback);
}
const shouldHaveReleases = versionNamePattern.test(base.branchname);
return checkHasReleases(options, (hasReleasesErr, hasReleases) => {
if (hasReleasesErr) {
return writeComment(options, "Unable to check for releases", callback);
}
if (shouldHaveReleases && !hasReleases) {
return closePullRequest(options, "Merging from feature to version is only allowed for repositories with releases", callback);
}
if (!shouldHaveReleases && hasReleases) {
return closePullRequest(options, "Merging from feature to master is only allowed for repositories without releases", callback);
}
if (options.action === "opened") {
return writeComment(options, `Merging feature #${issueNumber} (${issueTitle}) to ${base.branchname}`, callback);
}
return process.nextTick(callback);
});
});
};
export const commentOnPullRequest = (settings: Settings, originalOptions: ICommentOnPullRequestOptions, callback) => {
const optionsGithub = {
...originalOptions,
github: createGithub(settings, originalOptions.baseRepoOptions.owner),
};
const options = {
...optionsGithub,
onTenthAttempt: () => writeComment(optionsGithub, "Waiting for build to finish...", _.noop),
};
return checkPullRequest(options, () => getStatusMessageFromRelease(settings, options.headRepoOptions, (statusMessageErr, statusSuccessMessage) => {
const escapedErr = String(statusMessageErr || "").substring(0, maxCommentLength)
.replace(/`/g, "` ");
const message = statusMessageErr
? `Was not built:\r\n\r\n\`\`\`\r\n${escapedErr}\r\n\`\`\`\r\n\r\nDO NOT MERGE!`
: `Build OK\r\n\r\n${statusSuccessMessage}`;
const statusUrlMessage = `Build status URL: ${settings.siteRoot}status/${options.headRepoOptions.owner}/${options.headRepoOptions.reponame}/${options.headRepoOptions.rev}\r\n\r\n`;
return writeComment(options, `${message}\r\n\r\n${statusUrlMessage}`, callback);
}));
};