Skip to content

Commit 7aa220f

Browse files
Phlogistiqueclaude
andauthored
Re-post the conflict comment when the resumed head lacks the squash commit (#62)
A PR whose conflict comment predates the git-merge-onto switch got stranded: on resume the new head-contains-squash check failed, and the error told the user to run a `git-merge-onto` command their (old-format) comment never contained. Every push re-hit the same red run. When the resumed head lacks the squash commit, re-post the current conflict comment and keep the label armed instead of dead-ending, so the next push has working instructions. Also covers pushing before the re-parent is finished. The run still returns nonzero so the PR reads as unresolved. Observed on [scortexio/sensei#7997](https://github.com/scortexio/sensei/actions/runs/28799772953/job/85399685085?pr=7997). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7d6e326 commit 7aa220f

2 files changed

Lines changed: 66 additions & 28 deletions

File tree

tests/test_conflict_resolution_resume.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,31 @@ git -C "$ORIGIN" rev-parse --verify -q parent >/dev/null || fail "I: old base br
280280
grep -q -- "push origin :parent" "$CALLS" && fail "I: deletion must not be attempted"
281281
ok "I: sibling-listing API failure keeps the old base branch"
282282

283+
# ---------------------------------------------------------------------------
284+
echo "### Scenario J: head lacks the squash commit -> re-post the comment, keep the label"
285+
setup_repo
286+
# The pushed resolution does not contain the squash commit: the user pushed
287+
# early, or followed an old-format comment (a prior action version) whose
288+
# resolution never folds it in. Advance main with a commit the child lacks so
289+
# the ancestry check fails.
290+
git checkout -q main
291+
echo squash > s.txt && git add s.txt && git commit -qm squash
292+
SQUASH2=$(git rev-parse main)
293+
git push -q origin main
294+
git checkout -q child
295+
MOCK_LABELS="autorestack-needs-conflict-resolution"
296+
PR_BASE="parent" # matches marker -> not a manual retarget
297+
MOCK_COMMENTS_FILE="$WORK/comments.txt"
298+
{ echo "### conflict"; echo; marker parent main "$SQUASH2"; } > "$MOCK_COMMENTS_FILE"
299+
run_resume
300+
301+
grep -q "EXIT=1" "$WORK/out.log" || fail "J: run must fail while the PR is unresolved"
302+
grep -q "gh pr comment" "$CALLS" || fail "J: conflict comment not re-posted"
303+
grep -q -- "add-label autorestack-needs-conflict-resolution" "$CALLS" || fail "J: conflict label not kept"
304+
grep -q "remove-label" "$CALLS" && fail "J: label must NOT be removed"
305+
grep -q -- "--base" "$CALLS" && fail "J: base must NOT be edited"
306+
[[ "$(git -C "$ORIGIN" rev-parse child)" == "$CHILD_BEFORE" ]] || fail "J: child was pushed"
307+
ok "J: unresolved head re-posts the conflict comment and keeps the label armed"
308+
283309
echo
284310
echo "All conflict-resume tests passed 🎉 ($PASS scenarios)"

update-pr-stack.sh

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,38 @@ abort_merge_if_in_progress() {
120120
fi
121121
}
122122

123+
# Post the conflict-resolution comment and add the conflict label.
124+
# Args: head branch, merged parent branch, final target branch, squash hash, PR number.
125+
# The user re-parents the head from MERGED onto TARGET with git-merge-onto; the
126+
# state marker records both plus the squash so the resume can finalize.
127+
post_conflict_comment() {
128+
local BRANCH="$1" MERGED="$2" TARGET="$3" SQUASH_HASH="$4" PR_NUMBER="$5"
129+
{
130+
echo "### ⚠️ Automatic update blocked by a merge conflict"
131+
echo
132+
echo "Resolve it like this:"
133+
echo '```bash'
134+
echo "git fetch origin"
135+
echo "git switch $BRANCH"
136+
echo "git merge --ff-only origin/$BRANCH"
137+
echo "uvx git-merge-onto origin/$TARGET origin/$MERGED --absorbed"
138+
echo '```'
139+
echo
140+
echo 'Fix the conflicts (for instance with `git mergetool`), then run `git add -A && git commit` to finish the merge.'
141+
echo
142+
echo '```bash'
143+
echo "git push origin $BRANCH"
144+
echo '```'
145+
echo
146+
echo "Once you push, this action will resume and finish updating this pull request."
147+
echo
148+
format_state_marker "$MERGED" "$TARGET" "$SQUASH_HASH"
149+
} | try gh pr comment "$PR_NUMBER" -F - \
150+
|| die "could not post the conflict-resolution comment on PR #$PR_NUMBER"
151+
gh label create "$CONFLICT_LABEL" --description "PR needs manual conflict resolution" --color "d73a4a" 2>/dev/null || true
152+
run gh pr edit "$PR_NUMBER" --add-label "$CONFLICT_LABEL"
153+
}
154+
123155
# Args: head branch, base branch, PR number. git commands use the branch; gh
124156
# commands use the number, since a head branch can carry several PRs.
125157
update_direct_target() {
@@ -180,30 +212,7 @@ See $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
180212
abort_merge_if_in_progress
181213
local SQUASH_HASH_FOR_MARKER
182214
SQUASH_HASH_FOR_MARKER=$(git rev-parse SQUASH_COMMIT) || die "cannot resolve SQUASH_COMMIT"
183-
{
184-
echo "### ⚠️ Automatic update blocked by a merge conflict"
185-
echo
186-
echo "Resolve it like this:"
187-
echo '```bash'
188-
echo "git fetch origin"
189-
echo "git switch $BRANCH"
190-
echo "git merge --ff-only origin/$BRANCH"
191-
echo "uvx git-merge-onto origin/$BASE_BRANCH origin/$MERGED_BRANCH --absorbed"
192-
echo '```'
193-
echo
194-
echo 'Fix the conflicts (for instance with `git mergetool`), then run `git add -A && git commit` to finish the merge.'
195-
echo
196-
echo '```bash'
197-
echo "git push origin $BRANCH"
198-
echo '```'
199-
echo
200-
echo "Once you push, this action will resume and finish updating this pull request."
201-
echo
202-
format_state_marker "$MERGED_BRANCH" "$TARGET_BRANCH" "$SQUASH_HASH_FOR_MARKER"
203-
} | try gh pr comment "$PR_NUMBER" -F - \
204-
|| die "could not post the conflict-resolution comment on PR #$PR_NUMBER"
205-
gh label create "$CONFLICT_LABEL" --description "PR needs manual conflict resolution" --color "d73a4a" 2>/dev/null || true
206-
run gh pr edit "$PR_NUMBER" --add-label "$CONFLICT_LABEL"
215+
post_conflict_comment "$BRANCH" "$MERGED_BRANCH" "$TARGET_BRANCH" "$SQUASH_HASH_FOR_MARKER" "$PR_NUMBER"
207216
return 1
208217
}
209218

@@ -324,10 +333,13 @@ continue_after_resolution() {
324333
run git update-ref SQUASH_COMMIT "$SQUASH_HASH"
325334
run git checkout "$PR_BRANCH"
326335
if ! git merge-base --is-ancestor SQUASH_COMMIT "$PR_BRANCH"; then
327-
# Fail loudly rather than silently: the user pushed without finishing the
328-
# re-parent, so a red run is the signal they need to look again.
329-
echo "❌ '$PR_BRANCH' does not contain the squash commit; the conflict is not resolved." >&2
330-
echo " Follow the conflict comment on this PR (run its git-merge-onto command), then push again." >&2
336+
# The head does not contain the squash commit: the user pushed without
337+
# finishing the re-parent, or followed an old-format conflict comment (a
338+
# prior action version) whose resolution never folds the squash in. Re-post
339+
# the current instructions and keep the label on so the next push resumes.
340+
# Still return 1: a red run flags that the PR is not resolved yet.
341+
echo "⚠️ '$PR_BRANCH' does not contain the squash commit yet; re-posting the conflict comment." >&2
342+
post_conflict_comment "$PR_BRANCH" "$OLD_BASE" "$NEW_TARGET" "$SQUASH_HASH" "$PR_NUMBER"
331343
return 1
332344
fi
333345

0 commit comments

Comments
 (0)