Skip to content

Commit d0215d6

Browse files
authored
feat: check fast-track approvals (#588)
1 parent d1c52e6 commit d0215d6

File tree

7 files changed

+227
-0
lines changed

7 files changed

+227
-0
lines changed

lib/pr_checker.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const WAIT_TIME_SINGLE_APPROVAL = 24 * 7;
99

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

12+
const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to approve\.$/;
13+
1214
const {
1315
REVIEW_SOURCES: { FROM_COMMENT, FROM_REVIEW_COMMENT }
1416
} = require('./reviews');
@@ -168,6 +170,32 @@ class PRChecker {
168170
}
169171
}
170172

173+
if (isFastTracked) {
174+
const comment = this.comments.find((c) =>
175+
FAST_TRACK_RE.test(c.bodyText));
176+
if (!comment) {
177+
cli.error('Unable to find the fast-track request comment.');
178+
return false;
179+
}
180+
const [, requester] = comment.bodyText.match(FAST_TRACK_RE);
181+
const collaborators = Array.from(this.data.collaborators.values(),
182+
(c) => c.login);
183+
const approvals = comment.reactions.nodes.filter((r) =>
184+
r.user.login !== requester &&
185+
r.user.login !== pr.author.login &&
186+
collaborators.includes(r.user.login)).length;
187+
188+
if (requester === pr.author.login && approvals < 2) {
189+
cli.error('The fast-track request requires' +
190+
" at least two collaborators' approvals (👍).");
191+
return false;
192+
} else if (approvals === 0) {
193+
cli.error('The fast-track request requires' +
194+
" at least one collaborator's approval (👍).");
195+
return false;
196+
}
197+
}
198+
171199
const createTime = new Date(this.pr.createdAt);
172200
const msFromCreateTime = now.getTime() - createTime.getTime();
173201
const minutesFromCreateTime = Math.ceil(msFromCreateTime / MINUTE);

lib/queries/PRComments.gql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ query Comments($prid: Int!, $owner: String!, $repo: String!, $after: String) {
1313
author {
1414
login
1515
}
16+
reactions(content: THUMBS_UP, first: 100) {
17+
nodes {
18+
user {
19+
login
20+
}
21+
}
22+
}
1623
}
1724
}
1825
}

