name: Auto Backport on: pull_request_target: types: [closed] branches: [main] jobs: backport: if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport') runs-on: ubuntu-latest permissions: contents: write pull-requests: write issues: write steps: - name: Checkout repository 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: Extract version labels id: versions run: | # Extract version labels (e.g., "1.24", "1.22") VERSIONS="" LABELS='${{ toJSON(github.event.pull_request.labels) }}' for label in $(echo "$LABELS" | jq -r '.[].name'); do # Match version labels like "1.24" (major.minor only) if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then # Validate the branch exists before adding to list if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then VERSIONS="${VERSIONS}${label} " else echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist" fi fi done if [ -z "$VERSIONS" ]; then echo "::error::No version labels found (e.g., 1.24, 1.22)" exit 1 fi echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT echo "Found version labels: ${VERSIONS}" - name: Backport commits id: backport env: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} run: | FAILED="" SUCCESS="" for version in ${{ steps.versions.outputs.versions }}; do echo "::group::Backporting to core/${version}" TARGET_BRANCH="core/${version}" BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}" # Fetch target branch (fail if doesn't exist) if ! git fetch origin "${TARGET_BRANCH}"; then echo "::error::Target branch ${TARGET_BRANCH} does not exist" FAILED="${FAILED}${version}:branch-missing " echo "::endgroup::" continue fi # Create backport branch git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}" # Try cherry-pick if git cherry-pick "${MERGE_COMMIT}"; then git push origin "${BACKPORT_BRANCH}" SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} " echo "Successfully created backport branch: ${BACKPORT_BRANCH}" # Return to main (keep the branch, we need it for PR) git checkout main else # Get conflict info CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',') git cherry-pick --abort echo "::error::Cherry-pick failed due to conflicts" FAILED="${FAILED}${version}:conflicts:${CONFLICTS} " # Clean up the failed branch git checkout main git branch -D "${BACKPORT_BRANCH}" fi echo "::endgroup::" done echo "success=${SUCCESS}" >> $GITHUB_OUTPUT echo "failed=${FAILED}" >> $GITHUB_OUTPUT if [ -n "${FAILED}" ]; then exit 1 fi - name: Create PR for each successful backport if: steps.backport.outputs.success env: GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} run: | PR_TITLE="${{ github.event.pull_request.title }}" PR_NUMBER="${{ github.event.pull_request.number }}" PR_AUTHOR="${{ github.event.pull_request.user.login }}" for backport in ${{ steps.backport.outputs.success }}; do IFS=':' read -r version branch <<< "${backport}" if PR_URL=$(gh pr create \ --base "core/${version}" \ --head "${branch}" \ --title "[backport ${version}] ${PR_TITLE}" \ --body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \ --label "backport" 2>&1); then # Extract PR number from URL PR_NUM=$(echo "${PR_URL}" | grep -o '[0-9]*$') if [ -n "${PR_NUM}" ]; then gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}" fi else echo "::error::Failed to create PR for ${version}: ${PR_URL}" # Still try to comment on the original PR about the failure 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}\`" fi done - name: Comment on failures if: failure() && steps.backport.outputs.failed env: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER="${{ github.event.pull_request.number }}" PR_AUTHOR="${{ github.event.pull_request.user.login }}" MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" for failure in ${{ steps.backport.outputs.failed }}; do IFS=':' read -r version reason conflicts <<< "${failure}" if [ "${reason}" = "branch-missing" ]; then gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist" elif [ "${reason}" = "conflicts" ]; then # Convert comma-separated conflicts back to newlines for display CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /') 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'"
Conflicting files"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"
" gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}" fi done