Skip to content

Push the base-branch merge before asking for conflict resolution#33

Merged
Phlogistique merged 9 commits into
mainfrom
push-base-merge-before-conflict-comment
Jun 1, 2026
Merged

Push the base-branch merge before asking for conflict resolution#33
Phlogistique merged 9 commits into
mainfrom
push-base-merge-before-conflict-comment

Conversation

@Phlogistique

@Phlogistique Phlogistique commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Alternative to #32 for the same stuck-PR bug. Pick one.

Background: when a parent PR is squash-merged, the action brings each child branch up to date with three merges: the parent branch (the base-branch merge), the trunk as it stood just before the squash landed (SQUASH_COMMIT~), and the squash itself recorded with -s ours so the child's diff against its new base stays clean. When the pre-squash trunk merge conflicts, the action asks the user to resolve, and a follow-up run triggered by the user's push finishes the job.

Two separate bugs left a conflicting child PR broken:

  1. The follow-up run never finished the job. It removed the conflict label, retargeted the PR, and deleted the old base branch, but it never recorded the squash. The synthetic merge commit that keeps the child's diff against its new base clean was never created, and because the label was gone the action would never revisit the PR.
  2. Even with that fixed, the recipe could push the user into an unrecoverable state. The conflict comment asked the user to merge only the conflicting ref, starting from a head that did not contain the base-branch merge. When that merge is clean the head is clean against its base going in, so the only thing that can make it conflict with the base is the user's own resolution. If resolving the SQUASH_COMMIT~ conflict produced content that conflicts with the parent branch, the user's push left the head conflicting with its base. GitHub does not run pull_request workflows on a PR that conflicts with its base, so the follow-up run never fired and the PR was stuck for good.

This PR fixes both:

  • Push the base-branch merge before commenting, when it is clean. The head then always contains it, so the parent branch is an ancestor and no resolution can make the head conflict with its base. The PR stays mergeable and the synchronize trigger keeps working. If the base merge itself conflicts there is nothing safe to pre-push, so the existing comment-and-label fallback runs.
  • continue_after_resolution now records the squash with -s ours and pushes before retargeting. The synchronize payload is the child PR, so the squash commit and new target are reconstructed from the merged parent PR (its merge commit and base).

Pushing the base merge leaves the user's local branch behind origin, so the conflict comment now starts the recipe with git pull --ff-only origin <branch>. Without it the user resolves on a stale head and the final push is rejected as non-fast-forward. The comment still lists only the genuine conflict to merge.

The e2e conflict scenario already triggers this case (clean base merge, conflicting SQUASH_COMMIT~ merge), so coverage lives there: it checks the action pushed the base merge on top of the pre-conflict head and that the comment asks only for the genuine conflict, then resolves by following the comment verbatim (re-sync included) and checks continue_after_resolution records the squash so the branch ends up mergeable into the new target.

🤖 Generated with Claude Code

Alternative to #32 for the same stuck-PR bug — pick one.

After a parent PR is squash-merged, updating a descendant branch runs three
merges: the parent branch, the target's pre-squash state, then the squash
recorded with `-s ours`. When the pre-squash merge conflicted, the action
commented and labeled without pushing anything, so the head was left missing
both the base merge and the squash record. The branch never became mergeable,
and because GitHub skips `pull_request` workflows on a PR that conflicts with
its base, the `synchronize` event that resumes the action never fired again —
permanently stuck.

This splits the work so the user only resolves the genuine conflict:

- When the base-branch merge is clean and only the pre-squash merge conflicts,
  push the base merge to the branch before commenting and labeling. The head
  stays a descendant of its base, so the PR stays mergeable and the resume
  trigger keeps firing. If the base merge itself conflicts there is nothing safe
  to pre-push, so the existing comment-and-label fallback runs.
- After the user resolves and pushes, continue_after_resolution records the
  squash with `-s ours` and pushes before retargeting. The synchronize payload
  is the child PR, so the squash commit and new target are reconstructed from
  the merged parent PR (its merge commit and base).

