- 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`](../../../RELEASING.md) §Patch Releases. Invoke as:
```
/backport <PR-number> [target-branch]
```
Requires an authenticated [`gh`](https://cli.github.com/) 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:
```sh
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:
```sh
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
```sh
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:
```sh
git rev-parse --verify --quiet "${MERGE}^2"
```
- **Merge commit** (`^2` resolves): the PR's commits are preserved in `main`.
```sh
git log --reverse --format=%H "${MERGE}^1..${MERGE}^2" # oldest first
```
- **Squash or rebase merge** (`^2` missing): `MERGE` itself is the only commit to pick.
### 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:
```sh
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`](../../../.github/workflows/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:
1. The worktree path.
2. 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.
3. The push command (DO NOT RUN IT):
```sh
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.
عرض على GitHub