Skip to content

Commit

Permalink
feat: check fast-track approvals (#588)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alicia Lopez committed Dec 5, 2021
1 parent 0e152cd commit 7cf9f11
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 0 deletions.
28 changes: 28 additions & 0 deletions lib/pr_checker.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const WAIT_TIME_SINGLE_APPROVAL = 24 * 7;

const GITHUB_SUCCESS_CONCLUSIONS = ['SUCCESS', 'NEUTRAL', 'SKIPPED'];

const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to approve\.$/;

const {
REVIEW_SOURCES: { FROM_COMMENT, FROM_REVIEW_COMMENT }
} = require('./reviews');
Expand Down Expand Up @@ -168,6 +170,32 @@ class PRChecker {
}
}

if (isFastTracked) {
const comment = this.comments.find((c) =>
FAST_TRACK_RE.test(c.bodyText));
if (!comment) {
cli.error('Unable to find the fast-track request comment.');
return false;
}
const [, requester] = comment.bodyText.match(FAST_TRACK_RE);
const collaborators = Array.from(this.data.collaborators.values(),
(c) => c.login);
const approvals = comment.reactions.nodes.filter((r) =>
r.user.login !== requester &&
r.user.login !== pr.author.login &&
collaborators.includes(r.user.login)).length;

if (requester === pr.author.login && approvals < 2) {
cli.error('The fast-track request requires' +
" at least two collaborators' approvals (👍).");
return false;
} else if (approvals === 0) {
cli.error('The fast-track request requires' +
" at least one collaborator's approval (👍).");
return false;
}
}