The conflict comment is unchanged: it already lists the genuine conflict.

Offline test covers both halves: the squash-merge run pushes the base merge
before the unchanged comment, and the follow-up run records the squash so the
branch ends up mergeable into the new target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Phlogistique
Phlogistique force-pushed the push-base-merge-before-conflict-comment branch from 63f61f8 to 562ba64 Compare May 29, 2026 14:55
Phlogistique and others added 2 commits May 29, 2026 17:29
Pushing the base merge before commenting leaves the user's local branch
behind origin, so following the comment verbatim resolved on a stale head
and the final push was rejected as non-fast-forward. The recipe now
fast-forwards with `git pull --ff-only origin <branch>` first.

The standalone offline test is dropped. The existing e2e conflict scenario
already triggers the clean-base-merge / conflicting-pre-squash case, so it
now asserts the action pushed the base merge on top of the pre-conflict head
and that the comment asks only for the genuine conflict, then resolves by
following the comment verbatim (re-sync included) and checks the squash gets
recorded so the branch ends up mergeable into the new target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread update-pr-stack.sh Outdated
Comment on lines +115 to +123
# When the base merge was clean we already pushed it to this branch,
# so the local branch is now behind origin. Fast-forward to it before
# resolving, otherwise the final push is rejected as non-fast-forward.
# The line is a harmless no-op on the fallback path (nothing pushed).
if [[ "$BASE_MERGE_CLEAN" == true ]]; then
echo "git pull --ff-only origin $BRANCH # pick up the base merge this action already pushed"
else
echo "git pull --ff-only origin $BRANCH"
fi

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
# When the base merge was clean we already pushed it to this branch,
# so the local branch is now behind origin. Fast-forward to it before
# resolving, otherwise the final push is rejected as non-fast-forward.
# The line is a harmless no-op on the fallback path (nothing pushed).
if [[ "$BASE_MERGE_CLEAN" == true ]]; then
echo "git pull --ff-only origin $BRANCH # pick up the base merge this action already pushed"
else
echo "git pull --ff-only origin $BRANCH"
fi
echo "git pull --ff-only origin $BRANCH"

Phlogistique and others added 2 commits June 1, 2026 09:20
The sibling conflict scenario still resolved branches with a hardcoded merge against main, so it could pass even if the generated instructions regressed. Reusing the comment-following path makes PR6 and PR7 validate the same workflow users run, while keeping the PR3 flow on the shared helper.
- Note that the base-merge push must precede the label, or the synchronize
  it emits re-triggers this action against an unresolved branch.
- Collapse the duplicated `git pull --ff-only` recipe line to one echo plus
  a conditional trailing comment.
- Fold the new-target and squash-commit lookups into a single `gh pr list`
  call; `// ""` keeps the absent-PR guard firing.
- Reword the continuation conflict message to say it re-posted the comment
  and will retry, since the path is self-healing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread update-pr-stack.sh Outdated
Comment thread update-pr-stack.sh Outdated
Co-authored-by: Noé Rubinstein <noe.rubinstein@gmail.com>
Phlogistique added a commit that referenced this pull request Jun 1, 2026
The base branch accepted review suggestions while the stacked branch split conflict instructions into separately executable command blocks. Resolve that overlap by keeping the simplified pull command from the base branch and preserving the literal multi-block recipe covered by the stacked tests.
Phlogistique and others added 2 commits June 1, 2026 10:56
The pre-pushed base merge leaves the user's local branch strictly behind
origin, so the pull is always a fast-forward and the flag is a no-op in the
expected case. Plain `git pull` is one less thing in the recipe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The helper checked that the conflict comment contains `git pull origin
<branch>`, but that line is static text the action always emits, so the
assertion only restated the script. The helper still runs the pull.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Phlogistique
Phlogistique marked this pull request as ready for review June 1, 2026 09:04
@Phlogistique
Phlogistique merged commit a168db2 into main Jun 1, 2026
3 checks passed
@github-actions
github-actions Bot deleted the push-base-merge-before-conflict-comment branch June 1, 2026 09:13
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.

1 participant