Skip to content

feat: backport a merged PR stack as a single pull request - #433

Open
claude[bot] wants to merge 2 commits into
mainfrom
feat/stack-aware-backports
Open

claude[bot] wants to merge 2 commits into
mainfrom
feat/stack-aware-backports

Conversation

@claude

@claude claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Requested by Samuel Attard · Slack thread

Before: every merged PR of a GitHub stack triggered its own backport. Because each member only applies on top of the one below it, this produced backports that either failed or applied by accident: on electron/electron stack #53879 trop opened two backports that happened to apply and flagged the other four members needs-manual-bp.

After: merging the top PR of a stack opens one backport PR per target/ label on that top PR, containing the commits of every member bottom to top. Lower members are not backported on their own (their target/ labels are ignored) and are labeled in-flight/* / merged/* along with the top PR through the shared Backport of #N lines in the backport body, which labelClosedPR already handles. The Valid Backport check judges an original PR by the base of its stack (a member's own base.ref is the sibling branch), the Backport Labels Added check compares a lower member's labels with the top PR's, and the Backportable? dry run on a top PR checks the whole stack. /trop run backport and /trop run backport-to follow the same rule and tell the commenter to use the top PR when run on a lower member.

How: getStackMemberPRs (src/utils/stack-util.ts) resolves the members through GET /repos/{owner}/{repo}/stacks/{stack_number} (ordered bottom to top), fetches each lower member with pulls.get, appends the top PR payload last and retries up to three times (3s apart) while GitHub is still flagging lower members as merged; getStackTopPR is the lighter variant that only resolves the top. backportImpl is now a thin wrapper over backportStackImpl(robot, context, prs, ...): attempt one gathers every member's non-merge PR commits into one patch list, attempt two every member's squash commit (both bottom to top), and backportCommitsToBranch applies the list unchanged. The top PR drives the queue ident, check run, title, temp branch, breadcrumb comment and reviewer; getEffectiveBaseRef is reused for the title/Subject rewrite and the Valid Backport check. Single-PR behaviour, including the backport body, is unchanged.

Also fixes getHighestSemverLabel throwing Invalid semver labels on semver/none: it is now ranked below semver/patch, which the cross-member semver precedence needs (stack #53879 mixed semver/patch and semver/none) and which also fixes the pull_request.opened 500 seen on electron/electron #53914, where updateManualBackport called it with the original's semver/none.

Decisions to confirm

  • Labels (target/ removed, in-flight/ added, needs-manual-bp/ on failure) are updated on every member, not just the top PR.
  • The breadcrumb "I have automatically backported..." and "unable to backport" comments go to the top PR only.
  • Release notes are combined: one * bullet per member's notes under a single Notes: block, dropping members whose notes are none; a single set of notes is kept as-is.
  • Lower members' target/ labels are ignored for the backport itself. Their Backport Labels Added check passes when they carry no target/ labels or a subset of the top PR's, and fails with "Conflicting Backport Information" (naming the top PR and the mismatched labels) when they carry a target/ label the top PR does not, or no-backport against the top PR's targets. Adding or removing a target/ or no-backport label on the top PR re-evaluates the lower members' checks so a failure clears once the top PR is labeled to match; if the stack cannot be resolved the member's check is left queued.
  • If the stack cannot be resolved for a backport (member still unmerged after the retries, endpoint error), trop comments once on the top PR and swaps its target/ labels for needs-manual-bp/.

Known follow-ups

  • getOriginalBackportNumber (src/utils.ts, TODO "support multi-backports") is bypassed when a backport PR body lists more than one original, so /trop run backport on a multi-original backport PR labels the backport PR itself rather than the originals. Out of scope here.

🤖 Generated with Claude Code

https://claude.ai/code/session_015Eroy2J8ggyZ1vtFm2FcqB

When the top PR of a GitHub stack is merged, trop now opens one backport
PR per `target/*` label of that top PR, containing the commits of every
member of the stack bottom to top, instead of one backport per member.

- Backport targets are read from the top PR only; `target/*` labels on
  lower members are ignored and their merge does not start a backport.
- Members are resolved through the stack endpoint
  (`GET /repos/{owner}/{repo}/stacks/{stack_number}`), retrying briefly
  while GitHub is still flagging lower members as merged.
- backportImpl operates on an ordered list of PRs: the first attempt
  applies every member's PR commits, the second every member's squash
  commit. The top PR drives the queue ident, check run, title, temp
  branch and reviewer; the backport body lists one `Backport of #N` line
  per member and combines their release notes; labels are updated on
  every member.
- The Valid Backport check judges an original PR by the base of its
  stack, the Backport Labels Added check passes for lower members, and
  the Backportable? dry run checks the whole stack from the top PR.
- `semver/none` is now a valid (lowest) input of getHighestSemverLabel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Eroy2J8ggyZ1vtFm2FcqB
…ee with the top PR

A lower member of a stack no longer passes the Backport Labels Added
check unconditionally. Its labels are compared with the top PR's: a
`target/*` label the top PR does not carry, or `no-backport` against the
top PR's targets, fails the check naming the top PR and the mismatched
labels; no labels or a subset of the top PR's targets passes. When a
`target/*` or `no-backport` label is added to or removed from the top
PR, the lower members are re-evaluated so a failure clears once the top
PR is labeled to match. If the stack cannot be resolved the check is
left queued.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Eroy2J8ggyZ1vtFm2FcqB
@MarshallOfSound
MarshallOfSound marked this pull request as ready for review September 14, 2026 17:39
@MarshallOfSound
MarshallOfSound requested a review from a team as a code owner September 14, 2026 17:39
@MarshallOfSound
MarshallOfSound enabled auto-merge (squash) September 14, 2026 17:49

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the needs-manual-bp label handling in backportStackImpl's failure path (src/utils.ts around line 960), which applies the label to every stack member including lower ones that never carried the matching target/ label — this matches the PR's stated design of updating labels on every member rather than just the top PR, so it's not a bug.

Extended reasoning...

One confirmed finding (src/index.ts:271) is already queued to be posted inline: when getStackMemberPRs() throws for a top-of-stack PR, runCheck() returns early and skips the stale-check cleanup loop, unlike the single-PR path. I verified this directly by reading src/index.ts and confirming the early return bypasses the checkRuns cancellation logic below it.

Separately, I examined the candidate issue about backportStackImpl's failure executor applying needs-manual-bp/<branch> unconditionally to every stack member (src/utils.ts near line 960), including lower members that never had the corresponding target/<branch> label. Reading the code and cross-referencing the PR description's explicit "Decisions to confirm" section ("Labels ... are updated on every member, not just the top PR"), this appears to be intentional behavior rather than a bug, so it was correctly ruled out.

Given a confirmed finding will be posted inline and I have one concrete additional item I examined and ruled out, I'm using the narrow defer exception to record that without restating the inline finding itself.

Comment thread src/index.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants