From c3122f5adb97947a741870ac2ff4a387d73e2530 Mon Sep 17 00:00:00 2001 From: atif09 Date: Wed, 11 Feb 2026 22:39:21 +0530 Subject: [PATCH 1/8] [feature] Add reusable backport workflow #501 Automates cherry-picking fixes to stable branches via [backport X.Y] in commit messages or /backport X.Y comments, with conflict notification. Closes #501 --- .github/workflows/backport.yml | 31 +++++++ .github/workflows/reusable-backport.yml | 102 ++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 .github/workflows/backport.yml create mode 100644 .github/workflows/reusable-backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000..ac028e6a --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,31 @@ +name: Backport fixes to stable branch + +on: + push: + branches: + - master + issue_comment: + types: [created] + +permissions: + contents: write + pull-requests: write + +jobs: + backport-on-push: + if: github.event_name == 'push' + uses: ./.github/workflows/reusable-backport.yml + with: + commit_sha: ${{ github.sha }} + + backport-on-comment: + if: > + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.state == 'closed' && + contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && + startsWith(github.event.comment.body, '/backport') + uses: ./.github/workflows/reusable-backport.yml + with: + pr_number: ${{ github.event.issue.number }} + comment_body: ${{ github.event.comment.body }} diff --git a/.github/workflows/reusable-backport.yml b/.github/workflows/reusable-backport.yml new file mode 100644 index 00000000..f5ed2b3d --- /dev/null +++ b/.github/workflows/reusable-backport.yml @@ -0,0 +1,102 @@ +name: Backport fixes to stable branch + +on: + workflow_call: + inputs: + commit_sha: + required: false + type: string + default: "" + pr_number: + required: false + type: number + default: 0 + comment_body: + required: false + type: string + default: "" + +jobs: + parse: + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.extract.outputs.branches }} + sha: ${{ steps.extract.outputs.sha }} + steps: + - name: Extract backport targets + id: extract + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ -n "${{ inputs.commit_sha }}" ]; then + SHA="${{ inputs.commit_sha }}" + COMMIT_MSG=$(gh api repos/${{ github.repository }}/git/commits/$SHA --jq '.message') + BRANCHES=$(echo "$COMMIT_MSG" | grep -oP '\[backport\s+\K[^\]]+' | tr '\n' ',' | sed 's/,$//') + else + SHA=$(gh pr view ${{ inputs.pr_number }} --repo ${{ github.repository }} --json mergeCommit --jq '.mergeCommit.oid') + BRANCHES=$(echo "${{ inputs.comment_body }}" | grep -oP '/backport\s+\K\S+' | tr '\n' ',' | sed 's/,$//') + fi + + if [ -z "$BRANCHES" ]; then + echo "branches=[]" >> $GITHUB_OUTPUT + else + echo "branches=$(echo "$BRANCHES" | tr ',' '\n' | jq -R . | jq -sc .)" >> $GITHUB_OUTPUT + fi + echo "sha=$SHA" >> $GITHUB_OUTPUT + + backport: + needs: parse + if: needs.parse.outputs.branches != '[]' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: ${{ fromJSON(needs.parse.outputs.branches) }} + steps: + - uses: actions/checkout@v6 + 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: Cherry-pick and create PR + env: + GH_TOKEN: ${{ github.token }} + run: | + SHA="${{ needs.parse.outputs.sha }}" + BRANCH="${{ matrix.branch }}" + PR_DATA=$(gh api repos/${{ github.repository }}/commits/$SHA/pulls --jq '.[0] | {number, title}') + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number') + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}" + + git checkout -b "$BACKPORT_BRANCH" "origin/$BRANCH" + + if git cherry-pick -x "$SHA"; then + git push origin "$BACKPORT_BRANCH" + gh pr create \ + --base "$BRANCH" \ + --head "$BACKPORT_BRANCH" \ + --title "[backport] $PR_TITLE (to $BRANCH)" \ + --body "Backport of #$PR_NUMBER to \`$BRANCH\`." + else + git cherry-pick --abort || true + { + echo "❌ Cherry-pick to \`$BRANCH\` failed due to conflicts. Please backport manually:" + echo "" + echo "\`\`\`bash" + echo "git fetch origin $BRANCH master" + echo "git checkout -b backport/${PR_NUMBER}-to-${BRANCH} origin/$BRANCH" + echo "git cherry-pick -x $SHA" + echo "# resolve conflicts" + echo "git cherry-pick --continue" + echo "git push origin backport/${PR_NUMBER}-to-${BRANCH}" + echo "\`\`\`" + echo "" + echo "Then open a PR targeting \`$BRANCH\`." + } > /tmp/backport-comment.md + gh pr comment "$PR_NUMBER" --body-file /tmp/backport-comment.md + fi From c904ac65a6f6798733194e9f82a0d8d42bef990e Mon Sep 17 00:00:00 2001 From: atif09 Date: Wed, 11 Feb 2026 22:55:26 +0530 Subject: [PATCH 2/8] [fix] Prevent script injection in backport workflow #501 Moved direct interpolations to env variables in run blocks as recommended by CodeRabbit to prevent shell injection. Fixes #501 --- .github/workflows/reusable-backport.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/reusable-backport.yml b/.github/workflows/reusable-backport.yml index f5ed2b3d..1d6c2482 100644 --- a/.github/workflows/reusable-backport.yml +++ b/.github/workflows/reusable-backport.yml @@ -27,14 +27,18 @@ jobs: id: extract env: GH_TOKEN: ${{ github.token }} + COMMIT_SHA: ${{ inputs.commit_sha }} + PR_NUMBER: ${{ inputs.pr_number }} + COMMENT_BODY: ${{ inputs.comment_body }} + REPO: ${{ github.repository }} run: | - if [ -n "${{ inputs.commit_sha }}" ]; then - SHA="${{ inputs.commit_sha }}" - COMMIT_MSG=$(gh api repos/${{ github.repository }}/git/commits/$SHA --jq '.message') + if [ -n "$COMMIT_SHA" ]; then + SHA="$COMMIT_SHA" + COMMIT_MSG=$(gh api "repos/$REPO/git/commits/$SHA" --jq '.message') BRANCHES=$(echo "$COMMIT_MSG" | grep -oP '\[backport\s+\K[^\]]+' | tr '\n' ',' | sed 's/,$//') else - SHA=$(gh pr view ${{ inputs.pr_number }} --repo ${{ github.repository }} --json mergeCommit --jq '.mergeCommit.oid') - BRANCHES=$(echo "${{ inputs.comment_body }}" | grep -oP '/backport\s+\K\S+' | tr '\n' ',' | sed 's/,$//') + SHA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json mergeCommit --jq '.mergeCommit.oid') + BRANCHES=$(echo "$COMMENT_BODY" | grep -oP '/backport\s+\K\S+' | tr '\n' ',' | sed 's/,$//') fi if [ -z "$BRANCHES" ]; then @@ -65,10 +69,11 @@ jobs: - name: Cherry-pick and create PR env: GH_TOKEN: ${{ github.token }} + SHA: ${{ needs.parse.outputs.sha }} + BRANCH: ${{ matrix.branch }} + REPO: ${{ github.repository }} run: | - SHA="${{ needs.parse.outputs.sha }}" - BRANCH="${{ matrix.branch }}" - PR_DATA=$(gh api repos/${{ github.repository }}/commits/$SHA/pulls --jq '.[0] | {number, title}') + PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}') PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number') PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}" From c444280af448a13c895583c3cb77aad12a4c872f Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 12 Feb 2026 13:52:11 +0530 Subject: [PATCH 3/8] [docs] Add docs for reusable backport workflow #501 Added instructions and explanation related to the usage of this new custom workflow Related to #501 --- docs/developer/reusable-github-utils.rst | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/developer/reusable-github-utils.rst b/docs/developer/reusable-github-utils.rst index 4f9290f9..f81bed8b 100644 --- a/docs/developer/reusable-github-utils.rst +++ b/docs/developer/reusable-github-utils.rst @@ -115,3 +115,53 @@ example: git checkout $VERSION git reset --hard origin/master git push origin $VERSION --force-with-lease + +Backport Fixes to Stable Branch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This re-usable workflow automates cherry-picking fixes from ``master`` to +stable release branches. + +It supports two triggers: + +- **Commit message**: Add ``[backport X.Y]`` to the squash merge commit + body to automatically backport when merged to ``master``. +- **Comment**: Comment ``/backport X.Y`` on a merged PR (org members + only). + +If the cherry-pick fails due to conflicts, the bot comments with manual +resolution steps. + +.. code-block:: yaml + + name: Backport fixes to stable branch + + on: + push: + branches: + - master + issue_comment: + types: [created] + + permissions: + contents: write + pull-requests: write + + jobs: + backport-on-push: + if: github.event_name == 'push' + uses: openwisp/openwisp-utils/.github/workflows/reusable-backport.yml@master + with: + commit_sha: ${{ github.sha }} + + backport-on-comment: + if: > + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.state == 'closed' && + contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && + startsWith(github.event.comment.body, '/backport') + uses: openwisp/openwisp-utils/.github/workflows/reusable-backport.yml@master + with: + pr_number: ${{ github.event.issue.number }} + comment_body: ${{ github.event.comment.body }} From b8b7f7050f1b49146737e563406e42fbd4d70923 Mon Sep 17 00:00:00 2001 From: atif09 Date: Sun, 15 Feb 2026 15:11:56 +0530 Subject: [PATCH 4/8] [fix] Address review comments #501 - Timestamp suffix on branch name along with merge state validation and target branch existence check - Direct commit null handling and including main branch in push trigger - Branch name sanitization and github app token - Empty cherry-pick handling and duplicate PR check - gh pr create error handling + branch cleanup along with step summary (success + failure) - concurrency block where cancel-in-progress: false - [backport: X.Y] colon support - POSIX compatibility fix Related to #501 --- .github/workflows/backport.yml | 11 +++ .github/workflows/reusable-backport.yml | 97 ++++++++++++++++++------ docs/developer/reusable-github-utils.rst | 25 ++++-- 3 files changed, 105 insertions(+), 28 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index ac028e6a..a90b3639 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -4,9 +4,14 @@ on: push: branches: - master + - main issue_comment: types: [created] +concurrency: + group: backport-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + permissions: contents: write pull-requests: write @@ -17,6 +22,9 @@ jobs: uses: ./.github/workflows/reusable-backport.yml with: commit_sha: ${{ github.sha }} + secrets: + app_id: ${{ secrets.OPENWISP_BOT_APP_ID }} + private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }} backport-on-comment: if: > @@ -29,3 +37,6 @@ jobs: with: pr_number: ${{ github.event.issue.number }} comment_body: ${{ github.event.comment.body }} + secrets: + app_id: ${{ secrets.OPENWISP_BOT_APP_ID }} + private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }} diff --git a/.github/workflows/reusable-backport.yml b/.github/workflows/reusable-backport.yml index 1d6c2482..c29f51ed 100644 --- a/.github/workflows/reusable-backport.yml +++ b/.github/workflows/reusable-backport.yml @@ -15,6 +15,11 @@ on: required: false type: string default: "" + secrets: + app_id: + required: true + private_key: + required: true jobs: parse: @@ -35,10 +40,16 @@ jobs: if [ -n "$COMMIT_SHA" ]; then SHA="$COMMIT_SHA" COMMIT_MSG=$(gh api "repos/$REPO/git/commits/$SHA" --jq '.message') - BRANCHES=$(echo "$COMMIT_MSG" | grep -oP '\[backport\s+\K[^\]]+' | tr '\n' ',' | sed 's/,$//') + BRANCHES=$(echo "$COMMIT_MSG" | sed -n 's/.*\[backport[:[:space:]]\+\([^]]*\)\].*/\1/p' | tr '\n' ',' | sed 's/,$//') else - SHA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json mergeCommit --jq '.mergeCommit.oid') - BRANCHES=$(echo "$COMMENT_BODY" | grep -oP '/backport\s+\K\S+' | tr '\n' ',' | sed 's/,$//') + PR_DATA=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json state,mergeCommit) + PR_STATE=$(echo "$PR_DATA" | jq -r '.state') + if [ "$PR_STATE" != "MERGED" ]; then + echo "PR #$PR_NUMBER is not merged, skipping" + exit 0 + fi + SHA=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + BRANCHES=$(echo "$COMMENT_BODY" | sed -n 's|.*/backport[[:space:]]\+\([^[:space:]]*\).*|\1|p' | tr '\n' ',' | sed 's/,$//') fi if [ -z "$BRANCHES" ]; then @@ -57,51 +68,93 @@ jobs: matrix: branch: ${{ fromJSON(needs.parse.outputs.branches) }} steps: - - uses: actions/checkout@v6 + - name: Generate GitHub App Token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.app_id }} + private-key: ${{ secrets.private_key }} + + - uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ steps.generate-token.outputs.token }} - name: Configure Git run: | - git config user.name 'github-actions[bot]' - git config user.email 'github-actions[bot]@users.noreply.github.com' + git config user.name 'OpenWISP Automation Bot' + git config user.email 'support@openwisp.io' - name: Cherry-pick and create PR env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.generate-token.outputs.token }} SHA: ${{ needs.parse.outputs.sha }} BRANCH: ${{ matrix.branch }} REPO: ${{ github.repository }} run: | - PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}') - PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number') - PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') - BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}" + BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._/-]//g') + if ! git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + echo "::error::Target branch '$BRANCH' does not exist" + exit 1 + fi + PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}' || echo "null") + PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number // empty') + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // "Fix from master"') + if [ -z "$PR_NUMBER" ]; then + BACKPORT_BRANCH="backport/commit-${SHA:0:7}-to-${BRANCH}" + BODY="Backport of commit $SHA to \`$BRANCH\`." + PR_TITLE="Backport commit ${SHA:0:7} to $BRANCH" + else + BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}" + BODY="Backport of #$PR_NUMBER to \`$BRANCH\`." + EXISTING=$(gh pr list --base "$BRANCH" --search "Backport of #$PR_NUMBER" --state open --json number --jq '.[0].number') + if [ -n "$EXISTING" ]; then + echo "Backport PR #$EXISTING already exists" + gh pr comment "$PR_NUMBER" --body "Backport to \`$BRANCH\` already exists: #$EXISTING" + exit 0 + fi + fi + BACKPORT_BRANCH="${BACKPORT_BRANCH}-$(date +%s)" git checkout -b "$BACKPORT_BRANCH" "origin/$BRANCH" if git cherry-pick -x "$SHA"; then git push origin "$BACKPORT_BRANCH" - gh pr create \ - --base "$BRANCH" \ - --head "$BACKPORT_BRANCH" \ - --title "[backport] $PR_TITLE (to $BRANCH)" \ - --body "Backport of #$PR_NUMBER to \`$BRANCH\`." + if ! gh pr create \ + --base "$BRANCH" \ + --head "$BACKPORT_BRANCH" \ + --title "[backport] $PR_TITLE (to $BRANCH)" \ + --body "$BODY"; then + echo "::error::Failed to create PR" + git push origin --delete "$BACKPORT_BRANCH" || true + exit 1 + fi + echo "## ✅ Backport Successful" >> $GITHUB_STEP_SUMMARY + echo "- **Source**: #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY + echo "- **Target Branch**: $BRANCH" >> $GITHUB_STEP_SUMMARY + echo "- **Backport Branch**: $BACKPORT_BRANCH" >> $GITHUB_STEP_SUMMARY + elif [ $? -eq 1 ] && git diff --cached --quiet; then + echo "Commit $SHA appears to already be in $BRANCH (empty cherry-pick)" + git cherry-pick --abort || true + exit 0 else git cherry-pick --abort || true { echo "❌ Cherry-pick to \`$BRANCH\` failed due to conflicts. Please backport manually:" echo "" echo "\`\`\`bash" - echo "git fetch origin $BRANCH master" - echo "git checkout -b backport/${PR_NUMBER}-to-${BRANCH} origin/$BRANCH" + echo "git fetch origin $BRANCH" + echo "git checkout -b $BACKPORT_BRANCH origin/$BRANCH" echo "git cherry-pick -x $SHA" echo "# resolve conflicts" echo "git cherry-pick --continue" - echo "git push origin backport/${PR_NUMBER}-to-${BRANCH}" + echo "git push origin $BACKPORT_BRANCH" echo "\`\`\`" - echo "" - echo "Then open a PR targeting \`$BRANCH\`." } > /tmp/backport-comment.md - gh pr comment "$PR_NUMBER" --body-file /tmp/backport-comment.md + [ -n "$PR_NUMBER" ] && gh pr comment "$PR_NUMBER" --body-file /tmp/backport-comment.md + echo "## ❌ Backport Failed" >> $GITHUB_STEP_SUMMARY + echo "- **Source**: #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY + echo "- **Target Branch**: $BRANCH" >> $GITHUB_STEP_SUMMARY + echo "- **Error**: Cherry-pick failed due to conflicts" >> $GITHUB_STEP_SUMMARY + exit 1 fi diff --git a/docs/developer/reusable-github-utils.rst b/docs/developer/reusable-github-utils.rst index f81bed8b..69a6e7ab 100644 --- a/docs/developer/reusable-github-utils.rst +++ b/docs/developer/reusable-github-utils.rst @@ -119,18 +119,20 @@ example: Backport Fixes to Stable Branch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This re-usable workflow automates cherry-picking fixes from ``master`` to -stable release branches. +This re-usable workflow automates cherry-picking fixes from ``master`` or +``main`` to stable release branches. It supports two triggers: -- **Commit message**: Add ``[backport X.Y]`` to the squash merge commit - body to automatically backport when merged to ``master``. +- **Commit message**: Add ``[backport X.Y]`` or ``[backport: X.Y]`` to the + squash merge commit body to automatically backport when merged to + ``master`` or ``main``. - **Comment**: Comment ``/backport X.Y`` on a merged PR (org members only). -If the cherry-pick fails due to conflicts, the bot comments with manual -resolution steps. +If the cherry-pick fails due to conflicts, the bot comments on the PR with +manual resolution steps. If the target branch does not exist or the PR is +not yet merged, the workflow exits safely without failing. .. code-block:: yaml @@ -140,9 +142,14 @@ resolution steps. push: branches: - master + - main issue_comment: types: [created] + concurrency: + group: backport-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + permissions: contents: write pull-requests: write @@ -153,6 +160,9 @@ resolution steps. uses: openwisp/openwisp-utils/.github/workflows/reusable-backport.yml@master with: commit_sha: ${{ github.sha }} + secrets: + app_id: ${{ secrets.OPENWISP_BOT_APP_ID }} + private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }} backport-on-comment: if: > @@ -165,3 +175,6 @@ resolution steps. with: pr_number: ${{ github.event.issue.number }} comment_body: ${{ github.event.comment.body }} + secrets: + app_id: ${{ secrets.OPENWISP_BOT_APP_ID }} + private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }} From 546e72a72330b301acd3d37e6ece59328e04c2c0 Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 19 Feb 2026 03:04:17 +0530 Subject: [PATCH 5/8] [fix] Address review comments to tighten the security of the workflow #501 - Fix duplicate PR deduplication search - Tighten the branch name sanitization - Guard against empty SHA - Fix the cherry-pick exit code capture - echo null masking errors has been fixed - EXISTING PR search matching wrong PRs has been fixed - Fixed typo in GitHub API call Related to #501 --- .github/workflows/backport.yml | 1 + .github/workflows/reusable-backport.yml | 38 +++++++++++++++++++------ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index a90b3639..6926b547 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -30,6 +30,7 @@ jobs: if: > github.event_name == 'issue_comment' && github.event.issue.pull_request && + github.event.issue.pull_request.merged_at != '' && github.event.issue.state == 'closed' && contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && startsWith(github.event.comment.body, '/backport') diff --git a/.github/workflows/reusable-backport.yml b/.github/workflows/reusable-backport.yml index c29f51ed..9ab93cad 100644 --- a/.github/workflows/reusable-backport.yml +++ b/.github/workflows/reusable-backport.yml @@ -46,6 +46,8 @@ jobs: PR_STATE=$(echo "$PR_DATA" | jq -r '.state') if [ "$PR_STATE" != "MERGED" ]; then echo "PR #$PR_NUMBER is not merged, skipping" + echo "branches=[]" >> $GITHUB_OUTPUT + echo "sha=" >> $GITHUB_OUTPUT exit 0 fi SHA=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') @@ -61,7 +63,7 @@ jobs: backport: needs: parse - if: needs.parse.outputs.branches != '[]' + if: needs.parse.outputs.branches != '' && needs.parse.outputs.branches != '[]' && needs.parse.outputs.sha != '' runs-on: ubuntu-latest strategy: fail-fast: false @@ -82,7 +84,7 @@ jobs: - name: Configure Git run: | - git config user.name 'OpenWISP Automation Bot' + git config user.name 'OpenWISP Companion' git config user.email 'support@openwisp.io' - name: Cherry-pick and create PR @@ -92,12 +94,24 @@ jobs: BRANCH: ${{ matrix.branch }} REPO: ${{ github.repository }} run: | + SHA="${{ needs.parse.outputs.sha }}" + if [ -z "$SHA" ]; then + echo "No SHA to cherry-pick, skipping" + exit 0 + fi BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._/-]//g') + if echo "$BRANCH" | grep -q '\.\.'; then + echo "::error::Invalid branch name '$BRANCH'" + exit 1 + fi if ! git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then echo "::error::Target branch '$BRANCH' does not exist" exit 1 fi - PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}' || echo "null") + if ! PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}'); then + echo "::warning::Could not fetch PR data for $SHA, proceeding with commit-based backport branch" + PR_DATA="null" + fi PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number // empty') PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // "Fix from master"') if [ -z "$PR_NUMBER" ]; then @@ -108,7 +122,8 @@ jobs: BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}" BODY="Backport of #$PR_NUMBER to \`$BRANCH\`." - EXISTING=$(gh pr list --base "$BRANCH" --search "Backport of #$PR_NUMBER" --state open --json number --jq '.[0].number') + EXISTING=$(gh pr list --base "$BRANCH" --state open --json number,headRefName \ + --jq ".[] | select(.headRefName | startswith(\"backport/${PR_NUMBER}-to-${BRANCH}-\")) | .number" | head -1) if [ -n "$EXISTING" ]; then echo "Backport PR #$EXISTING already exists" gh pr comment "$PR_NUMBER" --body "Backport to \`$BRANCH\` already exists: #$EXISTING" @@ -117,13 +132,18 @@ jobs: fi BACKPORT_BRANCH="${BACKPORT_BRANCH}-$(date +%s)" git checkout -b "$BACKPORT_BRANCH" "origin/$BRANCH" - - if git cherry-pick -x "$SHA"; then - git push origin "$BACKPORT_BRANCH" + CP_EXIT=$? + git cherry-pick -x "$SHA" || CP_EXIT=$? + if [ $CP_EXIT -eq 0 ]; then + if ! git push origin "$BACKPORT_BRANCH"; then + echo "::error::Failed to push $BACKPORT_BRANCH" + git push origin --delete "$BACKPORT_BRANCH" || true + exit 1 + fi if ! gh pr create \ --base "$BRANCH" \ --head "$BACKPORT_BRANCH" \ - --title "[backport] $PR_TITLE (to $BRANCH)" \ + --title "[backport] #${PR_NUMBER}: $PR_TITLE (to $BRANCH)" \ --body "$BODY"; then echo "::error::Failed to create PR" git push origin --delete "$BACKPORT_BRANCH" || true @@ -133,7 +153,7 @@ jobs: echo "- **Source**: #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY echo "- **Target Branch**: $BRANCH" >> $GITHUB_STEP_SUMMARY echo "- **Backport Branch**: $BACKPORT_BRANCH" >> $GITHUB_STEP_SUMMARY - elif [ $? -eq 1 ] && git diff --cached --quiet; then + elif git diff --cached --quiet; then echo "Commit $SHA appears to already be in $BRANCH (empty cherry-pick)" git cherry-pick --abort || true exit 0 From 5c820938b921f26a657748caf57012f0b1d75651 Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 19 Feb 2026 04:03:56 +0530 Subject: [PATCH 6/8] [chores] Removing dead code and improving branch name sanitization #501 - Removing the redundant remote branch cleanup (the pointless delete on push failure) - Replacing custom branch name sanitization with git check-ref-format Related to #501 --- .github/workflows/reusable-backport.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/reusable-backport.yml b/.github/workflows/reusable-backport.yml index 9ab93cad..c9a8043d 100644 --- a/.github/workflows/reusable-backport.yml +++ b/.github/workflows/reusable-backport.yml @@ -99,8 +99,7 @@ jobs: echo "No SHA to cherry-pick, skipping" exit 0 fi - BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._/-]//g') - if echo "$BRANCH" | grep -q '\.\.'; then + if ! git check-ref-format --branch "$BRANCH" > /dev/null 2>&1; then echo "::error::Invalid branch name '$BRANCH'" exit 1 fi @@ -137,7 +136,6 @@ jobs: if [ $CP_EXIT -eq 0 ]; then if ! git push origin "$BACKPORT_BRANCH"; then echo "::error::Failed to push $BACKPORT_BRANCH" - git push origin --delete "$BACKPORT_BRANCH" || true exit 1 fi if ! gh pr create \ From 8f9fd9795d6661cae1adbcd29c05ff6449619968 Mon Sep 17 00:00:00 2001 From: atif09 Date: Thu, 19 Feb 2026 14:55:06 +0530 Subject: [PATCH 7/8] [fix] Correct cherry-pick exit code and docs update #501 - Included the correct backport-on-comment condition with merged_at check in docs - Fixed cherry-pick exit code capture Related to #501 --- .github/workflows/reusable-backport.yml | 6 +++--- docs/developer/reusable-github-utils.rst | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-backport.yml b/.github/workflows/reusable-backport.yml index c9a8043d..10335a99 100644 --- a/.github/workflows/reusable-backport.yml +++ b/.github/workflows/reusable-backport.yml @@ -131,9 +131,9 @@ jobs: fi BACKPORT_BRANCH="${BACKPORT_BRANCH}-$(date +%s)" git checkout -b "$BACKPORT_BRANCH" "origin/$BRANCH" - CP_EXIT=$? - git cherry-pick -x "$SHA" || CP_EXIT=$? - if [ $CP_EXIT -eq 0 ]; then + CHERRY_PICK_STATUS=0 + git cherry-pick -x "$SHA" || CHERRY_PICK_STATUS=$? + if [ $CHERRY_PICK_STATUS -eq 0 ]; then if ! git push origin "$BACKPORT_BRANCH"; then echo "::error::Failed to push $BACKPORT_BRANCH" exit 1 diff --git a/docs/developer/reusable-github-utils.rst b/docs/developer/reusable-github-utils.rst index 69a6e7ab..e691e23f 100644 --- a/docs/developer/reusable-github-utils.rst +++ b/docs/developer/reusable-github-utils.rst @@ -168,6 +168,7 @@ not yet merged, the workflow exits safely without failing. if: > github.event_name == 'issue_comment' && github.event.issue.pull_request && + github.event.issue.pull_request.merged_at != null && github.event.issue.state == 'closed' && contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && startsWith(github.event.comment.body, '/backport') From 5cb3f7a8d1016a6ae1230a28d28734a483175bfe Mon Sep 17 00:00:00 2001 From: atif09 Date: Sat, 21 Feb 2026 22:10:15 +0530 Subject: [PATCH 8/8] [fix] Use null check for merged_at validation #501 Removed the unreliable comparison against '' Related to #501 --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 6926b547..6f215670 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -30,7 +30,7 @@ jobs: if: > github.event_name == 'issue_comment' && github.event.issue.pull_request && - github.event.issue.pull_request.merged_at != '' && + github.event.issue.pull_request.merged_at != null && github.event.issue.state == 'closed' && contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && startsWith(github.event.comment.body, '/backport')