-
Notifications
You must be signed in to change notification settings - Fork 1.4k
260 lines (229 loc) · 10.5 KB
/
Copy pathbackport.yaml
File metadata and controls
260 lines (229 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# 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
# 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:
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Need all refs to discover release branches.
fetch-depth: 0
persist-credentials: false
- name: Find target release branches
id: find
env:
ESR_VERSIONS_FILE: scripts/release_channels/esr_versions.txt
run: |
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/||' \
| sort -t. -k2 -n -r \
| 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"
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
# Required to create the release-specific backport label if missing.
issues: 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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}"
# 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"
# 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
CONFLICTS=false
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}"
# 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
git push origin "$BACKPORT_BRANCH"
fi
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
# 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}."
# 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)."