- name
- backport-pr
- description
- 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".
- argument-hint
- <pr-number-or-url> [target-major] # e.g. 18211 v10; target defaults to v10
# 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>`.
View on GitHub