|
| 1 | +name: Auto Backport |
| 2 | + |
| 3 | +on: |
| 4 | + pull_request_target: |
| 5 | + types: [closed] |
| 6 | + branches: [main] |
| 7 | + |
| 8 | +jobs: |
| 9 | + backport: |
| 10 | + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport') |
| 11 | + runs-on: ubuntu-latest |
| 12 | + permissions: |
| 13 | + contents: write |
| 14 | + pull-requests: write |
| 15 | + issues: write |
| 16 | + |
| 17 | + steps: |
| 18 | + - name: Checkout repository |
| 19 | + uses: actions/checkout@v4 |
| 20 | + with: |
| 21 | + fetch-depth: 0 |
| 22 | + |
| 23 | + - name: Configure git |
| 24 | + run: | |
| 25 | + git config user.name "github-actions[bot]" |
| 26 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 27 | +
|
| 28 | + - name: Extract version labels |
| 29 | + id: versions |
| 30 | + run: | |
| 31 | + # Extract version labels (e.g., "1.24", "1.22") |
| 32 | + VERSIONS="" |
| 33 | + LABELS='${{ toJSON(github.event.pull_request.labels) }}' |
| 34 | + for label in $(echo "$LABELS" | jq -r '.[].name'); do |
| 35 | + # Match version labels like "1.24" (major.minor only) |
| 36 | + if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then |
| 37 | + # Validate the branch exists before adding to list |
| 38 | + if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then |
| 39 | + VERSIONS="${VERSIONS}${label} " |
| 40 | + else |
| 41 | + echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist" |
| 42 | + fi |
| 43 | + fi |
| 44 | + done |
| 45 | +
|
| 46 | + if [ -z "$VERSIONS" ]; then |
| 47 | + echo "::error::No version labels found (e.g., 1.24, 1.22)" |
| 48 | + exit 1 |
| 49 | + fi |
| 50 | +
|
| 51 | + echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT |
| 52 | + echo "Found version labels: ${VERSIONS}" |
| 53 | +
|
| 54 | + - name: Backport commits |
| 55 | + id: backport |
| 56 | + env: |
| 57 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 58 | + PR_TITLE: ${{ github.event.pull_request.title }} |
| 59 | + MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} |
| 60 | + run: | |
| 61 | + FAILED="" |
| 62 | + SUCCESS="" |
| 63 | +
|
| 64 | + for version in ${{ steps.versions.outputs.versions }}; do |
| 65 | + echo "::group::Backporting to core/${version}" |
| 66 | +
|
| 67 | + TARGET_BRANCH="core/${version}" |
| 68 | + BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}" |
| 69 | +
|
| 70 | + # Fetch target branch (fail if doesn't exist) |
| 71 | + if ! git fetch origin "${TARGET_BRANCH}"; then |
| 72 | + echo "::error::Target branch ${TARGET_BRANCH} does not exist" |
| 73 | + FAILED="${FAILED}${version}:branch-missing " |
| 74 | + echo "::endgroup::" |
| 75 | + continue |
| 76 | + fi |
| 77 | +
|
| 78 | + # Create backport branch |
| 79 | + git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}" |
| 80 | +
|
| 81 | + # Try cherry-pick |
| 82 | + if git cherry-pick "${MERGE_COMMIT}"; then |
| 83 | + git push origin "${BACKPORT_BRANCH}" |
| 84 | + SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} " |
| 85 | + echo "Successfully created backport branch: ${BACKPORT_BRANCH}" |
| 86 | + # Return to main (keep the branch, we need it for PR) |
| 87 | + git checkout main |
| 88 | + else |
| 89 | + # Get conflict info |
| 90 | + CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',') |
| 91 | + git cherry-pick --abort |
| 92 | +
|
| 93 | + echo "::error::Cherry-pick failed due to conflicts" |
| 94 | + FAILED="${FAILED}${version}:conflicts:${CONFLICTS} " |
| 95 | +
|
| 96 | + # Clean up the failed branch |
| 97 | + git checkout main |
| 98 | + git branch -D "${BACKPORT_BRANCH}" |
| 99 | + fi |
| 100 | +
|
| 101 | + echo "::endgroup::" |
| 102 | + done |
| 103 | +
|
| 104 | + echo "success=${SUCCESS}" >> $GITHUB_OUTPUT |
| 105 | + echo "failed=${FAILED}" >> $GITHUB_OUTPUT |
| 106 | +
|
| 107 | + if [ -n "${FAILED}" ]; then |
| 108 | + exit 1 |
| 109 | + fi |
| 110 | +
|
| 111 | + - name: Create PR for each successful backport |
| 112 | + if: steps.backport.outputs.success |
| 113 | + env: |
| 114 | + GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} |
| 115 | + run: | |
| 116 | + PR_TITLE="${{ github.event.pull_request.title }}" |
| 117 | + PR_NUMBER="${{ github.event.pull_request.number }}" |
| 118 | + PR_AUTHOR="${{ github.event.pull_request.user.login }}" |
| 119 | +
|
| 120 | + for backport in ${{ steps.backport.outputs.success }}; do |
| 121 | + IFS=':' read -r version branch <<< "${backport}" |
| 122 | +
|
| 123 | + if PR_URL=$(gh pr create \ |
| 124 | + --base "core/${version}" \ |
| 125 | + --head "${branch}" \ |
| 126 | + --title "[backport ${version}] ${PR_TITLE}" \ |
| 127 | + --body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \ |
| 128 | + --label "backport" 2>&1); then |
| 129 | +
|
| 130 | + # Extract PR number from URL |
| 131 | + PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$') |
| 132 | +
|
| 133 | + if [ -n "${PR_NUM}" ]; then |
| 134 | + gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}" |
| 135 | + fi |
| 136 | + else |
| 137 | + echo "::error::Failed to create PR for ${version}: ${PR_URL}" |
| 138 | + # Still try to comment on the original PR about the failure |
| 139 | + gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`" |
| 140 | + fi |
| 141 | + done |
| 142 | +
|
| 143 | + - name: Comment on failures |
| 144 | + if: failure() && steps.backport.outputs.failed |
| 145 | + env: |
| 146 | + GH_TOKEN: ${{ github.token }} |
| 147 | + run: | |
| 148 | + PR_NUMBER="${{ github.event.pull_request.number }}" |
| 149 | + PR_AUTHOR="${{ github.event.pull_request.user.login }}" |
| 150 | + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" |
| 151 | +
|
| 152 | + for failure in ${{ steps.backport.outputs.failed }}; do |
| 153 | + IFS=':' read -r version reason conflicts <<< "${failure}" |
| 154 | +
|
| 155 | + if [ "${reason}" = "branch-missing" ]; then |
| 156 | + gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist" |
| 157 | +
|
| 158 | + elif [ "${reason}" = "conflicts" ]; then |
| 159 | + # Convert comma-separated conflicts back to newlines for display |
| 160 | + CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /') |
| 161 | +
|
| 162 | + COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>" |
| 163 | + gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}" |
| 164 | + fi |
| 165 | + done |
0 commit comments