ci/github-script/commits: block on errors

Most of the checks we do for cherry-picks are dismissable warnings, with
one exception: When a commit hash has been found, but this hash is not
available in any of the pickable branches, we raise this with
severity=error. This should also *block* the merge and not be
dismissable. That's because this is a fixable issue in every case.
This commit is contained in:
Wolfgang Walther 2025-07-16 12:28:29 +02:00
parent b19798c8b0
commit 1fbcad0434
No known key found for this signature in database
GPG Key ID: B39893FA5F65CAE1
2 changed files with 89 additions and 86 deletions

View File

@ -55,7 +55,6 @@ jobs:
- name: Check cherry-picks
id: check
continue-on-error: true
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
@ -63,84 +62,9 @@ jobs:
github,
context,
core,
dry: context.eventName == 'pull_request',
})
- name: Request changes
if: ${{ github.event_name == 'pull_request_target' && steps.check.outcome == 'failure' }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { readFile } = require('node:fs/promises')
const body = await readFile('review.md', 'utf-8')
const pendingReview = (await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
})).find(review =>
review.user.login == 'github-actions[bot]' && (
// If a review is still pending, we can just update this instead
// of posting a new one.
review.state == 'CHANGES_REQUESTED' ||
// No need to post a new review, if an older one with the exact
// same content had already been dismissed.
review.body == body
)
)
// Either of those two requests could fail for very long comments. This can only happen
// with multiple commits all hitting the truncation limit for the diff. If you ever hit
// this case, consider just splitting up those commits into multiple PRs.
if (pendingReview) {
await github.rest.pulls.updateReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
review_id: pendingReview.id,
body
})
} else {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
event: 'REQUEST_CHANGES',
body
})
}
- name: Dismiss old reviews
if: ${{ github.event_name == 'pull_request_target' && steps.check.outcome == 'success' }}
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
await Promise.all(
(await github.paginate(github.rest.pulls.listReviews, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
})).filter(review =>
review.user.login == 'github-actions[bot]'
).map(async (review) => {
if (review.state == 'CHANGES_REQUESTED') {
await github.rest.pulls.dismissReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
review_id: review.id,
message: 'All cherry-picks are good now, thank you!'
})
}
await github.graphql(`mutation($node_id:ID!) {
minimizeComment(input: {
classifier: RESOLVED,
subjectId: $node_id
})
{ clientMutationId }
}`, { node_id: review.node_id })
})
)
- name: Log current API rate limits
env:
GH_TOKEN: ${{ github.token }}

View File

@ -1,6 +1,6 @@
module.exports = async function ({ github, context, core }) {
module.exports = async function ({ github, context, core, dry }) {
const { execFileSync } = require('node:child_process')
const { readFile, writeFile } = require('node:fs/promises')
const { readFile } = require('node:fs/promises')
const { join } = require('node:path')
const { classify } = require('../supportedBranches.js')
const withRateLimit = require('./withRateLimit.js')
@ -8,6 +8,8 @@ module.exports = async function ({ github, context, core }) {
await withRateLimit({ github, core }, async (stats) => {
stats.prs = 1
const pull_number = context.payload.pull_request.number
const job_url =
context.runId &&
(
@ -17,7 +19,7 @@ module.exports = async function ({ github, context, core }) {
})
).data.jobs[0].html_url +
'?pr=' +
context.payload.pull_request.number
pull_number
async function handle({ sha, commit }) {
// Using the last line with "cherry" + hash, because a chained backport
@ -115,7 +117,7 @@ module.exports = async function ({ github, context, core }) {
const commits = await github.paginate(github.rest.pulls.listCommits, {
...context.repo,
pull_number: context.payload.pull_request.number,
pull_number,
})
const results = await Promise.all(commits.map(handle))
@ -131,8 +133,46 @@ module.exports = async function ({ github, context, core }) {
})
// Only create step summary below in case of warnings or errors.
if (results.every(({ severity }) => severity == 'info')) return
else process.exitCode = 1
// Also clean up older reviews, when all checks are good now.
if (results.every(({ severity }) => severity == 'info')) {
if (!dry) {
await Promise.all(
(
await github.paginate(github.rest.pulls.listReviews, {
...context.repo,
pull_number,
})
)
.filter((review) => review.user.login == 'github-actions[bot]')
.map(async (review) => {
if (review.state == 'CHANGES_REQUESTED') {
await github.rest.pulls.dismissReview({
...context.repo,
pull_number,
review_id: review.id,
message: 'All cherry-picks are good now, thank you!',
})
}
await github.graphql(
`mutation($node_id:ID!) {
minimizeComment(input: {
classifier: RESOLVED,
subjectId: $node_id
})
{ clientMutationId }
}`,
{ node_id: review.node_id },
)
}),
)
}
return
}
// In the case of "error" severity, we also fail the job.
// Those should be considered blocking and not be dismissable via review.
if (results.some(({ severity }) => severity == 'error'))
process.exitCode = 1
core.summary.addRaw(
await readFile(join(__dirname, 'check-cherry-picks.md'), 'utf-8'),
@ -198,9 +238,48 @@ module.exports = async function ({ github, context, core }) {
`\n\n_Hint: The full diffs are also available in the [runner logs](${job_url}) with slightly better highlighting._`,
)
// Write to disk temporarily for next step in GHA.
await writeFile('review.md', core.summary.stringify())
const body = core.summary.stringify()
core.summary.write()
const pendingReview = (
await github.paginate(github.rest.pulls.listReviews, {
...context.repo,
pull_number,
})
).find(
(review) =>
review.user.login == 'github-actions[bot]' &&
// If a review is still pending, we can just update this instead
// of posting a new one.
(review.state == 'CHANGES_REQUESTED' ||
// No need to post a new review, if an older one with the exact
// same content had already been dismissed.
review.body == body),
)
if (dry) {
if (pendingReview)
core.info('pending review found: ' + pendingReview.html_url)
else core.info('no pending review found')
} else {
// Either of those two requests could fail for very long comments. This can only happen
// with multiple commits all hitting the truncation limit for the diff. If you ever hit
// this case, consider just splitting up those commits into multiple PRs.
if (pendingReview) {
await github.rest.pulls.updateReview({
...context.repo,
pull_number,
review_id: pendingReview.id,
body,
})
} else {
await github.rest.pulls.createReview({
...context.repo,
pull_number,
event: 'REQUEST_CHANGES',
body,
})
}
}
})
}