Skip to content

Conversation

@atif09
Copy link

@atif09 atif09 commented Feb 8, 2026

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #501 .

Description of Changes

  • Adds reusable workflow to automate cherry-picking of fixes to stable branches
  • Parses [backport X.Y] tags from squash merge commit messages
  • Supports multiple target branches (e.g., [backport 1.1] + [backport 1.2])
  • Supports /backport X.Y comment as a manual fallback on merged PRs
  • Restricts /backport command to organization members
  • Comments on the original PR when cherry-pick conflicts occur

Screenshots

N/A

Test PRs

  1. Commit message trigger: squash-merged PR with [backport 1.1] in body, also see the workflow not being triggered by a non-member comment (PR)
  2. Conflict handling: cherry-pick conflict (PR)
  3. Comment trigger: /backport 1.1 on merged PR (PR)
  4. Multiple branches: [backport 1.1] + [backport 1.2] (PR)
  5. Open PR: /backport 1.1 on unmerged PR (PR)
  6. Backport PR generated by command post-merge: commented with /backport 1.1 on the PR after it got merged (PR)
  7. Backport PR generated for specific branches mentioned in the squash-merge commit message body: two separate PRs opened according to the backport tags in the commit message body (PR)

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Two new GitHub Actions workflows are added to automate backporting fixes to stable release branches. The main workflow (backport.yml) triggers on pushes to the master branch or on issue comments containing "/backport" directives. It calls a reusable workflow (reusable-backport.yml) which parses commit messages or comment directives to identify target branches, attempts cherry-picking the specified commit to each target branch, and either creates a pull request on success or posts manual backport instructions if conflicts occur.

Sequence Diagram(s)

sequenceDiagram
    participant GHEvent as GitHub Event
    participant Parse as Parse Job
    participant Backport as Backport Job
    participant Git as Git Operations
    participant PR as PR/Comment System

    GHEvent->>Parse: Trigger (push/comment)
    Parse->>Parse: Extract commit SHA or<br/>parse comment body
    Parse->>Parse: Identify target branches<br/>from commit/comment
    Parse-->>Backport: Output branches + SHA

    rect rgba(76, 175, 80, 0.5)
        Note over Backport,PR: Success Path
        Backport->>Git: Cherry-pick SHA to<br/>backport branch
        Git-->>Backport: Success
        Backport->>PR: Create PR to target branch
    end

    rect rgba(244, 67, 54, 0.5)
        Note over Backport,PR: Failure Path
        Backport->>Git: Cherry-pick SHA
        Git-->>Backport: Conflict detected
        Backport->>Backport: Abort cherry-pick
        Backport->>PR: Post comment with<br/>manual instructions
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a reusable backport workflow for automating cherry-picking fixes to stable branches.
Linked Issues check ✅ Passed The implementation fully addresses issue #501 objectives: automates backporting via commit-message tags and comment triggers, handles conflicts with manual guidance, and creates PRs targeting stable branches.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the backport workflow feature. The two YAML files (backport.yml and reusable-backport.yml) are both essential and in-scope for the automation system.
Description check ✅ Passed The pull request description is comprehensive and complete, covering all required sections of the template with detailed information about the backport automation feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coveralls
Copy link

coveralls commented Feb 8, 2026

Coverage Status

