Skip to content

Handle merge-commit merges, skip rebase merges with a comment#49

Merged
Phlogistique merged 16 commits into
mainfrom
claude/skip-merge-commits-3njvdx
Jun 11, 2026
Merged

Handle merge-commit merges, skip rebase merges with a comment#49
Phlogistique merged 16 commits into
mainfrom
claude/skip-merge-commits-3njvdx

Conversation

@Phlogistique

@Phlogistique Phlogistique commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

The squash sequence assumes SQUASH_COMMIT~ is the target just before the merge, which only holds for squashes. GitHub records the merge method nowhere (payload, REST, GraphQL), so the action infers it and now handles each shape:

  • Merge commit: detected by the second parent. The merge carries the parent's commits into the target, so the children's heads are already valid; the action only retargets the children and deletes the merged branch, the same end state GitHub produces natively when the branch is deleted. Going through the squash sequence also produced a correct result (verified by simulation), but pushed a redundant synthetic merge to every child, with a CI run each.
  • Rebase: detected through GitHub's commit-PR association (/commits/{sha}/pulls, "the merged pull request that introduced the commit"). A squash introduces a single commit, so the commit below the merge sha belongs to an older PR or to none; a rebase introduces a copy of each PR commit, so the commit below still belongs to this PR (verified against real merges on a scratch repo). The association is computed asynchronously, ~15-30s after the merge in testing, so an empty answer is only trusted once the merge sha itself is associated; on API failure or timeout the run aborts rather than guessing. Not supported: the copies are new commits, so a child retargeted as-is would show the parent's changes in its diff, and the squash sequence can raise spurious conflicts against the intermediate copies (observed in simulation). The action comments on each child PR that the stack must be updated manually and leaves everything alone, including the merged branch.
  • Single-commit PRs merge identically under rebase and squash and correctly read as squashes.

A PR whose commits are all merge commits would defeat the rebase detection, but GitHub refuses to rebase-merge those (rebaseable: false, verified on a scratch repo).

Stacked on #45.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX

claude and others added 10 commits June 9, 2026 20:49
The squash-merge fan-out retargeted every updated child PR onto the
target branch and only afterwards pushed the new heads, batched into a
single non-atomic push together with the merged-branch deletion. If the
push failed (e.g. someone pushed to a child mid-run, rejecting the plain
push) or a pr edit died partway through the loop, set -e aborted the run
with PRs already retargeted but their heads stale - and unlike the
conflict-resume path there is no label to re-trigger the action, so
nothing ever repaired them.

Apply the ordering the resume path already uses: push the updated heads
first, then flip the bases, and delete the merged branch last (deleting
a PR's base branch closes the PR, so every child must be off it first).
A failed push now leaves the PRs untouched on their old base.

The unit test captures the run transcript and asserts the
push -> retarget -> delete order; it fails against the previous code.
Also corrects the README: pushes are plain, not forced, and branch
deletion is its own final step.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX
Since #40 the conflict comment's fast-forward step reads `git merge
--ff-only origin/<branch>`, which assert_conflict_comment_merges picks
up with its `^git merge` grep, so the extracted commands never match the
expected conflict merges. Skip the --ff-only line when extracting.

Also trim the new comments in the fan-out push/retarget/delete sequence.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX
The fix for the --ff-only line breaking assert_conflict_comment_merges
moved to a separate PR; the e2e job here stays red until that lands and
main is merged back in.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX
A head branch can carry several PRs (one per base), so gh calls keyed by
branch name can comment, label, or retarget the wrong one. Every gh call
that acts on a specific PR now uses the PR number: the fan-out carries
number/branch pairs from gh pr list, and the conflict-resolved run gets
PR_NUMBER from the event payload via action.yml.

The payload also already carries the PR's base branch, so the resume
takes it from a new PR_BASE variable instead of querying the API; the
resume test's gh mock no longer answers baseRefName queries, so a
reintroduced lookup fails loudly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX
The action triggers on any merged PR, but its merge sequence assumes the
squash shape: SQUASH_COMMIT~ as "target just before the merge" and an
-s ours record of the squash. A real merge commit satisfies neither -
and needs no fixing at all, since history is not rewritten and stacked
children keep correct diffs. Detect the second parent and bail out
before touching anything.

Rebase merges remain indistinguishable from squashes in the payload and
are processed as before.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01JHvKryT4QUpHYdNq9YEQxX
A full skip left child PRs based on the deleted-but-not-deleted merged
branch forever: nothing retargeted them and nothing deleted the branch,
the two things the action exists to automate. The children's heads need
no rewriting (the merge commit carries the parent's commits into the
target branch), so retarget them as-is and delete the branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Phlogistique Phlogistique changed the title Skip PRs merged with a merge commit Handle merge-commit merges without rewriting stacked heads Jun 10, 2026
GitHub records the merge method nowhere (payload, REST, GraphQL), so
detect it: a rebase leaves a patch-equivalent copy of every PR commit on
the target, a multi-commit squash does not. Single-commit PRs merge
identically either way and keep taking the squash path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Phlogistique Phlogistique changed the title Handle merge-commit merges without rewriting stacked heads Handle merge-commit merges, skip rebase merges with a comment Jun 10, 2026
Phlogistique and others added 5 commits June 10, 2026 13:06
Replaces the patch-id heuristic: ask which PR introduced the commit just
below the merge sha. An older PR or none means squash; this PR means a
rebase copy. The association is computed asynchronously, so an empty
answer is only trusted once the merge sha itself is associated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot changed the base branch from claude/pr-number-addressing-3njvdx to main June 11, 2026 11:22
@Phlogistique Phlogistique merged commit 0386ed1 into main Jun 11, 2026
@github-actions github-actions Bot deleted the claude/skip-merge-commits-3njvdx branch June 11, 2026 11:25
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