# Backport Pr

> Backport a merged PR to a maintenance major branch (v10 by default) in getsentry/sentry-javascript. Cherry-picks the PR's squash-merge commit onto the target branch, namespaces the commit/PR title scope (e.g. fix(core) -> fix(v10/core)), opens a draft backport PR, and documents any deviation from the original PR in its body. Use when asked to backport a PR, or port a fix to v10 (or an older major like v9). Trigger phrases include "backport", "port to v10", "release this on v10".

- Skill: `getsentry/backport-pr` (Agent Skill)
- Install (CLI): `npx skillmds@latest add getsentry/backport-pr`
- Raw SKILL.md: https://api.skillmd.com/api/skills/getsentry/backport-pr/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Sentry (https://skillmd.com/u/getsentry)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/getsentry/backport-pr

---


# Backport a PR to a maintenance major branch

`develop` is the current major (v11). A change that also needs to ship on a still-maintained
older major has to land on that major's branch too (`v10` by default). This skill cherry-picks
a merged `develop` PR onto that branch and opens a draft backport PR.

## Inputs

- **PR** (required): the already-merged PR on `develop` to backport, given as either a full
  GitHub URL or a bare number. `gh pr view` accepts both, so pass whichever the user gave
  through unchanged; `<PR>` in the commands below is that value.
- **Target major** (optional, default `v10`): the maintenance branch to backport onto.
  Accept `v10`, `10`, `v9`, etc. Normalize to a branch name like `v10`.

If no PR is given, ask for it. Do not guess.

## Convention

- **Base branch** = the target major branch (`v10`), which must already exist on `origin`.
- **Commit + PR title**: keep the original conventional-commit prefix but namespace the
  scope with the major, e.g.
  - `fix(core): Fix logs flush starvation` -> `fix(v10/core): Fix logs flush starvation`
  - `feat(node): Add X` -> `feat(v10/node): Add X`
  - If the original has no scope (e.g. `fix: ...`), use `fix(v10): ...`.
  - For a multi-scope title, prefix the whole group once, not each scope:
    `fix(cloudflare,deno,node): ...` -> `fix(v10/cloudflare,deno,node): ...`.
- **PR body** starts with a single line: `Backport of: #<original-pr-number>`. If the backported
  change deviates from the original in substance, append a `## Differences to the original PR`
  section listing each deviation and why it was necessary (see step 4). A clean cherry-pick
  keeps the body at the single line.
- **PR is opened as a draft.**
- **Working branch**: branch off the target major and give it a descriptive name.
- The changes come from the PR's **squash-merge commit** on `develop` (one commit per PR),
  so a single `git cherry-pick` normally covers the whole PR.

## Steps

### 1. Resolve the PR and target branch

```bash
# Fetch PR metadata (title, merge commit, base branch)
gh pr view <PR> --json number,title,baseRefName,mergeCommit,state,url
```

Verify:

- The PR is **merged** (`state == "MERGED"`). If not, stop and tell the user.
- Its `baseRefName` is `develop` (or the expected parent major). If it targeted something
  else, confirm with the user before continuing.

Grab `mergeCommit.oid` — this is the squash commit to cherry-pick. Also grab `number`: use
that bare number (not the raw input) wherever `#<PR>` appears below, so `Backport of:` reads
`Backport of: #18211` even when the user passed a URL.

Make sure the target branch exists and is up to date:

```bash
git fetch origin <major> develop
git rev-parse --verify origin/<major>   # errors if the branch doesn't exist
```

If `origin/<major>` doesn't exist, stop: the maintenance branch hasn't been created yet.

Then check whether the change is already on the target. A freshly cut major often still shares
history with `develop`, so a recent PR may already be present:

```bash
git merge-base --is-ancestor <mergeCommit-oid> origin/<major> && echo "ALREADY ON <major>"
```

If it prints `ALREADY ON`, the commit is in the target's history — usually meaning nothing to
backport. It's not conclusive on its own, though: a commit that was later reverted on the
maintenance branch still shows as an ancestor. So treat this as a strong signal to stop and
tell the user, but if you have reason to think the change was reverted, confirm the fix is
actually present (e.g. `git log origin/<major> -- <a changed file>`, or grep for the change)
before deciding. The cherry-pick in step 3 is the real backstop — it comes up empty only when
the change is genuinely still applied.

### 2. Create the backport branch off the target major

```bash
git checkout -b <branch> origin/<major>
```

### 3. Cherry-pick the merge commit

```bash
git cherry-pick <mergeCommit-oid>
```

