Skip to content

Commit 37099c4

Browse files
[feat] Add automatic backport workflow (#4778)
1 parent 6b31596 commit 37099c4

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

.github/workflows/backport.yaml

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: Auto Backport
2+
3+
on:
4+
pull_request_target:
5+
types: [closed]
6+
branches: [main]
7+
8+
jobs:
9+
backport:
10+
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
pull-requests: write
15+
issues: write
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Configure git
24+
run: |
25+
git config user.name "github-actions[bot]"
26+
git config user.email "github-actions[bot]@users.noreply.github.com"
27+
28+
- name: Extract version labels
29+
id: versions
30+
run: |
31+
# Extract version labels (e.g., "1.24", "1.22")
32+
VERSIONS=""
33+
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
34+
for label in $(echo "$LABELS" | jq -r '.[].name'); do
35+
# Match version labels like "1.24" (major.minor only)
36+
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
37+
# Validate the branch exists before adding to list
38+
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
39+
VERSIONS="${VERSIONS}${label} "
40+
else
41+
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
42+
fi
43+
fi
44+
done
45+
46+
if [ -z "$VERSIONS" ]; then
47+
echo "::error::No version labels found (e.g., 1.24, 1.22)"
48+
exit 1
49+
fi
50+
51+
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
52+
echo "Found version labels: ${VERSIONS}"
53+
54+
- name: Backport commits
55+
id: backport
56+
env:
57+
PR_NUMBER: ${{ github.event.pull_request.number }}
58+
PR_TITLE: ${{ github.event.pull_request.title }}
59+
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
60+
run: |
61+
FAILED=""
62+
SUCCESS=""
63+
64+
for version in ${{ steps.versions.outputs.versions }}; do
65+
echo "::group::Backporting to core/${version}"
66+
67+
TARGET_BRANCH="core/${version}"
68+
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
69+
70+
# Fetch target branch (fail if doesn't exist)
71+
if ! git fetch origin "${TARGET_BRANCH}"; then
72+
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
73+
FAILED="${FAILED}${version}:branch-missing "
74+
echo "::endgroup::"
75+
continue
76+
fi
77+
78+
# Create backport branch
79+
git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
80+
81+
# Try cherry-pick
82+
if git cherry-pick "${MERGE_COMMIT}"; then
83+
git push origin "${BACKPORT_BRANCH}"
84+
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
85+
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
86+
# Return to main (keep the branch, we need it for PR)
87+
git checkout main
88+
else
89+
# Get conflict info
90+
CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',')
91+
git cherry-pick --abort
92+
93+
echo "::error::Cherry-pick failed due to conflicts"
94+
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
95+
96+
# Clean up the failed branch
97+
git checkout main
98+
git branch -D "${BACKPORT_BRANCH}"
99+
fi
100+
101+
echo "::endgroup::"
102+
done
103+
104+
echo "success=${SUCCESS}" >> $GITHUB_OUTPUT
105+
echo "failed=${FAILED}" >> $GITHUB_OUTPUT
106+
107+
if [ -n "${FAILED}" ]; then
108+
exit 1
109+
fi
110+
111+
- name: Create PR for each successful backport
112+
if: steps.backport.outputs.success
113+
env:
114+
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
115+
run: |
116+
PR_TITLE="${{ github.event.pull_request.title }}"
117+
PR_NUMBER="${{ github.event.pull_request.number }}"
118+
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
119+
120+
for backport in ${{ steps.backport.outputs.success }}; do
121+
IFS=':' read -r version branch <<< "${backport}"
122+
123+
if PR_URL=$(gh pr create \
124+
--base "core/${version}" \
125+
--head "${branch}" \
126+
--title "[backport ${version}] ${PR_TITLE}" \
127+
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
128+
--label "backport" 2>&1); then
129+
130+
# Extract PR number from URL
131+
PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$')
132+
133+
if [ -n "${PR_NUM}" ]; then
134+
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
135+
fi
136+
else
137+
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
138+
# Still try to comment on the original PR about the failure
139+
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
140+
fi
141+
done
142+
143+
- name: Comment on failures
144+
if: failure() && steps.backport.outputs.failed
145+
env:
146+
GH_TOKEN: ${{ github.token }}
147+
run: |
148+
PR_NUMBER="${{ github.event.pull_request.number }}"
149+
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
150+
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
151+
152+
for failure in ${{ steps.backport.outputs.failed }}; do
153+
IFS=':' read -r version reason conflicts <<< "${failure}"
154+
155+
if [ "${reason}" = "branch-missing" ]; then
156+
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
157+
158+
elif [ "${reason}" = "conflicts" ]; then
159+
# Convert comma-separated conflicts back to newlines for display
160+
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
161+
162+
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
163+
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
164+
fi
165+
done

0 commit comments

Comments
 (0)