Skip to content

fix(cli): harden PR merge path against stale-base dead-ends (empty-diff no-op + conflict escape)#1943

Draft
flexi767 wants to merge 1 commit into
Runfusion:mainfrom
flexi767:fix/dormant-pr-noop-and-conflict-escape
Draft

fix(cli): harden PR merge path against stale-base dead-ends (empty-diff no-op + conflict escape)#1943
flexi767 wants to merge 1 commit into
Runfusion:mainfrom
flexi767:fix/dormant-pr-noop-and-conflict-escape

Conversation

@flexi767

@flexi767 flexi767 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

Two stale-base failure modes wedge the PR-based merge flow (processPullRequestMergeTask in packages/cli/src/commands/task-lifecycle.ts). Both pin the serial merge slot + file-scope leases on a dead-end task, starving ready work behind it.

  1. Empty-diff branchcreatePr throws No commits between <base> and <head>. The task was marked failed, even though a zero-commit branch is a legitimate no-op, not a failure. This is the PR-path analog of the engine-path fix in fix(engine): short-circuit zero-commits-ahead branch before AI-merge clean-room churn #1920 (noOpResult → done).

  2. Conflicting stale-base PR — a PR GitHub reports as CONFLICTING never becomes mergeable on its own (nothing in the PR path rebases the head branch). The task sat in awaiting-pr-checks unbounded: awaiting-pr-checks is not in TRANSIENT_MERGE_STATUSES, so the age-based stall escape never catches it, and the only other escape — getInReviewStallReason's merge-retries-exhausted — never fired because the PR path never incremented mergeRetries.

Fix

  1. finalizeNoOpMergeTask finalizes empty-diff branches as a terminal DONE, mirroring the engine's canonical noOpResult decision (merged:false, noOp:true). Both the group and per-task empty-diff sites route through it.

  2. Each CONFLICTING poll now counts against mergeRetries, so the existing merge-retries-exhausted escape disposes the task after maxAutoMergeRetries cycles. Detection is gated on prInfo.mergeable === "conflicting" (the genuine unresolvable signal), not blockingReasons (status/review/checks only). Pending / behind PRs still wait indefinitely — checks legitimately run, and behind is resolved by the pre-merge rebase.

No new machinery — fix #2 reuses the existing stall-escape counter rather than a hand-rolled fail branch.

Tests

packages/cli/src/commands/__tests__/task-lifecycle.test.ts:

  • empty-diff branch → no-op DONE (moveTask(id,"done"), updateTask({status:null,mergeRetries:0}), never failed)
  • conflicting not-ready PR → mergeRetries bumped
  • non-conflicting not-ready PR → mergeRetries not bumped

pnpm --filter @runfusion/fusion test (task-lifecycle suite 39/39), typecheck, and build all green.

🤖 Generated with Claude Code

Two failure modes wedged the PR-based merge flow (dormant under the
current direct-merge config, but the rollback target):

1. Empty-diff branches ("No commits between ...") were marked `failed`,
   pinning the serial merge slot + file leases on a task that is a
   legitimate no-op. Now finalized as a terminal DONE via
   `finalizeNoOpMergeTask`, mirroring the engine's canonical
   `noOpResult` decision (merger-ai.ts).

2. A stale-base PR that GitHub reports CONFLICTING never becomes
   mergeable on its own (nothing in the PR path rebases the head), so
   `awaiting-pr-checks` waited unbounded — no escape, because the PR
   path never incremented `mergeRetries`. Now each conflicting poll
   counts against `mergeRetries`, so the existing
   `getInReviewStallReason` "merge-retries-exhausted" escape disposes
   the task after `maxAutoMergeRetries` cycles. Pending/behind PRs
   still wait (checks legitimately run; "behind" is fixed by rebase).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d05dd0cd-3f24-4b38-81f8-a0c69f5cea39

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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