- If git reports the pick is **empty** ("nothing to commit" / "the previous cherry-pick is
  now empty"), the change is already on the target. Run `git cherry-pick --abort` and stop —
  do not force it through with `--allow-empty`. This is the same situation the ancestor check
  in step 1 guards against, caught here for changes that landed via a different commit.
- On **conflicts**: resolve them by consulting the original diff (`git show <oid>`).
  The target major may lack refactors that landed on `develop`, so adapt the change to the
  older code rather than force-porting it. Stage the resolved files with `git add -u` (tracked
  files only, so stray untracked workspace files don't get baked in), then
  `git cherry-pick --continue`. If the change can't be cleanly adapted, stop and surface the
  conflict to the user instead of guessing.
  Keep a running note of every adaptation you make here — which file, what you changed relative
  to the original, and why the target major needed it. Step 6 turns these notes into the PR
  body, and reconstructing them after the fact is unreliable.
- If the PR was **not** squash-merged (multiple commits, e.g. a merge commit), cherry-pick
  each relevant commit in order, or use `git cherry-pick -m 1 <merge-oid>` for a merge commit.

### 4. Build and verify

Run the repo's pre-commit checks. Do this **before** finalizing the commit in step 5, because
`yarn format` writes changes to the working tree — those fixes must end up inside the backport
commit, not left dangling after it (otherwise you'd push an unformatted tree and CI would fail
on a commit that doesn't match your local state).

```bash
yarn format
yarn lint:fix
yarn build:dev
```

Use `lint:fix`, not `lint` — plain `yarn lint` only reports, so auto-fixable issues would
otherwise survive to fail CI.

Run tests scoped to the touched packages when possible (full `yarn test` if unsure). If the
target major's toolchain differs and a check fails for reasons unrelated to the change, note
it for the user rather than silently skipping.

### 5. Finalize the commit (fold in verification changes)

First stage the format/lint fixes. Use `git add -u` so only tracked files the cherry-pick and
verification touched are staged, not unrelated local edits — sanity-check with `git status`
first if `yarn format` may have reformatted files outside the backport.

```bash
git add -u
```

Then finalize, depending on how step 3 went:

**Single squash commit (the usual case)** — amend HEAD to both namespace the subject scope and
fold in the staged fixes. The message is the namespaced title plus the one-line `Backport of:`
body (this replaces the squash-merge body, matching the convention above). Do **not** add a
`Co-Authored-By` line — the backport commit mirrors an existing commit, not new authored work.

Build the title as in the convention: `<prefix>(<major>/<scope>):` when the original had a
scope, or `<prefix>(<major>):` when it didn't (never emit an empty `<major>/`).

```bash
git commit --amend -m "<namespaced-title>" -m "Backport of: #<PR>"
```

Example subject: `fix(v10/core): Fix logs flush timeout starvation with continuous logging`

**Multiple commits (non-squash merge)** — leave the individual commit messages as-is; the
namespaced title lives on the PR (step 7), not on each commit. Just fold the staged fixes into
HEAD without rewording:

```bash
git commit --amend --no-edit
```

Confirm the tree is clean so nothing is left uncommitted before you push:

```bash
git status --porcelain   # expect no output
```

### 6. Diff the backport against the original PR

Even a cherry-pick that reported no conflicts can end up different — `yarn format`/`lint:fix`
on the older toolchain, dropped hunks, adaptations you made in step 3. Compare the final commit
against the original before writing the PR body, rather than trusting your memory of step 3:

```bash
git show <mergeCommit-oid> --stat
git show HEAD --stat            # file lists should match

diff <(git show <mergeCommit-oid>) <(git show HEAD)
```

Ignore noise that carries no meaning: commit hashes/headers, hunk line offsets, surrounding
context lines, and the reworded commit message. What counts as a **difference** is a change in
what the patch does — a file added or missing, a hunk dropped or added, changed code or test
expectations, an API adapted to the older major, a version/changelog entry that only exists on
one branch.

If the only differences are noise, the PR body stays the single `Backport of:` line. Otherwise
draft a section like:

```markdown
Backport of: #<PR>

## Differences to the original PR

- `packages/core/src/foo.ts`: used `getClient()` instead of `getCurrentScope().getClient()` — v10 has no `getClient()` on the scope.
- Dropped the `bar.test.ts` case for `baz()`; that helper doesn't exist on v10.
```

One bullet per deviation: what changed, and why the target major required it. Keep it factual —
this is the reviewer's diff-of-the-diff, not a summary of the original PR.

If a difference means the backport doesn't fully deliver the original fix, say so explicitly in
the PR body **and** to the user — that's a review decision, not a footnote.

### 7. Push and open the draft PR

The `Backport of: #<PR>` line references the original PR, so GitHub cross-links the two
automatically — no separate comment needed.

```bash
git push -u origin <branch>

gh pr create \
  --draft \
  --base <major> \
  --title "<namespaced-title>" \
  --body "Backport of: #<PR>"
```

When step 6 produced a differences section, pass the multi-line body via `--body-file` instead
(write it to a scratch file first) so the markdown survives intact.

## Notes

- Never push directly to `develop`, `master`, or the major branch. Work only on your
  backport branch and open a PR.
- One PR per backport. If asked to backport several PRs, repeat the whole flow per PR (each
  gets its own branch and draft PR).
- If asked to backport to multiple majors at once (e.g. v10 and v9), do them as separate
  branches/PRs, each based off its own `origin/<major>`.

