From 206ac9d3e9e59192f4ee3077c0056b8f0c1f2de6 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 24 Jun 2026 10:41:16 +0800 Subject: [PATCH 1/2] Show yellow indicator when PR checks are still running Add .checksPending(Int) to PullRequestMergeBlockingReason so worktree sidebar rows can distinguish between 'CI still running' and 'all checks passed'. When checks are in progress but none have failed, the label shows 'N checks running' in yellow instead of the green 'Mergeable'. Failed checks still take priority over pending ones, so users see the most important information first. Fixes #462 Signed-off-by: Alex --- .../Github/PullRequestMergeReadiness.swift | 8 +++ .../Repositories/Views/WorktreeRow.swift | 13 +++- .../PullRequestMergeReadinessTests.swift | 68 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/supacode/Clients/Github/PullRequestMergeReadiness.swift b/supacode/Clients/Github/PullRequestMergeReadiness.swift index 659dd738..66774459 100644 --- a/supacode/Clients/Github/PullRequestMergeReadiness.swift +++ b/supacode/Clients/Github/PullRequestMergeReadiness.swift @@ -4,6 +4,7 @@ nonisolated enum PullRequestMergeBlockingReason: Equatable, Hashable { case mergeConflicts case changesRequested case checksFailed(Int) + case checksPending(Int) case blocked } @@ -29,6 +30,10 @@ nonisolated struct PullRequestMergeReadiness: Equatable, Hashable { self.blockingReason = .checksFailed(breakdown.failed) return } + if breakdown.inProgress > 0 { + self.blockingReason = .checksPending(breakdown.inProgress) + return + } if mergeable == "MERGEABLE" { self.blockingReason = nil @@ -57,6 +62,9 @@ nonisolated struct PullRequestMergeReadiness: Equatable, Hashable { case .checksFailed(let count): let checksLabel = count == 1 ? "check" : "checks" return "\(count) \(checksLabel) failed" + case .checksPending(let count): + let checksLabel = count == 1 ? "check" : "checks" + return "\(count) \(checksLabel) running" case .blocked: return "Blocked" } diff --git a/supacode/Features/Repositories/Views/WorktreeRow.swift b/supacode/Features/Repositories/Views/WorktreeRow.swift index f3f3be28..5de0524e 100644 --- a/supacode/Features/Repositories/Views/WorktreeRow.swift +++ b/supacode/Features/Repositories/Views/WorktreeRow.swift @@ -288,11 +288,22 @@ private struct WorktreeRowInfoView: View { } else if let mergeReadiness { appendSeparator() var segment = AttributedString(mergeReadiness.label) - segment.foregroundColor = mergeReadiness.isBlocking ? .red : .green + segment.foregroundColor = mergeStatusColor(mergeReadiness) result.append(segment) } return Text(result) } + + private func mergeStatusColor(_ readiness: PullRequestMergeReadiness) -> Color { + switch readiness.blockingReason { + case .none: + return .green + case .mergeConflicts, .changesRequested, .checksFailed, .blocked: + return .red + case .checksPending: + return .yellow + } + } } // MARK: - Previews diff --git a/supacodeTests/PullRequestMergeReadinessTests.swift b/supacodeTests/PullRequestMergeReadinessTests.swift index e6ba180d..1e047760 100644 --- a/supacodeTests/PullRequestMergeReadinessTests.swift +++ b/supacodeTests/PullRequestMergeReadinessTests.swift @@ -72,6 +72,74 @@ struct PullRequestMergeReadinessTests { #expect(readiness.blockingReason == .blocked) #expect(readiness.label == "Blocked") } + + @Test func mergeReadinessUsesChecksPendingWhenInProgressAndNoFailures() { + let pullRequest = makePullRequest( + mergeable: "MERGEABLE", + mergeStateStatus: "CLEAN", + checks: [ + GithubPullRequestStatusCheck(status: "PENDING", conclusion: nil, state: nil), + GithubPullRequestStatusCheck(status: "PENDING", conclusion: nil, state: nil), + GithubPullRequestStatusCheck(status: "COMPLETED", conclusion: "SUCCESS", state: nil), + ] + ) + + let readiness = PullRequestMergeReadiness(pullRequest: pullRequest) + + #expect(readiness.blockingReason == .checksPending(2)) + #expect(readiness.label == "2 checks running") + #expect(readiness.isBlocking) + } + + @Test func mergeReadinessSingleCheckPending() { + let pullRequest = makePullRequest( + mergeable: "MERGEABLE", + mergeStateStatus: "CLEAN", + checks: [ + GithubPullRequestStatusCheck(status: "PENDING", conclusion: nil, state: nil), + ] + ) + + let readiness = PullRequestMergeReadiness(pullRequest: pullRequest) + + #expect(readiness.blockingReason == .checksPending(1)) + #expect(readiness.label == "1 check running") + } + + @Test func mergeReadinessChecksPendingTakesPriorityOverMergeable() { + // When checks are in progress but mergeable is already true, + // we should still show "checks running" rather than "Mergeable" + let pullRequest = makePullRequest( + mergeable: "MERGEABLE", + mergeStateStatus: "CLEAN", + checks: [ + GithubPullRequestStatusCheck(status: "IN_PROGRESS", conclusion: nil, state: nil), + ] + ) + + let readiness = PullRequestMergeReadiness(pullRequest: pullRequest) + + #expect(readiness.blockingReason == .checksPending(1)) + #expect(readiness.label == "1 check running") + } + + @Test func mergeReadinessChecksFailedTakesPriorityOverPending() { + // When there are both failed and in-progress checks, + // failed should take priority + let pullRequest = makePullRequest( + mergeable: "MERGEABLE", + mergeStateStatus: "CLEAN", + checks: [ + GithubPullRequestStatusCheck(status: "COMPLETED", conclusion: "FAILURE", state: nil), + GithubPullRequestStatusCheck(status: "PENDING", conclusion: nil, state: nil), + ] + ) + + let readiness = PullRequestMergeReadiness(pullRequest: pullRequest) + + #expect(readiness.blockingReason == .checksFailed(1)) + #expect(readiness.label == "1 check failed") + } } private func makePullRequest( -- 2.51.2 From 27ca2086050e87b75662c4dba669f863a4ce2700 Mon Sep 17 00:00:00 2001 From: onevcat Date: Wed, 24 Jun 2026 18:23:16 +0900 Subject: [PATCH 2/2] Treat expected checks as pending in merge readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on #496. The pending detection only counted breakdown.inProgress, so a PR whose only outstanding check is an 'expected' commit-status required context (declared but not yet reported) would fall through to a green "Mergeable" label — the exact class of bug #462 fixes. Fold breakdown.expected into the pending count so these PRs also show the yellow "N checks running" state. Adds tests for an expected-only check and a mixed in-progress + expected count. --- .../Github/PullRequestMergeReadiness.swift | 8 +++-- .../PullRequestMergeReadinessTests.swift | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/supacode/Clients/Github/PullRequestMergeReadiness.swift b/supacode/Clients/Github/PullRequestMergeReadiness.swift index 66774459..dbee437c 100644 --- a/supacode/Clients/Github/PullRequestMergeReadiness.swift +++ b/supacode/Clients/Github/PullRequestMergeReadiness.swift @@ -30,8 +30,12 @@ nonisolated struct PullRequestMergeReadiness: Equatable, Hashable { self.blockingReason = .checksFailed(breakdown.failed) return } - if breakdown.inProgress > 0 { - self.blockingReason = .checksPending(breakdown.inProgress) + // `expected` checks (legacy commit-status required contexts that have not + // reported yet) are still in flight, so treat them like in-progress checks + // rather than letting the PR fall through to a green "Mergeable". + let pendingChecks = breakdown.inProgress + breakdown.expected + if pendingChecks > 0 { + self.blockingReason = .checksPending(pendingChecks) return } diff --git a/supacodeTests/PullRequestMergeReadinessTests.swift b/supacodeTests/PullRequestMergeReadinessTests.swift index 1e047760..2ad19bc5 100644 --- a/supacodeTests/PullRequestMergeReadinessTests.swift +++ b/supacodeTests/PullRequestMergeReadinessTests.swift @@ -123,6 +123,42 @@ struct PullRequestMergeReadinessTests { #expect(readiness.label == "1 check running") } + @Test func mergeReadinessTreatsExpectedChecksAsPending() { + // A required commit-status context that has not reported yet shows up as an + // `expected` check. It is still in flight, so the PR must not fall through to + // a green "Mergeable" label. + let pullRequest = makePullRequest( + mergeable: "MERGEABLE", + mergeStateStatus: "CLEAN", + checks: [ + GithubPullRequestStatusCheck(status: nil, conclusion: nil, state: "EXPECTED") + ] + ) + + let readiness = PullRequestMergeReadiness(pullRequest: pullRequest) + + #expect(readiness.blockingReason == .checksPending(1)) + #expect(readiness.label == "1 check running") + #expect(readiness.isBlocking) + } + + @Test func mergeReadinessCountsInProgressAndExpectedChecksTogether() { + let pullRequest = makePullRequest( + mergeable: "MERGEABLE", + mergeStateStatus: "CLEAN", + checks: [ + GithubPullRequestStatusCheck(status: "IN_PROGRESS", conclusion: nil, state: nil), + GithubPullRequestStatusCheck(status: nil, conclusion: nil, state: "EXPECTED"), + GithubPullRequestStatusCheck(status: "COMPLETED", conclusion: "SUCCESS", state: nil), + ] + ) + + let readiness = PullRequestMergeReadiness(pullRequest: pullRequest) + + #expect(readiness.blockingReason == .checksPending(2)) + #expect(readiness.label == "2 checks running") + } + @Test func mergeReadinessChecksFailedTakesPriorityOverPending() { // When there are both failed and in-progress checks, // failed should take priority -- 2.51.2