chore(actions): Automated backport to release branches (#65)

* chore(actions): automated backport to release branches

* nit
This commit is contained in:
Cal Bera 2025-08-14 11:34:17 -07:00 committed by GitHub
parent 74b165a362
commit 58a72d4087
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 242 additions and 0 deletions

106
.github/workflows/BACKPORT_README.md vendored Normal file
View file

@ -0,0 +1,106 @@
# Backport Workflow Documentation
## Overview
This repository uses an automated backport workflow to cherry-pick merged pull requests from the `main` branch to release branches. The workflow is powered by the [korthout/backport-action](https://github.com/marketplace/actions/backport-merged-pull-requests-to-selected-branches).
## How It Works
### Automatic Backporting via Labels
1. **Add a backport label** to your pull request before or after merging:
- Label format: `backport/{version}`
- Examples: `backport/v1.0.0`, `backport/v2.3.1`, `backport/1.0`
2. **When the PR is merged to main**, the workflow automatically:
- Detects all `backport/*` labels
- Transforms them to target `release/{version}` branches
- Creates new pull requests with cherry-picked commits to each target branch
3. **Target branches** must exist:
- For label `backport/v1.0.0` → targets branch `release/v1.0.0`
- For label `backport/2.0` → targets branch `release/2.0`
- The release branch must already exist in the repository
### Manual Backporting via Comments
You can also trigger backports manually by commenting on a merged PR:
- Comment `/backport` on any merged pull request
- The workflow will look for backport labels and create the appropriate PRs
## Example Workflow
1. Create a pull request to `main` with your changes
2. Add label: `backport/v1.2.3`
3. Get the PR reviewed and merged
4. The bot automatically:
- Creates a new branch: `backport-{PR-number}-to-release-v1.2.3`
- Cherry-picks the commits from your merged PR
- Opens a new PR from that branch to `release/v1.2.3`
- Adds labels: `backport`, `automated`
- Assigns the original PR author
## Handling Multiple Backports
You can backport to multiple release branches by adding multiple labels:
- `backport/v1.0.0` - backports to `release/v1.0.0`
- `backport/v2.0.0` - backports to `release/v2.0.0`
- `backport/v3.0.0` - backports to `release/v3.0.0`
Each label will create a separate backport PR.
## Conflict Resolution
When cherry-pick conflicts occur:
1. The bot creates a **draft PR** with the conflicts committed
2. The draft PR includes instructions on how to resolve the conflicts
3. You need to:
- Check out the backport branch locally
- Resolve the conflicts
- Push the resolved changes
- Mark the PR as ready for review
## Backport PR Format
Backport PRs are created with:
- **Title**: `[Backport release/{version}] {original PR title}`
- **Description**: Includes original PR information, author, and description
- **Labels**: `backport`, `automated`
- **Assignee**: Original PR author
## Prerequisites
For the workflow to function correctly:
1. Release branches must follow the naming pattern: `release/{version}`
2. The workflow must have write permissions (already configured)
3. Release branches must exist before attempting to backport
## Troubleshooting
### Backport not triggering
- Ensure the PR is merged to `main` (not just closed)
- Verify the label format is exactly `backport/{version}`
- Check that the target `release/{version}` branch exists
### Conflicts in backport
- The bot will create a draft PR with conflicts
- Follow the instructions in the PR to resolve conflicts locally
### Multiple commits or merge commits
- The workflow is configured to skip merge commits
- Only non-merge commits will be cherry-picked
- If you need merge commits, update the `merge_commits` setting in the workflow
## Configuration
The workflow is defined in `.github/workflows/backport.yml`. Key settings:
- **Label pattern**: `backport/{version}`
- **Target branch pattern**: `release/{version}`
- **Conflict strategy**: Create draft PRs with conflicts
- **Merge commits**: Skipped (only cherry-picks non-merge commits)
## Security
- The workflow uses `GITHUB_TOKEN` for authentication
- Runs on `pull_request_target` to handle forks securely
- Only triggers on merged PRs to prevent abuse

136
.github/workflows/backport.yml vendored Normal file
View file

@ -0,0 +1,136 @@
name: Backport merged pull request
on:
pull_request_target:
types: [closed]
issue_comment:
types: [created]
permissions:
contents: write # Required to push branches
pull-requests: write # Required to create pull requests
jobs:
backport:
name: Backport pull request
runs-on: ubuntu-latest
# Only run when:
# - A pull request to main is merged (not just closed)
# - OR a comment starting with '/backport' is created by someone other than the bot
if: >
(
github.event_name == 'pull_request_target' &&
github.event.pull_request.merged &&
github.event.pull_request.base.ref == 'main'
) || (
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.user.id != 97796249 &&
startsWith(github.event.comment.body, '/backport')
)
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
# Fetch all history for all branches
fetch-depth: 0
- name: Extract backport targets from labels
id: extract-targets
uses: actions/github-script@v7
with:
script: |
const labels = context.payload.pull_request?.labels || [];
const backportLabels = labels
.map(label => label.name)
.filter(name => name.startsWith('backport/'))
.map(name => {
// Extract version from "backport/{version}"
const version = name.replace('backport/', '');
// Transform to "release/{version}"
return `release/${version}`;
});
if (backportLabels.length > 0) {
console.log(`Found backport targets: ${backportLabels.join(', ')}`);
// Join with spaces as the backport action expects
core.setOutput('targets', backportLabels.join(' '));
core.setOutput('has-targets', 'true');
} else {
console.log('No backport labels found');
core.setOutput('has-targets', 'false');
}
// Also output the original labels for the action
const originalLabels = labels
.map(label => label.name)
.filter(name => name.startsWith('backport/'))
.map(name => {
// Transform "backport/{version}" to "backport release/{version}"
const version = name.replace('backport/', '');
return `backport release/${version}`;
})
.join(',');
core.setOutput('transformed-labels', originalLabels);
- name: Create backport pull requests
if: steps.extract-targets.outputs.has-targets == 'true'
uses: korthout/backport-action@v3
with:
# GitHub token for authentication
github_token: ${{ secrets.GITHUB_TOKEN }}
# Directly specify target branches extracted from labels
target_branches: ${{ steps.extract-targets.outputs.targets }}
# Disable label pattern matching since we're using target_branches
label_pattern: ""
# Branch name template for the backport branch
# This creates branches like: backport-123-to-release-v1.0.0
branch_name: "backport-${pull_number}-to-${target_branch}"
# Title template for the backport PR
pull_title: "[Backport ${target_branch}] ${pull_title}"
# Description template for the backport PR
pull_description: |
# Backport to ${target_branch}
This is an automatic backport of pull request #${pull_number} to `${target_branch}`.
## Original PR Information
- **Title**: ${pull_title}
- **Author**: @${pull_author}
- **Original PR**: #${pull_number}
## Original Description
${pull_description}
## Related Issues
${issue_refs}
---
*This pull request was created automatically by the backport action.*
# Labels to add to the backport PR
add_labels: "automated"
# Add original PR author as assignee
add_author_as_assignee: true
# What to do when there are conflicts
# Options: "fail" or "draft_commit_conflicts"
# Using draft_commit_conflicts so conflicts don't block the process
conflicts_strategy: "draft_commit_conflicts"
# Cherry-picking strategy
# Options: "auto", "recursive", "ort"
cherry_picking: "auto"
# How to handle merge commits
# Options: "fail" or "skip"
# Using skip to handle PRs that were kept in sync with base via merge commits
merge_commits: "skip"