nixpkgs mirror (for testing)
github.com/NixOS/nixpkgs
nix
1// @ts-check
2
3const eventToState = {
4 COMMENT: 'COMMENTED',
5 REQUEST_CHANGES: 'CHANGES_REQUESTED',
6}
7
8/**
9 * @param {{
10 * github: InstanceType<import('@actions/github/lib/utils').GitHub>,
11 * context: import('@actions/github/lib/context').Context,
12 * core: import('@actions/core'),
13 * dry: boolean,
14 * reviewKey?: string,
15 * }} DismissReviewsProps
16 */
17async function dismissReviews({ github, context, core, dry, reviewKey }) {
18 const pull_number = context.payload.pull_request?.number
19 if (!pull_number) {
20 core.warning('dismissReviews called outside of pull_request context')
21 return
22 }
23
24 if (dry) {
25 return
26 }
27
28 const reviews = (
29 await github.paginate(github.rest.pulls.listReviews, {
30 ...context.repo,
31 pull_number,
32 })
33 ).filter(
34 (review) =>
35 review.user?.login === 'github-actions[bot]' &&
36 review.state !== 'DISMISSED',
37 )
38 const changesRequestedReviews = reviews.filter(
39 (review) => review.state === 'CHANGES_REQUESTED',
40 )
41
42 const commentRegex = new RegExp(
43 /<!-- nixpkgs review key: (.*)(?:; resolved: .*)? -->/,
44 )
45 const reviewKeyRegex = new RegExp(
46 `<!-- (nixpkgs review key: ${reviewKey})(?:; resolved: .*)? -->`,
47 )
48 const commentResolvedRegex = new RegExp(
49 /<!-- nixpkgs review key: .*; resolved: true -->/,
50 )
51
52 let reviewsToMinimize = reviews
53 let /** @type {typeof reviews} */ reviewsToDismiss = []
54 let /** @type {typeof reviews} */ reviewsToResolve = []
55
56 if (reviewKey && reviews.every((review) => commentRegex.test(review.body))) {
57 reviewsToMinimize = reviews.filter((review) =>
58 reviewKeyRegex.test(review.body),
59 )
60 }
61
62 // If we want to dismiss all reviews with the key reviewKey,
63 // but there are other requested changes from CI, we can't dismiss,
64 // because then the other requested changes will be dismissed too.
65 if (
66 changesRequestedReviews.every(
67 (review) =>
68 commentResolvedRegex.test(review.body) ||
69 (reviewKey && reviewKeyRegex.test(review.body)) ||
70 // If we are called by check-commits and the review body is clearly
71 // from `commits.js`, then we can safely dismiss the review.
72 // This helps with pre-existing reviews (before the comments were added).
73 (reviewKey &&
74 reviewKey === 'check-commits' &&
75 review.body.includes('PR / Check / cherry-pick')),
76 )
77 ) {
78 reviewsToDismiss = changesRequestedReviews
79 } else if (reviewsToMinimize.length) {
80 reviewsToResolve = reviewsToMinimize.filter(
81 (review) =>
82 review.state === 'CHANGES_REQUESTED' &&
83 !commentResolvedRegex.test(review.body),
84 )
85 }
86
87 await Promise.all([
88 ...reviewsToMinimize.map(async (review) =>
89 github.graphql(
90 `mutation($node_id:ID!) {
91 minimizeComment(input: {
92 classifier: OUTDATED,
93 subjectId: $node_id
94 })
95 { clientMutationId }
96 }`,
97 { node_id: review.node_id },
98 ),
99 ),
100 ...reviewsToDismiss.map(async (review) =>
101 github.rest.pulls.dismissReview({
102 ...context.repo,
103 pull_number,
104 review_id: review.id,
105 message: 'Review dismissed automatically',
106 }),
107 ),
108 ...reviewsToResolve.map(async (review) =>
109 github.rest.pulls.updateReview({
110 ...context.repo,
111 pull_number,
112 review_id: review.id,
113 body: review.body.replace(
114 reviewKeyRegex,
115 `<!-- nixpkgs review key: ${reviewKey}; resolved: true -->`,
116 ),
117 }),
118 ),
119 ])
120}
121
122/**
123 * @param {{
124 * github: InstanceType<import('@actions/github/lib/utils').GitHub>,
125 * context: import('@actions/github/lib/context').Context
126 * core: import('@actions/core'),
127 * dry: boolean,
128 * body: string,
129 * event: keyof eventToState,
130 * reviewKey: string,
131 * }} PostReviewProps
132 */
133async function postReview({
134 github,
135 context,
136 core,
137 dry,
138 body,
139 event = 'REQUEST_CHANGES',
140 reviewKey,
141}) {
142 const pull_number = context.payload.pull_request?.number
143 if (!pull_number) {
144 core.warning('postReview called outside of pull_request context')
145 return
146 }
147
148 const reviewKeyRegex = new RegExp(
149 `<!-- (nixpkgs review key: ${reviewKey})(?:; resolved: .*)? -->`,
150 )
151 const reviewKeyComment = `<!-- nixpkgs review key: ${reviewKey}; resolved: false -->`
152 body = body + '\n\n' + reviewKeyComment
153
154 const reviews = (
155 await github.paginate(github.rest.pulls.listReviews, {
156 ...context.repo,
157 pull_number,
158 })
159 ).filter(
160 (review) =>
161 review.user?.login === 'github-actions[bot]' &&
162 review.state !== 'DISMISSED',
163 )
164
165 /** @type {null | typeof reviews[number]} */
166 let pendingReview
167 const matchingReviews = reviews.filter((review) =>
168 reviewKeyRegex.test(review.body),
169 )
170
171 if (matchingReviews.length === 0) {
172 pendingReview = null
173 } else if (
174 matchingReviews.length === 1 &&
175 matchingReviews[0].state === eventToState[event]
176 ) {
177 pendingReview = matchingReviews[0]
178 } else {
179 await dismissReviews({
180 github,
181 context,
182 core,
183 dry,
184 reviewKey,
185 })
186 pendingReview = null
187 }
188
189 if (dry) {
190 if (pendingReview)
191 core.info(`pending review found: ${pendingReview.html_url}`)
192 else core.info('no pending review found')
193 core.info(body)
194 } else {
195 if (pendingReview) {
196 await Promise.all([
197 github.rest.pulls.updateReview({
198 ...context.repo,
199 pull_number,
200 review_id: pendingReview.id,
201 body,
202 }),
203 github.graphql(
204 `mutation($node_id:ID!) {
205 unminimizeComment(input: {
206 subjectId: $node_id
207 })
208 { clientMutationId }
209 }`,
210 { node_id: pendingReview.node_id },
211 ),
212 ])
213 } else {
214 await github.rest.pulls.createReview({
215 ...context.repo,
216 pull_number,
217 event,
218 body,
219 })
220 }
221 }
222}
223
224module.exports = {
225 dismissReviews,
226 postReview,
227}