test/fixtures/comments_with_ci.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
[
2+
{
3+
"publishedAt": "2017-10-22T04:16:36.458Z",
4+
"bodyText": "Fast-track has been requested by @bar. Please 👍 to approve.",
5+
"author": {
6+
"login": "github-actions"
7+
},
8+
"reactions": {
9+
"nodes": [
10+
{ "user": { "login": "bar" } },
11+
{ "user": { "login": "foo" } }
12+
]
13+
}
14+
},
215
{
316
"publishedAt": "2017-10-22T04:16:36.458Z",
417
"bodyText": "CI: https://ci.nodejs.org/job/node-test-pull-request/10984/\ncitgm: https://ci.nodejs.org/job/citgm-smoker/1030/"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[
2+
{
3+
"publishedAt": "2017-10-22T04:16:36.458Z",
4+
"bodyText": "Fast-track has been requested by @bar. Please 👍 to approve.",
5+
"author": {
6+
"login": "github-actions"
7+
},
8+
"reactions": {
9+
"nodes": [
10+
{ "user": { "login": "bar" } },
11+
{ "user": { "login": "non-collaborator" } }
12+
]
13+
}
14+
}
15+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"publishedAt": "2017-10-22T04:16:36.458Z",
4+
"bodyText": "Fast-track has been requested by @bar. Please 👍 to approve.",
5+
"author": {
6+
"login": "github-actions"
7+
},
8+
"reactions": {
9+
"nodes": [
10+
{ "user": { "login": "non-collaborator" } }
11+
]
12+
}
13+
}
14+
]

test/fixtures/data.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const noReviewers = {
3535
const approvingReviews = readJSON('reviews_approved.json');
3636
const requestingChangesReviews = readJSON('reviews_requesting_changes.json');
3737

38+
const commentsWithFastTrack = readJSON('comments_with_fast_track.json');
39+
const commentsWithFastTrackInsuffientApprovals =
40+
readJSON('comments_with_fast_track_insufficient_approvals.json');
3841
const commentsWithCI = readJSON('comments_with_ci.json');
3942
const commentsWithFailedCI = readJSON('comments_with_failed_ci.json');
4043
const commentsWithLGTM = readJSON('comments_with_lgtm.json');
@@ -134,6 +137,8 @@ module.exports = {
134137
requestedChangesReviewers,
135138
approvingReviews,
136139
requestingChangesReviews,
140+
commentsWithFastTrack,
141+
commentsWithFastTrackInsuffientApprovals,
137142
commentsWithCI,
138143
commentsWithFailedCI,
139144
commentsWithLGTM,

test/unit/pr_checker.test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const {
2525
jenkinsCI,
2626
requestingChangesReviews,
2727
noReviewers,
28+
commentsWithFastTrack,
29+
commentsWithFastTrackInsuffientApprovals,
2830
commentsWithCI,
2931
commentsWithFailedCI,
3032
commentsWithLGTM,
@@ -533,6 +535,149 @@ describe('PRChecker', () => {
533535
cli.assertCalledWith(expectedLogs);
534536
});
535537

538+
it('should error with 1 fast-track approval from the pr author', () => {
539+
const cli = new TestCLI();
540+
541+
const expectedLogs = {
542+
ok:
543+
[['Approvals: 4'],
544+
['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624'],
545+
['- Quux User (@Quux): LGTM'],
546+
['- Baz User (@Baz): https://github.com/nodejs/node/pull/16438#pullrequestreview-71488236'],
547+
['- Bar User (@bar) (TSC): lgtm']],
548+
info:
549+
[['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'],
550+
['This PR is being fast-tracked']],
551+
error:
552+
[['The fast-track request requires at' +
553+
" least two collaborators' approvals (👍)."]]
554+
};
555+
556+
const pr = Object.assign({}, firstTimerPR, {
557+
author: {
558+
login: 'bar'
559+
},
560+
createdAt: LT_48H,
561+
labels: {
562+
nodes: [
563+
{ name: 'fast-track' }
564+
]
565+
}
566+
});
567+
568+
const data = {
569+
pr,
570+
reviewers: allGreenReviewers,
571+
comments: commentsWithFastTrack,
572+
reviews: approvingReviews,
573+
commits: [],
574+
collaborators,
575+
authorIsNew: () => true,
576+
getThread() {
577+
return PRData.prototype.getThread.call(this);
578+
}
579+
};
580+
const checker = new PRChecker(cli, data, {}, argv);
581+
582+
cli.clearCalls();
583+
const status = checker.checkReviewsAndWait(new Date(NOW));
584+
assert(!status);
585+
cli.assertCalledWith(expectedLogs);
586+
});
587+
588+
it('should error when insufficient fast-track approvals', () => {
589+
const cli = new TestCLI();
590+
591+
const expectedLogs = {
592+
ok:
593+
[['Approvals: 4'],
594+
['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624'],
595+
['- Quux User (@Quux): LGTM'],
596+
['- Baz User (@Baz): https://github.com/nodejs/node/pull/16438#pullrequestreview-71488236'],
597+
['- Bar User (@bar) (TSC): lgtm']],
598+
info:
599+
[['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'],
600+
['This PR is being fast-tracked']],
601+
error:
602+
[['The fast-track request requires at' +
603+
" least one collaborator's approval (👍)."]]
604+
};
605+
606+
const pr = Object.assign({}, firstTimerPR, {
607+
createdAt: LT_48H,
608+
labels: {
609+
nodes: [
610+
{ name: 'fast-track' }
611+
]
612+
}
613+
});
614+
615+
const data = {
616+
pr,
617+
reviewers: allGreenReviewers,
618+
comments: commentsWithFastTrackInsuffientApprovals,
619+
reviews: approvingReviews,
620+
commits: [],
621+
collaborators,
622+
authorIsNew: () => true,
623+
getThread() {
624+
return PRData.prototype.getThread.call(this);
625+
}
626+
};
627+
const checker = new PRChecker(cli, data, {}, argv);
628+
629+
cli.clearCalls();
630+
const status = checker.checkReviewsAndWait(new Date(NOW));
631+
assert(!status);
632+
cli.assertCalledWith(expectedLogs);
633+
});
634+
635+
it('should error when missing fast-track request comment', () => {
636+
const cli = new TestCLI();
637+
638+
const expectedLogs = {
639+
ok:
640+
[['Approvals: 4'],
641+
['- Foo User (@foo): https://github.com/nodejs/node/pull/16438#pullrequestreview-71480624'],
642+
['- Quux User (@Quux): LGTM'],
643+
['- Baz User (@Baz): https://github.com/nodejs/node/pull/16438#pullrequestreview-71488236'],
644+
['- Bar User (@bar) (TSC): lgtm']],
645+
info:
646+
[['This PR was created on Fri, 30 Nov 2018 17:50:44 GMT'],
647+
['This PR is being fast-tracked']],
648+
error:
649+
[['Unable to find the fast-track request comment.']]
650+
};
651+
652+
const pr = Object.assign({}, firstTimerPR, {
653+
createdAt: LT_48H,
654+
labels: {
655+
nodes: [
656+
{ name: 'fast-track' }
657+
]
658+
}
659+
});
660+
661+
const data = {
662+
pr,
663+
reviewers: allGreenReviewers,
664+
comments: commentsWithLGTM,
665+
reviews: approvingReviews,
666+
commits: [],
667+
collaborators,
668+
authorIsNew: () => true,
669+
getThread() {
670+
return PRData.prototype.getThread.call(this);
671+
}
672+
};
673+
const checker = new PRChecker(cli, data, {}, argv);
674+
675+
cli.clearCalls();
676+
const status = checker.checkReviewsAndWait(new Date(NOW));
677+
assert(!status);
678+
cli.assertCalledWith(expectedLogs);
679+
});
680+
536681
it('should warn about approvals and CI for fast-tracked PR', () => {
537682
const cli = new TestCLI();
538683

0 commit comments

Comments
 (0)