coverage: 97.249%. remained the same
when pulling c444280 on atif09:issues/501-automate-backporting-fixes
into 24e9775 on openwisp:master.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.github/workflows/backport.yml:
- Around line 19-25: Tighten the issue_comment matcher to only trigger for the
exact /backport command (replace startsWith(github.event.comment.body,
'/backport') with a regex match like matches(github.event.comment.body,
'^/backport(\\s|$)') so variants like /backporting won't match) and in the
reusable workflow add a step that checks steps.check.outputs.merged and, when it
!= 'true', posts a comment back to the PR (using the existing check step name
"check" and a comment action or gh script) to notify the user the PR was closed
but not merged and the backport was skipped.

In @.github/workflows/reusable-backport.yml:
- Around line 27-31: Update the "Create backport pull requests" step that uses
korthout/backport-action@v4 to include an explicit experimental conflict
resolution setting so conflicts create draft backport commits and post
resolution instructions instead of failing; specifically, add the experimental
input with conflict_resolution set to "draft_commit_conflicts" alongside the
existing source_pr_number input in the step that uses
korthout/backport-action@v4 to enable PR commenting on cherry-pick conflicts.
🧹 Nitpick comments (1)
.github/workflows/backport.yml (1)

26-26: Consider using a local path for the reusable workflow within this repo.

Since backport.yml and reusable-backport.yml live in the same repository (openwisp-utils), referencing via the full path @master means any changes to the reusable workflow cannot be tested through a PR — it will always pull the version from master. Using a relative path enables PR-based testing of workflow changes.

-    uses: openwisp/openwisp-utils/.github/workflows/reusable-backport.yml@master
+    uses: ./.github/workflows/reusable-backport.yml

Other OpenWISP repos that consume this reusable workflow would still reference the full openwisp/openwisp-utils/...@master path.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9d85c3 and e00a3ba.

📒 Files selected for processing (2)
  • .github/workflows/backport.yml
  • .github/workflows/reusable-backport.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
🔇 Additional comments (2)
.github/workflows/backport.yml (1)

3-7: Workflow triggers are appropriate.

Using pull_request_target (fork-safe) and issue_comment is the standard pattern for this kind of automation. Note that issue_comment fires for every comment on every issue/PR, but the job-level if filters it correctly, so the job won't actually run for irrelevant comments.

.github/workflows/reusable-backport.yml (1)

24-25: No changes needed. Both concerns in the original review are incorrect.

  1. actions/checkout@v6 is published and current (v6.0.2 as of Feb 2026).
  2. korthout/backport-action explicitly supports shallow clones by design—it performs its own git fetch for refs/SHAs it needs. The default fetch-depth: 1 is fine; fetch-depth: 0 is only necessary if you encounter specific cherry-pick errors related to missing history.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@atif09 atif09 marked this pull request as draft February 8, 2026 12:19
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 8, 2026
@atif09 atif09 force-pushed the issues/501-automate-backporting-fixes branch from 1227109 to 5aa195a Compare February 8, 2026 12:33
@atif09 atif09 marked this pull request as ready for review February 8, 2026 12:35
@atif09
Copy link
Author

atif09 commented Feb 8, 2026

addressed all the comments made by code rabbit (along with the nitpicks)

Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see this @atif09!

I have a couple of questions:

  • Can you explain why we need two YAML files, what does each do?
  • Can this work by recognizing a flag in the commit message?
    From what I see it would work with either a comment or an issue label.

Request: please add a new docs section for this here:
https://github.com/openwisp/openwisp-utils/blob/master/docs/developer/reusable-github-utils.rst to describe why this was added and basic usage instructions.

@nemesifier nemesifier added the github_actions Pull requests that update GitHub Actions code label Feb 8, 2026
@github-project-automation github-project-automation bot moved this from To do (general) to In progress in OpenWISP Contributor's Board Feb 8, 2026
@atif09
Copy link
Author

atif09 commented Feb 8, 2026

1)the reusable-backport.yml contains the merged check, checkout and the cherry-pick action logic and it uses on: workflow_call so it can be called from any repo

the backport.yml is the caller specific to this repo, it defined triggers such as pull_request_target, issue_comment, and permissions and also checks if the person commenting is a member of the org or not, along with the other if conditions

this is similar to version-branch.yml and reusable-version-branch.yml, it is so that other modules can use it with their own caller

2)no, this approach doesnt recognize flags like [fix] or something of that sort in the commit message, it works by a comment trigger or an issue label trigger.

And the label is needed to recognize which branch to cherry pick to, and the keyword in the commit message wouldnt tell it the target branch

3)noted, will add the docs explaining this section's basic usage and other related info

Great to see this @atif09!

I have a couple of questions:

  • Can you explain why we need two YAML files, what does each do?
  • Can this work by recognizing a flag in the commit message?
    From what I see it would work with either a comment or an issue label.

