Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions .github/workflows/auto-cherrypick.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
name: Backport

on:
pull_request_target:
types:
- closed
workflow_dispatch:
inputs:
pr_number:
description: 'PR number'
required: true
type: string
target_branch:
description: 'Target branch'
required: true
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.pr_number || github.run_id }}
cancel-in-progress: true

jobs:
backport:
timeout-minutes: 30
if: >
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'pull_request_target' &&
github.event.pull_request.merged == true &&
((github.event.action == 'closed') ||
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'backport-to-'))))
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Execute Backport
id: execute-backport
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
INPUT_TARGET_BRANCH: ${{ inputs.target_branch }}
TRIGGER_USER: ${{ github.actor }}
run: |
# Helper function to write summary
write_summary() {
local status="$1"
local title="$2"
local details="$3"
echo "## $status $title" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "$details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
}

if [ "$EVENT_NAME" == "workflow_dispatch" ]; then
PR_NUMBER="$INPUT_PR_NUMBER"
IS_MANUAL="true"

if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "::error::Invalid PR number: $PR_NUMBER (must be a positive integer)"
write_summary "❌" "Backport Failed" "**Reason:** Invalid PR number \`$PR_NUMBER\` (must be a positive integer)"
exit 1
fi

if ! [[ "$INPUT_TARGET_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
echo "::error::Invalid branch name: $INPUT_TARGET_BRANCH (contains invalid characters)"
write_summary "❌" "Backport Failed" "**Reason:** Invalid branch name \`$INPUT_TARGET_BRANCH\` (contains invalid characters)"
exit 1
fi
else
PR_NUMBER="${{ github.event.pull_request.number }}"
IS_MANUAL="false"
fi

echo "Starting Backport for PR #$PR_NUMBER (Triggered by $TRIGGER_USER)"

if ! PR_DATA=$(gh pr view "$PR_NUMBER" --json mergeCommit,title,files,author,state,labels,body --jq . 2>&1); then
echo "::error::Failed to fetch PR data: $PR_DATA"
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Reason:** Failed to fetch PR data\n\n**Error:** \`$PR_DATA\`"
exit 1
fi

MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid // empty')
PR_TITLE=$(echo "$PR_DATA" | jq -r .title)
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login // "ghost"')
PR_STATE=$(echo "$PR_DATA" | jq -r .state)
CHANGED_FILES=$(echo "$PR_DATA" | jq -r '.files[].path // empty')

if [ -z "$PR_AUTHOR" ] || [ "$PR_AUTHOR" == "null" ]; then
PR_AUTHOR="ghost"
fi

PR_TITLE_CLEAN=$(echo "$PR_TITLE" | tr -d '\n\r' | sed 's/["`\\]/ /g' | head -c 200)
if [ -z "$PR_TITLE_CLEAN" ]; then
PR_TITLE_CLEAN="Backport PR #$PR_NUMBER"
fi

if [ "$PR_STATE" != "MERGED" ]; then
echo "::error::PR is not merged."
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Reason:** PR is not merged yet"
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', PR #'"$PR_NUMBER"$' is not merged yet.\n\n(cc @'"$TRIGGER_USER"$')'
exit 0
fi

if [ -z "$MERGE_COMMIT" ] || [ "$MERGE_COMMIT" == "null" ]; then
echo "::error::Cannot find merge commit for PR #$PR_NUMBER"
write_summary "❌" "Backport Failed" "**PR:** #$PR_NUMBER\n\n**Reason:** Cannot determine merge commit for this PR"
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', cannot determine merge commit for this PR.\n\n(cc @'"$TRIGGER_USER"$')'
exit 0
fi

if [ -n "$CHANGED_FILES" ] && echo "$CHANGED_FILES" | grep -q "^pymilvus/grpc_gen/"; then
echo "::notice::Skipping restricted paths."
write_summary "⚠️" "Backport Skipped" "**PR:** #$PR_NUMBER\n\n**Reason:** This PR modifies \`pymilvus/grpc_gen/\`. Please backport manually."
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"$')'
exit 0
fi

TARGET_BRANCHES=""
LABELED_BRANCHES=$(echo "$PR_DATA" | jq -r '.labels[].name' | grep '^backport-to-' | sed 's/^backport-to-//' || true)
if [ "$IS_MANUAL" == "true" ]; then
# Check if the target branch has corresponding label
if echo "$LABELED_BRANCHES" | grep -qx "$INPUT_TARGET_BRANCH"; then
TARGET_BRANCHES="$INPUT_TARGET_BRANCH"
else
echo "::error::Target branch '$INPUT_TARGET_BRANCH' is not labeled with 'backport-to-$INPUT_TARGET_BRANCH'"
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\`"
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"$')'
exit 0
fi
else
TARGET_BRANCHES="$LABELED_BRANCHES"
fi

if [ -z "$TARGET_BRANCHES" ]; then
echo "No target branches found."
write_summary "ℹ️" "No Backport Needed" "**PR:** #$PR_NUMBER\n\n**Reason:** No \`backport-to-*\` labels found on this PR"
exit 0
fi

BACKPORT_SUCCESS="false"
BACKPORT_COUNT=0
SUMMARY_RESULTS=""

while IFS= read -r TARGET_BRANCH; do
[ -z "$TARGET_BRANCH" ] && continue
TARGET_BRANCH=$(echo "$TARGET_BRANCH" | xargs)

echo "::group::Processing backport to $TARGET_BRANCH"

if ! git ls-remote --exit-code --heads origin "$TARGET_BRANCH" > /dev/null 2>&1; then
echo "::error::Target branch '$TARGET_BRANCH' does not exist."
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Branch does not exist |"
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', target branch `'"$TARGET_BRANCH"$'` does not exist.\n\n(cc @'"$TRIGGER_USER"$')'
echo "::endgroup::"
continue
fi

echo "Checking for existing backport PR..."
EXISTING_PR=$(gh pr list \
--base "$TARGET_BRANCH" \
--state all \
--search "in:title [Backport $TARGET_BRANCH]" \
--author "app/github-actions" \
--json number,title \
--jq ".[] | select(.title | contains(\"#$PR_NUMBER\")) | .number" \
| head -n 1)

if [ -n "$EXISTING_PR" ]; then
echo "::notice::Backport PR already exists: #$EXISTING_PR"
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ℹ️ Skipped | Already exists: #$EXISTING_PR |"
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"$')'
echo "::endgroup::"
continue
fi

git reset --hard HEAD
git clean -fdx

TARGET_BRANCH_SHORT="${TARGET_BRANCH:0:100}"
BRANCH_SUFFIX="$(date +%s)-$RANDOM"
BACKPORT_BRANCH="backport-$PR_NUMBER-to-$TARGET_BRANCH_SHORT-$BRANCH_SUFFIX"
BACKPORT_BRANCH="${BACKPORT_BRANCH:0:250}"

if ! git fetch origin "$TARGET_BRANCH"; then
echo "::error::Failed to fetch $TARGET_BRANCH"
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Failed to fetch branch |"
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', failed to fetch target branch `'"$TARGET_BRANCH"$'`.\n\n(cc @'"$TRIGGER_USER"$')'
echo "::endgroup::"
continue
fi

git checkout -b "$BACKPORT_BRANCH" "origin/$TARGET_BRANCH"

echo "Attempting cherry-pick of $MERGE_COMMIT..."

if git cherry-pick -x -s "$MERGE_COMMIT" 2>&1; then
echo "Cherry-pick successful."
else
echo "Standard cherry-pick failed, trying with -m 1..."
git cherry-pick --abort 2>/dev/null || true

if git cherry-pick -x -s -m 1 "$MERGE_COMMIT" 2>&1; then
echo "Cherry-pick with -m 1 successful."
else
echo "::error::Cherry-pick failed due to conflicts."
git cherry-pick --abort 2>/dev/null || true

SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Merge conflicts - manual backport required |"
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"$')'
echo "::endgroup::"
continue
fi
fi

if ! git push origin "$BACKPORT_BRANCH"; then
echo "::error::Failed to push branch $BACKPORT_BRANCH"
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Failed to push branch |"
gh pr comment "$PR_NUMBER" --body $'❌ **Backport Failed**\nHi @'"$PR_AUTHOR"$', failed to push backport branch for `'"$TARGET_BRANCH"$'`.\n\n(cc @'"$TRIGGER_USER"$')'
echo "::endgroup::"
continue
fi

PR_BODY="Backport of #$PR_NUMBER to \`$TARGET_BRANCH\`."
ASSIGNEE_ARG=""
if [ "$IS_MANUAL" == "true" ]; then
PR_BODY="Manual backport of #$PR_NUMBER to \`$TARGET_BRANCH\`."
ASSIGNEE_ARG="--assignee $TRIGGER_USER"
fi

if NEW_PR_URL=$(gh pr create \
--base "$TARGET_BRANCH" \
--head "$BACKPORT_BRANCH" \
--title "[Backport $TARGET_BRANCH] $PR_TITLE_CLEAN" \
--body "$PR_BODY" \
--label "backport" \
$ASSIGNEE_ARG 2>&1); then

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"$')'

echo "Backport PR created: $NEW_PR_URL"
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ✅ Success | $NEW_PR_URL |"
BACKPORT_SUCCESS="true"
BACKPORT_COUNT=$((BACKPORT_COUNT + 1))
else
echo "::error::Failed to create backport PR."
SUMMARY_RESULTS="${SUMMARY_RESULTS}\n| \`$TARGET_BRANCH\` | ❌ Failed | Failed to create PR |"
git push origin --delete "$BACKPORT_BRANCH" 2>/dev/null || true
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"$')'
fi

echo "::endgroup::"
done < <(echo "$TARGET_BRANCHES")

# Write final summary
if [ "$BACKPORT_SUCCESS" == "true" ]; then
echo "## ✅ Backport Completed" >> $GITHUB_STEP_SUMMARY
else
echo "## ❌ Backport Failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**PR:** #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY
echo "**Title:** $PR_TITLE_CLEAN" >> $GITHUB_STEP_SUMMARY
echo "**Triggered by:** @$TRIGGER_USER" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Branch | Status | Details |" >> $GITHUB_STEP_SUMMARY
echo "|--------|--------|---------|" >> $GITHUB_STEP_SUMMARY
echo -e "$SUMMARY_RESULTS" >> $GITHUB_STEP_SUMMARY

echo "success=$BACKPORT_SUCCESS" >> $GITHUB_OUTPUT
echo "count=$BACKPORT_COUNT" >> $GITHUB_OUTPUT
echo "Backport process completed. Success: $BACKPORT_SUCCESS, Count: $BACKPORT_COUNT"

- name: Label Original PR
if: steps.execute-backport.outputs.success == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
EVENT_NAME: ${{ github.event_name }}
run: |
if [ "$EVENT_NAME" == "workflow_dispatch" ]; then
PR_NUMBER="$INPUT_PR_NUMBER"
else
PR_NUMBER="${{ github.event.pull_request.number }}"
fi
gh pr edit "$PR_NUMBER" --add-label "backported" || echo "::warning::Failed to add 'backported' label"
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Note: the problems, features, and questions mentioned here are not limited to Py

`setup.py`: Package script for PyMilvus.

## Backporting (Cherry-pick)

We use a bot to automate backporting bug fixes to branches.

**How to use:**
Simply add a label `backport-to-<branch-name>` to your Pull Request (e.g., `backport-to-2.6`).
* ✅ **Success**: The bot will create a new backport PR automatically.
* ❌ **Failure**: The bot will comment on your PR if there are conflicts or restricted files (`proto_gen/`).

If the bot fails due to conflicts, please backport manually.

## Congratulations! You are now the contributor to the Milvus community!

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!
10 changes: 10 additions & 0 deletions CONTRIBUTING_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ PyMilvus 的 Github issue 列表中,打上了 [good-first-issue](https://githu

### 通过所有 Github Actions

## Cherry-pick

使用方法: 只需为你的 Pull Request 添加 backport-to-<branch-name> 格式的标签即可(例如:backport-to-2.6)。

✅ 成功:机器人会自动创建一个新的 Backport PR。

❌ 失败:如果存在代码冲突或修改了受限文件(如 proto_gen/),机器人会在 PR 中留言提醒。

如果机器人因冲突执行失败,请手动进行 Backport 操作。

## 恭喜你!你已经成为了 Milvus 社区的贡献者!

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