mirror of
https://github.com/coder/coder.git
synced 2026-06-04 13:38:21 +00:00
189 lines
6.5 KiB
YAML
189 lines
6.5 KiB
YAML
# Automatically backport merged PRs to the last N release branches when the
|
|
# "backport" label is applied. Works whether the label is added before or
|
|
# after the PR is merged.
|
|
#
|
|
# Usage:
|
|
# 1. Add the "backport" label to a PR targeting main.
|
|
# 2. When the PR merges (or if already merged), the workflow detects the
|
|
# latest release/* branches and opens one cherry-pick PR per branch.
|
|
#
|
|
# The created backport 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: Backport
|
|
on:
|
|
pull_request_target:
|
|
branches:
|
|
- main
|
|
types:
|
|
- closed
|
|
- labeled
|
|
|
|
permissions: {}
|
|
|
|
# Prevent duplicate runs for the same PR when both 'closed' and 'labeled'
|
|
# fire in quick succession.
|
|
concurrency:
|
|
group: backport-${{ github.event.pull_request.number }}
|
|
|
|
jobs:
|
|
detect:
|
|
name: Detect target branches
|
|
permissions:
|
|
contents: read
|
|
if: >
|
|
github.event.pull_request.merged == true &&
|
|
contains(github.event.pull_request.labels.*.name, 'backport')
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
branches: ${{ steps.find.outputs.branches }}
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
# Need all refs to discover release branches.
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Find latest release branches
|
|
id: find
|
|
run: |
|
|
# List remote release branches matching the exact release/2.X
|
|
# pattern (no suffixes like release/2.31_hotfix), sort by minor
|
|
# version descending, and take the top 3.
|
|
BRANCHES=$(
|
|
git branch -r \
|
|
| grep -E '^\s*origin/release/2\.[0-9]+$' \
|
|
| sed 's|.*origin/||' \
|
|
| sort -t. -k2 -n -r \
|
|
| head -3
|
|
)
|
|
|
|
if [ -z "$BRANCHES" ]; then
|
|
echo "No release branches found."
|
|
echo "branches=[]" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# Convert to JSON array for the matrix.
|
|
JSON=$(echo "$BRANCHES" | jq -Rnc '[inputs | select(length > 0)]')
|
|
echo "branches=$JSON" >> "$GITHUB_OUTPUT"
|
|
echo "Will backport to: $JSON"
|
|
|
|
backport:
|
|
name: "Backport to ${{ matrix.branch }}"
|
|
needs: detect
|
|
permissions:
|
|
contents: write
|
|
pull-requests: write
|
|
if: needs.detect.outputs.branches != '[]'
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
branch: ${{ fromJson(needs.detect.outputs.branches) }}
|
|
fail-fast: false
|
|
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 }}
|
|
SENDER: ${{ github.event.sender.login }}
|
|
BRANCH: ${{ matrix.branch }}
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
# Full history required for cherry-pick.
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Cherry-pick and open PR
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Configure git to authenticate pushes with the job token
|
|
# since persist-credentials is disabled on checkout.
|
|
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
|
|
|
RELEASE_VERSION="$BRANCH"
|
|
# Strip the release/ prefix for naming.
|
|
VERSION="${RELEASE_VERSION#release/}"
|
|
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}"
|
|
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
|
|
# 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 "Backport branch ${BACKPORT_BRANCH} already exists, skipping."
|
|
exit 0
|
|
fi
|
|
|
|
# Create the backport branch from the target release branch.
|
|
git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_VERSION}"
|
|
|
|
# Cherry-pick the merge commit. Use -x to record provenance and
|
|
# -m1 to pick the first parent (the main branch side).
|
|
CONFLICTS=false
|
|
if ! git cherry-pick -x -m1 "$MERGE_SHA"; then
|
|
echo "::warning::Cherry-pick to ${RELEASE_VERSION} had conflicts."
|
|
CONFLICTS=true
|
|
|
|
# Abort the failed cherry-pick and create an empty commit
|
|
# explaining the situation.
|
|
git cherry-pick --abort
|
|
git commit --allow-empty -m "Cherry-pick of #${PR_NUMBER} requires manual resolution
|
|
|
|
The automatic cherry-pick of ${MERGE_SHA} to ${RELEASE_VERSION} had conflicts.
|
|
Please cherry-pick manually:
|
|
|
|
git cherry-pick -x -m1 ${MERGE_SHA}"
|
|
fi
|
|
|
|
git push origin "$BACKPORT_BRANCH"
|
|
|
|
TITLE="${PR_TITLE} (#${PR_NUMBER})"
|
|
BODY=$(cat <<EOF
|
|
Backport of ${PR_URL}
|
|
|
|
Original PR: #${PR_NUMBER} — ${PR_TITLE}
|
|
Merge commit: ${MERGE_SHA}
|
|
Requested by: @${SENDER}
|
|
EOF
|
|
)
|
|
|
|
if [ "$CONFLICTS" = true ]; then
|
|
TITLE="${TITLE} (conflicts)"
|
|
BODY="${BODY}
|
|
|
|
> [!WARNING]
|
|
> The automatic cherry-pick had conflicts.
|
|
> Please resolve manually by cherry-picking the original merge commit:
|
|
>
|
|
> \`\`\`
|
|
> git fetch origin ${BACKPORT_BRANCH}
|
|
> git checkout ${BACKPORT_BRANCH}
|
|
> git reset --hard origin/${RELEASE_VERSION}
|
|
> git cherry-pick -x -m1 ${MERGE_SHA}
|
|
> # resolve conflicts, then push
|
|
> \`\`\`"
|
|
fi
|
|
|
|
# Check if a PR already exists for this branch (idempotency
|
|
# for re-runs).
|
|
EXISTING_PR=$(gh pr list --head "$BACKPORT_BRANCH" --base "$RELEASE_VERSION" --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_VERSION" \
|
|
--head "$BACKPORT_BRANCH" \
|
|
--title "$TITLE" \
|
|
--body "$BODY" \
|
|
--assignee "$SENDER" \
|
|
--reviewer "$SENDER"
|