From 76cbc580f0e4c858ecb7729dbcdb2d9ec9191677 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 9 Apr 2026 10:37:56 -0400 Subject: [PATCH] ci: add cherry-pick PR check for release branches (#24121) Adds a GitHub Actions workflow that runs on PRs targeting `release/*` branches to flag non-bug-fix cherry-picks. ## What it does - Triggers on `pull_request_target` (opened, reopened, edited) for `release/*` branches - Checks if the PR title starts with `fix:` or `fix(scope):` (conventional commit format) - If not a bug fix, comments on the PR informing the author and emits a warning (via `core.warning`), but does **not** fail the check - Deduplicates comments on title edits by updating an existing comment (identified by a hidden HTML marker) instead of creating a new one > [!NOTE] > Generated by Coder Agents --- .github/workflows/pr-cherry-pick-check.yaml | 93 +++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/pr-cherry-pick-check.yaml diff --git a/.github/workflows/pr-cherry-pick-check.yaml b/.github/workflows/pr-cherry-pick-check.yaml new file mode 100644 index 0000000000..41aa61429e --- /dev/null +++ b/.github/workflows/pr-cherry-pick-check.yaml @@ -0,0 +1,93 @@ +# Ensures that only bug fixes are cherry-picked to release branches. +# PRs targeting release/* must have a title starting with "fix:" or "fix(scope):". +name: PR Cherry-Pick Check + +on: + # zizmor: ignore[dangerous-triggers] Only reads PR metadata and comments; does not checkout PR code. + pull_request_target: + types: [opened, reopened, edited] + branches: + - "release/*" + +permissions: + pull-requests: write + +jobs: + check-cherry-pick: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit + + - name: Check PR title for bug fix + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const title = context.payload.pull_request.title; + const prNumber = context.payload.pull_request.number; + const baseBranch = context.payload.pull_request.base.ref; + const author = context.payload.pull_request.user.login; + + console.log(`PR #${prNumber}: "${title}" -> ${baseBranch}`); + + // Match conventional commit "fix:" or "fix(scope):" prefix. + const isBugFix = /^fix(\(.+\))?:/.test(title); + + if (isBugFix) { + console.log("PR title indicates a bug fix. No action needed."); + return; + } + + console.log("PR title does not indicate a bug fix. Commenting."); + + // Check for an existing comment from this bot to avoid duplicates + // on title edits. + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const marker = ""; + const existingComment = comments.find( + (c) => c.body && c.body.includes(marker), + ); + + const body = [ + marker, + `👋 Hey @${author}!`, + "", + `This PR is targeting the \`${baseBranch}\` release branch, but its title does not start with \`fix:\` or \`fix(scope):\`.`, + "", + "Only **bug fixes** should be cherry-picked to release branches. If this is a bug fix, please update the PR title to match the conventional commit format:", + "", + "```", + "fix: description of the bug fix", + "fix(scope): description of the bug fix", + "```", + "", + "If this is **not** a bug fix, it likely should not target a release branch.", + ].join("\n"); + + if (existingComment) { + console.log(`Updating existing comment ${existingComment.id}.`); + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } + + core.warning( + `PR #${prNumber} targets ${baseBranch} but is not a bug fix. Title must start with "fix:" or "fix(scope):".`, + );