From 8b8b2450da98b918acdadacca7a1a4bd4e05723c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 May 2026 08:11:21 -0700 Subject: [PATCH] Add backport-failed agent skill for manual patchback recovery (#12584) --- .claude/skills/backport-failed/SKILL.md | 174 ++++++++++++++++++++++++ CHANGES/12584.contrib.rst | 4 + docs/spelling_wordlist.txt | 1 + 3 files changed, 179 insertions(+) create mode 100644 .claude/skills/backport-failed/SKILL.md create mode 100644 CHANGES/12584.contrib.rst diff --git a/.claude/skills/backport-failed/SKILL.md b/.claude/skills/backport-failed/SKILL.md new file mode 100644 index 00000000000..414293b6736 --- /dev/null +++ b/.claude/skills/backport-failed/SKILL.md @@ -0,0 +1,174 @@ +--- +name: backport-failed +description: Recover from a patchback auto-backport failure. Given a merged PR number, parse the patchback comments to find which target branches failed, cherry-pick the merge commit onto each failed branch, resolve conflicts, push to the user's fork, and open backport PRs that exactly match patchback's title and body shape so they look indistinguishable from successful auto-backports. +user-invocable: true +allowed-tools: + - Bash + - Read + - Edit + - Write + - AskUserQuestion +--- + +# /backport-failed — Manual recovery for failed patchback backports + +When patchback (the auto-backport bot used by aio-libs and similar orgs) fails to cherry-pick a merged PR onto a stable branch, it leaves a comment like: + +> ### Backport to 3.13: 💔 cherry-picking failed — conflicts found +> ❌ Failed to cleanly apply `` on top of `patchback/backports/3.13//pr-` + +This skill produces the manual backport PRs that should have been created, matching patchback's exact shape so the PR list stays uniform. + +Arguments passed: `$ARGUMENTS` — usually a PR number (e.g. `12581`). If empty, ask the user for it via `AskUserQuestion`. + +## The exact patchback PR shape + +Every backport PR this skill opens **must** match this shape (study PRs authored by `patchback[bot]` — e.g. aio-libs/aiohttp#12574 — for the canonical form): + +- **Title:** `[PR #/ backport][] ` + - `` is the first 8 chars of the merge commit on master + - `` is the target branch, e.g. `3.13`, `3.14` +- **Body first line:** `**This is a backport of PR # as merged into master ().**` +- **Body remainder:** the *original* PR body verbatim (including the HTML template comments and the unchecked checklist items as patchback preserves them — copy what's on the original PR) +- **Base branch:** the stable branch (`3.13`, `3.14`, etc.) +- **Head branch on fork:** `patchback/backports///pr-` + +Do **not** add a "Drafted with Claude Code" footer, do **not** add Co-Authored-By, do **not** edit or summarize the original body. The goal is byte-for-byte indistinguishability from a successful patchback PR. + +## Procedure + +### 1. Resolve the PR number and gather metadata + +If `$ARGUMENTS` is empty, ask the user for the PR number with `AskUserQuestion`. Then in parallel: + +```bash +gh pr view --repo --json number,title,body,mergeCommit,headRepositoryOwner,baseRefName,labels,url +gh pr view --repo --comments +``` + +Detect the repo from the working directory's `upstream` remote (`git remote get-url upstream`). If there is no `upstream` remote, ask the user which remote points at the canonical repo. + +Extract: +- `orig_num` — the PR number +- `orig_title` — the PR title (use as-is, do not modify) +- `orig_body` — the PR body (use as-is, do not modify, do not strip HTML comments) +- `full_sha` — the merge commit SHA on master (from `mergeCommit.oid`) +- `short_sha` — first 8 chars of `full_sha` + +### 2. Find the failed target branches + +From the comments output, find each patchback comment that says **"💔 cherry-picking failed — conflicts found"** and extract the target branch from the heading (`Backport to :`). + +Sanity check against PR labels: patchback is driven by labels like `backport-3.13`, `backport-3.14`. The set of failed branches should be a subset of those labels. If a backport label has no patchback failure comment and no existing backport PR, that target probably succeeded automatically — skip it. + +To confirm a branch genuinely needs a manual backport, check whether a successful backport PR already exists: + +```bash +gh pr list --repo --search "[PR #/ backport]" --state all +``` + +If a PR with the expected title already exists for that branch, skip it. + +### 3. Verify the working tree is clean enough to checkout branches + +```bash +git status --porcelain +``` + +If there are staged/unstaged changes to tracked files, stop and tell the user. Untracked files are fine — patchback's workflow doesn't touch them. + +Remember the current branch so you can return to it at the end: + +```bash +git rev-parse --abbrev-ref HEAD +``` + +### 4. For each failed branch, create the backport + +Run these steps **sequentially per branch** — never parallelize across target branches (they share the working tree). + +```bash +git fetch upstream +git checkout -b patchback/backports///pr- upstream/ +git cherry-pick -x +``` + +If the commit is a merge commit (`git cat-file -p | grep -c '^parent '` returns > 1), use `git cherry-pick -m1 -x ` instead. For squash-merged PRs (the common case on aio-libs) it's a single-parent commit, so plain `-x` works. + +#### Resolving conflicts + +If cherry-pick reports conflicts: + +1. Run `git status` to list conflicted files. +2. For each file, Read it, locate the `<<<<<<<`/`=======`/`>>>>>>>` markers, and resolve. +3. **Conflict-resolution principles** (in order): + - Preserve the *intent* of the original PR — the change being backported is the source of truth for what should land. + - Adapt to the stable branch's existing context. The conflict usually means surrounding code differs (e.g. a function got renamed on master but not on 3.13). Apply the PR's logical change to the stable branch's version of the code. + - For news fragments (`CHANGES/*.rst`) the file usually doesn't exist on the stable branch yet — just take the master version. + - If you cannot confidently resolve a conflict, **stop and ask the user**. Do not guess. Show the conflict markers and your proposed resolution, and let them confirm. +4. `git add ` for each resolved file. +5. `git cherry-pick --continue` (use a non-interactive editor: `GIT_EDITOR=true git cherry-pick --continue` to keep the cherry-pick's default message which already contains the `(cherry picked from commit ...)` trailer from `-x`). + +#### Push to the user's fork + +The user's fork remote is conventionally `origin` for aio-libs contributors (or named after their GitHub handle). Detect it: + +```bash +git remote -v | grep -E "(fetch)" | grep -v upstream +``` + +If multiple non-upstream remotes exist, prefer one matching the GitHub user from `gh api user --jq .login`. Ask the user if ambiguous. + +```bash +git push patchback/backports///pr- +``` + +#### Open the backport PR + +Use a HEREDOC so the body is preserved exactly. The body of the new PR is the **original PR's body**, prepended with the `**This is a backport...**` line and a blank line: + +```bash +gh pr create \ + --repo \ + --base \ + --head :patchback/backports///pr- \ + --title "[PR #/ backport][] " \ + --body "$(cat <<'EOF' +**This is a backport of PR # as merged into master ().** + + +EOF +)" +``` + +If `` contains an `EOF` marker itself (rare), pick a different HEREDOC delimiter (e.g. `PATCHBACK_BODY`). + +Report the new PR URL. + +### 5. Return to the original branch + +After all backports are done (or after stopping for user input): + +```bash +git checkout +``` + +Leave the patchback branches in place locally — the user may want to amend them. + +## Output to the user + +At the end, print a short summary: + +- Which branches had failed backports +- For each: the new PR URL, or `skipped (already exists)`, or `paused for user (conflict in )` + +Keep it terse — one line per branch. + +## Edge cases + +- **No patchback failure comments found.** Tell the user the PR's auto-backports look healthy and ask if they want to force a manual backport anyway. +- **Cherry-pick is clean (no conflicts).** Still legitimate — patchback may have failed for a transient reason. Proceed to push and PR creation as normal. +- **PR was a merge commit, not a squash.** Use `-m1` as noted above. +- **The fork already has the branch.** `git push --force-with-lease` only if the user confirms (`AskUserQuestion`). Never `--force` blindly. +- **News fragment filename differs.** Patchback preserves whatever filename the original PR used; do not rename. +- **Stable branch is end-of-life.** If `gh api repos///branches/` 404s, tell the user — the backport label may be stale. diff --git a/CHANGES/12584.contrib.rst b/CHANGES/12584.contrib.rst new file mode 100644 index 00000000000..77d94065ae8 --- /dev/null +++ b/CHANGES/12584.contrib.rst @@ -0,0 +1,4 @@ +Added a Claude Code agent skill at ``.claude/skills/backport-failed/`` that +walks a contributor through recovering a patchback auto-backport that failed +with conflicts, including reopening the backport PR with the same title and +body shape patchback produces -- by :user:`bdraco`. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index c81e125a3d4..ac9b0a41f2d 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -239,6 +239,7 @@ Paolini param params parsers +patchback pathlib payloads peername