Skip to content

Commit 7d5372f

Browse files
committed
feat: add backport ci
Signed-off-by: Ashing Zheng <[email protected]>
1 parent 6b98bdb commit 7d5372f

File tree

3 files changed

+452
-0
lines changed

3 files changed

+452
-0
lines changed

.github/scripts/backport-commit.sh

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env bash
2+
3+
# Safe backport helper. Creates a PR in the current repository that cherry-picks a commit from upstream.
4+
5+
set -euo pipefail
6+
7+
# ANSI colors for readability
8+
RED='\033[0;31m'
9+
GREEN='\033[0;32m'
10+
YELLOW='\033[1;33m'
11+
NC='\033[0m'
12+
13+
die() {
14+
echo -e "${RED}$1${NC}" >&2
15+
exit "${2:-1}"
16+
}
17+
18+
require_env() {
19+
local name="$1"
20+
local value="${!name:-}"
21+
if [[ -z "$value" ]]; then
22+
die "Environment variable $name is required"
23+
fi
24+
}
25+
26+
if [[ $# -ne 1 ]]; then
27+
die "Usage: $0 <commit-sha>"
28+
fi
29+
30+
COMMIT_SHA="$1"
31+
32+
if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then
33+
die "Invalid commit SHA: $COMMIT_SHA"
34+
fi
35+
36+
SOURCE_REPO="${SOURCE_REPO:-apache/apisix-ingress-controller}"
37+
TARGET_BRANCH="${TARGET_BRANCH:-master}"
38+
GITHUB_REPO="${GITHUB_REPOSITORY:-}"
39+
40+
require_env SOURCE_REPO
41+
require_env TARGET_BRANCH
42+
require_env GH_TOKEN
43+
44+
[[ "$SOURCE_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "Invalid SOURCE_REPO: $SOURCE_REPO"
45+
[[ "$TARGET_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Invalid TARGET_BRANCH: $TARGET_BRANCH"
46+
47+
if [[ -z "$GITHUB_REPO" ]]; then
48+
GITHUB_REPO="$(gh repo view --json nameWithOwner -q '.nameWithOwner')"
49+
fi
50+
51+
[[ "$GITHUB_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "Invalid target repo: $GITHUB_REPO"
52+
53+
echo -e "${YELLOW}Backporting commit ${COMMIT_SHA} from ${SOURCE_REPO}${NC}"
54+
55+
if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then
56+
die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script"
57+
fi
58+
59+
COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")"
60+
COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")"
61+
COMMIT_URL="https://github.com/${SOURCE_REPO}/commit/${COMMIT_SHA}"
62+
SHORT_SHA="${COMMIT_SHA:0:7}"
63+
BRANCH_NAME="backport/${SHORT_SHA}-to-${TARGET_BRANCH}"
64+
65+
[[ "$BRANCH_NAME" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Generated branch name is unsafe: $BRANCH_NAME"
66+
67+
echo -e "${YELLOW}Generated branch name: ${BRANCH_NAME}${NC}"
68+
69+
EXISTING_PR="$(gh pr list --state all --search "\"backport ${SHORT_SHA}\" in:title" --json url --jq '.[0].url' 2>/dev/null || true)"
70+
if [[ -n "$EXISTING_PR" ]]; then
71+
echo -e "${GREEN}PR already exists: ${EXISTING_PR}. Skipping duplicate.${NC}"
72+
exit 0
73+
fi
74+
75+
git fetch origin "$TARGET_BRANCH" --quiet
76+
git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"
77+
78+
if git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then
79+
git checkout "$BRANCH_NAME"
80+
git reset --hard "origin/$TARGET_BRANCH"
81+
else
82+
git checkout -b "$BRANCH_NAME"
83+
fi
84+
85+
PARENT_COUNT="$(git rev-list --parents -n 1 "$COMMIT_SHA" | awk '{print NF-1}')"
86+
HAS_CONFLICTS=false
87+
88+
echo -e "${YELLOW}Running cherry-pick...${NC}"
89+
90+
cherry_pick() {
91+
if [[ "$PARENT_COUNT" -gt 1 ]]; then
92+
git cherry-pick -x -m 1 "$COMMIT_SHA"
93+
else
94+
git cherry-pick -x "$COMMIT_SHA"
95+
fi
96+
}
97+
98+
if ! cherry_pick; then
99+
echo -e "${YELLOW}Cherry-pick reported conflicts; leaving markers for manual resolution.${NC}"
100+
HAS_CONFLICTS=true
101+
git add .
102+
git -c core.editor=true cherry-pick --continue || true
103+
fi
104+
105+
echo -e "${YELLOW}Pushing branch to origin...${NC}"
106+
if ! git push -u origin "$BRANCH_NAME"; then
107+
echo -e "${YELLOW}Push failed, trying force-with-lease...${NC}"
108+
git fetch origin "$BRANCH_NAME" || true
109+
git branch --set-upstream-to="origin/$BRANCH_NAME" "$BRANCH_NAME" || true
110+
git push -u origin "$BRANCH_NAME" --force-with-lease || {
111+
git checkout "$TARGET_BRANCH"
112+
git branch -D "$BRANCH_NAME" || true
113+
die "Unable to push branch ${BRANCH_NAME}"
114+
}
115+
fi
116+
117+
echo -e "${YELLOW}Creating pull request...${NC}"
118+
119+
if [[ "$HAS_CONFLICTS" == "true" ]]; then
120+
PR_TITLE="🔥 [CONFLICTS] backport ${SHORT_SHA} from ${SOURCE_REPO}"
121+
PR_BODY=$(cat <<EOF
122+
## ⚠️ Backport With Conflicts
123+
124+
- Upstream commit: ${COMMIT_URL}
125+
- Original title: ${COMMIT_TITLE}
126+
- Original author: ${COMMIT_AUTHOR}
127+
128+
This PR contains unresolved conflicts. Please resolve them before merging.
129+
130+
### Suggested workflow
131+
1. \`git fetch origin ${BRANCH_NAME}\`
132+
2. \`git checkout ${BRANCH_NAME}\`
133+
3. Resolve conflicts, commit, and push updates.
134+
135+
> Created automatically by backport-bot.
136+
EOF
137+
)
138+
LABEL_FLAGS=(--label backport --label automated --label needs-manual-action --label conflicts)
139+
else
140+
PR_TITLE="chore: backport ${SHORT_SHA} from ${SOURCE_REPO}"
141+
PR_BODY=$(cat <<EOF
142+
## 🔄 Automated Backport
143+
144+
- Upstream commit: ${COMMIT_URL}
145+
- Original title: ${COMMIT_TITLE}
146+
- Original author: ${COMMIT_AUTHOR}
147+
148+
Please review and run the relevant validation before merging.
149+
150+
> Created automatically by backport-bot.
151+
EOF
152+
)
153+
LABEL_FLAGS=(--label backport --label automated)
154+
fi
155+
156+
set +e
157+
PR_RESPONSE="$(gh pr create \
158+
--title "$PR_TITLE" \
159+
--body "$PR_BODY" \
160+
--head "$BRANCH_NAME" \
161+
--base "$TARGET_BRANCH" \
162+
--repo "$GITHUB_REPO" \
163+
"${LABEL_FLAGS[@]}" 2>&1)"
164+
PR_EXIT_CODE=$?
165+
set -e
166+
167+
if [[ $PR_EXIT_CODE -ne 0 ]]; then
168+
echo -e "${RED}Failed to create PR:${NC}\n${PR_RESPONSE}"
169+
if grep -q "already exists" <<<"$PR_RESPONSE"; then
170+
echo -e "${YELLOW}Detected existing PR, assuming success.${NC}"
171+
git checkout "$TARGET_BRANCH"
172+
exit 0
173+
fi
174+
git checkout "$TARGET_BRANCH"
175+
git push origin --delete "$BRANCH_NAME" || true
176+
git branch -D "$BRANCH_NAME" || true
177+
die "PR creation failed"
178+
fi
179+
180+
echo -e "${GREEN}Pull request created successfully:${NC} ${PR_RESPONSE}"
181+
182+
git checkout "$TARGET_BRANCH"
183+
184+
echo -e "${GREEN}Backport finished for ${COMMIT_SHA}.${NC}"
185+

.github/scripts/dry-run-test.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
3+
# Dry-run cherry-pick test for backport commits.
4+
5+
set -euo pipefail
6+
7+
RED='\033[0;31m'
8+
GREEN='\033[0;32m'
9+
YELLOW='\033[1;33m'
10+
NC='\033[0m'
11+
12+
die() {
13+
echo -e "${RED}$1${NC}" >&2
14+
exit "${2:-1}"
15+
}
16+
17+
if [[ $# -ne 1 ]]; then
18+
die "Usage: $0 <commit-sha>"
19+
fi
20+
21+
COMMIT_SHA="$1"
22+
23+
if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then
24+
die "Invalid commit SHA: $COMMIT_SHA"
25+
fi
26+
27+
SOURCE_REPO="${SOURCE_REPO:-apache/apisix-ingress-controller}"
28+
TARGET_BRANCH="${TARGET_BRANCH:-master}"
29+
SHORT_SHA="${COMMIT_SHA:0:7}"
30+
31+
[[ "$TARGET_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Invalid TARGET_BRANCH: $TARGET_BRANCH"
32+
33+
echo -e "${YELLOW}Running dry-run cherry-pick for ${SHORT_SHA}${NC}"
34+
35+
if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then
36+
die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script"
37+
fi
38+
39+
COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")"
40+
COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")"
41+
42+
echo -e "${YELLOW}Title: ${COMMIT_TITLE}${NC}"
43+
echo -e "${YELLOW}Author: ${COMMIT_AUTHOR}${NC}"
44+
45+
TEMP_BRANCH="dry-run-test-${SHORT_SHA}"
46+
47+
git fetch origin "$TARGET_BRANCH" --quiet
48+
git checkout "$TARGET_BRANCH" --quiet
49+
git reset --hard "origin/$TARGET_BRANCH" --quiet
50+
51+
if git rev-parse --verify "$TEMP_BRANCH" >/dev/null 2>&1; then
52+
git branch -D "$TEMP_BRANCH" --quiet
53+
fi
54+
55+
git checkout -b "$TEMP_BRANCH" --quiet
56+
57+
PARENT_COUNT="$(git rev-list --parents -n 1 "$COMMIT_SHA" | awk '{print NF-1}')"
58+
59+
echo -e "${YELLOW}Testing cherry-pick...${NC}"
60+
61+
success=false
62+
if [[ "$PARENT_COUNT" -gt 1 ]]; then
63+
echo -e "${YELLOW}Merge commit detected; using -m 1${NC}"
64+
if git cherry-pick -x -m 1 "$COMMIT_SHA" --no-commit 2>/dev/null; then
65+
success=true
66+
else
67+
git cherry-pick --abort 2>/dev/null || git reset --hard HEAD --quiet
68+
fi
69+
else
70+
if git cherry-pick -x "$COMMIT_SHA" --no-commit 2>/dev/null; then
71+
success=true
72+
else
73+
git cherry-pick --abort 2>/dev/null || git reset --hard HEAD --quiet
74+
fi
75+
fi
76+
77+
git reset --hard HEAD --quiet
78+
git checkout "$TARGET_BRANCH" --quiet
79+
git branch -D "$TEMP_BRANCH" --quiet 2>/dev/null || true
80+
81+
if [[ "$success" == "true" ]]; then
82+
echo -e "${GREEN}Dry-run successful. Commit ${SHORT_SHA} can be backported cleanly.${NC}"
83+
exit 0
84+
fi
85+
86+
echo -e "${RED}Dry-run failed. Commit ${SHORT_SHA} requires manual conflict resolution.${NC}"
87+
exit 1
88+

0 commit comments

Comments
 (0)