Skip to content

fix(desktop): harden PR merge detection fallback#2668

Merged
Kitenite merged 5 commits into
superset-sh:mainfrom
Kitenite:kitenite/pr-detection-issue
Mar 21, 2026
Merged

fix(desktop): harden PR merge detection fallback#2668
Kitenite merged 5 commits into
superset-sh:mainfrom
Kitenite:kitenite/pr-detection-issue

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Mar 21, 2026

Summary

  • add deterministic PR resolution that tries tracking lookup, exact head-branch lookup, then head-SHA lookup
  • merge by explicit PR number when resolution succeeds, instead of relying on gh pr merge to infer the current branch
  • fall back to the legacy implicit branch merge path whenever the new resolution flow cannot prove a better answer
  • tighten branch matching with fork-owner metadata to avoid stale or unrelated PR matches

Repro

  • reproduced against /Users/kietho/.superset/worktrees/superset/kitenite/update-status-after-squash
  • in that worktree, gh pr view reported no PR for the branch, while explicit branch/SHA lookup still found PR #2665

Testing

  • bun test apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.test.ts apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
  • bunx biome check apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.test.ts apps/desktop/src/lib/trpc/routers/changes/git-utils.ts apps/desktop/src/lib/trpc/routers/changes/git-operations.ts apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
  • bun run --cwd apps/desktop typecheck

Summary by cubic

Hardened PR merge detection in Desktop by resolving the current PR deterministically and merging by PR number. Centralized the merge flow with safe fallbacks, better fork matching, and clearer, user-facing errors.

  • Bug Fixes

    • Deterministic PR resolution: tracking ref → head branch → head commit, with fork‑owner matching; prefer matches on head SHA and open state.
    • Merge by PR number with --repo when known; fall back to legacy branch merge if resolution fails or gh reports “no pull request” (case-insensitive); surface “already merged/closed” as user errors.
  • Refactors

    • Centralized merge and cache invalidation in merge-pull-request.ts and worktree-status-caches.ts; extracted PR matching to pr-resolution.ts and repo/URL helpers to repo-context.ts.
    • Rewired exports through workspaces/utils/github/index.ts and trimmed github.ts to status/deploy logic.

Written for commit 0405d15. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added repository-aware PR resolution and an improved merge flow that prefers explicit PR merges and clears related caches.
    • Introduced repository context detection and URL normalization, plus a field to capture PR head repository owner.
  • Bug Fixes

    • Improved pull request detection, merge error classification, and fallbacks for forked branches and missing PRs.
  • Tests

    • Added coverage for PR matching logic and error message pattern validation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Introduces explicit PR resolution and owner-aware branch matching, a new merge helper with repo-context-aware merge + legacy fallback, cache-clear utilities, schema extension for PR head owner, and related tests for message patterns and branch/PR matching.

Changes

Cohort / File(s) Summary
Merge flow & utils
apps/desktop/src/lib/trpc/routers/changes/git-operations.ts, .../changes/utils/merge-pull-request.ts, .../changes/utils/worktree-status-caches.ts, .../changes/git-utils.ts
Refactored mergePR to call new mergePullRequest(...); added clearWorktreeStatusCaches(...); introduced isNoPullRequestFoundMessage(...) and moved cache-clear responsibility into helper. Added explicit handling for already-merged/closed PR states.
PR resolution & repo context
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/pr-resolution.ts, .../repo-context.ts, .../index.ts, .../github.ts
New PR-resolution module (getPRForBranch, branch-candidate generation, fork-aware matching), repo-context resolution with URL normalization and caching, and re-exports. github.ts trimmed to delegate PR lookup to new modules.
Types
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
Extended GHPRResponseSchema with optional/nullable headRepositoryOwner { login: string } field to support fork-owner checks.
Tests
apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.test.ts
Added tests for isNoPullRequestFoundMessage, expanded tests covering getPRHeadBranchCandidates() ordering/deduplication and prMatchesLocalBranch() owner-aware matching; adjusted import locations.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Router as mergePR mutation
    participant MergeHelper as mergePullRequest
    participant RepoCtx as getRepoContext
    participant PRResolver as getPRForBranch
    participant GitCLI as gh CLI
    participant Legacy as legacy branch-based gh merge

    Client->>Router: request merge
    Router->>MergeHelper: mergePullRequest(worktreePath,strategy)
    MergeHelper->>RepoCtx: getRepoContext(worktreePath)
    RepoCtx-->>MergeHelper: repoContext|null
    MergeHelper->>PRResolver: getPRForBranch(worktreePath,localBranch,repoContext,headSha)
    PRResolver->>GitCLI: gh pr view / gh pr list / gh pr list --search (branch/sha)
    GitCLI-->>PRResolver: PR candidates / null
    PRResolver-->>MergeHelper: PR data|null
    alt PR found
        MergeHelper->>GitCLI: gh pr merge <number> --<strategy> [--repo ...]
        alt merge succeeds
            GitCLI-->>MergeHelper: success
            MergeHelper->>WorktreeCaches: clearWorktreeStatusCaches(path)
            MergeHelper-->>Router: {success:true,mergedAt}
        else "no pull request found" error
            MergeHelper->>Legacy: fallback merge by branch
            Legacy-->>MergeHelper: result
            MergeHelper-->>Router: result
        end
    else PR not found or error
        MergeHelper->>Legacy: fallback merge by branch
        Legacy-->>MergeHelper: result
        MergeHelper-->>Router: result
    end
    Router-->>Client: merge result or TRPC error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I nibble code and hop around,
