Backporting CPython Changes
Do what cherry_picker (the tool behind the miss-islington bot) does, but intelligently: when the cherry-pick conflicts, adapt the change to the maintenance branch's own idioms instead of giving up. The output must be indistinguishable in form from an automated backport - same branch naming, same commit message format, same PR style - so the bots and human workflows built around cherry_picker keep working.
Inputs
From the invocation (e.g. /cpython:backport 3.13 https://github.com/python/cpython/pull/12345) or conversation, determine:
- Target maintenance branch (e.g.
3.13). If not given, check the original PR'sneeds backport to X.Ylabels and ask which one(s) to do. For multiple targets, work newest-first: each completed backport becomes the cherry-pick source for the next older branch (step 3), so the resolutions worked out for 3.14 carry down to 3.13 instead of being rediscovered. - Source change: a PR URL/number, or a commit sha on
main. Given only an issue number, find the merged PR that closed it. - Branch support phase: confirm the target branch accepts this kind of change (devguide versions table). A branch in security-only phase takes security fixes exclusively, with release-manager signoff - if the target is past bugfix support and this isn't a security fix, stop and ask rather than prepare a PR the release manager will close.
Workflow
1. Gather facts about the original change
gh pr view <N> --repo python/cpython --json number,title,author,mergeCommit,labels,url
git log -1 --format=%B <merge-sha> # full original commit message
Confirm the PR actually merged (mergeCommit non-null) - an open or closed-unmerged PR has nothing valid to backport. Given only a commit sha, recover the PR number from the (GH-N) suffix of its squash-merge title; the commit message format in step 4 depends on it.
Fetch upstream main first if the merge commit isn't local. Then look for sibling backport PRs:
gh pr list --repo python/cpython --state all --search '"GH-<N>" in:title'
A merged backport to a newer branch (e.g. the 3.14 backport when you're targeting 3.13) is more than a template: its merge commit already embodies the conflict resolutions needed to move the change one branch away from main, so prefer it as your cherry-pick source (step 3), fetching that maintenance branch so the commit is local — an older backport should never have to relearn what the newer one already worked out. It also supplies the commit message and PR body to mirror. A failed miss-islington attempt tells you exactly where the conflicts are.
2. Prepare the target branch
Work in a checkout of the maintenance branch - check git worktree list and sibling directories (e.g. ../3.13) before creating anything new. Sync it first:
git fetch upstream <version>
git checkout -b backport-<sha7>-<version> upstream/<version>
<sha7> is the first 7 characters of your source commit - the squash-merge on main, or the newer branch's backport merge commit when you're basing on one (step 3). This matches what cherry_picker itself would produce when run against that commit - do not deviate from the backport-<sha7>-<version> shape.
3. Cherry-pick and resolve conflicts intelligently
Choose the source commit closest to your target. Maintenance branches diverge from main progressively, so when a newer branch's backport of this change has already merged, cherry-pick that backport's merge commit instead of the original commit on main - the adaptations made for 3.14 carry the change most of the way to 3.13, often turning a conflicted pick into a clean one. Use the main commit only when no newer merged backport exists.
git cherry-pick -x <source-sha>
Conflicts are where you add value over the automated tool. For each conflicted hunk:
- Understand why it conflicts: the maintenance branch may predate a refactor. Common cases: helper APIs that don't exist yet (
*_LockHeldvariants,_Py*_CASTmacros from free-threading work), functions later converted to Argument Clinic, test files split into packages (test_os.pyvstest_os/), renamed or moved files. - Regenerate, don't hand-merge, generated files. For conflicts in generated artifacts (Argument Clinic output,
configure, frozen modules,pycore_global_strings.hand kin), resolve the source-of-truth file and rerun the target branch's own generator (make regen-*; forconfigure, the autoconf setup that branch pins). Hand-merged generator output can look clean yet fail themake regen-allconsistency check in CI. Doc/whatsnew/conflicts have a standard answer. An entry in main's whatsnew file can't apply where that file doesn't exist: if the change is user-visible on the target branch, move the entry into that branch's own current whatsnew file; otherwise drop the hunk.- Translate the fix into the target branch's existing idioms. Never introduce a symbol that doesn't exist on that branch. Find how the branch already does the equivalent thing - often a nearby function received the same fix in an earlier release and shows the exact local pattern to follow.
- Preserve semantics exactly, change nothing extra. The backport should be the original fix expressed in the older branch's dialect, not an improvement pass.
Misc/NEWS.d/ entries cherry-pick as-is with their original filename. If a change touches files that don't exist at all on the target branch, stop and ask - the fix may not apply or may need a different approach there.
4. Commit message - cherry_picker format
[<version>] <original title, any prior [X.Y] prefix removed> (GH-<original-PR-number>)
<original commit body>
(cherry picked from commit <full source sha>)
Co-authored-by: <Original Author <email>>
The (GH-NNNN) suffix must reference the original PR - bots use it to link the backport and manage labels. That holds even when you cherry-picked from a newer branch's backport: the cherry picked from line names the commit you actually picked, but the title's GH number, the "prior [X.Y] prefix removed" rule, and the co-author all still point at the original change from main. Add the Co-authored-by: trailer only when the person committing the backport differs from the original author (that's why automated backports have it: miss-islington is the committer).
5. Verify on the target branch
Start with a cheap structural check: git range-diff <source-sha>^..<source-sha> HEAD^..HEAD shows exactly what drifted between the original commit and your backport - fast confirmation of "preserve semantics, change nothing extra", and the honest raw material for the manual-backport-notes paragraph in step 6.
Then load the build skill: rebuild and run the tests the change touches - the full affected test files, not just the new cases. Load the style skill for pre-commit/patchcheck before finishing. A backport that doesn't compile or pass on the older branch is worse than no backport.
6. Push and PR - ask first
Never push or open a PR without confirmation; the user may need to push to their fork themselves (auth setups vary).
- Offer the push:
git push <fork-remote> backport-<sha7>-<version>(name the remote you found; don't assumeoriginis the fork). - Offer the PR once pushed, and ask whether it should be a draft:
gh pr create --repo python/cpython --base <version> \
--head <fork-owner>:backport-<sha7>-<version> \
--title "[<version>] <original title> (GH-<N>)" [--draft]
PR body: mirror the sibling automated backport's body when one exists (cherry-picked-from line, co-author trailer). If you resolved conflicts by hand, append a short "Manual backport notes ()" paragraph stating exactly what had to change for this branch and that the affected tests pass - and, when you based the pick on a newer branch's backport rather than main, say so (e.g. "cherry-picked from the 3.14 backport, GH-MMMMM"). Reviewers of manual backports need to know what differs from the original.
After creation, bedevere/miss-islington handle the needs backport to X.Y label swap automatically when the title matches [X.Y] ... (GH-NNNN) - no manual label edits needed.
Report
Whether or not a PR was opened, end with: the branch name and commit sha, what conflicted and how each conflict was adapted (or "clean cherry-pick"), test results, and the remaining steps with exact commands.