Request: please add a new docs section for this here:
https://github.com/openwisp/openwisp-utils/blob/master/docs/developer/reusable-github-utils.rst to describe why this was added and basic usage instructions.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 10, 2026
@atif09 atif09 force-pushed the issues/501-automate-backporting-fixes branch from af63e32 to fa23440 Compare February 10, 2026 10:02
@atif09 atif09 marked this pull request as draft February 10, 2026 10:08
@atif09 atif09 force-pushed the issues/501-automate-backporting-fixes branch from fa23440 to 7e77420 Compare February 10, 2026 11:24
@atif09 atif09 marked this pull request as ready for review February 10, 2026 21:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.github/workflows/reusable-backport.yml:
- Line 96: The reusable workflow currently hardcodes "master" in the manual
backport echo command (the line that echoes "git fetch origin $BRANCH master");
add a workflow_call input named default_branch (string, optional, default
"master"), wire that input into the job environment as DEFAULT_BRANCH, and
update the echo/command to use $DEFAULT_BRANCH instead of the literal "master"
(e.g., "git fetch origin $BRANCH $DEFAULT_BRANCH") so calling repos can override
the default branch safely; ensure DEFAULT_BRANCH is passed via env to avoid
injection.
- Around line 71-79: The run block currently interpolates untrusted values
directly (SHA="${{ needs.parse.outputs.sha }}" and BRANCH="${{ matrix.branch
}}"), allowing script injection; instead move those interpolations into the
job's env section (set env: SHA: ${{ needs.parse.outputs.sha }}, BRANCH: ${{
matrix.branch }}) and reference $SHA and $BRANCH inside run, then explicitly
sanitize them at the start of the run step (e.g., strip newlines/CRs and reject
or escape shell metacharacters) before using them to build PR_NUMBER,
BACKPORT_BRANCH, PR_TITLE; fail early if BRANCH contains unsafe characters so
gh/other commands never receive unsanitized input.
- Around line 33-42: Avoid directly interpolating GitHub Action inputs into the
run script; instead export them as environment variables and reference those
vars inside the shell to prevent pre-execution interpolation and command
injection. Update the run block so that inputs.commit_sha, inputs.pr_number, and
inputs.comment_body are passed via env (e.g., export SHA_INPUT, PR_INPUT,
COMMENT_INPUT) and then use SHA_INPUT/PR_INPUT/COMMENT_INPUT inside the shell to
populate variables like SHA, COMMIT_MSG, COMMENT and derive BRANCHES, keeping
any gh api calls using the env var values rather than the ${{ ... }}
interpolation.
🧹 Nitpick comments (1)
.github/workflows/reusable-backport.yml (1)

77-79: Redundant API call: PR number and title fetched in two separate requests to the same endpoint.

Lines 77 and 79 both call repos/{repo}/commits/{sha}/pulls. Combine into a single call to reduce API usage and latency.

Proposed fix
-          PR_NUMBER=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0].number')
+          PR_INFO=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}')
+          PR_NUMBER=$(echo "$PR_INFO" | jq -r '.number')
+          PR_TITLE=$(echo "$PR_INFO" | jq -r '.title')
           BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}"
-          PR_TITLE=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0].title')
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e77420 and 2102181.

📒 Files selected for processing (3)
  • .github/workflows/backport.yml
  • .github/workflows/reusable-backport.yml
  • backport-test-file.txt
✅ Files skipped from review due to trivial changes (1)
  • backport-test-file.txt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-10T20:38:21.134Z
Learnt from: nemesifier
Repo: openwisp/openwisp-utils PR: 593
File: openwisp_utils/releaser/commitizen.py:5-9
Timestamp: 2026-02-10T20:38:21.134Z
Learning: In openwisp-utils commitizen implementation, the `_TITLE_ISSUE_EXTRACT_RE` pattern intentionally matches any space-preceded `#<number>` in commit titles without anchoring to the end. This is acceptable because 99.9% of such patterns are actual issue references, and rare edge cases (like version numbers) will be handled manually if they occur.

Applied to files:

  • .github/workflows/backport.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.0.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.0.0
🔇 Additional comments (2)
.github/workflows/backport.yml (1)

1-12: Triggers and permissions look appropriate.

Push-to-master captures merge events, and issue_comment enables the manual /backport fallback. contents: write and pull-requests: write are the minimum required for pushing branches and creating PRs.

.github/workflows/reusable-backport.yml (1)