Where PRs and branches both are found.
Forks and owners, matched just right,
Merges land softly in the night.
Hooray! A carrot for each green light 🌿

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description provides clear summary, reproduction steps, and testing commands, but is missing formal sections from the template. Add standard sections: 'Related Issues', 'Type of Change', 'Screenshots (if applicable)', and 'Additional Notes' to match the repository template structure.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: hardening PR merge detection with fallback behavior, which is the core objective of this refactoring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts:414">
P2: Do not use an empty catch in the new branch-based PR lookup; log the error context before returning null.

(Based on your team's feedback about avoiding empty catch blocks that hide failures.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/desktop/src/lib/trpc/routers/changes/git-operations.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/changes/git-operations.ts:659">
P2: The broad fallback catch reruns legacy branch merge after explicit merge errors, which masks non-`no pull request` failures and can invoke `gh pr merge` multiple times on the same request.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts Outdated
Comment thread apps/desktop/src/lib/trpc/routers/changes/git-operations.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts (1)

373-417: Consider parallelizing branch candidate lookups.

The sequential for...of loop over getPRHeadBranchCandidates means two API calls when the local branch has an owner prefix (e.g., "kitenite/feature" → queries for both "kitenite/feature" and "feature").

Parallelizing with Promise.all could reduce latency:

♻️ Optional: Parallelize branch candidate lookups
 async function findPRByHeadBranch(
 	worktreePath: string,
 	localBranch: string,
 	repoContext?: RepoContext,
 	headSha?: string,
 ): Promise<GitHubStatus["pr"]> {
 	try {
 		const matches = new Map<number, GHPRResponse>();
+		const candidates = getPRHeadBranchCandidates(localBranch);
 
-		for (const branchCandidate of getPRHeadBranchCandidates(localBranch)) {
+		const results = await Promise.all(
+			candidates.map(async (branchCandidate) => {
+				try {
-			const { stdout } = await execWithShellEnv(
+					const { stdout } = await execWithShellEnv(
-				"gh",
+						"gh",
-				[
+						[
-					"pr",
+							"pr",
-					"list",
+							"list",
-					...getPullRequestRepoArgs(repoContext),
+							...getPullRequestRepoArgs(repoContext),
-					"--state",
+							"--state",
-					"all",
+							"all",
-					"--head",
+							"--head",
-					branchCandidate,
+							branchCandidate,
-					"--limit",
+							"--limit",
-					"20",
+							"20",
-					"--json",
+							"--json",
-					PR_JSON_FIELDS,
+							PR_JSON_FIELDS,
-				],
+						],
-				{ cwd: worktreePath },
+						{ cwd: worktreePath },
-			);
+					);
+					return parsePRListResponse(stdout);
+				} catch {
+					return [];
+				}
+			}),
+		);
 
-			for (const candidate of parsePRListResponse(stdout)) {
+		for (const prList of results) {
+			for (const candidate of prList) {
 				if (prMatchesLocalBranch(localBranch, candidate)) {
 					matches.set(candidate.number, candidate);
 				}
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts` around
lines 373 - 417, findPRByHeadBranch currently queries gh sequentially for each
branch candidate; change it to issue all execWithShellEnv calls in parallel by
mapping getPRHeadBranchCandidates(localBranch) to an array of Promises and
awaiting Promise.all, then iterate over the completed stdout results to call
parsePRListResponse and prMatchesLocalBranch and populate the matches Map; keep
existing behavior for getPullRequestRepoArgs, PR_JSON_FIELDS, error handling,
and the subsequent sortPRCandidates/formatPRData flow so bestMatch selection is
unchanged (refer to function names: findPRByHeadBranch,
getPRHeadBranchCandidates, execWithShellEnv, parsePRListResponse,
prMatchesLocalBranch, sortPRCandidates, formatPRData).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts`:
- Around line 373-417: findPRByHeadBranch currently queries gh sequentially for
each branch candidate; change it to issue all execWithShellEnv calls in parallel
by mapping getPRHeadBranchCandidates(localBranch) to an array of Promises and
awaiting Promise.all, then iterate over the completed stdout results to call
parsePRListResponse and prMatchesLocalBranch and populate the matches Map; keep
existing behavior for getPullRequestRepoArgs, PR_JSON_FIELDS, error handling,
and the subsequent sortPRCandidates/formatPRData flow so bestMatch selection is
unchanged (refer to function names: findPRByHeadBranch,
getPRHeadBranchCandidates, execWithShellEnv, parsePRListResponse,
prMatchesLocalBranch, sortPRCandidates, formatPRData).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8dc2391c-29f0-4302-8c27-8db46d56840f

📥 Commits

Reviewing files that changed from the base of the PR and between 791a344 and 91045f2.

📒 Files selected for processing (6)
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-utils.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts

@Kitenite Kitenite force-pushed the kitenite/pr-detection-issue branch from 91045f2 to a3c4f35 Compare March 21, 2026 01:23
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/changes/utils/merge-pull-request.ts`:
- Around line 72-98: The code currently falls back to runMerge(legacyMergeArgs)
for any error thrown after attempting an explicit PR merge, undoing the
hardening; fix by tracking whether the PR lookup/explicit-merge attempt actually
succeeded and only allow the legacy fallback when the failure was due to "no
pull request found" or when the PR lookup never succeeded. Concretely: introduce
a boolean flag (e.g., prAttempted) before the inner try around runMerge(args),
set prAttempted = true when getPRForBranch/explicit merge is attempted, and in
the outer catch only call runMerge(legacyMergeArgs) when prAttempted is false or
when isNoPullRequestFoundMessage(error.message) returns true; otherwise rethrow
the error (except for PR_ALREADY_MERGED_MESSAGE/PR_CLOSED_MESSAGE handling which
should remain unchanged). Ensure you reference runMerge,
isNoPullRequestFoundMessage, PR_ALREADY_MERGED_MESSAGE, and PR_CLOSED_MESSAGE
when making the change.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/repo-context.ts`:
- Around line 36-58: When data.isFork is true but data.parent is missing, do not
apply the origin vs ghUrl heuristic or construct a RepoContext; instead return
null to avoid producing a misleading repo/upstream pair. Update the branch
handling around data.isFork/data.parent in repo-context.ts (the block using
data.isFork, data.parent, getOriginUrl, normalizeGitHubUrl and constructing
context) so that if data.isFork && !data.parent the function returns null
immediately; keep the existing fork handling only for data.isFork && data.parent
and preserve the non-fork fallback for when data.isFork is false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 772852b5-d21b-4da1-923a-c71b998cc9c2

📥 Commits

Reviewing files that changed from the base of the PR and between 91045f2 and a3c4f35.

📒 Files selected for processing (11)
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-utils.ts
  • apps/desktop/src/lib/trpc/routers/changes/utils/merge-pull-request.ts
  • apps/desktop/src/lib/trpc/routers/changes/utils/worktree-status-caches.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.test.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/index.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/pr-resolution.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/repo-context.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/desktop/src/lib/trpc/routers/changes/git-utils.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
  • apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts

Comment thread apps/desktop/src/lib/trpc/routers/changes/utils/merge-pull-request.ts Outdated
Comment on lines +36 to +58
if (data.isFork && data.parent) {
context = {
repoUrl: data.url,
upstreamUrl: data.parent.url,
isFork: true,
};
} else {
const originUrl = await getOriginUrl(worktreePath);
const ghUrl = normalizeGitHubUrl(data.url);

if (originUrl && ghUrl && originUrl !== ghUrl) {
context = {
repoUrl: originUrl,
upstreamUrl: ghUrl,
isFork: true,
};
} else {
context = {
repoUrl: data.url,
upstreamUrl: data.url,
isFork: false,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't reuse the origin !== ghUrl heuristic once gh has already told us this is a fork.

When data.isFork is true but parent is missing, ghUrl is still the fork URL. This fallback branch then either marks the repo as a non-fork or swaps repoUrl/upstreamUrl, which makes downstream --repo resolution query the wrong repository. Returning null here is safer than constructing a misleading RepoContext.

🛠️ Suggested guard
 		if (data.isFork && data.parent) {
 			context = {
 				repoUrl: data.url,
 				upstreamUrl: data.parent.url,
 				isFork: true,
 			};
 		} else {
 			const originUrl = await getOriginUrl(worktreePath);
 			const ghUrl = normalizeGitHubUrl(data.url);
 
+			if (data.isFork) {
+				return null;
+			}
+
 			if (originUrl && ghUrl && originUrl !== ghUrl) {
 				context = {
 					repoUrl: originUrl,
 					upstreamUrl: ghUrl,
 					isFork: true,
 				};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/github/repo-context.ts`
around lines 36 - 58, When data.isFork is true but data.parent is missing, do
not apply the origin vs ghUrl heuristic or construct a RepoContext; instead
return null to avoid producing a misleading repo/upstream pair. Update the
branch handling around data.isFork/data.parent in repo-context.ts (the block
using data.isFork, data.parent, getOriginUrl, normalizeGitHubUrl and
constructing context) so that if data.isFork && !data.parent the function
returns null immediately; keep the existing fork handling only for data.isFork
&& data.parent and preserve the non-fork fallback for when data.isFork is false.

…itenite/pr-detection-issue

# Conflicts:
#	apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
#	apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
#	apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.test.ts
#	apps/desktop/src/lib/trpc/routers/workspaces/utils/github/github.ts
#	apps/desktop/src/lib/trpc/routers/workspaces/utils/github/types.ts
@Kitenite Kitenite merged commit a8d0ebd into superset-sh:main Mar 21, 2026
7 checks passed
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