Serenity Operating System
1/*
2 * Copyright (c) 2022, Luke Wilde <lukew@serenityos.org>
3 * Copyright (c) 2023, kleines Filmröllchen <filmroellchen@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8const Label = {
9 CommunityApproved: "✅ pr-community-approved",
10 HasConflicts: "⚠️ pr-has-conflicts",
11 IsBlocked: "⛔️ pr-is-blocked",
12 MaintainerApprovedButAwaitingCi: "✅ pr-maintainer-approved-but-awaiting-ci",
13 NeedsReview: "👀 pr-needs-review",
14 Unclear: "🤔 pr-unclear",
15 WaitingForAuthor: "⏳ pr-waiting-for-author",
16};
17
18const subjectiveLabels = [Label.IsBlocked, Label.Unclear];
19
20function removeExistingPrLabels(currentLabels, keepSubjectiveLabels) {
21 return currentLabels.filter(
22 label =>
23 !label.includes("pr-") ||
24 label === Label.HasConflicts ||
25 (keepSubjectiveLabels && subjectiveLabels.includes(label))
26 );
27}
28
29async function labelsForGenericPullRequestChange(currentLabels) {
30 const filteredLabels = removeExistingPrLabels(currentLabels, true);
31 filteredLabels.push(Label.NeedsReview);
32 return filteredLabels;
33}
34
35async function labelsForPullRequestEffectivelyClosed(currentLabels) {
36 return removeExistingPrLabels(currentLabels, false);
37}
38
39function apiErrorHandler(error) {
40 console.log(
41 "::warning::Encountered error during event handling, not updating labels. Error:",
42 error
43 );
44}
45
46module.exports = ({ github, context }) => {
47 async function labelsForPullRequestReviewSubmitted(currentLabels, { pull_request, review }) {
48 let newLabels = currentLabels;
49 const isBlocked = currentLabels.some(label => label === Label.IsBlocked);
50
51 if (review.state.toLowerCase() === "approved") {
52 const maintainers = (
53 await github.rest.teams.listMembersInOrg({
54 org: "SerenityOS",
55 team_slug: "maintainers",
56 })
57 ).data;
58
59 const approvedByMaintainer = maintainers.some(
60 maintainerInArray => maintainerInArray.login === review.user.login
61 );
62
63 if (approvedByMaintainer) {
64 newLabels = newLabels.filter(
65 label => !(label === Label.NeedsReview || label === Label.WaitingForAuthor)
66 );
67
68 if (!newLabels.includes(Label.MaintainerApprovedButAwaitingCi))
69 newLabels.push(Label.MaintainerApprovedButAwaitingCi);
70 } else {
71 if (!newLabels.includes(Label.CommunityApproved))
72 newLabels.push(Label.CommunityApproved);
73 }
74 } else if (!isBlocked) {
75 // Remove approval labels.
76 newLabels = newLabels.filter(
77 label =>
78 !(
79 label === Label.CommunityApproved ||
80 label === Label.MaintainerApprovedButAwaitingCi
81 )
82 );
83
84 if (review.user.login === pull_request.user.login) newLabels.push(Label.NeedsReview);
85 else newLabels.push(Label.WaitingForAuthor);
86 }
87
88 return newLabels;
89 }
90
91 const eventHandlers = {
92 opened: labelsForGenericPullRequestChange,
93 reopened: labelsForGenericPullRequestChange,
94 submitted: labelsForPullRequestReviewSubmitted,
95 dismissed: labelsForGenericPullRequestChange,
96 converted_to_draft: labelsForPullRequestEffectivelyClosed,
97 ready_for_review: labelsForGenericPullRequestChange,
98 synchronize: labelsForGenericPullRequestChange, // synchronize is triggered when the branch is changed
99 edited: labelsForGenericPullRequestChange,
100 review_requested: labelsForGenericPullRequestChange,
101 closed: labelsForPullRequestEffectivelyClosed,
102 };
103
104 const eventName = context.payload.action;
105 const handlerForCurrentEvent = eventHandlers[eventName];
106
107 async function updateLabels(currentLabelsAsObjects) {
108 const currentLabels = [];
109 currentLabelsAsObjects.forEach(labelObject => currentLabels.push(labelObject.name));
110
111 const isEffectivelyClosed =
112 context.payload.pull_request.draft ||
113 context.payload.pull_request.state.toLowerCase() === "closed";
114
115 const newLabels = await (isEffectivelyClosed
116 ? labelsForPullRequestEffectivelyClosed(currentLabels, context.payload)
117 : handlerForCurrentEvent(currentLabels, context.payload));
118
119 console.log(
120 `Received '${eventName}' event for ${
121 isEffectivelyClosed ? "draft/closed" : "open"
122 } pull request, changing labels from '${currentLabels}' to '${newLabels}'`
123 );
124
125 return github.rest.issues.setLabels({
126 issue_number: context.payload.pull_request.number,
127 owner: context.repo.owner,
128 repo: context.repo.repo,
129 labels: newLabels,
130 });
131 }
132
133 if (handlerForCurrentEvent) {
134 github.rest.issues
135 .listLabelsOnIssue({
136 issue_number: context.payload.pull_request.number,
137 owner: context.repo.owner,
138 repo: context.repo.repo,
139 })
140 .then(result => updateLabels(result.data))
141 .catch(apiErrorHandler);
142 } else {
143 console.log(`::warning::No handler for the '${eventName}' event, not updating labels.`);
144 }
145};