diff --git a/CHANGELOG.md b/CHANGELOG.md index b70d11c4e..5afa031d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed issue where repo permissions could go stale when authentication or token refresh related errors occured. [#1215](https://github.com/sourcebot-dev/sourcebot/pull/1215) - [EE] Fixed issue where repo permissions could go stale when an upstream endpoint returned HTTP 410 Gone (e.g. Bitbucket Cloud's CHANGE-2770). [#1216](https://github.com/sourcebot-dev/sourcebot/pull/1216) +- [EE] Fixed Bitbucket Cloud account-driven permission sync after Atlassian's CHANGE-2770 removed `GET /2.0/user/permissions/repositories`. [#1217](https://github.com/sourcebot-dev/sourcebot/pull/1217) ## [4.17.2] - 2026-05-16 diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts index 241a79a8d..9051164a7 100644 --- a/packages/backend/src/bitbucket.ts +++ b/packages/backend/src/bitbucket.ts @@ -9,7 +9,6 @@ import micromatch from "micromatch"; import { SchemaRepository as CloudRepository, SchemaRepositoryUserPermission as CloudRepositoryUserPermission, - SchemaRepositoryPermission as CloudRepositoryPermission, } from "@coderabbitai/bitbucket/cloud/openapi"; import { SchemaRestRepository as ServerRepository } from "@coderabbitai/bitbucket/server/openapi"; import { processPromiseResults } from "./connectionUtils.js"; @@ -660,24 +659,54 @@ export const getExplicitUserPermissionsForCloudRepo = async ( /** * Returns the UUIDs of all private repositories accessible to the authenticated Bitbucket Cloud user. * Used for account-driven permission syncing. - * - * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-user-permissions-repositories-get + * + * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/#api-user-workspaces-get + * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-repositories-workspace-get */ export const getReposForAuthenticatedBitbucketCloudUser = async ( client: BitbucketClient, ): Promise> => { - const path = `/user/permissions/repositories` as CloudGetRequestPath; - - const permissions = await fetchWithRetry(() => getPaginatedCloud(path, async (p, query) => { - const { data } = await client.apiClient.GET(p, { - params: { query }, - }); - return data; - }), 'user repository permissions', logger); + interface CloudUserWorkspaceAccess { + readonly workspace?: { readonly slug?: string }; + } - return permissions - .filter(p => p.repository?.uuid != null) - .map(p => ({ uuid: p.repository!.uuid as string })); + const memberships = await fetchWithRetry( + () => getPaginatedCloud( + `/user/workspaces` as CloudGetRequestPath, + async (path, query) => { + const { data } = await client.apiClient.GET(path, { params: { query } }); + return data; + }, + ), + 'user workspace memberships', + logger, + ); + + const slugs = memberships + .map(m => m.workspace?.slug) + .filter((slug): slug is string => typeof slug === 'string'); + + const reposByWorkspace = await Promise.all(slugs.map(workspace => fetchWithRetry( + () => getPaginatedCloud( + `/repositories/${workspace}` as CloudGetRequestPath, + async (path, query) => { + const { data } = await client.apiClient.GET(path, { + params: { + path: { workspace }, + query: { role: 'member', q: 'is_private=true', ...query }, + }, + }); + return data; + }, + ), + `repos for workspace ${workspace}`, + logger, + ))); + + return reposByWorkspace + .flat() + .filter(repo => repo.uuid != null) + .map(repo => ({ uuid: repo.uuid as string })); }; /**