Skip to content

Commit 4a3de0f

Browse files
author
silas.jiang
committed
support auto cherry pick
Signed-off-by: silas.jiang <silas.jiang@zilliz.com>
1 parent da333ee commit 4a3de0f

File tree

3 files changed

+320
-0
lines changed

3 files changed

+320
-0
lines changed
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
name: Backport
2+
3+
on:
4+
pull_request_target:
5+
types:
6+
- closed
7+
workflow_dispatch:
8+
inputs:
9+
pr_number:
10+
description: 'PR number'
11+
required: true
12+
type: string
13+
target_branch:
14+
description: 'Target branch'
15+
required: true
16+
type: string
17+
18+
concurrency:
19+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
backport:
24+
timeout-minutes: 30
25+
if: >
26+
github.event_name == 'workflow_dispatch' ||
27+
(github.event_name == 'pull_request_target' &&
28+
github.event.pull_request.merged == true &&
29+
((github.event.action == 'closed') ||
30+
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'backport-to-'))))
31+
permissions:
32+
contents: write
33+
pull-requests: write
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 0
40+
41+
- name: Configure Git
42+
run: |
43+
git config user.name "github-actions[bot]"
44+
git config user.email "github-actions[bot]@users.noreply.github.com"
45+
46+
- name: Execute Backport
47+
id: execute-backport
48+
env:
49+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
EVENT_NAME: ${{ github.event_name }}
51+
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
52+
INPUT_TARGET_BRANCH: ${{ inputs.target_branch }}
53+
TRIGGER_USER: ${{ github.actor }}
54+
run: |
55+
# Helper function to write summary
56+
write_summary() {
57+
local status="$1"
58+
local title="$2"
59+
local details="$3"
60+
echo "## $status $title" >> $GITHUB_STEP_SUMMARY
61+
echo "" >> $GITHUB_STEP_SUMMARY
62+
echo "$details" >> $GITHUB_STEP_SUMMARY
63+
echo "" >> $GITHUB_STEP_SUMMARY
64+
}
65+
66+
if [ "$EVENT_NAME" == "workflow_dispatch" ]; then
67+
PR_NUMBER="$INPUT_PR_NUMBER"
68+
IS_MANUAL="true"
69+
70+
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
71+
echo "::error::Invalid PR number: $PR_NUMBER (must be a positive integer)"
72+
write_summary "❌" "Backport Failed" "**Reason:** Invalid PR number \`$PR_NUMBER\` (must be a positive integer)"
73+
exit 1
74+
fi
75+
76+
if ! [[ "$INPUT_TARGET_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
77+
echo "::error::Invalid branch name: $INPUT_TARGET_BRANCH (contains invalid characters)"
78+
write_summary "❌" "Backport Failed" "**Reason:** Invalid branch name \`$INPUT_TARGET_BRANCH\` (contains invalid characters)"
79+
exit 1
80+
fi
81+
else
82+
PR_NUMBER="${{ github.event.pull_request.number }}"
83+
IS_MANUAL="false"
84+
fi
85+
86+
echo "Starting Backport for PR #$PR_NUMBER (Triggered by $TRIGGER_USER)"
87+
88+
if ! PR_DATA=$(gh pr view "$PR_NUMBER" --json mergeCommit,title,files,author,state,labels,body --jq . 2>&1); then
89+
echo "::error::Failed to fetch PR data: $PR_DATA"
90+
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Reason:** Failed to fetch PR data\n\n**Error:** \`$PR_DATA\`"
91+
exit 1
92+
fi
93+
94+
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid // empty')
95+
PR_TITLE=$(echo "$PR_DATA" | jq -r .title)
96+
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login // "ghost"')
97+
PR_STATE=$(echo "$PR_DATA" | jq -r .state)
98+
CHANGED_FILES=$(echo "$PR_DATA" | jq -r '.files[].path // empty')
99+
100+
if [ -z "$PR_AUTHOR" ] || [ "$PR_AUTHOR" == "null" ]; then
101+
PR_AUTHOR="ghost"
102+
fi
103+
104+
PR_TITLE_CLEAN=$(echo "$PR_TITLE" | tr -d '\n\r' | sed 's/["`\\]/ /g' | head -c 200)
105+
if [ -z "$PR_TITLE_CLEAN" ]; then
106+
PR_TITLE_CLEAN="Backport PR #$PR_NUMBER"
107+
fi
108+
109+
if [ "$PR_STATE" != "MERGED" ]; then
110+
echo "::error::PR is not merged."
111+
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Reason:** PR is not merged yet"
112+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', PR #'"$PR_NUMBER"$' is not merged yet.\n\n(cc @'"$TRIGGER_USER"$')'
113+
exit 0
114+
fi
115+
116+
if [ -z "$MERGE_COMMIT" ] || [ "$MERGE_COMMIT" == "null" ]; then
117+
echo "::error::Cannot find merge commit for PR #$PR_NUMBER"
118+
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Reason:** Cannot determine merge commit for this PR"
119+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', cannot determine merge commit for this PR.\n\n(cc @'"$TRIGGER_USER"$')'
120+
exit 0
121+
fi
122+
123+
if [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | grep -q "^pymilvus/grpc_gen/"; then
124+
echo "::notice::Skipping restricted paths."
125+
write_summary "⚠️" "Backport Skipped" "**PR:** #$PR_NUMBER\n\n**Reason:** This PR modifies \`pymilvus/grpc_gen/\`. Please backport manually."
126+
gh pr comment "$PR_NUMBER" --body $'⚠️ **Backport Skipped**\nHi @'"$PR_AUTHOR"$', this PR modifies `pymilvus/grpc_gen/`. Please backport manually.\n\n(cc @'"$TRIGGER_USER"$')'
127+
exit 0
128+
fi
129+
130+
TARGET_BRANCHES=""
131+
LABELED_BRANCHES=$(echo "$PR_DATA" | jq -r '.labels[].name' | grep '^backport-to-' | sed 's/^backport-to-//' || true)
132+
if [ "$IS_MANUAL" == "true" ]; then
133+
# Check if the target branch has corresponding label
134+
if echo "$LABELED_BRANCHES" | grep -qx "$INPUT_TARGET_BRANCH"; then
135+
TARGET_BRANCHES="$INPUT_TARGET_BRANCH"
136+
else
137+
echo "::error::Target branch '$INPUT_TARGET_BRANCH' is not labeled with 'backport-to-$INPUT_TARGET_BRANCH'"
138+
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Target Branch:** \`$INPUT_TARGET_BRANCH\`\n\n**Reason:** Missing label \`backport-to-$INPUT_TARGET_BRANCH\`\n\n**Available Labels:** \`$LABELED_BRANCHES\`"
139+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', target branch `'"$INPUT_TARGET_BRANCH"$'` is not in the backport labels. Please add label `backport-to-'"$INPUT_TARGET_BRANCH"$'` first.\n\n(cc @'"$TRIGGER_USER"$')'
140+
exit 0
141+
fi
142+
else
143+
TARGET_BRANCHES="$LABELED_BRANCHES"
144+
fi
145+
146+
if [ -z "$TARGET_BRANCHES" ]; then
147+
echo "No target branches found."
148+
write_summary "ℹ️" "No Backport Needed" "**PR:** #$PR_NUMBER\n\n**Reason:** No \`backport-to-*\` labels found on this PR"
149+
exit 0
150+
fi
151+
152+
BACKPORT_SUCCESS="false"
153+
BACKPORT_COUNT=0
154+
SUMMARY_RESULTS=""
155+
156+
while IFS= read -r TARGET_BRANCH; do
157+
[ -z "$TARGET_BRANCH" ] && continue
158+
TARGET_BRANCH=$(echo "$TARGET_BRANCH" | xargs)
159+
160+
echo "::group::Processing backport to $TARGET_BRANCH"
161+
162+
if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null 2>&1; then
163+
echo "::error::Target branch '$TARGET_BRANCH' does not exist."
164+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Branch does not exist |"
165+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', target branch `'"$TARGET_BRANCH"$'` does not exist.\n\n(cc @'"$TRIGGER_USER"$')'
166+
echo "::endgroup::"
167+
continue
168+
fi
169+
170+
echo "Checking for existing backport PR..."
171+
EXISTING_PR=$(gh pr list \
172+
--base "$TARGET_BRANCH" \
173+
--state all \
174+
--search "in:title [Backport $TARGET_BRANCH]" \
175+
--author "app/github-actions" \
176+
--json number,title \
177+
--jq ".[] | select(.title | contains(\"#$PR_NUMBER\")) | .number" \
178+
| head -n 1)
179+
180+
if [ -n "$EXISTING_PR" ]; then
181+
echo "::notice::Backport PR already exists: #$EXISTING_PR"
182+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ℹ️ Skipped | Already exists: #$EXISTING_PR |"
183+
gh pr comment "$PR_NUMBER" --body $'ℹ️ **Backport Already Exists**\nHi @'"$PR_AUTHOR"$', a backport PR for `'"$TARGET_BRANCH"$'` already exists: #'"$EXISTING_PR"$'\n\n(cc @'"$TRIGGER_USER"$')'
184+
echo "::endgroup::"
185+
continue
186+
fi
187+
188+
git reset --hard HEAD
189+
git clean -fdx
190+
191+
TARGET_BRANCH_SHORT="${TARGET_BRANCH:0:100}"
192+
BRANCH_SUFFIX="$(date +%s)-$RANDOM"
193+
BACKPORT_BRANCH="backport-$PR_NUMBER-to-$TARGET_BRANCH_SHORT-$BRANCH_SUFFIX"
194+
BACKPORT_BRANCH="${BACKPORT_BRANCH:0:250}"
195+
196+
if ! git fetch origin "$TARGET_BRANCH"; then
197+
echo "::error::Failed to fetch $TARGET_BRANCH"
198+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Failed to fetch branch |"
199+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', failed to fetch target branch `'"$TARGET_BRANCH"$'`.\n\n(cc @'"$TRIGGER_USER"$')'
200+
echo "::endgroup::"
201+
continue
202+
fi
203+
204+
git checkout -b "$BACKPORT_BRANCH" "origin/$TARGET_BRANCH"
205+
206+
echo "Attempting cherry-pick of $MERGE_COMMIT..."
207+
208+
if git cherry-pick -x -s "$MERGE_COMMIT" 2>&1; then
209+
echo "Cherry-pick successful."
210+
else
211+
echo "Standard cherry-pick failed, trying with -m 1..."
212+
git cherry-pick --abort 2>/dev/null || true
213+
214+
if git cherry-pick -x -s -m 1 "$MERGE_COMMIT" 2>&1; then
215+
echo "Cherry-pick with -m 1 successful."
216+
else
217+
echo "::error::Cherry-pick failed due to conflicts."
218+
git cherry-pick --abort 2>/dev/null || true
219+
220+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Merge conflicts - manual backport required |"
221+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', I could not cherry-pick this to `'"$TARGET_BRANCH"$'` due to **merge conflicts**. Please backport manually.\n\n(cc @'"$TRIGGER_USER"$')'
222+
echo "::endgroup::"
223+
continue
224+
fi
225+
fi
226+
227+
if ! git push origin "$BACKPORT_BRANCH"; then
228+
echo "::error::Failed to push branch $BACKPORT_BRANCH"
229+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Failed to push branch |"
230+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', failed to push backport branch for `'"$TARGET_BRANCH"$'`.\n\n(cc @'"$TRIGGER_USER"$')'
231+
echo "::endgroup::"
232+
continue
233+
fi
234+
235+
PR_BODY="Backport of #$PR_NUMBER to \`$TARGET_BRANCH\`."
236+
ASSIGNEE_ARG=""
237+
if [ "$IS_MANUAL" == "true" ]; then
238+
PR_BODY="Manual backport of #$PR_NUMBER to \`$TARGET_BRANCH\`."
239+
ASSIGNEE_ARG="--assignee $TRIGGER_USER"
240+
fi
241+
242+
if NEW_PR_URL=$(gh pr create \
243+
--base "$TARGET_BRANCH" \
244+
--head "$BACKPORT_BRANCH" \
245+
--title "[Backport $TARGET_BRANCH] $PR_TITLE_CLEAN" \
246+
--body "$PR_BODY" \
247+
--label "backport" \
248+
$ASSIGNEE_ARG 2>&1); then
249+
250+
gh pr comment "$PR_NUMBER" --body $'✅ **Backport Created**\nHi @'"$PR_AUTHOR"$', Backport PR for `'"$TARGET_BRANCH"$'` has been created: '"$NEW_PR_URL"$'\n\n(cc @'"$TRIGGER_USER"$')'
251+
252+
echo "Backport PR created: $NEW_PR_URL"
253+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ✅ Success | $NEW_PR_URL |"
254+
BACKPORT_SUCCESS="true"
255+
BACKPORT_COUNT=$((BACKPORT_COUNT + 1))
256+
else
257+
echo "::error::Failed to create backport PR."
258+
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Failed to create PR |"
259+
git push origin --delete "$BACKPORT_BRANCH" 2>/dev/null || true
260+
gh pr comment "$PR_NUMBER" --body $'❌ **Backport PR Creation Failed**\nHi @'"$PR_AUTHOR"$', Failed to create backport PR for `'"$TARGET_BRANCH"$'`.\n\nError: '"$NEW_PR_URL"$'\n\n(cc @'"$TRIGGER_USER"$')'
261+
fi
262+
263+
echo "::endgroup::"
264+
done < <(echo "$TARGET_BRANCHES")
265+
266+
# Write final summary
267+
if [ "$BACKPORT_SUCCESS" == "true" ]; then
268+
echo "## ✅ Backport Completed" >> $GITHUB_STEP_SUMMARY
269+
else
270+
echo "## ❌ Backport Failed" >> $GITHUB_STEP_SUMMARY
271+
fi
272+
echo "" >> $GITHUB_STEP_SUMMARY
273+
echo "**PR:** #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY
274+
echo "**Title:** $PR_TITLE_CLEAN" >> $GITHUB_STEP_SUMMARY
275+
echo "**Triggered by:** @$TRIGGER_USER" >> $GITHUB_STEP_SUMMARY
276+
echo "" >> $GITHUB_STEP_SUMMARY
277+
echo "### Results" >> $GITHUB_STEP_SUMMARY
278+
echo "" >> $GITHUB_STEP_SUMMARY
279+
echo "| Branch | Status | Details |" >> $GITHUB_STEP_SUMMARY
280+
echo "|--------|--------|---------|" >> $GITHUB_STEP_SUMMARY
281+
echo -e "$SUMMARY_RESULTS" >> $GITHUB_STEP_SUMMARY
282+
283+
echo "success=$BACKPORT_SUCCESS" >> $GITHUB_OUTPUT
284+
echo "count=$BACKPORT_COUNT" >> $GITHUB_OUTPUT
285+
echo "Backport process completed. Success: $BACKPORT_SUCCESS, Count: $BACKPORT_COUNT"
286+
287+
- name: Label Original PR
288+
if: steps.execute-backport.outputs.success == 'true'
289+
env:
290+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
291+
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
292+
EVENT_NAME: ${{ github.event_name }}
293+
run: |
294+
if [ "$EVENT_NAME" == "workflow_dispatch" ]; then
295+
PR_NUMBER="$INPUT_PR_NUMBER"
296+
else
297+
PR_NUMBER="${{ github.event.pull_request.number }}"
298+
fi
299+
gh pr edit "$PR_NUMBER" --add-label "backported" || echo "::warning::Failed to add 'backported' label"

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ Note: the problems, features, and questions mentioned here are not limited to Py
6767

6868
`setup.py`: Package script for PyMilvus.
6969

70+
## Backporting (Cherry-pick)
71+
72+
We use a bot to automate backporting bug fixes to branches.
73+
74+
**How to use:**
75+
Simply add a label `backport-to-<branch-name>` to your Pull Request (e.g., `backport-to-2.6`).
76+
***Success**: The bot will create a new backport PR automatically.
77+
***Failure**: The bot will comment on your PR if there are conflicts or restricted files (`proto_gen/`).
78+
79+
If the bot fails due to conflicts, please backport manually.
80+
7081
## Congratulations! You are now the contributor to the Milvus community!
7182

7283
Apart from dealing with codes and machines, you are always welcome to communicate with any member from the Milvus community. New faces join us every day, and they may as well encounter the same challenges as you faced beore. Feel free to help them. You can pass on the collaborative spirit from the assistance you acquired when you first joined the community. Let us build a collaborative, open-source, exuberant, and tolerant community together!

CONTRIBUTING_CN.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ PyMilvus 的 Github issue 列表中,打上了 [good-first-issue](https://githu
9191

9292
### 通过所有 Github Actions
9393

94+
## Cherry-pick
95+
96+
使用方法: 只需为你的 Pull Request 添加 backport-to-<branch-name> 格式的标签即可(例如:backport-to-2.6)。
97+
98+
✅ 成功:机器人会自动创建一个新的 Backport PR。
99+
100+
❌ 失败:如果存在代码冲突或修改了受限文件(如 proto_gen/),机器人会在 PR 中留言提醒。
101+
102+
如果机器人因冲突执行失败,请手动进行 Backport 操作。
103+
94104
## 恭喜你!你已经成为了 Milvus 社区的贡献者!
95105

96106
除了和代码、机器打交道,你还可以和 Milvus 社区中的人交流。社区中每天都有很多新面孔加入,当他们遇到的困难正好是你所了解的地方,请尽情的帮助这些人。回想你初次接触 Milvus 接受过的帮助,你也可以将这样的交流互助精神不断传递下去,我们一起共创一个协作、开源、开放、包容的社区。

0 commit comments

Comments
 (0)