const createTime = new Date(this.pr.createdAt);
const msFromCreateTime = now.getTime() - createTime.getTime();
const minutesFromCreateTime = Math.ceil(msFromCreateTime / MINUTE);
Expand Down
7 changes: 7 additions & 0 deletions lib/queries/PRComments.gql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ query Comments($prid: Int!, $owner: String!, $repo: String!, $after: String) {
author {
login
}
reactions(content: THUMBS_UP, first: 100) {
nodes {
user {
login
}
}
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/comments_with_ci.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
[
{
"publishedAt": "2017-10-22T04:16:36.458Z",
"bodyText": "Fast-track has been requested by @bar. Please 👍 to approve.",
"author": {
"login": "github-actions"
},
"reactions": {
"nodes": [
{ "user": { "login": "bar" } },
{ "user": { "login": "foo" } }
]
}
},
{
"publishedAt": "2017-10-22T04:16:36.458Z",
"bodyText": "CI: https://ci.nodejs.org/job/node-test-pull-request/10984/\ncitgm: https://ci.nodejs.org/job/citgm-smoker/1030/"
Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/comments_with_fast_track.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"publishedAt": "2017-10-22T04:16:36.458Z",
"bodyText": "Fast-track has been requested by @bar. Please 👍 to approve.",
"author": {
"login": "github-actions"
},
"reactions": {
"nodes": [
{ "user": { "login": "bar" } },
{ "user": { "login": "non-collaborator" } }
]
}
}
]
14 changes: 14 additions & 0 deletions test/fixtures/comments_with_fast_track_insufficient_approvals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"publishedAt": "2017-10-22T04:16:36.458Z",
"bodyText": "Fast-track has been requested by @bar. Please 👍 to approve.",
"author": {
"login": "github-actions"
},
"reactions": {
"nodes": [
{ "user": { "login": "non-collaborator" } }
]
}
}
]
5 changes: 5 additions & 0 deletions test/fixtures/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const noReviewers = {
const approvingReviews = readJSON('reviews_approved.json');
const requestingChangesReviews = readJSON('reviews_requesting_changes.json');

const commentsWithFastTrack = readJSON('comments_with_fast_track.json');
const commentsWithFastTrackInsuffientApprovals =
readJSON('comments_with_fast_track_insufficient_approvals.json');
const commentsWithCI = readJSON('comments_with_ci.json');
const commentsWithFailedCI = readJSON('comments_with_failed_ci.json');
const commentsWithLGTM = readJSON('comments_with_lgtm.json');
Expand Down Expand Up @@ -134,6 +137,8 @@ module.exports = {
requestedChangesReviewers,
approvingReviews,
requestingChangesReviews,
commentsWithFastTrack,
commentsWithFastTrackInsuffientApprovals,
commentsWithCI,
commentsWithFailedCI,
commentsWithLGTM,
Expand Down
145 changes: 145 additions & 0 deletions test/unit/pr_checker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const {
jenkinsCI,
requestingChangesReviews,
noReviewers,
commentsWithFastTrack,
commentsWithFastTrackInsuffientApprovals,
commentsWithCI,
commentsWithFailedCI,
commentsWithLGTM,
Expand Down Expand Up @@ -533,6 +535,149 @@ describe('PRChecker', () => {
cli.assertCalledWith(expectedLogs);
});

it('should error with 1 fast-track approval from the pr author', () => {
const cli = new TestCLI();

const expectedLogs = {
ok:
[['Approvals: 4'],
['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624'],
['- Quux User (@Quux): LGTM'],
['- Baz User (@Baz): https://github.com/nodejs/node/pull/16438#pullrequestreview-71488236'],
['- Bar User (@bar) (TSC): lgtm']],
info:
[['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'],
['This PR is being fast-tracked']],
error:
[['The fast-track request requires at' +
" least two collaborators' approvals (👍)."]]
};

const pr = Object.assign({}, firstTimerPR, {
author: {
login: 'bar'
},
createdAt: LT_48H,
labels: {
nodes: [
{ name: 'fast-track' }
]
}
});

const data = {
pr,
reviewers: allGreenReviewers,
comments: commentsWithFastTrack,
reviews: approvingReviews,
commits: [],
collaborators,
authorIsNew: () => true,
getThread() {
return PRData.prototype.getThread.call(this);
}
};
const checker = new PRChecker(cli, data, {}, argv);

cli.clearCalls();
const status = checker.checkReviewsAndWait(new Date(NOW));
assert(!status);
cli.assertCalledWith(expectedLogs);
});

it('should error when insufficient fast-track approvals', () => {
const cli = new TestCLI();

const expectedLogs = {
ok:
[['Approvals: 4'],
['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624'],
['- Quux User (@Quux): LGTM'],
['- Baz User (@Baz): https://github.com/nodejs/node/pull/16438#pullrequestreview-71488236'],
['- Bar User (@bar) (TSC): lgtm']],
info:
[['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'],
['This PR is being fast-tracked']],
error:
[['The fast-track request requires at' +
" least one collaborator's approval (👍)."]]
};

const pr = Object.assign({}, firstTimerPR, {
createdAt: LT_48H,
labels: {
nodes: [
{ name: 'fast-track' }
]
}
});

const data = {
pr,
reviewers: allGreenReviewers,
comments: commentsWithFastTrackInsuffientApprovals,
reviews: approvingReviews,
commits: [],
collaborators,
authorIsNew: () => true,
getThread() {
return PRData.prototype.getThread.call(this);
}
};
const checker = new PRChecker(cli, data, {}, argv);

cli.clearCalls();
const status = checker.checkReviewsAndWait(new Date(NOW));
assert(!status);
cli.assertCalledWith(expectedLogs);
});

it('should error when missing fast-track request comment', () => {
const cli = new TestCLI();

const expectedLogs = {
ok:
[['Approvals: 4'],
['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624'],
['- Quux User (@Quux): LGTM'],
['- Baz User (@Baz): https://github.com/nodejs/node/pull/16438#pullrequestreview-71488236'],
['- Bar User (@bar) (TSC): lgtm']],
info:
[['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'],
['This PR is being fast-tracked']],
error:
[['Unable to find the fast-track request comment.']]
};

const pr = Object.assign({}, firstTimerPR, {
createdAt: LT_48H,
labels: {
nodes: [
{ name: 'fast-track' }
]
}
});

const data = {
pr,
reviewers: allGreenReviewers,
comments: commentsWithLGTM,
reviews: approvingReviews,
commits: [],
collaborators,
authorIsNew: () => true,
getThread() {
return PRData.prototype.getThread.call(this);
}
};
const checker = new PRChecker(cli, data, {}, argv);

cli.clearCalls();
const status = checker.checkReviewsAndWait(new Date(NOW));
assert(!status);
cli.assertCalledWith(expectedLogs);
});

it('should warn about approvals and CI for fast-tracked PR', () => {
const cli = new TestCLI();

Expand Down

0 comments on commit 7cf9f11

Please sign in to comment.