From 7bc161f8692dfb969489f40bf3eb5bd27bc01f10 Mon Sep 17 00:00:00 2001 From: oliver <97427849+flamboh@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:40:10 -0700 Subject: [PATCH] fix(web): links to issues no longer strand the pull request viewer (#14242) --- .../AzureDevOpsPullRequestProvider.ts | 1 + .../BitbucketPullRequestProvider.ts | 6 +++++ .../pullRequest/ForgejoPullRequestProvider.ts | 4 ++- .../pullRequest/GitHubPullRequestProvider.ts | 1 + .../pullRequest/GitLabPullRequestProvider.ts | 1 + .../src/pullRequest/PullRequestProvider.ts | 8 +++++- .../pullRequest/PullRequestService.test.ts | 27 +++++++++++++++++++ .../src/pullRequest/PullRequestService.ts | 7 ++++- .../pullRequest/PullRequestDetailPanel.tsx | 9 ++++++- .../pullRequest/pullRequestDetail.logic.ts | 7 +++++ apps/web/src/state/query.ts | 7 +++-- packages/contracts/src/pullRequest.ts | 1 + 12 files changed, 73 insertions(+), 6 deletions(-) diff --git a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts b/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts index b11ba6e20563813c433eabe027baa19e1b059d0b..2cdf78c56a1456a264fdad465833c3d7384d0c33 100644 --- a/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts +++ b/apps/server/src/pullRequest/AzureDevOpsPullRequestProvider.ts @@ -117,6 +117,7 @@ export function azureDevOpsProviderFailure( if (error._tag === "AzureDevOpsCliUnavailableError") return { reason: "missing-tool" }; if (error._tag === "AzureDevOpsCliAuthenticationError") return { reason: "unauthenticated" }; if (error._tag === "AzureDevOpsCliRateLimitError") return { reason: "rate-limited" }; + if (error._tag === "AzureDevOpsPullRequestNotFoundError") return { reason: "not-found" }; return { reason: "failed" }; } diff --git a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts b/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts index e0ee4c0a85b7b5669edeb3e2856be7dd99b716c0..f553dfd13bc1a23a4518181ecb8832d447b2fe80 100644 --- a/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts +++ b/apps/server/src/pullRequest/BitbucketPullRequestProvider.ts @@ -82,6 +82,12 @@ export function bitbucketProviderFailure( ...(error.retryAt === undefined ? {} : { retryAt: error.retryAt }), }; } + if ( + (error._tag === "BitbucketResponseError" || error._tag === "BitbucketResponseBodyReadError") && + error.status === 404 + ) { + return { reason: "not-found" }; + } return { reason: "failed" }; } diff --git a/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts b/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts index 73d582c9042c8f61617e6b713075dc89ce3fb0c2..12d931279a74c4644f31acc3363a4f3a56955857 100644 --- a/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts +++ b/apps/server/src/pullRequest/ForgejoPullRequestProvider.ts @@ -89,7 +89,9 @@ export const make = Effect.gen(function* () { ? "unauthenticated" : error.reason === "rate-limit" ? "rate-limited" - : "failed", + : error.reason === "not-found" + ? "not-found" + : "failed", }), ), ); diff --git a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts b/apps/server/src/pullRequest/GitHubPullRequestProvider.ts index 5e70c2ee1ed4cefe28ac1837dc9539b59607d835..a51825c2e11b528ab87f4b5d9a9ef0730c54d132 100644 --- a/apps/server/src/pullRequest/GitHubPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GitHubPullRequestProvider.ts @@ -115,6 +115,7 @@ export function gitHubProviderFailure( if (error._tag === "SourceControlRateLimitPausedError") { return { reason: "rate-limited", retryAt: error.retryAt }; } + if (error._tag === "GitHubPullRequestNotFoundError") return { reason: "not-found" }; return { reason: "failed" }; } diff --git a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts b/apps/server/src/pullRequest/GitLabPullRequestProvider.ts index ad089382352a15fd0acdc3e581d0f181d49b3bdc..45034b84787c3210c69bb603a7d71800a06e64bb 100644 --- a/apps/server/src/pullRequest/GitLabPullRequestProvider.ts +++ b/apps/server/src/pullRequest/GitLabPullRequestProvider.ts @@ -102,6 +102,7 @@ export function gitLabProviderFailure( if (error._tag === "GitLabCliUnavailableError") return { reason: "missing-tool" }; if (error._tag === "GitLabCliAuthenticationError") return { reason: "unauthenticated" }; if (error._tag === "GitLabCliRateLimitError") return { reason: "rate-limited" }; + if (error._tag === "GitLabMergeRequestNotFoundError") return { reason: "not-found" }; return { reason: "failed" }; } diff --git a/apps/server/src/pullRequest/PullRequestProvider.ts b/apps/server/src/pullRequest/PullRequestProvider.ts index cc370637c6014647b7d11e74478b24f701c42612..f13af63304d22d8b7254d54654a86e2244a93345 100644 --- a/apps/server/src/pullRequest/PullRequestProvider.ts +++ b/apps/server/src/pullRequest/PullRequestProvider.ts @@ -52,7 +52,13 @@ export class PullRequestProviderError extends Schema.TaggedError + Effect.gen(function* () { + const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 121 }; + const service = yield* makeService({ + projects: [project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" })], + providers: [ + fakeProvider("github", { + getChangeRequest: () => + Effect.fail( + new PullRequestProviderError({ + provider: "github", + operation: "getChangeRequest", + reason: "not-found", + detail: "Pull request not found. Check the PR number or URL and try again.", + }), + ), + }), + ], + }); + + const error = yield* Effect.flip(service.detail(reference)); + + assert.strictEqual(error._tag, "PullRequestOperationError"); + assert.strictEqual(error._tag === "PullRequestOperationError" && error.reason, "not-found"); + }), +); + it.effect("keeps recent detail on a transient refresh failure but not after invalidation", () => Effect.gen(function* () { let failing = false; diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 293318f91c4bac4a6322badf8f4fcc0f089fbe5e..ec523d398237a17194e52ad9122cb0bed31f186a 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -477,7 +477,12 @@ function toPullRequestError( return (error) => isProviderUnusable(error) ? toUnavailableError(error) - : new PullRequestOperationError({ operation, detail: error.detail, cause: error }); + : new PullRequestOperationError({ + operation, + detail: error.detail, + ...(error.reason === "not-found" ? { reason: "not-found" as const } : {}), + cause: error, + }); } function withRateLimitBackoff( diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 5c82e03aa8ea8395ac68ad8aef52acff2cfd327b..bf5bd62de91ce8207d8acf9b302dd3c0725304c8 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -144,6 +144,7 @@ import { handoffReviewComments, latestPullRequestReviewOutcomes, loadingPullRequestCheckoutCommand, + isPullRequestNotFound, isStackedPullRequestBase, pullRequestActionMenuHasGroup, pullRequestActionNeedsHostRefresh, @@ -2719,7 +2720,13 @@ export function PullRequestDetailPanel({ > {detailQuery.error && !detail ? ( (fals Atom.withLabel("web-environment-query:empty"), ); -export interface EnvironmentQueryView { +export interface EnvironmentQueryView { readonly data: A | null; readonly dataUpdatedAt: number; readonly error: string | null; + readonly failure: E | null; readonly isPending: boolean; readonly isSuccess: boolean; readonly refresh: () => void; @@ -25,7 +26,7 @@ export function formatEnvironmentQueryError(cause: Cause.Cause): string export function useEnvironmentQuery( atom: Atom.Atom> | null, -): EnvironmentQueryView { +): EnvironmentQueryView { const selectedAtom = atom ?? EMPTY_ASYNC_RESULT_ATOM; const result = useAtomValue(selectedAtom); const refresh = useAtomRefresh(selectedAtom); @@ -38,6 +39,8 @@ export function useEnvironmentQuery( ? (Option.getOrNull(result.previousSuccess)?.timestamp ?? 0) : 0, error: result._tag === "Failure" ? formatEnvironmentQueryError(result.cause) : null, + failure: + result._tag === "Failure" ? Option.getOrNull(Cause.findErrorOption(result.cause)) : null, isPending: atom !== null && result.waiting, isSuccess: result._tag === "Success", refresh, diff --git a/packages/contracts/src/pullRequest.ts b/packages/contracts/src/pullRequest.ts index c8feaad9a41f2a0c9fa2a86b44e26608661b7145..b17d0141daef7802d84e5204ab2b59dcc082f6f5 100644 --- a/packages/contracts/src/pullRequest.ts +++ b/packages/contracts/src/pullRequest.ts @@ -1407,6 +1407,7 @@ export class PullRequestOperationError extends Schema.TaggedError