--- name: backport description: >- Backports a merged pull request from `main` onto a patch-release branch, e.g. `5.0.x`. Activates on `/backport [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 [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-`, 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 --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 --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-` (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- $CANON/`. - **Fresh**: `git worktree add -b backport-pr- .claude/worktrees/backport-pr- $CANON/`. - **Leftover branch** (worktree gone, branch remains, `git worktree add` errors with `a branch named '…' already exists`): glance at `git log backport-pr- ^$CANON/` to confirm nothing valuable, `git branch -D backport-pr-`, 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/ --grep="cherry picked from commit " --format=%H ``` Non-empty means already backported. Skip it and note that in the summary. Otherwise `git cherry-pick -x -s `. - **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 -- ` 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: ` 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` | `` | ... | `` 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- && git push -u origin backport-pr- ``` If any row is not `picked`, state that on one line above the table.