62-62: actions/checkout@v6 is available. Released on Nov 20, 2025 (latest patch v6.0.2 as of Jan 9, 2026). Note that v6 requires Actions Runner v2.329.0+ due to credential-handling changes.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@atif09 atif09 force-pushed the issues/501-automate-backporting-fixes branch from 2102181 to 3ede984 Compare February 11, 2026 17:09
Automates cherry-picking fixes to stable branches via [backport X.Y]
in commit messages or /backport X.Y comments, with conflict notification.

Closes openwisp#501
@atif09 atif09 force-pushed the issues/501-automate-backporting-fixes branch from 3ede984 to c3122f5 Compare February 11, 2026 17:11
Moved direct interpolations to env variables in run blocks
as recommended by CodeRabbit to prevent shell injection.

Fixes openwisp#501
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 11, 2026
@atif09
Copy link
Author

atif09 commented Feb 11, 2026

i have tested these new changes on test PRs again on my fork, and they are working
please check the PR description for the test PRs which were opened keeping this new approach in mind

Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs missing. PS: can you show this action in action somewhere before we merge it?

Make sure to have different branches (eg: 1.2, 1.3, 1.4) and backport only to specific branches indicated in the commit message.

Would a comment post merge also work?

@atif09
Copy link
Author

atif09 commented Feb 12, 2026

PS: can you show this action in action somewhere before we merge it?

sorry i didnt understand, do you mean in a repo which i made (besides the fork)?

Docs missing.

noted, was waiting on your feedback before adding docs, ill add them

Make sure to have different branches (eg: 1.2, 1.3, 1.4) and backport only to specific branches indicated in the commit message.

  • i created multiple new branches first, and the action opened backport PRs for specific branches mentioned in the commit message body: PR

Would a comment post merge also work?

  • backport PR opened post-merge after commment: PR

i have also updated the PR description with these new test PRs

Added instructions and explanation related to the usage of this new custom workflow

Related to openwisp#501
Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work @atif09! It looks like is doing just what we need!

I reviewed the code manually and also with some automated tools, I have found a few things that need to be improved before we can merge this and use it in all our repos.

Critical Issues (Must Fix)

1. Race Condition on Branch Creation 🚨

Problem: The branch naming strategy backport/${PR_NUMBER}-to-${BRANCH} can collide on workflow re-runs or if multiple commits from the same PR are backported.

Current Code:

BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}"
git checkout -b "$BACKPORT_BRANCH" "origin/$BRANCH"

Impact: Subsequent runs will fail with "fatal: A branch named '...' already exists", or worse, may push to an existing branch with unintended commits.

Fix:

# Option 1: Timestamp-based
BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}-$(date +%s)"

# Option 2: Check-and-skip (preferred for idempotency)
EXISTING_PR=$(gh pr list --base "$BRANCH" --head "$BACKPORT_BRANCH" --state open --json number --jq '.[0].number')
if [ -n "$EXISTING_PR" ]; then
  echo "Backport PR #$EXISTING_PR already exists, skipping"
  exit 0
fi
git checkout -B "$BACKPORT_BRANCH" "origin/$BRANCH"  # -B forces reset if exists

2. Missing PR Merge State Validation 🚨

Problem: The comment trigger only checks issue.state == 'closed' but not whether the PR was actually merged (could be closed without merging).

Current Code:

if: >
  github.event_name == 'issue_comment' &&
  github.event.issue.pull_request &&
  github.event.issue.state == 'closed' &&
  ...

Impact: Users could trigger backports on unmerged PRs, leading to cherry-picking commits that shouldn't be backported.

Fix: Add validation in the parse job:

if [ -n "$PR_NUMBER" ]; then
  MERGED=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json merged --jq '.merged')
  if [ "$MERGED" != "true" ]; then
    echo "PR #$PR_NUMBER is not merged, exiting"
    exit 0
  fi
fi

3. No Target Branch Existence Check 🚨

Problem: The workflow doesn't validate that the target branch (e.g., 1.1) exists before attempting to cherry-pick.

Impact: Confusing failures late in the workflow when git checkout fails on non-existent branches.

Fix:

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

4. Fragile PR Association on Direct Commits 🚨

Problem: When commits are pushed directly to master (not via PR), gh api "repos/$REPO/commits/$SHA/pulls" returns an empty array, causing jq to return null.

Current Code:

PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}')
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')

