mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
ci: backport to ESR and ESR-1 release branches (#27460)
## What Extend the backport workflow so the `backport` label fans out to **every actively supported release channel**, not just the latest three minors. Target branches are now the union of: - the latest 3 `release/2.X` branches (mainline `n`, stable `n-1`, security `n-2`), and - the active **ESR** and **maintenance ESR (ESR-1)** branches. The set is de-duplicated, so a branch that is both stable and ESR (today `release/2.34`) is backported once. Dry-run against the current branch list yields `release/2.29`, `release/2.33`, `release/2.34`, `release/2.35`. ## Why ESR / ESR-1 are designated biannually and can sit well below the top-3 window, so the previous `head -3` heuristic silently skipped them (e.g. the maintenance ESR `release/2.29`). The current ESR was only covered by coincidence when it happened to equal stable. ## Changes - Add `scripts/release_channels/esr_versions.txt` as the single source of truth for active ESR minors. - `scripts/update-release-calendar.sh` now reads that file instead of a hardcoded `ESR_VERSIONS` array (calendar output verified unchanged). - `backport.yaml` `detect` job unions the latest 3 branches with the ESR branches (existence-checked, warns and skips missing ones) and de-duplicates. - Backport PRs now get a `backport/v<version>` label, mirroring `cherry-pick.yaml`, with `issues: write` added to create the label. ### Resilience to partial failures Even with the independent matrix (`fail-fast: false`), a single branch's job could previously abort without leaving anything behind, forcing the remaining branches to be backported entirely by hand. Fixed so each branch always ends with a PR (real or placeholder): - Label, assignee, and reviewer are attached **after** the PR is created, as best-effort steps. Requesting review from / assigning the PR author is rejected by GitHub, which previously aborted `gh pr create` under `set -e` and left no PR. - Idempotency now keys off an existing backport **PR** rather than the branch, and an existing backport branch is reused instead of bailing, so a re-run recovers a branch that was pushed before its PR was opened. - The workflow now comments on the original PR with each created backport link, flagging conflicts that still need manual resolution. - Conflicting cherry-picks continue to open a placeholder PR with copy-paste resolution steps. ## Validation - `actionlint`, `shellcheck -x`, and `zizmor` all pass. - Re-ran `update-release-calendar.sh`; ESR statuses (`2.29 Extended Support Release`, `2.34 Stable (ESR)`) are identical after the refactor. - Dry-ran the detection logic against the live branch list (see set above). <details> <summary>Implementation plan</summary> # Plan: Backport to all supported release channels (mainline, stable, security, ESR, ESR-1) ## Goal The backport GitHub Action should open cherry-pick PRs against every actively supported release branch: | Channel | Meaning | Example today | |-------------------------|-----------------------------|----------------| | Mainline | last release (n) | `release/2.35` | | Stable | n-1 | `release/2.34` | | Security Support | n-2 | `release/2.33` | | ESR | current Extended Support | `release/2.34` | | Maintenance ESR (ESR-1) | previous ESR still patched | `release/2.29` | All channels map to `release/2.X` branches. ## What we targeted before `.github/workflows/backport.yaml` took the exact `release/2.X` branches, sorted by minor descending, and kept the top 3 (mainline/stable/security). ESR and ESR-1 are not derivable from version ordering, so the maintenance ESR was silently skipped. ## Source of truth for ESR branches `scripts/update-release-calendar.sh` already encoded the active ESR minors (`ESR_VERSIONS=(29 34)`), driving the release calendar. Rather than maintaining a second list, this list was extracted into a shared data file consumed by both the calendar script and the workflow. ## Changes 1. Extract the ESR minors into `scripts/release_channels/esr_versions.txt`; update `update-release-calendar.sh` to read it. 2. Extend the `detect` job to emit the union of the top-3 branches and one `release/2.<minor>` per ESR entry, existence-checked and de-duplicated. 3. Add per-release `backport/v<version>` labels (with `issues: write`), mirroring the cherry-pick workflow. ## Assumptions - Major version is always `2` (matches existing code). - The ESR list is maintained manually when ESR versions change. - `cherry-pick.yaml` stays single-branch and is out of scope. - Missing ESR branches are skipped with a warning, not a failure. </details> --- *Opened by Coder Agents on behalf of @f0ssel.*
This commit is contained in:
+112
-40
@@ -1,16 +1,24 @@
|
||||
# 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.
|
||||
# Automatically backport merged PRs to every actively supported release branch
|
||||
# when the "backport" label is applied. Works whether the label is added before
|
||||
# or after the PR is merged.
|
||||
#
|
||||
# Target branches are the union of:
|
||||
# - the latest 3 release/2.X branches (mainline, stable, security), and
|
||||
# - the active ESR / ESR-1 branches listed in
|
||||
# scripts/release_channels/esr_versions.txt.
|
||||
# The set is de-duplicated, so a branch that is both stable and ESR is
|
||||
# backported once.
|
||||
#
|
||||
# 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.
|
||||
# target 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
|
||||
# - Label: backport/v<version> to identify the target release
|
||||
|
||||
name: Backport
|
||||
on:
|
||||
@@ -46,13 +54,17 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Find latest release branches
|
||||
- name: Find target release branches
|
||||
id: find
|
||||
env:
|
||||
ESR_VERSIONS_FILE: scripts/release_channels/esr_versions.txt
|
||||
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=$(
|
||||
set -euo pipefail
|
||||
|
||||
# Mainline, stable, security: the latest 3 release/2.X branches
|
||||
# (exact pattern, no suffixes like release/2.31_hotfix), sorted by
|
||||
# minor version descending.
|
||||
TOP3=$(
|
||||
git branch -r \
|
||||
| grep -E '^\s*origin/release/2\.[0-9]+$' \
|
||||
| sed 's|.*origin/||' \
|
||||
@@ -60,6 +72,26 @@ jobs:
|
||||
| head -3
|
||||
)
|
||||
|
||||
# ESR and ESR-1: the active ESR versions from the shared source of
|
||||
# truth. Each entry maps to the release/<version> branch. Skip
|
||||
# entries whose branch does not exist.
|
||||
ESR_BRANCHES=""
|
||||
if [ -f "$ESR_VERSIONS_FILE" ]; then
|
||||
while read -r RELEASE; do
|
||||
BRANCH="release/${RELEASE}"
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/${BRANCH}"; then
|
||||
ESR_BRANCHES="${ESR_BRANCHES}${BRANCH}"$'\n'
|
||||
else
|
||||
echo "::warning::ESR branch ${BRANCH} not found, skipping."
|
||||
fi
|
||||
done < <(grep -vE '^\s*(#|$)' "$ESR_VERSIONS_FILE")
|
||||
else
|
||||
echo "::warning::${ESR_VERSIONS_FILE} not found, backporting to latest 3 only."
|
||||
fi
|
||||
|
||||
# Union the two sets and de-duplicate.
|
||||
BRANCHES=$(printf '%s\n%s\n' "$TOP3" "$ESR_BRANCHES" | sed '/^$/d' | sort -u)
|
||||
|
||||
if [ -z "$BRANCHES" ]; then
|
||||
echo "No release branches found."
|
||||
echo "branches=[]" >> "$GITHUB_OUTPUT"
|
||||
@@ -77,6 +109,8 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
# Required to create the release-specific backport label if missing.
|
||||
issues: write
|
||||
if: needs.detect.outputs.branches != '[]'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -112,37 +146,58 @@ jobs:
|
||||
VERSION="${RELEASE_VERSION#release/}"
|
||||
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${VERSION}"
|
||||
|
||||
# Label applied to the backport PR so PRs for a specific release can
|
||||
# be filtered easily (e.g. backport/v2.34).
|
||||
BACKPORT_LABEL="backport/v${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."
|
||||
# Idempotency: if a backport PR already exists for this branch
|
||||
# (open, closed, or merged), there is nothing to do. This is the
|
||||
# primary guard so a re-run that previously pushed a branch but
|
||||
# failed before opening the PR still recovers below.
|
||||
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
|
||||
|
||||
# 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
|
||||
if git ls-remote --exit-code origin "refs/heads/${BACKPORT_BRANCH}" >/dev/null 2>&1; then
|
||||
# The branch exists from an earlier run that failed before opening
|
||||
# the PR. Reuse it as-is and fall through to PR creation rather
|
||||
# than starting over or bailing out.
|
||||
echo "Backport branch ${BACKPORT_BRANCH} already exists; reusing it to open the PR."
|
||||
else
|
||||
# Create the backport branch from the target release branch.
|
||||
git checkout -b "$BACKPORT_BRANCH" "origin/${RELEASE_VERSION}"
|
||||
|
||||
# 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
|
||||
# Cherry-pick the merge commit. Use -x to record provenance and
|
||||
# -m1 to pick the first parent (the main branch side).
|
||||
#
|
||||
# Every target branch is validated to exist in the detect job, so a
|
||||
# failure here is a genuine conflict, not a missing branch. Rather
|
||||
# than abort the whole backport, leave a placeholder commit and open
|
||||
# a PR with copy-paste resolution steps so it can be finished by
|
||||
# hand (or closed) instead of recreated from scratch.
|
||||
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
|
||||
fi
|
||||
|
||||
git push origin "$BACKPORT_BRANCH"
|
||||
git push origin "$BACKPORT_BRANCH"
|
||||
fi
|
||||
|
||||
TITLE="${PR_TITLE} (#${PR_NUMBER})"
|
||||
BODY=$(cat <<EOF
|
||||
@@ -171,18 +226,35 @@ jobs:
|
||||
> \`\`\`"
|
||||
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
|
||||
# Ensure the release-specific label exists. Best-effort: label
|
||||
# problems must never prevent the PR from being opened.
|
||||
gh label create "$BACKPORT_LABEL" \
|
||||
--description "Backport PR targeting ${RELEASE_VERSION}" \
|
||||
--color "D93F0B" \
|
||||
--force || echo "::warning::Could not create label ${BACKPORT_LABEL}."
|
||||
|
||||
gh pr create \
|
||||
--base "$RELEASE_VERSION" \
|
||||
--head "$BACKPORT_BRANCH" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY" \
|
||||
--assignee "$SENDER" \
|
||||
--reviewer "$SENDER"
|
||||
# Create the PR first, then attach label/assignee/reviewer
|
||||
# separately. Requesting a review from (or assigning) the PR author
|
||||
# is rejected by GitHub, and doing it inline with `gh pr create`
|
||||
# under `set -e` would abort the job and leave no PR. Attaching them
|
||||
# afterwards as best-effort guarantees the PR always gets created.
|
||||
NEW_PR_URL=$(
|
||||
gh pr create \
|
||||
--base "$RELEASE_VERSION" \
|
||||
--head "$BACKPORT_BRANCH" \
|
||||
--title "$TITLE" \
|
||||
--body "$BODY"
|
||||
)
|
||||
|
||||
gh pr edit "$NEW_PR_URL" --add-label "$BACKPORT_LABEL" || echo "::warning::Could not add label ${BACKPORT_LABEL} to ${NEW_PR_URL}."
|
||||
gh pr edit "$NEW_PR_URL" --add-assignee "$SENDER" || echo "::warning::Could not assign @${SENDER} to ${NEW_PR_URL}."
|
||||
gh pr edit "$NEW_PR_URL" --add-reviewer "$SENDER" || echo "::warning::Could not request review from @${SENDER} on ${NEW_PR_URL}."
|
||||
|
||||
# Notify the requester on the original PR which backport was opened,
|
||||
# flagging conflicts that still need manual resolution. Best-effort:
|
||||
# don't fail the job if the original PR is locked.
|
||||
COMMENT="Backport to \`${RELEASE_VERSION}\` created: ${NEW_PR_URL}"
|
||||
if [ "$CONFLICTS" = true ]; then
|
||||
COMMENT="${COMMENT} (:warning: conflicts need manual resolution)"
|
||||
fi
|
||||
gh pr comment "$PR_NUMBER" --body "$COMMENT" || echo "::warning::Failed to comment on #${PR_NUMBER} (PR may be locked)."
|
||||
|
||||
Reference in New Issue
Block a user