From 7d5372fc510bee5c6a5e8e8141d6a24bae142ff0 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 14:40:16 +0800 Subject: [PATCH 01/12] feat: add backport ci Signed-off-by: Ashing Zheng --- .github/scripts/backport-commit.sh | 185 +++++++++++++++++++++++++++++ .github/scripts/dry-run-test.sh | 88 ++++++++++++++ .github/workflows/backport.yaml | 179 ++++++++++++++++++++++++++++ 3 files changed, 452 insertions(+) create mode 100755 .github/scripts/backport-commit.sh create mode 100755 .github/scripts/dry-run-test.sh create mode 100644 .github/workflows/backport.yaml diff --git a/.github/scripts/backport-commit.sh b/.github/scripts/backport-commit.sh new file mode 100755 index 00000000..757d29a0 --- /dev/null +++ b/.github/scripts/backport-commit.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +# Safe backport helper. Creates a PR in the current repository that cherry-picks a commit from upstream. + +set -euo pipefail + +# ANSI colors for readability +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +die() { + echo -e "${RED}$1${NC}" >&2 + exit "${2:-1}" +} + +require_env() { + local name="$1" + local value="${!name:-}" + if [[ -z "$value" ]]; then + die "Environment variable $name is required" + fi +} + +if [[ $# -ne 1 ]]; then + die "Usage: $0 " +fi + +COMMIT_SHA="$1" + +if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then + die "Invalid commit SHA: $COMMIT_SHA" +fi + +SOURCE_REPO="${SOURCE_REPO:-apache/apisix-ingress-controller}" +TARGET_BRANCH="${TARGET_BRANCH:-master}" +GITHUB_REPO="${GITHUB_REPOSITORY:-}" + +require_env SOURCE_REPO +require_env TARGET_BRANCH +require_env GH_TOKEN + +[[ "$SOURCE_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "Invalid SOURCE_REPO: $SOURCE_REPO" +[[ "$TARGET_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Invalid TARGET_BRANCH: $TARGET_BRANCH" + +if [[ -z "$GITHUB_REPO" ]]; then + GITHUB_REPO="$(gh repo view --json nameWithOwner -q '.nameWithOwner')" +fi + +[[ "$GITHUB_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "Invalid target repo: $GITHUB_REPO" + +echo -e "${YELLOW}Backporting commit ${COMMIT_SHA} from ${SOURCE_REPO}${NC}" + +if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then + die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script" +fi + +COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")" +COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")" +COMMIT_URL="https://github.com/${SOURCE_REPO}/commit/${COMMIT_SHA}" +SHORT_SHA="${COMMIT_SHA:0:7}" +BRANCH_NAME="backport/${SHORT_SHA}-to-${TARGET_BRANCH}" + +[[ "$BRANCH_NAME" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Generated branch name is unsafe: $BRANCH_NAME" + +echo -e "${YELLOW}Generated branch name: ${BRANCH_NAME}${NC}" + +EXISTING_PR="$(gh pr list --state all --search "\"backport ${SHORT_SHA}\" in:title" --json url --jq '.[0].url' 2>/dev/null || true)" +if [[ -n "$EXISTING_PR" ]]; then + echo -e "${GREEN}PR already exists: ${EXISTING_PR}. Skipping duplicate.${NC}" + exit 0 +fi + +git fetch origin "$TARGET_BRANCH" --quiet +git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH" + +if git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then + git checkout "$BRANCH_NAME" + git reset --hard "origin/$TARGET_BRANCH" +else + git checkout -b "$BRANCH_NAME" +fi + +PARENT_COUNT="$(git rev-list --parents -n 1 "$COMMIT_SHA" | awk '{print NF-1}')" +HAS_CONFLICTS=false + +echo -e "${YELLOW}Running cherry-pick...${NC}" + +cherry_pick() { + if [[ "$PARENT_COUNT" -gt 1 ]]; then + git cherry-pick -x -m 1 "$COMMIT_SHA" + else + git cherry-pick -x "$COMMIT_SHA" + fi +} + +if ! cherry_pick; then + echo -e "${YELLOW}Cherry-pick reported conflicts; leaving markers for manual resolution.${NC}" + HAS_CONFLICTS=true + git add . + git -c core.editor=true cherry-pick --continue || true +fi + +echo -e "${YELLOW}Pushing branch to origin...${NC}" +if ! git push -u origin "$BRANCH_NAME"; then + echo -e "${YELLOW}Push failed, trying force-with-lease...${NC}" + git fetch origin "$BRANCH_NAME" || true + git branch --set-upstream-to="origin/$BRANCH_NAME" "$BRANCH_NAME" || true + git push -u origin "$BRANCH_NAME" --force-with-lease || { + git checkout "$TARGET_BRANCH" + git branch -D "$BRANCH_NAME" || true + die "Unable to push branch ${BRANCH_NAME}" + } +fi + +echo -e "${YELLOW}Creating pull request...${NC}" + +if [[ "$HAS_CONFLICTS" == "true" ]]; then + PR_TITLE="🔥 [CONFLICTS] backport ${SHORT_SHA} from ${SOURCE_REPO}" + PR_BODY=$(cat < Created automatically by backport-bot. +EOF +) + LABEL_FLAGS=(--label backport --label automated --label needs-manual-action --label conflicts) +else + PR_TITLE="chore: backport ${SHORT_SHA} from ${SOURCE_REPO}" + PR_BODY=$(cat < Created automatically by backport-bot. +EOF +) + LABEL_FLAGS=(--label backport --label automated) +fi + +set +e +PR_RESPONSE="$(gh pr create \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --head "$BRANCH_NAME" \ + --base "$TARGET_BRANCH" \ + --repo "$GITHUB_REPO" \ + "${LABEL_FLAGS[@]}" 2>&1)" +PR_EXIT_CODE=$? +set -e + +if [[ $PR_EXIT_CODE -ne 0 ]]; then + echo -e "${RED}Failed to create PR:${NC}\n${PR_RESPONSE}" + if grep -q "already exists" <<<"$PR_RESPONSE"; then + echo -e "${YELLOW}Detected existing PR, assuming success.${NC}" + git checkout "$TARGET_BRANCH" + exit 0 + fi + git checkout "$TARGET_BRANCH" + git push origin --delete "$BRANCH_NAME" || true + git branch -D "$BRANCH_NAME" || true + die "PR creation failed" +fi + +echo -e "${GREEN}Pull request created successfully:${NC} ${PR_RESPONSE}" + +git checkout "$TARGET_BRANCH" + +echo -e "${GREEN}Backport finished for ${COMMIT_SHA}.${NC}" + diff --git a/.github/scripts/dry-run-test.sh b/.github/scripts/dry-run-test.sh new file mode 100755 index 00000000..2827f622 --- /dev/null +++ b/.github/scripts/dry-run-test.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Dry-run cherry-pick test for backport commits. + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +die() { + echo -e "${RED}$1${NC}" >&2 + exit "${2:-1}" +} + +if [[ $# -ne 1 ]]; then + die "Usage: $0 " +fi + +COMMIT_SHA="$1" + +if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then + die "Invalid commit SHA: $COMMIT_SHA" +fi + +SOURCE_REPO="${SOURCE_REPO:-apache/apisix-ingress-controller}" +TARGET_BRANCH="${TARGET_BRANCH:-master}" +SHORT_SHA="${COMMIT_SHA:0:7}" + +[[ "$TARGET_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Invalid TARGET_BRANCH: $TARGET_BRANCH" + +echo -e "${YELLOW}Running dry-run cherry-pick for ${SHORT_SHA}${NC}" + +if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then + die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script" +fi + +COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")" +COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")" + +echo -e "${YELLOW}Title: ${COMMIT_TITLE}${NC}" +echo -e "${YELLOW}Author: ${COMMIT_AUTHOR}${NC}" + +TEMP_BRANCH="dry-run-test-${SHORT_SHA}" + +git fetch origin "$TARGET_BRANCH" --quiet +git checkout "$TARGET_BRANCH" --quiet +git reset --hard "origin/$TARGET_BRANCH" --quiet + +if git rev-parse --verify "$TEMP_BRANCH" >/dev/null 2>&1; then + git branch -D "$TEMP_BRANCH" --quiet +fi + +git checkout -b "$TEMP_BRANCH" --quiet + +PARENT_COUNT="$(git rev-list --parents -n 1 "$COMMIT_SHA" | awk '{print NF-1}')" + +echo -e "${YELLOW}Testing cherry-pick...${NC}" + +success=false +if [[ "$PARENT_COUNT" -gt 1 ]]; then + echo -e "${YELLOW}Merge commit detected; using -m 1${NC}" + if git cherry-pick -x -m 1 "$COMMIT_SHA" --no-commit 2>/dev/null; then + success=true + else + git cherry-pick --abort 2>/dev/null || git reset --hard HEAD --quiet + fi +else + if git cherry-pick -x "$COMMIT_SHA" --no-commit 2>/dev/null; then + success=true + else + git cherry-pick --abort 2>/dev/null || git reset --hard HEAD --quiet + fi +fi + +git reset --hard HEAD --quiet +git checkout "$TARGET_BRANCH" --quiet +git branch -D "$TEMP_BRANCH" --quiet 2>/dev/null || true + +if [[ "$success" == "true" ]]; then + echo -e "${GREEN}Dry-run successful. Commit ${SHORT_SHA} can be backported cleanly.${NC}" + exit 0 +fi + +echo -e "${RED}Dry-run failed. Commit ${SHORT_SHA} requires manual conflict resolution.${NC}" +exit 1 + diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml new file mode 100644 index 00000000..9d7f872a --- /dev/null +++ b/.github/workflows/backport.yaml @@ -0,0 +1,179 @@ +name: Auto Backport from Upstream + +on: + # schedule: + # - cron: "*/30 * * * *" + push: + branches: + - feat/add_backport_ci + workflow_dispatch: + inputs: + force_sync: + description: "Force sync all recent commits (ignores watermark)" + required: false + default: "false" + dry_run: + description: "List planned actions only (no pushes or PRs)" + required: false + default: "false" + +concurrency: + group: auto-backport + cancel-in-progress: false + +env: + SOURCE_REPO: apache/apisix-ingress-controller + SOURCE_BRANCH: master + TARGET_BRANCH: ${{ github.event.repository.default_branch || 'master' }} + MAX_COMMITS_PER_RUN: 5 + +jobs: + auto-backport: + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + actions: write + contents: write + pull-requests: write + issues: write + repository-projects: write + env: + GH_TOKEN: ${{ secrets.BACKPORT_PAT }} + GITHUB_REPOSITORY: ${{ github.repository }} + DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} + FORCE_SYNC: ${{ github.event.inputs.force_sync || 'false' }} + + steps: + - name: Checkout target repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.BACKPORT_PAT }} + + - name: Show run configuration + run: | + echo "Dry run: $DRY_RUN" + echo "Force sync: $FORCE_SYNC" + echo "Source repo: $SOURCE_REPO" + echo "Source branch: $SOURCE_BRANCH" + echo "Target branch: $TARGET_BRANCH" + + - name: Configure git identity + run: | + git config --global user.name "backport-bot[bot]" + git config --global user.email "backport-bot[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: | + git remote add upstream "https://github.com/${SOURCE_REPO}.git" 2>/dev/null || true + git remote set-url upstream "https://github.com/${SOURCE_REPO}.git" + + - name: Fetch upstream branch + run: | + git fetch upstream "${SOURCE_BRANCH}" --prune --tags + + - name: Read last processed commit watermark + id: watermark + run: | + LAST_SHA=$(gh variable get LAST_BACKPORT_SHA -R "${GITHUB_REPOSITORY}" --json value --jq '.value' 2>/dev/null || echo "") + if [[ -z "$LAST_SHA" || "$FORCE_SYNC" == "true" ]]; then + LAST_SHA=$(git log "upstream/${SOURCE_BRANCH}" --since="7 days ago" --format="%H" | tail -n 1) + fi + echo "last_sha=${LAST_SHA}" >> "$GITHUB_OUTPUT" + echo "Last processed SHA: ${LAST_SHA:-}" + + - name: Collect new commits + id: collect_commits + run: | + LAST_SHA="${{ steps.watermark.outputs.last_sha }}" + if [[ -n "$LAST_SHA" ]]; then + COMMITS=$(git log "upstream/${SOURCE_BRANCH}" ^"$LAST_SHA" --format="%H" --reverse | head -"${MAX_COMMITS_PER_RUN}") + else + COMMITS=$(git log "upstream/${SOURCE_BRANCH}" -1 --format="%H") + fi + echo "commits<<'EOF'" >> "$GITHUB_OUTPUT" + echo "$COMMITS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + if [[ -z "$COMMITS" ]]; then + COUNT=0 + else + COUNT=$(echo "$COMMITS" | wc -l | tr -d '[:space:]') + fi + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "Commits to process: ${COUNT}" + + - name: Ensure labels exist + if: env.DRY_RUN != 'true' + run: | + gh label create backport --color EDEDED --description "Automated backport" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true + gh label create automated --color EDEDED --description "Created by automation" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true + gh label create backport-failed --color D73A4A --description "Backport failed" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true + gh label create needs-manual-action --color FBCA04 --description "Manual intervention required" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true + gh label create conflicts --color D93F0B --description "Contains merge conflicts" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true + + - name: Process commits (dry run) + if: env.DRY_RUN == 'true' && steps.collect_commits.outputs.count != '0' + run: | + chmod +x .github/scripts/dry-run-test.sh + SUCCESS=0 + FAILURE=0 + LAST_PROCESSED="" + while IFS= read -r COMMIT; do + [[ -z "$COMMIT" ]] && continue + if .github/scripts/dry-run-test.sh "$COMMIT"; then + SUCCESS=$((SUCCESS + 1)) + LAST_PROCESSED="$COMMIT" + else + FAILURE=$((FAILURE + 1)) + fi + done <<< "${{ steps.collect_commits.outputs.commits }}" + echo "SUCCESS_COUNT=$SUCCESS" >> "$GITHUB_ENV" + echo "FAILURE_COUNT=$FAILURE" >> "$GITHUB_ENV" + echo "LAST_PROCESSED_SHA=$LAST_PROCESSED" >> "$GITHUB_ENV" + + - name: Process commits + if: env.DRY_RUN != 'true' && steps.collect_commits.outputs.count != '0' + env: + GH_TOKEN: ${{ github.token }} + run: | + chmod +x .github/scripts/backport-commit.sh + SUCCESS=0 + FAILURE=0 + LAST_PROCESSED="" + while IFS= read -r COMMIT; do + [[ -z "$COMMIT" ]] && continue + if .github/scripts/backport-commit.sh "$COMMIT"; then + SUCCESS=$((SUCCESS + 1)) + LAST_PROCESSED="$COMMIT" + else + echo "Commit ${COMMIT} failed to backport" + FAILURE=$((FAILURE + 1)) + fi + done <<< "${{ steps.collect_commits.outputs.commits }}" + echo "SUCCESS_COUNT=$SUCCESS" >> "$GITHUB_ENV" + echo "FAILURE_COUNT=$FAILURE" >> "$GITHUB_ENV" + echo "LAST_PROCESSED_SHA=$LAST_PROCESSED" >> "$GITHUB_ENV" + + - name: Update watermark + if: env.DRY_RUN != 'true' && env.LAST_PROCESSED_SHA != '' + run: | + if [[ "${FAILURE_COUNT:-0}" == "0" ]]; then + gh variable set LAST_BACKPORT_SHA -b "${LAST_PROCESSED_SHA}" -R "${GITHUB_REPOSITORY}" + else + echo "Failures detected; watermark will not be updated." + fi + + - name: Summary + run: | + echo "Successful cherry-picks: ${SUCCESS_COUNT:-0}" + echo "Failed cherry-picks: ${FAILURE_COUNT:-0}" + echo "Last processed SHA: ${LAST_PROCESSED_SHA:-none}" + { + echo "# Backport Summary" + echo + echo "- Successful: ${SUCCESS_COUNT:-0}" + echo "- Failed: ${FAILURE_COUNT:-0}" + echo "- Last processed: ${LAST_PROCESSED_SHA:-none}" + echo "- Dry run: ${DRY_RUN}" + echo "- Force sync: ${FORCE_SYNC}" + } >> "$GITHUB_STEP_SUMMARY" From 832ba8bb44e756d284bcfd1362c58e567cdca5d6 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 14:46:34 +0800 Subject: [PATCH 02/12] fix: r Signed-off-by: Ashing Zheng --- .github/workflows/backport.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 9d7f872a..c3bd1918 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -70,7 +70,7 @@ jobs: - name: Fetch upstream branch run: | - git fetch upstream "${SOURCE_BRANCH}" --prune --tags + git fetch --prune --no-tags upstream "${SOURCE_BRANCH}" - name: Read last processed commit watermark id: watermark From c8c87bb3fed30ce0e027d9e1a206d3fc6865e0bd Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 14:50:55 +0800 Subject: [PATCH 03/12] fix: r Signed-off-by: Ashing Zheng --- .github/workflows/backport.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index c3bd1918..972d0323 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -91,13 +91,15 @@ jobs: else COMMITS=$(git log "upstream/${SOURCE_BRANCH}" -1 --format="%H") fi - echo "commits<<'EOF'" >> "$GITHUB_OUTPUT" - echo "$COMMITS" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + { + echo "commits<> "$GITHUB_OUTPUT" if [[ -z "$COMMITS" ]]; then COUNT=0 else - COUNT=$(echo "$COMMITS" | wc -l | tr -d '[:space:]') + COUNT=$(printf '%s\n' "$COMMITS" | grep -c '[0-9a-f]') fi echo "count=${COUNT}" >> "$GITHUB_OUTPUT" echo "Commits to process: ${COUNT}" From 235683b2006d0e1a79b3fa148b343626d53851d0 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 15:03:10 +0800 Subject: [PATCH 04/12] fix: r Signed-off-by: Ashing Zheng --- .github/scripts/backport-commit.sh | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/scripts/backport-commit.sh b/.github/scripts/backport-commit.sh index 757d29a0..cdf7fb7b 100755 --- a/.github/scripts/backport-commit.sh +++ b/.github/scripts/backport-commit.sh @@ -52,6 +52,22 @@ fi echo -e "${YELLOW}Backporting commit ${COMMIT_SHA} from ${SOURCE_REPO}${NC}" +ORIGINAL_REF="" +ORIGINAL_COMMIT="" +if ORIGINAL_REF=$(git symbolic-ref --quiet HEAD 2>/dev/null); then + ORIGINAL_REF=${ORIGINAL_REF#refs/heads/} +else + ORIGINAL_COMMIT=$(git rev-parse HEAD) +fi + +restore_original_ref() { + if [[ -n "$ORIGINAL_REF" ]]; then + git checkout "$ORIGINAL_REF" >/dev/null 2>&1 || true + elif [[ -n "$ORIGINAL_COMMIT" ]]; then + git checkout --detach "$ORIGINAL_COMMIT" >/dev/null 2>&1 || true + fi +} + if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script" fi @@ -110,6 +126,7 @@ if ! git push -u origin "$BRANCH_NAME"; then git push -u origin "$BRANCH_NAME" --force-with-lease || { git checkout "$TARGET_BRANCH" git branch -D "$BRANCH_NAME" || true + restore_original_ref die "Unable to push branch ${BRANCH_NAME}" } fi @@ -169,17 +186,18 @@ if [[ $PR_EXIT_CODE -ne 0 ]]; then if grep -q "already exists" <<<"$PR_RESPONSE"; then echo -e "${YELLOW}Detected existing PR, assuming success.${NC}" git checkout "$TARGET_BRANCH" + restore_original_ref exit 0 fi git checkout "$TARGET_BRANCH" git push origin --delete "$BRANCH_NAME" || true git branch -D "$BRANCH_NAME" || true + restore_original_ref die "PR creation failed" fi echo -e "${GREEN}Pull request created successfully:${NC} ${PR_RESPONSE}" -git checkout "$TARGET_BRANCH" +restore_original_ref echo -e "${GREEN}Backport finished for ${COMMIT_SHA}.${NC}" - From 4ddde8289e3a936f84dc77c9a1300fcefe07d779 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 15:09:24 +0800 Subject: [PATCH 05/12] fix: r Signed-off-by: Ashing Zheng --- .github/scripts/dry-run-test.sh | 88 --------------------------------- .github/workflows/backport.yaml | 32 +----------- 2 files changed, 2 insertions(+), 118 deletions(-) delete mode 100755 .github/scripts/dry-run-test.sh diff --git a/.github/scripts/dry-run-test.sh b/.github/scripts/dry-run-test.sh deleted file mode 100755 index 2827f622..00000000 --- a/.github/scripts/dry-run-test.sh +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env bash - -# Dry-run cherry-pick test for backport commits. - -set -euo pipefail - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -die() { - echo -e "${RED}$1${NC}" >&2 - exit "${2:-1}" -} - -if [[ $# -ne 1 ]]; then - die "Usage: $0 " -fi - -COMMIT_SHA="$1" - -if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then - die "Invalid commit SHA: $COMMIT_SHA" -fi - -SOURCE_REPO="${SOURCE_REPO:-apache/apisix-ingress-controller}" -TARGET_BRANCH="${TARGET_BRANCH:-master}" -SHORT_SHA="${COMMIT_SHA:0:7}" - -[[ "$TARGET_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Invalid TARGET_BRANCH: $TARGET_BRANCH" - -echo -e "${YELLOW}Running dry-run cherry-pick for ${SHORT_SHA}${NC}" - -if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then - die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script" -fi - -COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")" -COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")" - -echo -e "${YELLOW}Title: ${COMMIT_TITLE}${NC}" -echo -e "${YELLOW}Author: ${COMMIT_AUTHOR}${NC}" - -TEMP_BRANCH="dry-run-test-${SHORT_SHA}" - -git fetch origin "$TARGET_BRANCH" --quiet -git checkout "$TARGET_BRANCH" --quiet -git reset --hard "origin/$TARGET_BRANCH" --quiet - -if git rev-parse --verify "$TEMP_BRANCH" >/dev/null 2>&1; then - git branch -D "$TEMP_BRANCH" --quiet -fi - -git checkout -b "$TEMP_BRANCH" --quiet - -PARENT_COUNT="$(git rev-list --parents -n 1 "$COMMIT_SHA" | awk '{print NF-1}')" - -echo -e "${YELLOW}Testing cherry-pick...${NC}" - -success=false -if [[ "$PARENT_COUNT" -gt 1 ]]; then - echo -e "${YELLOW}Merge commit detected; using -m 1${NC}" - if git cherry-pick -x -m 1 "$COMMIT_SHA" --no-commit 2>/dev/null; then - success=true - else - git cherry-pick --abort 2>/dev/null || git reset --hard HEAD --quiet - fi -else - if git cherry-pick -x "$COMMIT_SHA" --no-commit 2>/dev/null; then - success=true - else - git cherry-pick --abort 2>/dev/null || git reset --hard HEAD --quiet - fi -fi - -git reset --hard HEAD --quiet -git checkout "$TARGET_BRANCH" --quiet -git branch -D "$TEMP_BRANCH" --quiet 2>/dev/null || true - -if [[ "$success" == "true" ]]; then - echo -e "${GREEN}Dry-run successful. Commit ${SHORT_SHA} can be backported cleanly.${NC}" - exit 0 -fi - -echo -e "${RED}Dry-run failed. Commit ${SHORT_SHA} requires manual conflict resolution.${NC}" -exit 1 - diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 972d0323..fef3af08 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -12,10 +12,6 @@ on: description: "Force sync all recent commits (ignores watermark)" required: false default: "false" - dry_run: - description: "List planned actions only (no pushes or PRs)" - required: false - default: "false" concurrency: group: auto-backport @@ -40,7 +36,6 @@ jobs: env: GH_TOKEN: ${{ secrets.BACKPORT_PAT }} GITHUB_REPOSITORY: ${{ github.repository }} - DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} FORCE_SYNC: ${{ github.event.inputs.force_sync || 'false' }} steps: @@ -52,7 +47,6 @@ jobs: - name: Show run configuration run: | - echo "Dry run: $DRY_RUN" echo "Force sync: $FORCE_SYNC" echo "Source repo: $SOURCE_REPO" echo "Source branch: $SOURCE_BRANCH" @@ -105,7 +99,6 @@ jobs: echo "Commits to process: ${COUNT}" - name: Ensure labels exist - if: env.DRY_RUN != 'true' run: | gh label create backport --color EDEDED --description "Automated backport" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true gh label create automated --color EDEDED --description "Created by automation" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true @@ -113,28 +106,8 @@ jobs: gh label create needs-manual-action --color FBCA04 --description "Manual intervention required" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true gh label create conflicts --color D93F0B --description "Contains merge conflicts" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true - - name: Process commits (dry run) - if: env.DRY_RUN == 'true' && steps.collect_commits.outputs.count != '0' - run: | - chmod +x .github/scripts/dry-run-test.sh - SUCCESS=0 - FAILURE=0 - LAST_PROCESSED="" - while IFS= read -r COMMIT; do - [[ -z "$COMMIT" ]] && continue - if .github/scripts/dry-run-test.sh "$COMMIT"; then - SUCCESS=$((SUCCESS + 1)) - LAST_PROCESSED="$COMMIT" - else - FAILURE=$((FAILURE + 1)) - fi - done <<< "${{ steps.collect_commits.outputs.commits }}" - echo "SUCCESS_COUNT=$SUCCESS" >> "$GITHUB_ENV" - echo "FAILURE_COUNT=$FAILURE" >> "$GITHUB_ENV" - echo "LAST_PROCESSED_SHA=$LAST_PROCESSED" >> "$GITHUB_ENV" - - name: Process commits - if: env.DRY_RUN != 'true' && steps.collect_commits.outputs.count != '0' + if: steps.collect_commits.outputs.count != '0' env: GH_TOKEN: ${{ github.token }} run: | @@ -157,7 +130,7 @@ jobs: echo "LAST_PROCESSED_SHA=$LAST_PROCESSED" >> "$GITHUB_ENV" - name: Update watermark - if: env.DRY_RUN != 'true' && env.LAST_PROCESSED_SHA != '' + if: env.LAST_PROCESSED_SHA != '' run: | if [[ "${FAILURE_COUNT:-0}" == "0" ]]; then gh variable set LAST_BACKPORT_SHA -b "${LAST_PROCESSED_SHA}" -R "${GITHUB_REPOSITORY}" @@ -176,6 +149,5 @@ jobs: echo "- Successful: ${SUCCESS_COUNT:-0}" echo "- Failed: ${FAILURE_COUNT:-0}" echo "- Last processed: ${LAST_PROCESSED_SHA:-none}" - echo "- Dry run: ${DRY_RUN}" echo "- Force sync: ${FORCE_SYNC}" } >> "$GITHUB_STEP_SUMMARY" From 34bcc4e5d065b34c277401ef2b64900383fa637e Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 16:20:02 +0800 Subject: [PATCH 06/12] fix: r Signed-off-by: Ashing Zheng --- .github/scripts/backport-commit.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/scripts/backport-commit.sh b/.github/scripts/backport-commit.sh index cdf7fb7b..1c1dbc35 100755 --- a/.github/scripts/backport-commit.sh +++ b/.github/scripts/backport-commit.sh @@ -76,13 +76,21 @@ COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")" COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")" COMMIT_URL="https://github.com/${SOURCE_REPO}/commit/${COMMIT_SHA}" SHORT_SHA="${COMMIT_SHA:0:7}" +if [[ -z "$COMMIT_TITLE" ]]; then + COMMIT_TITLE="Backport ${SHORT_SHA} from ${SOURCE_REPO}" +fi +TITLE_SUFFIX=" (${SHORT_SHA})" +if [[ "$COMMIT_TITLE" == *"$SHORT_SHA"* ]]; then + TITLE_SUFFIX="" +fi BRANCH_NAME="backport/${SHORT_SHA}-to-${TARGET_BRANCH}" [[ "$BRANCH_NAME" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Generated branch name is unsafe: $BRANCH_NAME" echo -e "${YELLOW}Generated branch name: ${BRANCH_NAME}${NC}" -EXISTING_PR="$(gh pr list --state all --search "\"backport ${SHORT_SHA}\" in:title" --json url --jq '.[0].url' 2>/dev/null || true)" +SEARCH_QUERY="${COMMIT_URL} in:body" +EXISTING_PR="$(gh pr list --state all --search "$SEARCH_QUERY" --json url --jq '.[0].url' 2>/dev/null || true)" if [[ -n "$EXISTING_PR" ]]; then echo -e "${GREEN}PR already exists: ${EXISTING_PR}. Skipping duplicate.${NC}" exit 0 @@ -134,7 +142,7 @@ fi echo -e "${YELLOW}Creating pull request...${NC}" if [[ "$HAS_CONFLICTS" == "true" ]]; then - PR_TITLE="🔥 [CONFLICTS] backport ${SHORT_SHA} from ${SOURCE_REPO}" + PR_TITLE="conflict: ${COMMIT_TITLE}${TITLE_SUFFIX}" PR_BODY=$(cat < Date: Fri, 17 Oct 2025 16:28:53 +0800 Subject: [PATCH 07/12] fix: r Signed-off-by: Ashing Zheng --- .github/scripts/backport-commit.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/scripts/backport-commit.sh b/.github/scripts/backport-commit.sh index 1c1dbc35..07173c11 100755 --- a/.github/scripts/backport-commit.sh +++ b/.github/scripts/backport-commit.sh @@ -89,8 +89,7 @@ BRANCH_NAME="backport/${SHORT_SHA}-to-${TARGET_BRANCH}" echo -e "${YELLOW}Generated branch name: ${BRANCH_NAME}${NC}" -SEARCH_QUERY="${COMMIT_URL} in:body" -EXISTING_PR="$(gh pr list --state all --search "$SEARCH_QUERY" --json url --jq '.[0].url' 2>/dev/null || true)" +EXISTING_PR="$(gh pr list --state all --head "$BRANCH_NAME" --json url --jq '.[0].url' 2>/dev/null || true)" if [[ -n "$EXISTING_PR" ]]; then echo -e "${GREEN}PR already exists: ${EXISTING_PR}. Skipping duplicate.${NC}" exit 0 @@ -144,6 +143,8 @@ echo -e "${YELLOW}Creating pull request...${NC}" if [[ "$HAS_CONFLICTS" == "true" ]]; then PR_TITLE="conflict: ${COMMIT_TITLE}${TITLE_SUFFIX}" PR_BODY=$(cat < + ## ⚠️ Backport With Conflicts - Upstream commit: ${COMMIT_URL} @@ -164,6 +165,8 @@ EOF else PR_TITLE="${COMMIT_TITLE}${TITLE_SUFFIX}" PR_BODY=$(cat < + ## 🔄 Automated Backport - Upstream commit: ${COMMIT_URL} From 055adc1b4661237b72644d31fae5456c73c0414e Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 16:34:54 +0800 Subject: [PATCH 08/12] fix: r Signed-off-by: Ashing Zheng --- .github/workflows/backport.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index fef3af08..0417b18b 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,11 +1,8 @@ name: Auto Backport from Upstream on: - # schedule: - # - cron: "*/30 * * * *" - push: - branches: - - feat/add_backport_ci + schedule: + - cron: "*/30 * * * *" workflow_dispatch: inputs: force_sync: From be7f08d1317dd07fbccf2bb6712f2b0178fceca6 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 17:01:15 +0800 Subject: [PATCH 09/12] Revert "fix: r" This reverts commit 055adc1b4661237b72644d31fae5456c73c0414e. --- .github/workflows/backport.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 0417b18b..fef3af08 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,8 +1,11 @@ name: Auto Backport from Upstream on: - schedule: - - cron: "*/30 * * * *" + # schedule: + # - cron: "*/30 * * * *" + push: + branches: + - feat/add_backport_ci workflow_dispatch: inputs: force_sync: From 54327c5efe8688e6e2d9d4a59239b4c012e40856 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 17:20:51 +0800 Subject: [PATCH 10/12] Revert "Revert "fix: r"" This reverts commit be7f08d1317dd07fbccf2bb6712f2b0178fceca6. --- .github/workflows/backport.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index fef3af08..0417b18b 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,11 +1,8 @@ name: Auto Backport from Upstream on: - # schedule: - # - cron: "*/30 * * * *" - push: - branches: - - feat/add_backport_ci + schedule: + - cron: "*/30 * * * *" workflow_dispatch: inputs: force_sync: From 18230b4403c61099b601b9f7eedebc2afeb26e90 Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 17:33:36 +0800 Subject: [PATCH 11/12] fix: test github token Signed-off-by: Ashing Zheng --- .github/workflows/backport.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 0417b18b..e163fdb9 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,8 +1,11 @@ name: Auto Backport from Upstream on: - schedule: - - cron: "*/30 * * * *" + # schedule: + # - cron: "*/30 * * * *" + push: + branches: + - feat/add_backport_ci workflow_dispatch: inputs: force_sync: @@ -105,8 +108,8 @@ jobs: - name: Process commits if: steps.collect_commits.outputs.count != '0' - env: - GH_TOKEN: ${{ github.token }} + # env: + # GH_TOKEN: ${{ github.token }} run: | chmod +x .github/scripts/backport-commit.sh SUCCESS=0 From 2255f549edf473e289bcab965d5c7e6a84dc9ade Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Fri, 17 Oct 2025 17:37:23 +0800 Subject: [PATCH 12/12] Revert "fix: test github token" This reverts commit 18230b4403c61099b601b9f7eedebc2afeb26e90. --- .github/workflows/backport.yaml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index e163fdb9..0417b18b 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -1,11 +1,8 @@ name: Auto Backport from Upstream on: - # schedule: - # - cron: "*/30 * * * *" - push: - branches: - - feat/add_backport_ci + schedule: + - cron: "*/30 * * * *" workflow_dispatch: inputs: force_sync: @@ -108,8 +105,8 @@ jobs: - name: Process commits if: steps.collect_commits.outputs.count != '0' - # env: - # GH_TOKEN: ${{ github.token }} + env: + GH_TOKEN: ${{ github.token }} run: | chmod +x .github/scripts/backport-commit.sh SUCCESS=0