ci/github-script/commits: various fixes and improvements (#425789)

authored by Wolfgang Walther and committed by GitHub fa0ef8a6 a9bb81c6

+110 -96
+1 -77
.github/workflows/check.yml
··· 55 56 - name: Check cherry-picks 57 id: check 58 - continue-on-error: true 59 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 60 with: 61 script: | ··· 63 github, 64 context, 65 core, 66 }) 67 - 68 - - name: Request changes 69 - if: ${{ github.event_name == 'pull_request_target' && steps.check.outcome == 'failure' }} 70 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 71 - with: 72 - script: | 73 - const { readFile } = require('node:fs/promises') 74 - const body = await readFile('review.md', 'utf-8') 75 - 76 - const pendingReview = (await github.paginate(github.rest.pulls.listReviews, { 77 - owner: context.repo.owner, 78 - repo: context.repo.repo, 79 - pull_number: context.payload.pull_request.number 80 - })).find(review => 81 - review.user.login == 'github-actions[bot]' && ( 82 - // If a review is still pending, we can just update this instead 83 - // of posting a new one. 84 - review.state == 'CHANGES_REQUESTED' || 85 - // No need to post a new review, if an older one with the exact 86 - // same content had already been dismissed. 87 - review.body == body 88 - ) 89 - ) 90 - 91 - // Either of those two requests could fail for very long comments. This can only happen 92 - // with multiple commits all hitting the truncation limit for the diff. If you ever hit 93 - // this case, consider just splitting up those commits into multiple PRs. 94 - if (pendingReview) { 95 - await github.rest.pulls.updateReview({ 96 - owner: context.repo.owner, 97 - repo: context.repo.repo, 98 - pull_number: context.payload.pull_request.number, 99 - review_id: pendingReview.id, 100 - body 101 - }) 102 - } else { 103 - await github.rest.pulls.createReview({ 104 - owner: context.repo.owner, 105 - repo: context.repo.repo, 106 - pull_number: context.payload.pull_request.number, 107 - event: 'REQUEST_CHANGES', 108 - body 109 - }) 110 - } 111 - 112 - - name: Dismiss old reviews 113 - if: ${{ github.event_name == 'pull_request_target' && steps.check.outcome == 'success' }} 114 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 115 - with: 116 - script: | 117 - await Promise.all( 118 - (await github.paginate(github.rest.pulls.listReviews, { 119 - owner: context.repo.owner, 120 - repo: context.repo.repo, 121 - pull_number: context.payload.pull_request.number 122 - })).filter(review => 123 - review.user.login == 'github-actions[bot]' 124 - ).map(async (review) => { 125 - if (review.state == 'CHANGES_REQUESTED') { 126 - await github.rest.pulls.dismissReview({ 127 - owner: context.repo.owner, 128 - repo: context.repo.repo, 129 - pull_number: context.payload.pull_request.number, 130 - review_id: review.id, 131 - message: 'All cherry-picks are good now, thank you!' 132 - }) 133 - } 134 - await github.graphql(`mutation($node_id:ID!) { 135 - minimizeComment(input: { 136 - classifier: RESOLVED, 137 - subjectId: $node_id 138 - }) 139 - { clientMutationId } 140 - }`, { node_id: review.node_id }) 141 - }) 142 - ) 143 144 - name: Log current API rate limits 145 env:
··· 55 56 - name: Check cherry-picks 57 id: check 58 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 59 with: 60 script: | ··· 62 github, 63 context, 64 core, 65 + dry: context.eventName == 'pull_request', 66 }) 67 68 - name: Log current API rate limits 69 env:
+7 -4
ci/github-script/check-cherry-picks.md
··· 1 - This report is automatically generated by the `check-cherry-picks` CI workflow. 2 3 - Some of the commits in this PR have not been cherry-picked exactly and require the author's and reviewer's attention. 4 5 - Please make sure to follow the [backporting guidelines](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#how-to-backport-pull-requests) and cherry-pick with the `-x` flag. This requires changes to go to the unstable branches (`master` / `staging`) first, before backporting them. 6 7 - Occasionally, it is not possible to cherry-pick exactly the same patch. This most frequently happens when resolving merge conflicts while cherry-picking or when updating minor versions of packages which have already advanced to the next major on unstable. If you need to merge this PR despite the warnings, please [dismiss](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/dismissing-a-pull-request-review) this review.
··· 1 + This report is automatically generated by the `PR / Check / cherry-pick` CI workflow. 2 3 + Some of the commits in this PR require the author's and reviewer's attention. 4 5 + Please follow the [backporting guidelines](https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md#how-to-backport-pull-requests) and cherry-pick with the `-x` flag. 6 + This requires changes to the unstable `master` and `staging` branches first, before backporting them. 7 8 + Occasionally, it is not possible to cherry-pick exactly the same patch. 9 + This most frequently happens when resolving merge conflicts or when updating minor versions of packages which have already advanced to the next major on unstable. 10 + If you need to merge this PR despite the warnings, please [dismiss](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/dismissing-a-pull-request-review) this review shortly before merging.
+102 -15
ci/github-script/commits.js
··· 1 - module.exports = async function ({ github, context, core }) { 2 const { execFileSync } = require('node:child_process') 3 - const { readFile, writeFile } = require('node:fs/promises') 4 const { join } = require('node:path') 5 const { classify } = require('../supportedBranches.js') 6 const withRateLimit = require('./withRateLimit.js') ··· 8 await withRateLimit({ github, core }, async (stats) => { 9 stats.prs = 1 10 11 const job_url = 12 context.runId && 13 ( 14 - await github.rest.actions.listJobsForWorkflowRun({ 15 ...context.repo, 16 run_id: context.runId, 17 }) 18 - ).data.jobs[0].html_url + 19 '?pr=' + 20 - context.payload.pull_request.number 21 22 async function handle({ sha, commit }) { 23 // Using the last line with "cherry" + hash, because a chained backport ··· 70 __dirname, 71 'range-diff', 72 '--no-color', 73 '--no-notes', 74 '--creation-factor=100', 75 `${original_sha}~..${original_sha}`, 76 `${sha}~..${sha}`, ··· 113 114 const commits = await github.paginate(github.rest.pulls.listCommits, { 115 ...context.repo, 116 - pull_number: context.payload.pull_request.number, 117 }) 118 119 const results = await Promise.all(commits.map(handle)) 120 121 - // Log all results without truncation and with better highlighting to the job log. 122 results.forEach(({ sha, commit, severity, message, colored_diff }) => { 123 core.startGroup(`Commit ${sha}`) 124 core.info(`Author: ${commit.author.name} ${commit.author.email}`) ··· 129 }) 130 131 // Only create step summary below in case of warnings or errors. 132 - if (results.every(({ severity }) => severity == 'info')) return 133 - else process.exitCode = 1 134 135 core.summary.addRaw( 136 await readFile(join(__dirname, 'check-cherry-picks.md'), 'utf-8'), ··· 177 } 178 179 core.summary.addRaw('<details><summary>Show diff</summary>') 180 - core.summary.addRaw('\n\n```diff', true) 181 - core.summary.addRaw(truncated.join('\n'), true) 182 - core.summary.addRaw('```', true) 183 core.summary.addRaw('</details>') 184 } 185 ··· 191 `\n\n_Hint: The full diffs are also available in the [runner logs](${job_url}) with slightly better highlighting._`, 192 ) 193 194 - // Write to disk temporarily for next step in GHA. 195 - await writeFile('review.md', core.summary.stringify()) 196 197 - core.summary.write() 198 }) 199 }
··· 1 + module.exports = async function ({ github, context, core, dry }) { 2 const { execFileSync } = require('node:child_process') 3 + const { readFile } = require('node:fs/promises') 4 const { join } = require('node:path') 5 const { classify } = require('../supportedBranches.js') 6 const withRateLimit = require('./withRateLimit.js') ··· 8 await withRateLimit({ github, core }, async (stats) => { 9 stats.prs = 1 10 11 + const pull_number = context.payload.pull_request.number 12 + 13 const job_url = 14 context.runId && 15 ( 16 + await github.paginate(github.rest.actions.listJobsForWorkflowRun, { 17 ...context.repo, 18 run_id: context.runId, 19 + per_page: 100, 20 }) 21 + ).find(({ name }) => name == 'Check / cherry-pick').html_url + 22 '?pr=' + 23 + pull_number 24 25 async function handle({ sha, commit }) { 26 // Using the last line with "cherry" + hash, because a chained backport ··· 73 __dirname, 74 'range-diff', 75 '--no-color', 76 + '--ignore-all-space', 77 '--no-notes', 78 + // 100 means "any change will be reported"; 0 means "no change will be reported" 79 '--creation-factor=100', 80 `${original_sha}~..${original_sha}`, 81 `${sha}~..${sha}`, ··· 118 119 const commits = await github.paginate(github.rest.pulls.listCommits, { 120 ...context.repo, 121 + pull_number, 122 }) 123 124 const results = await Promise.all(commits.map(handle)) 125 126 + // Log all results without truncation, with better highlighting and all whitespace changes to the job log. 127 results.forEach(({ sha, commit, severity, message, colored_diff }) => { 128 core.startGroup(`Commit ${sha}`) 129 core.info(`Author: ${commit.author.name} ${commit.author.email}`) ··· 134 }) 135 136 // Only create step summary below in case of warnings or errors. 137 + // Also clean up older reviews, when all checks are good now. 138 + if (results.every(({ severity }) => severity == 'info')) { 139 + if (!dry) { 140 + await Promise.all( 141 + ( 142 + await github.paginate(github.rest.pulls.listReviews, { 143 + ...context.repo, 144 + pull_number, 145 + }) 146 + ) 147 + .filter((review) => review.user.login == 'github-actions[bot]') 148 + .map(async (review) => { 149 + if (review.state == 'CHANGES_REQUESTED') { 150 + await github.rest.pulls.dismissReview({ 151 + ...context.repo, 152 + pull_number, 153 + review_id: review.id, 154 + message: 'All cherry-picks are good now, thank you!', 155 + }) 156 + } 157 + await github.graphql( 158 + `mutation($node_id:ID!) { 159 + minimizeComment(input: { 160 + classifier: RESOLVED, 161 + subjectId: $node_id 162 + }) 163 + { clientMutationId } 164 + }`, 165 + { node_id: review.node_id }, 166 + ) 167 + }), 168 + ) 169 + } 170 + return 171 + } 172 + 173 + // In the case of "error" severity, we also fail the job. 174 + // Those should be considered blocking and not be dismissable via review. 175 + if (results.some(({ severity }) => severity == 'error')) 176 + process.exitCode = 1 177 178 core.summary.addRaw( 179 await readFile(join(__dirname, 'check-cherry-picks.md'), 'utf-8'), ··· 220 } 221 222 core.summary.addRaw('<details><summary>Show diff</summary>') 223 + core.summary.addCodeBlock( 224 + truncated 225 + .join('\n') 226 + .replace(/&/g, '&amp;') 227 + .replace(/</g, '&lt;') 228 + .replace(/>/g, '&gt;'), 229 + 'diff', 230 + ) 231 core.summary.addRaw('</details>') 232 } 233 ··· 239 `\n\n_Hint: The full diffs are also available in the [runner logs](${job_url}) with slightly better highlighting._`, 240 ) 241 242 + const body = core.summary.stringify() 243 + core.summary.write() 244 245 + const pendingReview = ( 246 + await github.paginate(github.rest.pulls.listReviews, { 247 + ...context.repo, 248 + pull_number, 249 + }) 250 + ).find( 251 + (review) => 252 + review.user.login == 'github-actions[bot]' && 253 + // If a review is still pending, we can just update this instead 254 + // of posting a new one. 255 + (review.state == 'CHANGES_REQUESTED' || 256 + // No need to post a new review, if an older one with the exact 257 + // same content had already been dismissed. 258 + review.body == body), 259 + ) 260 + 261 + if (dry) { 262 + if (pendingReview) 263 + core.info('pending review found: ' + pendingReview.html_url) 264 + else core.info('no pending review found') 265 + } else { 266 + // Either of those two requests could fail for very long comments. This can only happen 267 + // with multiple commits all hitting the truncation limit for the diff. If you ever hit 268 + // this case, consider just splitting up those commits into multiple PRs. 269 + if (pendingReview) { 270 + await github.rest.pulls.updateReview({ 271 + ...context.repo, 272 + pull_number, 273 + review_id: pendingReview.id, 274 + body, 275 + }) 276 + } else { 277 + await github.rest.pulls.createReview({ 278 + ...context.repo, 279 + pull_number, 280 + event: 'REQUEST_CHANGES', 281 + body, 282 + }) 283 + } 284 + } 285 }) 286 }