From fce05d04289d6cb7019b2a76763543dfce67189a Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 2 Apr 2026 15:19:06 +0200 Subject: [PATCH] feat: add backport PR script (#23973) _Disclaimer: created using Claude Opus 4.6._ ``` # Examples: # ./scripts/backport-pr.sh 2.30 23969 # ./scripts/backport-pr.sh --dry-run 2.30 23969 ``` Here's one I created: https://github.com/coder/coder/pull/23972 Signed-off-by: Danny Kopping --- scripts/backport-pr.sh | 136 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100755 scripts/backport-pr.sh diff --git a/scripts/backport-pr.sh b/scripts/backport-pr.sh new file mode 100755 index 0000000000..da370220c9 --- /dev/null +++ b/scripts/backport-pr.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Usage: ./scripts/backport-pr.sh [--dry-run] +# +# 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 "$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 <