| 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
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:
git fetch origin <major> develop
git rev-parse --verify origin/<major>
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:
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
git checkout -b <branch> origin/<major>
3. Cherry-pick the merge commit
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).
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.
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>/).
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:
git commit --amend --no-edit
Confirm the tree is clean so nothing is left uncommitted before you push:
git status --porcelain
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:
git show <mergeCommit-oid> --stat
git show HEAD --stat
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:
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.
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>.