From 2c3c97d4b5260f1ba0267ecb0c3f716ec7f95d31 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 12 Nov 2025 12:22:19 -0800 Subject: [PATCH] ci: fix backport workflow not cleaning up branch on failure and not able to update existing PRs/branches on re-run (#6620) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes issue in which a failed backport runs would not cleanup the branch (issue 1) and then on the next backport attempt, it would bail out early because it checks if a branch with that name already exists (issue 2). The workflow now treats existing backport branches as reusable unless an open PR already references them (issue 2 solution), force-updates any reused branch with the latest cherry-pick, and records them so a new cleanup step can delete the branch if the run fails (issue 1 solution). That prevents stranded refs from blocking future backport runs while keeping active backport PRs intact. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6620-ci-fix-backport-workflow-not-cleaning-up-branch-on-failure-and-not-able-to-update-existi-2a36d73d365081efbbcbfa75f0c1bbe7) by [Unito](https://www.unito.io) --- .github/workflows/pr-backport.yaml | 73 +++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-backport.yaml b/.github/workflows/pr-backport.yaml index 8729c5d57..5b47a4dbb 100644 --- a/.github/workflows/pr-backport.yaml +++ b/.github/workflows/pr-backport.yaml @@ -164,6 +164,7 @@ jobs: PENDING=() SKIPPED=() + REUSED=() for target in $REQUESTED_TARGETS; do SAFE_TARGET=$(echo "$target" | tr '/' '-') @@ -176,10 +177,22 @@ jobs: if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" | grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then - SKIPPED+=("$target") - else - PENDING+=("$target") + OPEN_PR=$( + gh pr list \ + --state open \ + --head "${BACKPORT_BRANCH}" \ + --json number \ + --jq 'if length > 0 then .[0].number else "" end' + ) + if [ -n "$OPEN_PR" ]; then + SKIPPED+=("${target} (PR #${OPEN_PR})") + continue + fi + + REUSED+=("$BACKPORT_BRANCH") fi + + PENDING+=("$target") done SKIPPED_JOINED="${SKIPPED[*]:-}" @@ -187,16 +200,20 @@ jobs: echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT + echo "reused-branches=${REUSED[*]:-}" >> $GITHUB_OUTPUT if [ -z "$PENDING_JOINED" ]; then echo "skip=true" >> $GITHUB_OUTPUT if [ -n "$SKIPPED_JOINED" ]; then - echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}" + echo "::warning::Backport branches exist: ${SKIPPED_JOINED}" fi else echo "skip=false" >> $GITHUB_OUTPUT if [ -n "$SKIPPED_JOINED" ]; then - echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}" + echo "::notice::Skipping backport targets: ${SKIPPED_JOINED}" + fi + if [ "${#REUSED[@]}" -gt 0 ]; then + echo "::notice::Reusing backport branches: ${REUSED[*]}" fi fi @@ -208,7 +225,12 @@ jobs: run: | FAILED="" SUCCESS="" - + + CREATED_BRANCHES_FILE="$( + mktemp "$RUNNER_TEMP/backport-branches-XXXXXX" + )" + echo "CREATED_BRANCHES_FILE=$CREATED_BRANCHES_FILE" >> "$GITHUB_ENV" + # Get PR data for manual triggers if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit) @@ -223,6 +245,12 @@ jobs: TARGET_BRANCH="${target}" SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-') BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}" + REMOTE_BACKPORT_EXISTS=false + + if git ls-remote --exit-code origin "${BACKPORT_BRANCH}" >/dev/null 2>&1; then + REMOTE_BACKPORT_EXISTS=true + echo "::notice::Updating existing branch ${BACKPORT_BRANCH}" + fi echo "::group::Backporting to ${TARGET_BRANCH}" @@ -247,7 +275,12 @@ jobs: # Try cherry-pick if git cherry-pick "${MERGE_COMMIT}"; then - git push origin "${BACKPORT_BRANCH}" + if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then + git push --force-with-lease origin "${BACKPORT_BRANCH}" + else + git push origin "${BACKPORT_BRANCH}" + fi + echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE" SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} " echo "Successfully created backport branch: ${BACKPORT_BRANCH}" # Return to main (keep the branch, we need it for PR) @@ -271,6 +304,13 @@ jobs: echo "success=${SUCCESS}" >> $GITHUB_OUTPUT echo "failed=${FAILED}" >> $GITHUB_OUTPUT + if [ -s "$CREATED_BRANCHES_FILE" ]; then + CREATED_LIST=$(paste -sd' ' "$CREATED_BRANCHES_FILE") + echo "created-branches=${CREATED_LIST}" >> $GITHUB_OUTPUT + else + echo "created-branches=" >> $GITHUB_OUTPUT + fi + if [ -n "${FAILED}" ]; then exit 1 fi @@ -348,6 +388,25 @@ jobs: fi done + - name: Cleanup stranded backport branches + if: steps.filter-targets.outputs.skip != 'true' && failure() + run: | + FILE="${CREATED_BRANCHES_FILE:-}" + + if [ -z "$FILE" ] || [ ! -f "$FILE" ]; then + echo "No backport branches recorded for cleanup" + exit 0 + fi + + while IFS= read -r branch; do + [ -z "$branch" ] && continue + printf 'Deleting branch %s\n' "${branch}" + if ! git push origin --delete "$branch"; then + echo "::warning::Failed to delete ${branch}" + fi + done < "$FILE" + + - name: Remove needs-backport label if: steps.filter-targets.outputs.skip != 'true' && success() run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"