From 7cf9f11de13a2416e5c72b7d09efb3e9053caa56 Mon Sep 17 00:00:00 2001 From: Alicia Lopez Date: Sun, 5 Dec 2021 11:06:29 +0100 Subject: [PATCH] feat: check fast-track approvals (#588) --- lib/pr_checker.js | 28 ++++ lib/queries/PRComments.gql | 7 + test/fixtures/comments_with_ci.json | 13 ++ test/fixtures/comments_with_fast_track.json | 15 ++ ...ith_fast_track_insufficient_approvals.json | 14 ++ test/fixtures/data.js | 5 + test/unit/pr_checker.test.js | 145 ++++++++++++++++++ 7 files changed, 227 insertions(+) create mode 100644 test/fixtures/comments_with_fast_track.json create mode 100644 test/fixtures/comments_with_fast_track_insufficient_approvals.json diff --git a/lib/pr_checker.js b/lib/pr_checker.js index 179333e..d4881e0 100644 --- a/lib/pr_checker.js +++ b/lib/pr_checker.js @@ -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'); @@ -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); diff --git a/lib/queries/PRComments.gql b/lib/queries/PRComments.gql index 58ca38b..5234dc4 100644 --- a/lib/queries/PRComments.gql +++ b/lib/queries/PRComments.gql @@ -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 + } + } + } } } } diff --git a/test/fixtures/comments_with_ci.json b/test/fixtures/comments_with_ci.json index 1cdcb0f..d38d054 100644 --- a/test/fixtures/comments_with_ci.json +++ b/test/fixtures/comments_with_ci.json @@ -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/" diff --git a/test/fixtures/comments_with_fast_track.json b/test/fixtures/comments_with_fast_track.json new file mode 100644 index 0000000..75f6155 --- /dev/null +++ b/test/fixtures/comments_with_fast_track.json @@ -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" } } + ] + } + } +] diff --git a/test/fixtures/comments_with_fast_track_insufficient_approvals.json b/test/fixtures/comments_with_fast_track_insufficient_approvals.json new file mode 100644 index 0000000..d455a51 --- /dev/null +++ b/test/fixtures/comments_with_fast_track_insufficient_approvals.json @@ -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" } } + ] + } + } +] diff --git a/test/fixtures/data.js b/test/fixtures/data.js index 384989f..5c33287 100644 --- a/test/fixtures/data.js +++ b/test/fixtures/data.js @@ -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'); @@ -134,6 +137,8 @@ module.exports = { requestedChangesReviewers, approvingReviews, requestingChangesReviews, + commentsWithFastTrack, + commentsWithFastTrackInsuffientApprovals, commentsWithCI, commentsWithFailedCI, commentsWithLGTM, diff --git a/test/unit/pr_checker.test.js b/test/unit/pr_checker.test.js index 6f1539e..970589a 100644 --- a/test/unit/pr_checker.test.js +++ b/test/unit/pr_checker.test.js @@ -25,6 +25,8 @@ const { jenkinsCI, requestingChangesReviews, noReviewers, + commentsWithFastTrack, + commentsWithFastTrackInsuffientApprovals, commentsWithCI, commentsWithFailedCI, commentsWithLGTM, @@ -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();