Automating backport pull requests with labels Jump to heading
Manual backporting has a familiar failure mode: the fix merges to main, everyone agrees it needs to go to the release branch, and three weeks later a customer reports the bug again because nobody did it. Label-driven automation makes the decision and the action the same step. A reviewer adds a label such as backport release/2.4 to the pull request; when it merges, a workflow cherry-picks it onto a new branch from release/2.4 and opens a pull request. If the cherry-pick conflicts, the workflow says so on the original pull request and names the person who should finish it. This page builds that workflow without third-party actions, within cherry-pick and backporting.
When to use this approach Jump to heading
- You maintain one or more release branches that receive fixes from main.
- Backports are sometimes forgotten, or done inconsistently.
- Fixes land on main first, and release branches only ever receive cherry-picks β the forward-only flow described in cherry-picking hotfixes across release branches.
- You want backports reviewed and tested like any other change, through pull requests.
Step 1 β Define labels that name target branches Jump to heading
One label per supported release branch, with a fixed prefix the workflow can parse. Create them in every repository that has release branches.
for b in release/2.3 release/2.4; do
gh label create "backport $b" --color FBCA04 --description "Cherry-pick to $b after merge" --force
done
gh label list | grep '^backport ' Step 2 β Trigger on merged pull requests with backport labels Jump to heading
Run on pull_request closed events, filter to merged ones that carry at least one backport label, and fan out one job per target branch.
# .github/workflows/backport.yml
on:
pull_request:
types: [closed, labeled]
permissions: { contents: write, pull-requests: write }
jobs:
targets:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
outputs: { branches: "${{ steps.t.outputs.branches }}" }
steps:
- id: t
run: |
echo "branches=$(jq -c '[.pull_request.labels[].name | select(startswith("backport ")) | sub("^backport ";"")]' "$GITHUB_EVENT_PATH")" >> "$GITHUB_OUTPUT"
backport:
needs: targets
if: needs.targets.outputs.branches != '[]'
strategy: { matrix: { target: "${{ fromJSON(needs.targets.outputs.branches) }}" }, fail-fast: false }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: ./ci/backport.sh "${{ matrix.target }}" "${{ github.event.pull_request.number }}" "${{ github.event.pull_request.merge_commit_sha }}"
env: { GH_TOKEN: "${{ github.token }}" } Including labeled in the trigger types means adding a label to an already-merged pull request also triggers a backport β useful when the need is discovered after merge.
Step 3 β Cherry-pick and open the pull request Jump to heading
The script handles merge commits and squash commits alike: it checks the parent count and adds -m 1 only for merges.
#!/bin/sh
# ci/backport.sh <target-branch> <pr-number> <merge-sha>
set -eu
target=$1 pr=$2 sha=$3
branch="backport/${target#release/}/pr-$pr"
git config user.name "backport-bot"; git config user.email "[email protected]"
git switch -c "$branch" "origin/$target"
parents=$(git rev-list --parents -n1 "$sha" | wc -w)
mflag=""; [ "$parents" -gt 2 ] && mflag="-m 1"
if git cherry-pick -x $mflag "$sha"; then
git push -u origin "$branch"
title=$(gh pr view "$pr" --json title --jq .title)
gh pr create --base "$target" --head "$branch" \
--title "[${target#release/}] $title" \
--body "Backport of #$pr to \`$target\`, created automatically."
else
git cherry-pick --abort
author=$(gh pr view "$pr" --json author --jq .author.login)
gh pr comment "$pr" --body "@$author automatic backport to \`$target\` hit conflicts. Run:
\`\`\`
git switch -c $branch origin/$target
git cherry-pick -x $mflag $sha
\`\`\`
then resolve and open a PR against \`$target\`."
exit 1
fi # Verification: label a merged test PR and watch for the new pull request
gh pr edit 812 --add-label "backport release/2.4"
gh run watch && gh pr list --base release/2.4 --head "backport/2.4/pr-812" Step 4 β Make backport pull requests flow through the normal gates Jump to heading
Backports must pass the release branchβs checks, which may differ from mainβs. Do not let the bot bypass review on release branches; a cherry-pick that applies cleanly can still be wrong for old code.
# CODEOWNERS on release branches: release managers approve every backport
* @acme/release-managers Pull requests opened with the default workflow token do not trigger other workflows on some forges, which would leave the backport without CI. If that applies to you, open the pull request with an app token instead, as described in verified bot commits with a GitHub App identity.
Step 5 β Report what was backported where Jump to heading
With every backport going through the same workflow and carrying -x trailers, producing a per-release list is a query, not an investigation.
# Fixes on release/2.4 that came from main, with their source commits
git log --format='%h %s' --grep='cherry picked from commit' v2.4.0..origin/release/2.4
# Pull requests labelled for 2.4 that do not yet have a merged backport
gh pr list --state merged --label "backport release/2.4" --json number,title --jq '.[] | "\(.number) \(.title)"' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should backports to several branches be chained, newest first? Jump to heading
The workflow above picks from main to each branch independently, which is simplest. When older branches conflict often, chaining β picking the 2.4 backport onto 2.3 β reduces conflicts because each step crosses less history. That requires the job for 2.3 to wait for the 2.4 backport to merge.
Can the bot auto-merge clean backports? Jump to heading
It can, if release branch CI is strong and the release managers are comfortable with it. Many teams require one approval anyway, because a clean cherry-pick is not proof that the fix suits old code.
What about existing tools that do this? Jump to heading
Several open-source backport actions and bots exist and handle edge cases well. The script here is short enough to understand fully, which matters for something that writes to release branches; use a tool when you need its extra features.
Related Jump to heading
- Cherry-Pick & Backporting β the parent topic.
- Cherry-Picking a Merge Commit with --mainline β the
-m 1logic the script uses. - Finding Unported Commits with git cherry β catching fixes that were never labelled.
- Auto-Labelling Pull Requests by Changed Path β other uses of label-driven automation.