| name | backport |
| description | Backport changes (current branch, a PR, a branch, or specific commits/SHAs) onto a target branch (typically a release branch like `rel/*`). Use when the user says "backport this", "backport PR ...", "cherry-pick to <branch>", or asks to port commits to another branch. Handles stashing uncommitted work, branch creation, cherry-picking with conflict handling, pushing, and opening a PR. |
Backport Skill
Create a backport pull request that applies changes from a source (current branch, a PR, another branch, or specific commits) onto a target branch — interactively, from the local workspace. The target is typically a release branch (e.g. rel/0.32), but any existing branch is allowed except the source's own base branch (a backport onto the same base is a no-op).
Inputs to resolve
Before running any git commands, determine:
- Source — one of:
- Current branch (default if the user says "backport this" without specifying).
- PR number (e.g. "backport PR #1234").
- Branch name.
- Commit SHA(s) or range.
- Target branch — any existing branch on
origin. Typical case is a release branch (rel/*). If the user did not specify, list candidates with git branch -r --list 'origin/rel/*' first; if none match or the user wants something else, fall back to git branch -r and ask. Refuse the source's own base branch (e.g. if the PR or current branch already targets main, refuse main; if it targets rel/0.32, refuse rel/0.32) — a backport onto the same base is a no-op. Verify the target exists on origin before proceeding.
- Squash — only if the user explicitly says "squash". Default: no squash.
If anything is ambiguous, ask once before touching the working tree.
Workflow
Execute these phases in order. Stop and report on any error.
Phase A — Pre-flight & safety
- Verify tooling:
git --version, gh --version, gh auth status. If gh is missing or unauthenticated, abort with a clear message — do not continue. Cloud-agent override: when running as the GitHub.com cloud agent (see "Running as the GitHub cloud agent" below), a failing gh auth status must not abort the backport — the git work uses the platform's credential helper, not gh. See that section.
- Capture state: original branch (
git rev-parse --abbrev-ref HEAD), git status --porcelain, list of unpushed commits.
- If source ≠ current branch and the working tree is dirty (including untracked files):
- Run
git stash push -u -m "backport-skill autostash <ISO-timestamp>".
- Record the resulting stash ref (
git stash list -1 --format=%gd) so it can be restored later.
- If source = current branch: do not stash. Warn the user if there are uncommitted changes that won't be included, and confirm before proceeding.
git fetch origin <target>. Abort if the target branch does not exist on origin.
Phase B — Resolve the commit list
- PR source:
gh pr view <num> --json number,title,body,headRefName,baseRefName,mergeCommit,commits,state. If the command fails (non-zero exit, PR not found, or insufficient permissions), abort with a clear error message showing the PR number and the gh error output. Then branch on state:
MERGED: prefer the squash-merge commit if the PR was squash-merged; otherwise use the listed commits in order.
OPEN: warn that backporting an unmerged PR may include incomplete or intermediate work, and confirm with the user before proceeding. Use the listed commits in order.
CLOSED (not merged): legitimate but unusual. Warn the user that the PR was closed without merging (the work may have been abandoned, superseded, or rewritten elsewhere) and confirm before proceeding. Use the listed commits in order, or offer squash-and-reapply if the user prefers a single commit.
- Branch source:
git log --reverse --format=%H origin/<target>..<branch>.
- SHA(s) / range: use as given (validate with
git cat-file -e <sha>).
Show the resolved list (count + short log) to the user and confirm before continuing.
Phase C — Create the backport branch
- Branch name:
backport/<id>-to-<target-slug> where:
- Prefix is
backport/ for local runs (the default — this skill running in a VS Code workspace). Use the copilot/ prefix only when running as the GitHub.com cloud agent; see "Running as the GitHub cloud agent" below. If you are executing git commands on the user's local machine, you are not the cloud agent — use backport/.
<id> is the PR number (PR source), the source branch's last segment, or <short-sha> (single-commit / range).
<target-slug> is the target branch with every / replaced by -. Compute it, don't hand-write it (e.g. in shell: target_slug="${target//\//-}"). Examples: rel/0.32 → rel-0.32, rel/0.34 → rel-0.34.
- Full example: PR #3036 onto
rel/0.34 gives branch backport/3036-to-rel-0.34 (correct), not backport/3036-to-rel/0.34 (wrong — the / from the target was left in).
- Verify before checkout: the final branch name must contain exactly one
/ (the separator right after backport). If it contains two, the slug wasn't applied — recompute it before creating the branch.
- Collision handling (local or
origin/<branch> exists): ask the user — overwrite (delete local + remote, recreate) or use a numeric suffix (-2, -3, …). Never silently overwrite.
git checkout -b <name> origin/<target>.
Phase D — Cherry-pick
- Squash:
git merge --squash <source> then git commit -m "Backport #<n>: <original-title>" (or a synthesized title for non-PR sources).
- Otherwise:
git cherry-pick <sha…> in order.
Conflict policy (relaxed but safe):
Cloud-agent override: when running inside the GitHub Copilot cloud agent (see "Running as the GitHub cloud agent" below), use the stricter policy: auto-resolve only trivial cases per step 2; for anything ambiguous, do not invent a resolution — commit the conflict markers as a WIP: commit and mark the PR as draft. Skip steps 3 and 4 (no interactive prompts, no squash-and-retry).
- On conflict, run
git status and git diff to inspect.
- Auto-resolve only trivial cases:
- Import-order: only the order of
import / require statements differs; identifiers are identical.
- Formatting-only: whitespace, trailing comma, or semicolon changes where no identifiers, literals, or control flow differ between sides.
- Additive non-overlapping hunks: one side adds lines while the other side is unchanged in that region.
- Pure deletions on one side: one side deletes a block, the other leaves it untouched, and the deleted block is not referenced by code added on either side.
After resolving, verify with
git diff --check and ! grep -R '<<<<<<<' -- . before staging.
- For anything ambiguous (semantic overlap, both branches modified the same hunk meaningfully, version/lockfile bumps, generated files): stop and present the conflicting files and hunks to the user. Offer:
- Resolve manually & continue — wait for the user, then
git add + git cherry-pick --continue.
- Abort —
git cherry-pick --abort, delete the backport branch, restore (Phase G).
- Squash and retry — only when (a) more than one commit, (b) the current run was not already squash, (c) the conflicting commit is not the last. Abort current cherry-pick, delete the branch, restart from Phase C with squash enabled.
- Record every conflict and its resolution for the PR body.
Phase E — Local validation (recommended)
Cherry-picks onto older release branches often produce code that compiles on the source's base but breaks on the target (different deps, removed APIs, stricter lint config). Catch this before pushing:
- Ask the user whether to run validation. Default: yes. Offer to skip for speed.
- If yes, run the repo's standard checks following .github/copilot-instructions.md. Because the working tree was switched from the original branch to
origin/<target> and may have been mutated by the cherry-pick, node_modules is almost certainly stale — always start with npm install. Then run, in this order: npm run build, npm run l10n, npm run prettier-fix, npm run lint. Always run npm run l10n regardless of whether the cherry-pick obviously touches user-facing strings: dependency bumps can alter embedded error messages, and the target release branch may carry pre-existing l10n drift that the CI l10n:check will fail on. If the bundle changes, commit the regenerated bundle as a separate chore: regenerate l10n bundle commit on the backport branch before the Phase F push.
- Pull translations from the source's base branch — only when the user opted into validation in step 1, the cherry-pick changed
l10n/bundle.l10n.json (English bundle), and the source PR is merged. After the original PR merged, a localization bot typically commits translated strings to the source's base (e.g. main) — those translations apply equally to the backport and should not be re-translated by hand. Pull only the affected keys, never wholesale-replace language files: the source base may have other strings the target branch must not gain. Procedure:
- Determine the set of changed keys in the English bundle on the backport branch versus the target. Compare keys (top-level JSON object keys) between
git show origin/<target>:l10n/bundle.l10n.json and the current l10n/bundle.l10n.json. Record:
- Added keys — present in current, absent in target.
- Modified keys — present in both but with different values (rare, but possible if the English text changed).
- Removed keys are irrelevant for translation pulling (already gone from the English bundle, will be dropped from language files by
npm run l10n).
- Do the same comparison for
package.nls.json (its translations live in package.nls.<lang>.json).
- For each language file (
l10n/bundle.l10n.<lang>.json and package.nls.<lang>.json, every present in the repo), read the source-base version with , then for each added/modified key copy that key's translated value into the local language file. Leave all other keys in the local file untouched. of the local file: translation pipelines often write CRLF and use a non-obvious collation order, and re-sorting or re-serializing produces a massive cosmetic diff that reviewers will reject. Insert each new key adjacent to the nearest preceding key (in source-base order) that already exists locally; serialize with , then convert back to if the local file used CRLF. If the source-base version is missing a key (translation bot hasn't run yet), skip it — will leave the English fallback.
Phase F — Push & open the PR
git push -u origin <branch> — never --force or --force-with-lease. If the push fails, surface the full git error. If it is a permission or branch-protection error (e.g. protected-branch hook, missing write access), advise the user to check repository settings; do not retry with force flags. Proceed to Phase G failure cleanup.
- Write the PR body to a file first, then pass it with
--body-file. Prefer the editor's file-creation tool (cross-platform); if you use a shell heredoc, write to the OS temp directory outside the repo so it can't be accidentally staged or committed — e.g. "${TMPDIR:-/tmp}/backport-body.md" on macOS/Linux or "$env:TEMP\backport-body.md" on Windows PowerShell — and delete it once the PR is created. Then run gh pr create --base <target> --head <branch> --title "[<target>] <original-title>" --body-file <path>. Always use --body-file; never --body "…". An inline multi-line body gets mangled — the shell eats backticks/$/quotes and any literal \n sequences are printed verbatim in the PR description. Authoring rules for the body file:
- Real newlines only. Type actual line breaks; never write the two characters
\n to mean a newline.
- Do not hard-wrap sentences. Keep each paragraph or bullet on a single line and separate blocks with one blank line. In PR descriptions and comments (unlike rendered
.md files in a repo) GitHub honors a single newline as a hard line break, so column-wrapped prose shows up broken mid-sentence; hard-wrapping also makes the body diff noisily and read awkwardly in plaintext previews. If you reuse the original PR description, re-flow it first — strip any existing column wrapping so each paragraph is one continuous line.
- No task-list checkboxes (
- [ ] / - [x]). GitHub turns them into a "N tasks done / Progress 100%" bar on the PR list, and pre-checked boxes falsely imply a reviewer checklist is already complete. Use plain - bullets for the validation summary and everything else.
- Title: must start with the target branch in square brackets, e.g.
[rel/0.34] Fix tree refresh race. Use the exact target branch name (including any rel/ prefix) and keep the rest identical to the original PR title (or first commit subject for non-PR sources).
- (or / SHA list for non-PR sources); the original PR description (when applicable); a section listing each file + a one-line description, when applicable.
Phase G — Cleanup
- On success:
git checkout <originalBranch>. If a stash was created in A.3, git stash pop <stashRef>. Print the new PR URL.
- On failure or user-requested abort: leave the backport branch and stash intact. Print the stash ref (if any) and the recovery commands (
git checkout <originalBranch> && git stash pop <stashRef>).
Constraints
- Refuse the source's own base branch as the target (a backport onto the same base is a no-op). Any other existing branch on
origin is allowed. main is allowed only when it is not the source's base; if the source already targets main, refuse main per the rule above.
- Never use
--force / --force-with-lease.
- Preserve original commit messages during cherry-pick.
- Do not modify files outside what is required for conflict resolution or to fix validation failures introduced by the cherry-pick.
- Never silently overwrite an existing local or remote branch.
- Never auto-resolve ambiguous conflicts; always ask.
- Never restore the autostash on failure — leave it for the user to inspect.
- Branch prefix: use
backport/ for local runs. Use the copilot/ prefix only when running as the GitHub.com cloud agent (it cannot push other prefixes). Never use copilot/ for a local run.
- Branch slug: replace every
/ in the target with - when building the branch name; the final name has exactly one / (right after backport/copilot). Compute the slug, don't hand-copy the target branch.
- PR body: always pass it via
--body-file, never inline --body. No literal \n, no hard-wrapped sentences, no task-list checkboxes (- [ ] / - [x]).
Reporting
When done, summarize:
- Source, target, backport branch name, commits cherry-picked.
- Any conflicts encountered and how they were resolved.
- Whether local validation ran and its result (or that the user opted to skip).
- The new PR URL.
- Whether the original branch and stash were restored.
Running as the GitHub cloud agent
When this skill runs inside the GitHub Copilot cloud agent (e.g. invoked by @copilot on a merged PR or assigned to a backport issue), the environment differs from a local VS Code workspace. Adjust the workflow as follows:
- No interactive prompts. You cannot pause to ask the user. Resolve everything from the request body and repository state up front; if a required input is missing or ambiguous (target branch, source PR), stop and report rather than guess.
- Do not abort on
gh auth status failure (Phase A step 1 override). In the cloud agent, git push auth comes from the platform's credential helper and the PR is opened by the platform — neither depends on gh. A failing gh auth status (e.g. GITHUB_TOKEN reported invalid) must not stop the backport. Proceed with all git work (fetch, branch, cherry-pick, push); if the token really is unusable, git push will fail later with its own clear error — that is strictly better than abandoning a completed cherry-pick at pre-flight. Only PR-metadata commands (gh pr edit) need gh: if it is unauthenticated, first retry once as GH_TOKEN="$GITHUB_TOKEN" gh pr edit …; if that still fails, set the base/title/body via the REST API (gh api -X PATCH repos/{owner}/{repo}/pulls/{number} -f base=<target> -f title="[<target>] …", or curl with the token), and if even that is impossible, leave the PR open and state explicitly in the body which fields — especially the base branch — still need to be set manually so it does not silently target main.
- Branch naming: use
copilot/backport-<id>-to-<target-slug> instead of backport/.... This copilot/ prefix applies only here, in the GitHub.com cloud agent, because the cloud agent can only push branches starting with copilot/; a local run must use the backport/ prefix from Phase C. The <target-slug> rule is unchanged — replace every / with -, so rel/0.34 yields copilot/backport-<id>-to-rel-0.34 (one / only, right after copilot).
- Skip Phase A.3 stash logic — the cloud agent runs in a fresh ephemeral checkout; there is no user working tree to preserve.
- Apply the Phase D cloud-agent override described above (commit conflict markers as
WIP: and mark the PR draft instead of asking, aborting, or squash-and-retry).
- — the cloud agent's GitHub Actions environment runs CI as the validation gate; do not run / build / lint to save time and avoid burning Actions minutes.
All other constraints (no --force, refuse the source's own base, preserve commit messages, never silently overwrite existing branches) apply unchanged.