nixpkgs mirror (for testing) github.com/NixOS/nixpkgs
nix
at release-25.11 227 lines 6.0 kB view raw
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}