-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmulti-approvers.js
103 lines (87 loc) · 3.45 KB
/
multi-approvers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const APPROVED = 'APPROVED';
const COMMENTED = 'COMMENTED';
const MIN_APPROVED_COUNT = 2;
/** Returns the number of approvals from members in the given list. */
function inOrgApprovedCount(members, submittedReviews, prLogin) {
const reviewStateByLogin = {};
submittedReviews
// Remove the PR user.
.filter((r) => r.user.login !== prLogin)
// Only consider users in the org.
.filter((r) => members.has(r.user.login))
// Sort chronologically ascending. Note that a reviewer can submit multiple reviews.
.sort((a, b) => new Date(a.submitted_at) - new Date(b.submitted_at))
.forEach((r) => {
const reviewerLogin = r.user.login;
// Set state if it does not exist.
if (!Object.hasOwn(reviewStateByLogin, reviewerLogin)) {
reviewStateByLogin[reviewerLogin] = r.state;
return;
}
// Always update state if not approved.
if (reviewStateByLogin[reviewerLogin] !== APPROVED) {
reviewStateByLogin[reviewerLogin] = r.state;
return;
}
// Do not update approved state for a comment.
if (reviewStateByLogin[reviewerLogin] === APPROVED && r.state !== COMMENTED) {
reviewStateByLogin[reviewerLogin] = r.state;
}
})
return Object.values(reviewStateByLogin).filter((s) => s === APPROVED).length;
}
/** Checks that approval requirements are satisfied. */
async function onPullRequest({orgMembersPath, prNumber, repoName, repoOwner, github, core}) {
const members = require(orgMembersPath).reduce((acc, v) => acc.set(v.login, v), new Map());
const prResponse = await github.rest.pulls.get({owner: repoOwner, repo: repoName, pull_number: prNumber});
const prLogin = prResponse.data.user.login;
if (members.has(prLogin)) {
// Do nothing if the pull request owner is a member of the org.
core.info(`Pull request login ${prLogin} is a member of the org, therefore no special approval rules apply.`);
return;
}
const submittedReviews = await github.paginate(github.rest.pulls.listReviews, {
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
});
const approvedCount = inOrgApprovedCount(members, submittedReviews, prLogin);
core.info(`Found ${approvedCount} ${APPROVED} reviews.`);
if (approvedCount < MIN_APPROVED_COUNT) {
core.setFailed(`This pull request has ${approvedCount} of ${MIN_APPROVED_COUNT} required approvals from members of the org.`);
}
}
/**
* Re-runs the approval checks on pull request review.
*
* This is required because GitHub treats checks made by pull_request and
* pull_request_review as different status checks.
*/
async function onPullRequestReview({workflowRef, repoName, repoOwner, branch, prNumber, github, core}) {
// Get the filename of the workflow.
const workflowFilename = workflowRef.split('@')[0].split('/').pop();
// Get all failed runs.
const runs = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner: repoOwner,
repo: repoName,
workflow_id: workflowFilename,
branch,
event: 'pull_request',
status: 'failure',
per_page: 100,
});
const failedRuns = runs
.filter((r) =>
r.pull_requests.map((pr) => pr.number).includes(prNumber)
)
.sort((v) => v.id);
// If there are failed runs for this PR, re-run the workflow.
if (failedRuns.length > 0) {
await github.rest.actions.reRunWorkflow({
owner: repoOwner,
repo: repoName,
run_id: failedRuns[0].id,
});
}
}
module.exports = {onPullRequest, onPullRequestReview};