| name | backport |
| description | Backports a merged pull request from `main` onto a patch-release branch, e.g. `5.0.x`. Activates on `/backport <PR-number> [target-branch]`, and whenever the user asks to backport, port, or cherry-pick a merged PR, commit, or fix onto a patch, release, or maintenance branch, including phrasings like "backport #1234 to 5.0.x", "cherry-pick that fix onto 5.0.x", or "get this into the next patch release". Not for forward-porting onto `main`! |
| allowed-tools | Bash, AskUserQuestion, Read, Edit |
Backporting a Pull Request
Automates the patch-release backport flow from RELEASING.md §Patch Releases. Invoke as:
/backport <PR-number> [target-branch]
Requires an authenticated gh CLI (gh auth status).
Rules (DO NOT VIOLATE)
- Never
git push. Print the push command at the end and let the user run it.
- Never add
Co-Authored-By: Claude ... to any commit.
- Always cherry-pick with
git cherry-pick -x -s (records origin SHA, adds signoff, matches existing patch-branch history).
- Always work in
.claude/worktrees/backport-pr-<N>, never in the primary checkout.
- Never hand-format code. If a resolution touches source, run
npm run prettier-fix before committing.
- If a conflict cannot be resolved unambiguously and does not apply conceptually, ask the user via
AskUserQuestion. DO NOT IMPROVISE.
Resolving the canonical remote
origin may point at a fork. Resolve the canonical remote once and use $CANON everywhere below:
CANON=$(git remote -v | awk '/DependencyTrack\/frontend.*\(fetch\)/ {print $1; exit}')
If empty, ask the user which remote tracks the canonical repo.
Workflow
1. Validate state
-
Confirm CWD is the primary repo (not already a worktree).
-
Resolve $CANON per §Resolving the canonical remote, then git fetch $CANON.
-
If target-branch was omitted, derive it from the PR's backport label:
gh pr view <N> --json labels -q '.labels[].name | select(startswith("backport/"))'
If backport/5.0.5, the target branch is 5.0.x. On zero or multiple matches,
ask the user via AskUserQuestion, offering branches matching [0-9]+\.[0-9]+\.x.
2. Locate the PR's commits
gh pr view <N> --json state,baseRefName,mergeCommit
state is not MERGED: abort. DO NOT GUESS.
baseRefName is not main: tell the user which branch the PR targeted and ask before continuing.
(master still exists on the remote but is stale. Treat it like any other unexpected base.)
Take MERGE from mergeCommit.oid, then check how it was merged:
git rev-parse --verify --quiet "${MERGE}^2"
3. Set up the worktree
Path: .claude/worktrees/backport-pr-<N> (in-tree, git-ignored).
- Reuse (path exists, worktree registered):
cd in, verify git status is clean (else ask the user), then git checkout -B backport-pr-<N> $CANON/<target-branch>.
- Fresh:
git worktree add -b backport-pr-<N> .claude/worktrees/backport-pr-<N> $CANON/<target-branch>.
- Leftover branch (worktree gone, branch remains,
git worktree add errors with a branch named '…' already exists): glance at git log backport-pr-<N> ^$CANON/<target-branch> to confirm nothing valuable, git branch -D backport-pr-<N>, retry.
- If
git worktree add half-succeeded (partial directory plus a stale branch), delete both and git worktree prune before retrying.
A fresh worktree has no node_modules. Only install (§6) if a check actually needs it.
4. Apply each commit
For each SHA from step 2, in order.
First, skip what is already there:
git log $CANON/<target-branch> --grep="cherry picked from commit <sha>" --format=%H
Non-empty means already backported. Skip it and note that in the summary.
Otherwise git cherry-pick -x -s <sha>.
- Clean: continue.
- Trivial conflict (import order, non-overlapping adjacent edits): resolve,
git add, GIT_EDITOR=true git cherry-pick --continue (--continue opens $EDITOR and hangs otherwise).
- Non-trivial but conceptually applies:
git cherry-pick --abort, recreate manually, commit per §Manual commit format.
- Does not apply conceptually (target refactored/removed):
git cherry-pick --abort, then AskUserQuestion with options (skip / reduced port / port differently). DO NOT INVENT A RESOLUTION.
Inspecting a conflict before resolving
Conflict markers can include unrelated main-only lines that anchored the hunk's context. Naively accepting "incoming" smuggles those into the backport.
Before resolving, run git show <sha> -- <conflicted-file> to show the authoritative diff. If the >>>>>>> side has extra lines git show doesn't list, drop them.
Locale files
src/i18n/locales/*.json conflict constantly.
Keys are sorted alphabetically and every locale carries every key.
Resolve per key, not per hunk:
- Take only the keys the picked commit actually adds or changes, leave the rest at the target branch's state.
- A new key goes into
en.json with its English value and into every other locale file with a null value.
- Run
npm run prettier-fix afterwards to restore key order and formatting.
5. Manual commit format
For manually-recreated commits (not cherry-picked):
- Mirror the original subject + body.
- Add
Co-Authored-By: <Name> <email> for the original commit's author. Omit if that email equals git config user.email. Never add Co-Authored-By: Claude ....
git commit -s (adds Signed-off-by). Author identity = default git config. Pass the message via HEREDOC.
6. Post-backport checks
Run from the worktree. Anything beyond the first row needs dependencies first:
npm ci (uses the target branch's package-lock.json, never copy or symlink node_modules
from the primary checkout, the lockfiles differ across branches).
| If any commit touches | Run |
|---|
nothing but .github/**, docker/**, docs | nothing |
src/i18n/locales/** | npm run vue-i18n-extract |
src/**, package.json, *.config.js | npm run prettier then npm run build |
npm run eslint is covered by lint.yaml in CI and is slow
locally. Run it only if a resolution changed non-trivial logic.
On failure, report and stop.
7. Summary
Print, in this order:
-
The worktree path.
-
One row per commit from step 2. Every commit gets a row, including skipped ones:
| Status | Commit | Subject |
|---|
picked / manual / skipped / already | <short-sha> | ... |
<short-sha> is the source commit on main, not the new one. For skipped, give the reason.
-
The push command (DO NOT RUN IT):
cd .claude/worktrees/backport-pr-<N> && git push -u origin backport-pr-<N>
If any row is not picked, state that on one line above the table.