-
Notifications
You must be signed in to change notification settings - Fork 17
reviewable.io review completion condition
Jeremy Nimmer edited this page Mar 19, 2022
·
11 revisions
This is the most recent reviewable.io "review completion condition script" for Drake, which repository admins can edit at https://reviewable.io/repositories#RobotLocomotion%2Fdrake. This page is for backup purposes. Please try to keep it sync'd with the service.
// Any time you edit this file, paste the final version into the drake-ci wiki for backup:
// https://github.com/RobotLocomotion/drake-ci/wiki/reviewable.io-review-completion-condition
// Our review status will report the bad_reasons when non-empty, or good_reasons otherwise.
// A non-empty bad_reasons will prevent merging.
var good_reasons = [];
var bad_reasons = [];
// Check for a project-wide moratorium.
var isMoratorium = false;
if (isMoratorium) {
bad_reasons.push('moratorium on merges is in effect');
}
// Keep this list sync'd with https://drake.mit.edu/developers.html
var platformReviewers = {'ericcousineau-tri' : true,
'ggould-tri' : true,
'jwnimmer-tri' : true,
'rpoyner-tri' : true,
'sammy-tri' : true,
'sherm1' : true,
'xuchenhan-tri' : true,
'russtedrake' : true,
};
// Tabulate the emoji approvals.
var approvals = {};
_.each(review.sentiments, function(sentiment) {
var key = sentiment.username.toLowerCase();
var emojis = _.indexBy(sentiment.emojis);
if (emojis.lgtm_cancel) {
delete approvals[key];
} else if (emojis.lgtm_strong || emojis.lgtm || emojis.LGTM) {
approvals[key] = sentiment.username;
}
});
// Tabulate the files under review.
// Set the File Matrix group for 'dev' files
// Set the File Matrix group for 'pydrake' files
var platformRequiredFiles = []
var platformExemptFiles = []
var completion_files = review.files;
for (var i in review.files) {
var file = review.files[i];
var revisions = file["revisions"];
var currentRevision = revisions[revisions.length - 1];
var reverted = currentRevision["reverted"];
var path = file["path"];
if (reverted) {
// They do not require any code reviews.
completion_files[i].group = "Reverted";
} else if (path.includes("/dev/")) {
platformExemptFiles.push(path);
if (!("group" in file)) {
completion_files[i].group = "Dev Folders";
}
} else {
platformRequiredFiles.push(path);
}
if (path.includes("bindings/pydrake")) {
if (!("group" in file)) {
completion_files[i].group = "pydrake";
}
}
}
// Helpers
function plural(noun, len) {
if (len > 1) return noun + 's';
return noun;
}
// Check that all discussions are resolved.
var unresolved = review.summary.numUnresolvedDiscussions;
if (unresolved === 0) {
good_reasons.push('all discussions resolved');
} else {
bad_reasons.push(unresolved + ' unresolved ' + plural('discussion', unresolved))
}
// Check that every assignee has approved.
var platformReviewerAssigned = false;
var assignees = review.pullRequest.assignees;
var good_assignees = [];
var bad_assignees = [];
for (var i in assignees) {
var username = assignees[i].username;
var key = username.toLowerCase();
var displayName;
if (platformReviewers[key]) {
displayName = username + "(platform)";
platformReviewerAssigned = true;
} else {
displayName = username;
}
if (approvals[key]) {
good_assignees.push(displayName);
} else {
bad_assignees.push(displayName);
}
}
if (good_assignees.length > 0) {
good_reasons.push('LGTM from ' + plural('assignee', good_assignees.length) + ' ' + good_assignees);
}
if (bad_assignees.length > 0) {
bad_reasons.push('LGTM missing from ' + plural('assignee', bad_assignees.length) + ' ' + bad_assignees);
}
// Check for platform reviewer approval (when required).
if (platformRequiredFiles.length == 0) {
good_reasons.push("exempt from platform review")
} else {
if (!platformReviewerAssigned) {
bad_reasons.push("needs platform reviewer assigned")
}
}
// Check for minimum reviewer count.
var singleReviewerOk = review.labels.indexOf('status: single reviewer ok') >= 0;
if (!singleReviewerOk && (platformRequiredFiles.length > 0)) {
if (assignees.length < 2) {
bad_reasons.push("needs at least two assigned reviewers")
}
} else if (assignees.length < 1) {
bad_reasons.push("needs at least one assigned reviewer")
}
// Check for DNM label.
if (review.labels.indexOf('status: do not merge') >= 0) {
bad_reasons.push('labeled "do not merge"');
}
// Check for commit curation.
var multipleCommits = review.pullRequest.numCommits > 1;
var properlyCurated = review.labels.indexOf('status: commits are properly curated') >= 0;
var squashingNow = review.labels.indexOf('status: squashing now') >= 0;
if (multipleCommits && !(properlyCurated || squashingNow)) {
bad_reasons.push('commits need curation (https://drake.mit.edu/reviewable.html#curated-commits)');
}
var mergeStyle = (multipleCommits && properlyCurated) ? 'merge' : 'squash';
// Check that merges are always onto the tip.
var baseSha = review.revisions.slice(-1)[0].baseCommitSha;
var headSha = review.pullRequest.target.headCommitSha;
if (mergeStyle == 'merge') {
if (baseSha == headSha) {
good_reasons.push('base commit is latest master');
} else {
bad_reasons.push('when planning a "properly curated" merge commit the PR must always be rebased onto latest master');
}
}
// Check for release notes.
var has_release_notes_label = false;
for (var i in review.labels) {
var label = review.labels[i];
if (label.indexOf('release notes:') >= 0) {
has_release_notes_label = true;
}
}
if (!has_release_notes_label) {
bad_reasons.push('missing label for release notes');
}
// List bad reasons (only) if there are any, otherwise list the good ones (only).
var description;
if (bad_reasons.length > 0) {
description = bad_reasons.join(', ');
} else {
description = good_reasons.join(', ');
}
// Make commit messages nice.
// TODO(jwnimmer-tri) Return this below.
var defaultSquashCommitMessage = review.pullRequest.title + ' (#' + review.pullRequest.number + ')\n\n' + review.pullRequest.body.trim()
return {
completed: (bad_reasons.length === 0),
description: description,
mergeStyle: mergeStyle,
disableGitHubApprovals: true,
syncRequestedReviewers: false,
files: completion_files,
debug: {approvals: approvals, baseSha: baseSha, headSha: headSha,
defaultSquashCommitMessage: defaultSquashCommitMessage,
platformExemptFiles: platformExemptFiles,
platformRequiredFiles: platformRequiredFiles}
};