Impact: Script fails with null values or creates PRs with "null" in the title.

Fix:

PR_DATA=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq '.[0] | {number, title}')
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
  # Handle direct commit to master
  BACKPORT_BRANCH="backport/commit-${SHA:0:7}-to-${BRANCH}"
  PR_TITLE="Backport commit ${SHA:0:7} to $BRANCH"
  BODY="Backport of commit $SHA to \`$BRANCH\`."
else
  BACKPORT_BRANCH="backport/${PR_NUMBER}-to-${BRANCH}"
  BODY="Backport of #$PR_NUMBER to \`$BRANCH\`."
fi

5. Missing Support for main Branch 🚨

Problem: The trigger workflow only listens to master, but many repositories use main.

Current Code:

on:
  push:
    branches:
      - master

Fix:

on:
  push:
    branches:
      - master
      - main

Security Issues

6. Branch Name Injection Risk ⚠️

Problem: The branch name extracted from user comments (/backport X.Y) is used directly in shell commands without sanitization.

Current Code:

BRANCHES=$(echo "$COMMENT_BODY" | grep -oP '/backport\s+\K\S+' | tr '\n' ',' | sed 's/,$//')
# ... later used in:
git checkout -b "$BACKPORT_BRANCH" "origin/$BRANCH"

Risk: While GitHub Actions env var quoting provides some protection, malicious branch names with special characters could cause unexpected behavior.

Fix:

# Sanitize branch names - only allow alphanumeric, dots, dashes, and slashes
BRANCH=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._/-]//g')

7. Workflow Token Permissions ⚠️

Observation: The workflow correctly uses minimal permissions (contents: write, pull-requests: write). However, using GITHUB_TOKEN for pushing and creating PRs means subsequent workflows (like CI) won't be triggered automatically due to GitHub's token restrictions.

This is a problem because we need CI to run on these PRs. I looked for possible solutions and it seems the only secure way to do this is to create a github app, this is an investment of time I am willing to make as we can reuse the same app for other tasks which deal with onboarding and guiding contributors. Here's the details:

Option 1: GitHub App Token (Recommended) ⭐

Best balance of security and functionality.

I already created the Github App and added the organization secrets.

  • Update Trigger Workflow (.github/workflows/backport.yml):
name: Backport fixes to stable branch

