authorgravatar for 35393046+lnieuwenhuis@users.noreply.github.comLars Nieuwenhuis <35393046+lnieuwenhuis@users.noreply.github.com> 2026-08-15 12:43:20+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 12:43:20+02:00
log75472802bc5ddaba860dc652000223600e529937
tree9a818403e9ca2e4dc7b1a179200a048f4233ed67
parentafca73d3683c99057ea8af1ad7d77511a0faf680
signature Signed by PGP key B5690EEEBB952194

fix(server): treat removed Bitbucket permissions endpoint as unknown, not blocking (#6525)


2 files changed, 58 insertions(+), 1 deletions(-)

apps/server/src/pullRequest/BitbucketPullRequestApi.test.ts+40
......@@ -867,6 +867,46 @@ layer("BitbucketPullRequestApi.layer", (it) => {
867867 }),
868868 );
869869
870 it.effect(
871 "reads a removed permissions endpoint as granted rather than failing the merge on it",
872 () =>
873 Effect.gen(function* () {
874 // Bitbucket retired /user/permissions/repositories under CHANGE-2770: every account now
875 // gets HTTP 410 here, whatever it may do.
876 mockedRequest.mockReturnValue(
877 Effect.fail(
878 new BitbucketApi.BitbucketResponseError({
879 operation: "request",
880 status: 410,
881 responseBodyLength: 0,
882 }),
883 ),
884 );
885 const api = yield* BitbucketPullRequestApi.BitbucketPullRequestApi;
886
887 assert.isTrue(yield* api.getRepositoryPermission({ repository: "acme/web" }));
888 }),
889 );
890
891 it.effect("still fails the permission read on a failure that is not the removed endpoint", () =>
892 Effect.gen(function* () {
893 mockedRequest.mockReturnValue(
894 Effect.fail(
895 new BitbucketApi.BitbucketResponseError({
896 operation: "request",
897 status: 401,
898 responseBodyLength: 0,
899 }),
900 ),
901 );
902 const api = yield* BitbucketPullRequestApi.BitbucketPullRequestApi;
903
904 const error = yield* Effect.flip(api.getRepositoryPermission({ repository: "acme/web" }));
905
906 assert.strictEqual(error._tag, "BitbucketResponseError");
907 }),
908 );
909
870910 it.effect("reads the workspace's people and marks whoever is already a reviewer", () =>
871911 Effect.gen(function* () {
872912 mockedRequest
apps/server/src/pullRequest/BitbucketPullRequestApi.ts+18-1
......@@ -107,6 +107,16 @@ export type BitbucketPullRequestApiError =
107107 | BitbucketRepositoryUnsupportedError
108108 | BitbucketDiffCommitError;
109109
110/**
111 * `/user/permissions/repositories` answering CHANGE-2770's removal notice rather than a
112 * permission — Bitbucket sends this for every account now, not only ones it would have refused.
113 */
114function isRepositoryPermissionRemovedError(
115 error: BitbucketPullRequestApiError,
116): error is BitbucketApi.BitbucketResponseError {
117 return error._tag === "BitbucketResponseError" && error.status === 410;
118}
119
110120/**
111121 * Bitbucket's own ceiling. Asking for more does not fail — it answers with an empty page and no
112122 * error at all, so this is a number to respect rather than to push against.
......@@ -553,6 +563,13 @@ export const make = Effect.gen(function* () {
553563 // Nothing on the repository, the pull request or the workspace states what the credentials
554564 // may do, so this endpoint is the one request Bitbucket makes unavoidable. It is asked
555565 // alongside the reads the detail was already making, so it costs no round trip of its own.
566 //
567 // Bitbucket permanently removed this endpoint (CHANGE-2770): every account now gets HTTP 410
568 // in place of an answer, whatever it may do. That is the deprecated-endpoint signal, not a
569 // permission being refused, so it is read the same way an unreachable read already is
570 // elsewhere — as a permission that could not be learned, which grants rather than blocks, and
571 // leaves the actual merge or write to say why if the account may not do it. Any other failure
572 // (a bad token, a network fault, an unreadable body) still fails as it did before.
556573 getRepositoryPermission: (input) =>
557574 withRepository(input.repository, () =>
558575 readPage({
......@@ -562,7 +579,7 @@ export const make = Effect.gen(function* () {
562579 )}`,
563580 decode: decodeRepositoryPermissionJson,
564581 }),
565 ),
582 ).pipe(Effect.catchIf(isRepositoryPermissionRemovedError, () => Effect.succeed(true))),
566583
567584 getPullRequestDiff: (input) =>
568585 input.commit !== undefined && !isCommitSha(input.commit)