ci/github-script/reminders: don't remind on cherry-picks (#572900)

This commit is contained in:
Matt Sturgeon
2026-10-14 06:33:43 +00:00
committed by GitHub
3 changed files with 91 additions and 35 deletions

View File

@@ -154,6 +154,10 @@ jobs:
sparse-checkout: |
ci/github-script
- name: Install dependencies
run: npm ci --package-lock-only=false --no-audit bottleneck
working-directory: trusted/ci/github-script
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
if: github.event_name != 'pull_request' && vars.NIXPKGS_CI_CLIENT_ID
id: app-token

View File

@@ -60,6 +60,49 @@ function isIgnoredBranchError(error: unknown) {
)
}
export function cherryPickedFromSha(message: string): string | undefined {
// Using the last line with "cherry" + hash, because a chained backport
// can result in multiple of those lines. Only the last one counts.
return Array.from(message.matchAll(/cherry.*([0-9a-f]{40})/g))
.at(-1)
?.at(1)
}
interface ShaIsInDevelopmentBranchProps {
github: InstanceType<typeof GitHub>
context: typeof actionsContext
sha: string
}
export async function shaIsInDevelopmentBranch({
github,
context,
sha,
}: ShaIsInDevelopmentBranchProps): Promise<boolean> {
let branches: string[] | undefined
try {
branches = (
await github.request<CommitBranches>({
// This is an undocumented endpoint to fetch the branches a commit is part of.
// There is no equivalent in neither the REST nor the GraphQL API.
// The endpoint itself is unlikely to go away, because GitHub uses it to display
// the list of branches on the detail page of a commit.
url: `https://github.com/${context.repo.owner}/${context.repo.repo}/branch_commits/${sha}`,
headers: {
accept: 'application/json',
},
})
).data.branches
.map(({ branch }) => branch)
.filter((branch) => classify(branch).type.includes('development'))
} catch (e) {
// For some unknown reason a 404 error comes back as 500 without any more details in a GitHub Actions runner.
// Ignore these to return a regular error message below.
if (!isIgnoredBranchError(e)) throw e
}
return branches !== undefined && branches.length > 0
}
export default async ({
github,
context,
@@ -103,13 +146,9 @@ export default async ({
type: 'no-cherry-pick',
}
// Using the last line with "cherry" + hash, because a chained backport
// can result in multiple of those lines. Only the last one counts.
const cherry = Array.from(
commit.message.matchAll(/cherry.*([0-9a-f]{40})/g),
).at(-1)
const original_sha = cherryPickedFromSha(commit.message)
if (!cherry)
if (!original_sha)
return {
sha,
commit,
@@ -118,30 +157,13 @@ export default async ({
type: 'no-commit-hash',
}
const original_sha = cherry[1]
let branches: string[] | undefined
try {
branches = (
await github.request<CommitBranches>({
// This is an undocumented endpoint to fetch the branches a commit is part of.
// There is no equivalent in neither the REST nor the GraphQL API.
// The endpoint itself is unlikely to go away, because GitHub uses it to display
// the list of branches on the detail page of a commit.
url: `https://github.com/${context.repo.owner}/${context.repo.repo}/branch_commits/${original_sha}`,
headers: {
accept: 'application/json',
},
})
).data.branches
.map(({ branch }) => branch)
.filter((branch) => classify(branch).type.includes('development'))
} catch (e) {
// For some unknown reason a 404 error comes back as 500 without any more details in a GitHub Actions runner.
// Ignore these to return a regular error message below.
if (!isIgnoredBranchError(e)) throw e
}
if (!branches?.length)
if (
!(await shaIsInDevelopmentBranch({
github,
context,
sha: original_sha,
}))
)
return {
sha,
commit,

View File

@@ -3,6 +3,7 @@ import path from 'node:path'
import type * as actionsCore from '@actions/core'
import type { context as actionsContext } from '@actions/github'
import type { GitHub } from '@actions/github/lib/utils'
import { cherryPickedFromSha, shaIsInDevelopmentBranch } from './commits.ts'
import { getCommitDetailsForPR } from './get-pr-commit-details.ts'
import { dismissReviews, postReview } from './reviews.ts'
import { classify } from './supportedBranches.ts'
@@ -63,12 +64,12 @@ export default async function postReminders({
return
}
const baseBranchType = classify(
const { type: baseBranchType, stable } = classify(
pr.base.ref.replace(/^refs\/heads\//, ''),
).type
const headBranchType = classify(
)
const { type: headBranchType } = classify(
pr.head.ref.replace(/^refs\/heads\//, ''),
).type
)
if (
baseBranchType.includes('development') &&
@@ -85,7 +86,36 @@ export default async function postReminders({
}
const details = await getCommitDetailsForPR({ core, pr, repoPath })
const changedPaths = details.flatMap(({ changedPaths }) => changedPaths)
let changedPaths: string[]
if (stable && details.length < 250) {
const prCommits = await github.paginate(github.rest.pulls.listCommits, {
...context.repo,
pull_number,
})
changedPaths = (
await Promise.all(
prCommits.map(async ({ files, commit }) => {
// Ignore cherry-picks from development branches to stable branches
const original_sha = cherryPickedFromSha(commit.message)
if (
!files ||
(original_sha &&
(await shaIsInDevelopmentBranch({
github,
context,
sha: original_sha,
})))
) {
return []
} else {
return files?.map(({ filename }) => filename)
}
}),
)
).flat(1)
} else {
changedPaths = details.flatMap(({ changedPaths }) => changedPaths)
}
for (const { key, paths } of reminders) {
if (matchesAny(changedPaths, paths)) {