From ab77154975a5156616ae31bb7bf2145535396971 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 8 Apr 2026 10:22:33 -0400 Subject: [PATCH] ci: add cherry-pick to latest release workflow (#24051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a GitHub Actions workflow that cherry-picks merged PRs to the latest release branch when the `cherry-pick` label is applied. ## How it works 1. Add the `cherry-pick` label to any PR targeting `main` (before or after merge). 2. On merge (or on label if already merged), the workflow detects the latest `release/*` branch. 3. It cherry-picks the merge commit (`-x -m1`) and opens a PR. This complements the `backport` label (see #24025) which targets the latest **3** release branches. `cherry-pick` targets only the **latest** one — useful for getting fixes into the current release. Created PRs follow existing repo conventions: - **Branch:** `backport/-to-` - **Title:** ` (#)` — e.g. `fix(site): correct button alignment (#12345)` - **Body:** links back to the original PR and merge commit If the cherry-pick encounters conflicts, the workflow aborts the cherry-pick, creates an empty commit with resolution instructions, and opens the PR with a `[CONFLICT]` prefix so the author can resolve manually. Also: - Removes `scripts/backport-pr.sh` (replaced by this workflow) - Removes `.github/cherry-pick-bot.yml` (old bot config) - Adds a section to the contributing docs explaining the `cherry-pick` label > [!NOTE] > Generated with [Coder Agents](https://coder.com/agents) --- .github/cherry-pick-bot.yml | 2 - .github/workflows/cherry-pick.yaml | 139 ++++++++++++++++++++++++ docs/about/contributing/CONTRIBUTING.md | 13 +++ scripts/backport-pr.sh | 136 ----------------------- 4 files changed, 152 insertions(+), 138 deletions(-) delete mode 100644 .github/cherry-pick-bot.yml create mode 100644 .github/workflows/cherry-pick.yaml delete mode 100755 scripts/backport-pr.sh diff --git a/.github/cherry-pick-bot.yml b/.github/cherry-pick-bot.yml deleted file mode 100644 index 1f62315d79..0000000000 --- a/.github/cherry-pick-bot.yml +++ /dev/null @@ -1,2 +0,0 @@ -enabled: true -preservePullRequestTitle: true diff --git a/.github/workflows/cherry-pick.yaml b/.github/workflows/cherry-pick.yaml new file mode 100644 index 0000000000..23641177a1 --- /dev/null +++ b/.github/workflows/cherry-pick.yaml @@ -0,0 +1,139 @@ +# Automatically cherry-pick merged PRs to the latest release branch when the +# "cherry-pick" label is applied. Works whether the label is added before or +# after the PR is merged. +# +# Usage: +# 1. Add the "cherry-pick" label to a PR targeting main. +# 2. When the PR merges (or if already merged), the workflow detects the +# latest release/* branch and opens a cherry-pick PR against it. +# +# The created PRs follow existing repo conventions: +# - Branch: backport/-to- +# - Title: (#) +# - Body: links back to the original PR and merge commit + +name: Cherry-pick to release +on: + pull_request_target: + branches: + - main + types: + - closed + - labeled + +permissions: + contents: write + pull-requests: write + +# Prevent duplicate runs for the same PR when both 'closed' and 'labeled' +# fire in quick succession. +concurrency: + group: cherry-pick-${{ github.event.pull_request.number }} + +jobs: + cherry-pick: + name: Cherry-pick to latest release + if: > + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'cherry-pick') + runs-on: ubuntu-latest + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # Full history required for cherry-pick and branch discovery. + fetch-depth: 0 + + - name: Cherry-pick and open PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + # Find the latest release branch matching the exact release/2.X + # pattern (no suffixes like release/2.31_hotfix). + RELEASE_BRANCH=$( + git branch -r \ + | grep -E '^\s*origin/release/2\.[0-9]+$' \ + | sed 's|.*origin/||' \ + | sort -t. -k2 -n -r \ + | head -1 + ) + + if [ -z "$RELEASE_BRANCH" ]; then + echo "::error::No release branch found." + exit 1 + fi + + # Strip the release/ prefix for naming. + VERSION="${RELEASE_BRANCH#release/}" + BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}" + + echo "Target branch: $RELEASE_BRANCH" + echo "Backport branch: $BACKPORT_BRANCH" + + # Check if backport branch already exists (idempotency for re-runs). + if git ls-remote --exit-code origin "refs/heads/${BACKPORT_BRANCH}" >/dev/null 2>&1; then + echo "Branch ${BACKPORT_BRANCH} already exists, skipping." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Create the backport branch from the target release branch. + git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_BRANCH}" + + # Cherry-pick the merge commit. Use -x to record provenance and + # -m1 to pick the first parent (the main branch side). + CONFLICT=false + if ! git cherry-pick -x -m1 "$MERGE_SHA"; then + CONFLICT=true + echo "::warning::Cherry-pick to ${RELEASE_BRANCH} had conflicts." + + # Abort the failed cherry-pick and create an empty commit with + # instructions so the PR can still be opened. + git cherry-pick --abort + git commit --allow-empty -m "cherry-pick of #${PR_NUMBER} failed — resolve conflicts manually + + Cherry-pick of ${MERGE_SHA} onto ${RELEASE_BRANCH} had conflicts. + To resolve: + git fetch origin ${BACKPORT_BRANCH} + git checkout ${BACKPORT_BRANCH} + git cherry-pick -x -m1 ${MERGE_SHA} + # resolve conflicts + git push origin ${BACKPORT_BRANCH}" + fi + + git push origin "$BACKPORT_BRANCH" + + BODY=$(cat < -# -# Backports a merged PR to a release branch by cherry-picking its merge commit -# and opening a new PR targeting the release branch. -# -# Examples: -# ./scripts/backport-pr.sh 2.30 23969 -# ./scripts/backport-pr.sh --dry-run 2.30 23969 - -set -euo pipefail -# shellcheck source=scripts/lib.sh -source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -cdroot - -dry_run=0 - -# Parse flags. -while [[ $# -gt 0 ]]; do - case "$1" in - --dry-run | -n) - dry_run=1 - shift - ;; - -*) - error "Unknown flag: $1" - ;; - *) - break - ;; - esac -done - -if [[ $# -lt 2 ]]; then - echo "Usage: $0 [--dry-run] " >&2 - echo " e.g. $0 2.30 23969" >&2 - exit 1 -fi - -release_version="$1" -pr_number="$2" -release_branch="release/${release_version}" - -dependencies gh jq git - -# Authenticate with GitHub. -gh_auth - -# Validate that the PR exists and is merged. -log "Fetching PR #${pr_number}..." -pr_json=$(gh pr view "$pr_number" --json mergeCommit,title,number,state,headRefName,url) - -pr_state=$(echo "$pr_json" | jq -r '.state') -if [[ "$pr_state" != "MERGED" ]]; then - error "PR #${pr_number} is not merged (state: ${pr_state})." -fi - -merge_commit=$(echo "$pr_json" | jq -r '.mergeCommit.oid') -pr_title=$(echo "$pr_json" | jq -r '.title') -pr_url=$(echo "$pr_json" | jq -r '.url') - -if [[ -z "$merge_commit" || "$merge_commit" == "null" ]]; then - error "Could not determine merge commit for PR #${pr_number}." -fi - -log "PR: #${pr_number} - ${pr_title}" -log "Merge commit: ${merge_commit}" -log "Release branch: ${release_branch}" - -# Make sure we have the latest refs. -maybedryrun "$dry_run" git fetch origin - -# Validate the release branch exists on the remote. -if ! git rev-parse "origin/${release_branch}" >/dev/null 2>&1; then - error "Release branch '${release_branch}' does not exist on origin." -fi - -backport_branch="backport/${pr_number}-to-${release_version}" -log "Backport branch: ${backport_branch}" - -if [[ "$dry_run" == 1 ]]; then - log "" - log "DRYRUN: Would cherry-pick ${merge_commit} onto ${release_branch} via branch ${backport_branch}" - log "DRYRUN: Would create PR targeting ${release_branch}" - exit 0 -fi - -# Check for uncommitted changes that would block checkout. -if ! git diff-index --quiet HEAD --; then - error "You have uncommitted changes. Please commit or stash them first." -fi - -# Create the backport branch from the release branch. -log "Creating branch ${backport_branch} from origin/${release_branch}..." -git checkout -b "$backport_branch" "origin/${release_branch}" - -# Cherry-pick the merge commit. -log "Cherry-picking ${merge_commit}..." -if ! git cherry-pick -x "$merge_commit"; then - log "" - log "Cherry-pick failed due to conflicts." - log "Resolve the conflicts, then run:" - log " git cherry-pick --continue" - log " git push origin ${backport_branch}" - log " gh pr create --base ${release_branch} --head ${backport_branch} --title \"chore: backport #${pr_number} to ${release_version}\" --body \"Backport of ${pr_url}\"" - log "" - log "Or abort with: git cherry-pick --abort && git checkout - && git branch -D ${backport_branch}" - exit 1 -fi - -# Push the backport branch. -log "Pushing ${backport_branch}..." -git push origin "$backport_branch" - -# Create the PR. -log "Creating PR..." -backport_pr_url=$(gh pr create \ - --draft \ - --label "cherry-pick/v${release_version}" \ - --base "$release_branch" \ - --head "$backport_branch" \ - --title "chore: backport #${pr_number} to ${release_version}" \ - --body "$( - cat <