Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Backport fixes to stable branch

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

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.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')
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 }}
5 changes: 2 additions & 3 deletions .github/workflows/bot-workflow.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
name: Changelog Bot

on:
# Trigger when a PR review is submitted with approval
pull_request_review:
types: [submitted]
issue_comment:
types: [created]

jobs:
changelog:
Expand Down
171 changes: 171 additions & 0 deletions .github/workflows/reusable-backport.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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[:[:space:]]\+\([^]]*\)\].*/\1/p' | tr '\n' ',' | sed 's/,$//')
else
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
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@v4
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: |
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")
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] #${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"
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 '$BACKPORT_BRANCH'"
exit 1
fi
if ! gh pr create \
--base "$BRANCH" \
--head "$BACKPORT_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
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 [ $CHERRY_PICK_STATUS -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"
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 "- **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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
single tag test
1 change: 1 addition & 0 deletions conflict-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
stable version
1 change: 1 addition & 0 deletions conflict_test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
different content on main
1 change: 1 addition & 0 deletions sc1-conflict.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version-B
1 change: 1 addition & 0 deletions test-backport.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix: scenario-1-test-1771442522