on:
  push:
    branches:
      - master
      - main
  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 }}
    secrets:
      app_id: ${{ secrets.OPENWISP_BOT_APP_ID }}
      private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}

  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 }}
    secrets:
      app_id: ${{ secrets.OPENWISP_BOT_APP_ID }}
      private_key: ${{ secrets.OPENWISP_BOT_PRIVATE_KEY }}
  • Update Reusable Workflow (.github/workflows/reusable-backport.yml):
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: ""
    secrets:
      app_id:
        required: true
      private_key:
        required: true

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 }}
          COMMIT_SHA: ${{ inputs.commit_sha }}
          PR_NUMBER: ${{ inputs.pr_number }}
          COMMENT_BODY: ${{ inputs.comment_body }}
          REPO: ${{ github.repository }}
        run: |
          if [ -n "$COMMIT_SHA" ]; then
            SHA="$COMMIT_SHA"
            COMMIT_MSG=$(gh api "repos/$REPO/git/commits/$SHA" --jq '.message')
            BRANCHES=$(echo "$COMMIT_MSG" | sed -n 's/.*\[backport[:\s]\+\([^]]*\)\].*/\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/,$//')
          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:
      - 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@v6
        with:
          fetch-depth: 0
          token: ${{ steps.generate-token.outputs.token }}

      - name: Configure Git
        run: |
          git config user.name 'OpenWISP Automation Bot'
          git config user.email 'support@openwisp.io'

      - name: Cherry-pick and create PR
        env:
          GH_TOKEN: ${{ steps.generate-token.outputs.token }}
          SHA: ${{ needs.parse.outputs.sha }}
          BRANCH: ${{ matrix.branch }}
          REPO: ${{ github.repository }}
        run: |
          # Validate target branch exists
          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

          # Get PR data (handle direct commits)
          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
            # Handle direct commit to master
            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\`."
            
            # Check for existing backport PR
            EXISTING=$(gh pr list --base "$BRANCH" --head "$BACKPORT_BRANCH" --state open --json number --jq '.[0].number' || echo "")
            if [ -n "$EXISTING" ]; then
              echo "Backport PR #$EXISTING already exists, skipping"
              exit 0
            fi
          fi

          # Create unique branch name to avoid collisions
          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"
            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 "- **Target Branch**: $BRANCH" >> $GITHUB_STEP_SUMMARY
          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"
              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_BRANCH"
              echo "\`\`\`"
            } > /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 "- **Target Branch**: $BRANCH" >> $GITHUB_STEP_SUMMARY
            echo "- **Error**: Cherry-pick conflicts" >> $GITHUB_STEP_SUMMARY
            exit 1
          fi

Pros:

  • ✅ CI runs automatically on backport PRs
  • ✅ Fine-grained permissions control
  • ✅ Scoped to specific repositories
  • ✅ No personal user tokens required
  • ✅ Can be centrally managed at org level
  • ✅ Reusable for other automation workflows

Cons:

  • ⚠️ Requires one-time setup of GitHub App
  • ⚠️ App needs to be installed on each repo

Moderate Issues (Should Fix)

8. Empty Cherry-Pick Not Handled

Problem: If a commit was already cherry-picked manually, git returns "The previous cherry-pick is now empty" which is treated as a failure.

Fix:

if git cherry-pick -x "$SHA"; then
  # Success - create PR
  ...
elif [ $? -eq 1 ] && git diff --cached --quiet; then
  # Empty commit - already backported
  echo "Commit $SHA appears to already be in $BRANCH (empty cherry-pick)"
  git cherry-pick --abort || true
  exit 0
else
  # Real conflict - handle failure
  ...
fi

9. No Duplicate PR Prevention

Problem: The workflow doesn't check if a backport PR already exists before creating a new one.

Fix: Add this check before creating PR:

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

10. Insufficient Error Handling for gh pr create

Problem: If PR creation fails (e.g., network issue, validation error), the workflow fails silently after the push.

Fix:

if ! gh pr create \\
    --base "$BRANCH" \\
    --head "$BACKPORT_BRANCH" \\
    --title "[backport] $PR_TITLE (to $BRANCH)" \\
    --body "$BODY"; then
  echo "::error::Failed to create PR"
  # Optionally cleanup branch
  git push origin --delete "$BACKPORT_BRANCH" || true
  exit 1
fi

11. No Workflow Summary

Recommendation: Add a step summary for better visibility in the GitHub UI:

if git cherry-pick -x "$SHA"; then
  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
else
  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
fi

Minor Issues & Improvements

12. PCRE Dependency

It seems that grep -P (Perl regex) is not POSIX portable. While Ubuntu supports it, using sed is more portable:

# Instead of:
BRANCHES=$(echo "$COMMIT_MSG" | grep -oP '\[backport\s+\K[^\]]+' | tr '\n' ',' | sed 's/,$//')

# Use:
BRANCHES=$(echo "$COMMIT_MSG" | sed -n 's/.*\[backport[[:space:]]\+\([^]]*\)\].*/\1/p' | tr '\n' ',' | sed 's/,$//')

13. Missing Concurrency Control

Add concurrency to prevent race conditions on rapid pushes:

concurrency:
  group: backport-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

14. Regex Could Support More Formats

Let's support both [backport 1.1] and [backport: 1.1] for better UX:

BRANCHES=$(echo "$COMMIT_MSG" | sed -n 's/.*\[backport[:\s]\+\([^]]*\)\].*/\1/p')

Documentation Review

The documentation is generally well-written and clear. Suggested improvements:

  1. Clarify that target branches must exist before triggering backport
  2. Document the limitation that GITHUB_TOKEN won't trigger CI on the backport PR
  3. Add examples of both commit message and comment triggers
  4. Mention the branch naming convention for generated backport branches

Copy link
Member

@nemesifier nemesifier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created the Github App and installed in all OpenWISP repos.
I also added the organizations secrets needed for the github action:

The secrets are:

  • OPENWISP_BOT_APP_ID
  • OPENWISP_BOT_PRIVATE_KEY

I updated the previous review text to match these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github_actions Pull requests that update GitHub Actions code

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[feature] Automate backporting fixes to the last stable release branch

3 participants