mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
ci: add cherry-pick to latest release workflow (#24051)
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/<pr>-to-<version>` - **Title:** `<original PR title> (#<pr>)` — 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)
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
enabled: true
|
||||
preservePullRequestTitle: true
|
||||
@@ -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/<pr>-to-<version>
|
||||
# - Title: <original PR title> (#<pr>)
|
||||
# - 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 <<EOF
|
||||
Cherry-pick of ${PR_URL}
|
||||
|
||||
Original PR: #${PR_NUMBER} — ${PR_TITLE}
|
||||
Merge commit: ${MERGE_SHA}
|
||||
EOF
|
||||
)
|
||||
|
||||
TITLE="${PR_TITLE} (#${PR_NUMBER})"
|
||||
if [ "$CONFLICT" = true ]; then
|
||||
TITLE="[CONFLICT] ${TITLE}"
|
||||
fi
|
||||
|
||||
# Check if a PR already exists for this branch (idempotency
|
||||
# for re-runs). Use --state all to catch closed/merged PRs too.
|
||||
EXISTING_PR=$(gh pr list --head "$BACKPORT_BRANCH" --base "$RELEASE_BRANCH" --state all --json number --jq '.[0].number // empty')
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #${EXISTING_PR} already exists for ${BACKPORT_BRANCH}, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr create \
|
||||
--base "$RELEASE_BRANCH" \
|
||||
--head "$BACKPORT_BRANCH" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY"
|
||||
@@ -291,6 +291,19 @@ specification, however, it's still possible to merge PRs on GitHub with a badly
|
||||
formatted title. Take care when merging single-commit PRs as GitHub may prefer
|
||||
to use the original commit title instead of the PR title.
|
||||
|
||||
### Cherry-picking to the latest release branch
|
||||
|
||||
When a merged PR on `main` should also ship in the current release,
|
||||
add the **`cherry-pick`** label to the PR (before or after merge).
|
||||
|
||||
The automation detects the latest `release/*` branch, cherry-picks the
|
||||
merge commit, and opens a PR for review. The PR reuses the original
|
||||
title (e.g. `fix(site): correct button alignment (#12345)`) so the
|
||||
change is meaningful in release notes.
|
||||
|
||||
If the cherry-pick encounters conflicts, the PR is still created with
|
||||
instructions for manual resolution.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Breaking changes can be triggered in two ways:
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage: ./scripts/backport-pr.sh [--dry-run] <release-version> <pr-number>
|
||||
#
|
||||
# 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] <release-version> <pr-number>" >&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 <<EOF
|
||||
Backport of ${pr_url}
|
||||
|
||||
Original PR: #${pr_number} — ${pr_title}
|
||||
Merge commit: ${merge_commit}
|
||||
EOF
|
||||
)")
|
||||
|
||||
log ""
|
||||
log "Backport PR created: ${backport_pr_url}"
|
||||
|
||||
# Return to previous branch.
|
||||
git checkout -
|
||||
Reference in New Issue
Block a user