diff --git a/CHANGELOG.md b/CHANGELOG.md
index e41a1ba65..cb05995bc 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
### Added
- Added PostHog events for chat UI interactions (details card expand/collapse, copy answer, table of contents toggle) and repo tracking in `wa_chat_message_sent`. [#922](https://github.com/sourcebot-dev/sourcebot/pull/922)
- Added Bitbucket Cloud OAuth identity provider support (`provider: "bitbucket-cloud"`) for SSO and account-linked permission syncing. [#924](https://github.com/sourcebot-dev/sourcebot/pull/924)
+- Added permission syncing support for Bitbucket Cloud. [#925](https://github.com/sourcebot-dev/sourcebot/pull/925)
### Changed
- Hide version upgrade toast for askgithub deployment (`EXPERIMENT_ASK_GH_ENABLED`). [#931](https://github.com/sourcebot-dev/sourcebot/pull/931)
diff --git a/docs/docs/features/permission-syncing.mdx b/docs/docs/features/permission-syncing.mdx
index cb801288d..12ddf3645 100644
--- a/docs/docs/features/permission-syncing.mdx
+++ b/docs/docs/features/permission-syncing.mdx
@@ -39,7 +39,7 @@ We are actively working on supporting more code hosts. If you'd like to see a sp
|:----------|------------------------------|
| [GitHub (GHEC & GHEC Server)](/docs/features/permission-syncing#github) | ✅ |
| [GitLab (Self-managed & Cloud)](/docs/features/permission-syncing#gitlab) | ✅ |
-| Bitbucket Cloud | 🛑 |
+| [Bitbucket Cloud](/docs/features/permission-syncing#bitbucket-cloud) | 🟠 Partial |
| Bitbucket Data Center | 🛑 |
| Gitea | 🛑 |
| Gerrit | 🛑 |
@@ -78,6 +78,28 @@ Permission syncing works with **GitLab Self-managed** and **GitLab Cloud**. User
- OAuth tokens require the `read_api` scope in order to use the [List projects for the authenticated user API](https://docs.gitlab.com/ee/api/projects.html#list-all-projects) during [User driven syncing](/docs/features/permission-syncing#how-it-works).
- [Internal GitLab projects](https://docs.gitlab.com/user/public_access/#internal-projects-and-groups) are **not** enforced by permission syncing and therefore are visible to all users. Only [private projects](https://docs.gitlab.com/user/public_access/#private-projects-and-groups) are enforced.
+## Bitbucket Cloud
+
+Prerequisites:
+- Configure Bitbucket Cloud as an [external identity provider](/docs/configuration/idp).
+
+Permission syncing works with **Bitbucket Cloud**. OAuth tokens must assume the `account` and `repository` scopes.
+
+
+**Partial coverage for repo-driven syncing.** Bitbucket Cloud's [repository user permissions API](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-repositories-workspace-repo-slug-permissions-config-users-get) only returns users who have been **directly and explicitly** granted access to a repository. Users who have access via any of the following are **not** captured by repo-driven syncing:
+
+- Membership in a [group that is added to the repository](https://support.atlassian.com/bitbucket-cloud/docs/grant-repository-access-to-users-and-groups/)
+- Membership in the [project that contains the repository](https://support.atlassian.com/bitbucket-cloud/docs/configure-project-permissions-for-users-and-groups/)
+- Membership in a group that is part of a project containing the repository
+
+These users **will** still gain access via [user-driven syncing](/docs/features/permission-syncing#how-it-works), which fetches all private repositories accessible to each authenticated user. However, there may be a delay between when a repository is added and when affected users gain access in Sourcebot (up to the `experiment_userDrivenPermissionSyncIntervalMs` interval, which defaults to 24 hours).
+
+If your workspace relies heavily on group or project-level permissions rather than direct user grants, we recommend reducing the `experiment_userDrivenPermissionSyncIntervalMs` interval to limit the window of delay.
+
+
+**Notes:**
+- A Bitbucket Cloud [external identity provider](/docs/configuration/idp) must be configured to (1) correlate a Sourcebot user with a Bitbucket Cloud user, and (2) to list repositories that the user has access to for [User driven syncing](/docs/features/permission-syncing#how-it-works).
+- OAuth tokens require the `account` and `repository` scopes. The `repository` scope is required to list private repositories during [User driven syncing](/docs/features/permission-syncing#how-it-works).
# How it works
diff --git a/packages/backend/src/bitbucket.ts b/packages/backend/src/bitbucket.ts
index ef0b547e6..fd4c03ed3 100644
--- a/packages/backend/src/bitbucket.ts
+++ b/packages/backend/src/bitbucket.ts
@@ -1,5 +1,5 @@
-import { createBitbucketCloudClient } from "@coderabbitai/bitbucket/cloud";
-import { createBitbucketServerClient } from "@coderabbitai/bitbucket/server";
+import { createBitbucketCloudClient as createBitbucketCloudClientBase } from "@coderabbitai/bitbucket/cloud";
+import { createBitbucketServerClient as createBitbucketServerClientBase } from "@coderabbitai/bitbucket/server";
import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/bitbucket.type";
import type { ClientOptions, ClientPathsWithMethod } from "openapi-fetch";
import { createLogger } from "@sourcebot/shared";
@@ -8,6 +8,8 @@ import * as Sentry from "@sentry/node";
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";
@@ -34,10 +36,10 @@ interface BitbucketClient {
shouldExcludeRepo: (repo: BitbucketRepository, config: BitbucketConnectionConfig) => boolean;
}
-type CloudAPI = ReturnType;
+type CloudAPI = ReturnType;
type CloudGetRequestPath = ClientPathsWithMethod;
-type ServerAPI = ReturnType;
+type ServerAPI = ReturnType;
type ServerGetRequestPath = ClientPathsWithMethod;
type CloudPaginatedResponse = {
@@ -68,8 +70,8 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConnectionCon
}
const client = config.deploymentType === 'server' ?
- serverClient(config.url!, config.user, token) :
- cloudClient(config.user, token);
+ createBitbucketServerClient(config.url!, config.user, token) :
+ createBitbucketCloudClient(config.user, token);
let allRepos: BitbucketRepository[] = [];
let allWarnings: string[] = [];
@@ -102,11 +104,10 @@ export const getBitbucketReposFromConfig = async (config: BitbucketConnectionCon
};
}
-function cloudClient(user: string | undefined, token: string | undefined): BitbucketClient {
-
+export function createBitbucketCloudClient(user: string | undefined, token: string | undefined): BitbucketClient {
const authorizationString =
token
- ? !user || user == "x-token-auth"
+ ? (!user || user === "x-token-auth")
? `Bearer ${token}`
: `Basic ${Buffer.from(`${user}:${token}`).toString('base64')}`
: undefined;
@@ -119,7 +120,7 @@ function cloudClient(user: string | undefined, token: string | undefined): Bitbu
},
};
- const apiClient = createBitbucketCloudClient(clientOptions);
+ const apiClient = createBitbucketCloudClientBase(clientOptions);
var client: BitbucketClient = {
deploymentType: BITBUCKET_CLOUD,
token: token,
@@ -378,7 +379,7 @@ export function cloudShouldExcludeRepo(repo: BitbucketRepository, config: Bitbuc
return false;
}
-function serverClient(url: string, user: string | undefined, token: string | undefined): BitbucketClient {
+function createBitbucketServerClient(url: string, user: string | undefined, token: string | undefined): BitbucketClient {
const authorizationString = (() => {
// If we're not given any credentials we return an empty auth string. This will only work if the project/repos are public
if(!user && !token) {
@@ -400,7 +401,7 @@ function serverClient(url: string, user: string | undefined, token: string | und
},
};
- const apiClient = createBitbucketServerClient(clientOptions);
+ const apiClient = createBitbucketServerClientBase(clientOptions);
var client: BitbucketClient = {
deploymentType: BITBUCKET_SERVER,
token: token,
@@ -560,7 +561,7 @@ export function serverShouldExcludeRepo(repo: BitbucketRepository, config: Bitbu
const repoSlug = serverRepo.slug!;
const repoName = `${projectName}/${repoSlug}`;
let reason = '';
-
+
const shouldExclude = (() => {
if (config.exclude?.repos) {
if (micromatch.isMatch(repoName, config.exclude.repos)) {
@@ -587,4 +588,69 @@ export function serverShouldExcludeRepo(repo: BitbucketRepository, config: Bitbu
return true;
}
return false;
-}
\ No newline at end of file
+}
+
+/**
+ * Returns the account IDs of users who have been *explicitly* granted permission on a Bitbucket Cloud repository.
+ *
+ * @note This only covers direct user-to-repo grants. It does NOT include users who have access via:
+ * - A group that is explicitly added to the repo
+ * - Membership in the project that contains the repo
+ * - A group that is part of a project that contains the repo
+ * As a result, permission syncing may under-grant access for workspaces that rely on group or
+ * project-level permissions rather than direct user grants.
+ *
+ * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-repositories-workspace-repo-slug-permissions-config-users-get
+ */
+export const getExplicitUserPermissionsForCloudRepo = async (
+ client: BitbucketClient,
+ workspace: string,
+ repoSlug: string,
+): Promise> => {
+ const path = `/repositories/${workspace}/${repoSlug}/permissions-config/users` as CloudGetRequestPath;
+
+ const users = await getPaginatedCloud(path, async (p, query) => {
+ const response = await client.apiClient.GET(p, {
+ params: {
+ path: { workspace, repo_slug: repoSlug },
+ query,
+ },
+ });
+ const { data, error } = response;
+ if (error) {
+ throw new Error(`Failed to get explicit user permissions for ${workspace}/${repoSlug}: ${JSON.stringify(error)}`);
+ }
+ return data;
+ });
+
+ return users
+ .filter(u => u.user?.account_id != null)
+ .map(u => ({ accountId: u.user!.account_id as string }));
+};
+
+/**
+ * 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
+ */
+export const getReposForAuthenticatedBitbucketCloudUser = async (
+ client: BitbucketClient,
+): Promise> => {
+ const path = `/user/permissions/repositories` as CloudGetRequestPath;
+
+ const permissions = await getPaginatedCloud(path, async (p, query) => {
+ const response = await client.apiClient.GET(p, {
+ params: { query },
+ });
+ const { data, error } = response;
+ if (error) {
+ throw new Error(`Failed to get user repository permissions: ${JSON.stringify(error)}`);
+ }
+ return data;
+ });
+
+ return permissions
+ .filter(p => p.repository?.uuid != null)
+ .map(p => ({ uuid: p.repository!.uuid as string }));
+};
\ No newline at end of file
diff --git a/packages/backend/src/constants.ts b/packages/backend/src/constants.ts
index dc3d7fcce..76e865ed7 100644
--- a/packages/backend/src/constants.ts
+++ b/packages/backend/src/constants.ts
@@ -7,11 +7,13 @@ export const SINGLE_TENANT_ORG_ID = 1;
export const PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES: CodeHostType[] = [
'github',
'gitlab',
+ 'bitbucketCloud',
];
export const PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS: IdentityProviderType[] = [
'github',
'gitlab',
+ 'bitbucket-cloud',
];
export const REPOS_CACHE_DIR = path.join(env.DATA_CACHE_DIR, 'repos');
diff --git a/packages/backend/src/ee/accountPermissionSyncer.ts b/packages/backend/src/ee/accountPermissionSyncer.ts
index 283a0fcf9..3cbc02639 100644
--- a/packages/backend/src/ee/accountPermissionSyncer.ts
+++ b/packages/backend/src/ee/accountPermissionSyncer.ts
@@ -1,5 +1,5 @@
import * as Sentry from "@sentry/node";
-import { PrismaClient, AccountPermissionSyncJobStatus, Account} from "@sourcebot/db";
+import { PrismaClient, AccountPermissionSyncJobStatus, Account, PermissionSyncSource} from "@sourcebot/db";
import { env, hasEntitlement, createLogger, loadConfig, decryptOAuthToken } from "@sourcebot/shared";
import { Job, Queue, Worker } from "bullmq";
import { Redis } from "ioredis";
@@ -14,6 +14,7 @@ import {
getOAuthScopesForAuthenticatedUser as getGitLabOAuthScopesForAuthenticatedUser,
getProjectsForAuthenticatedUser,
} from "../gitlab.js";
+import { createBitbucketCloudClient, getReposForAuthenticatedBitbucketCloudUser } from "../bitbucket.js";
import { Settings } from "../types.js";
import { setIntervalAsync } from "../utils.js";
@@ -266,6 +267,27 @@ export class AccountPermissionSyncer {
}
});
+ repos.forEach(repo => aggregatedRepoIds.add(repo.id));
+ } else if (account.provider === 'bitbucket-cloud') {
+ if (!accessToken) {
+ throw new Error(`User '${account.user.email}' does not have a Bitbucket Cloud OAuth access token associated with their account. Please re-authenticate with Bitbucket Cloud to refresh the token.`);
+ }
+
+ // @note: we don't pass a user here since we want to use a bearer token
+ // for authentication.
+ const client = createBitbucketCloudClient(/* user = */ undefined, accessToken)
+ const bitbucketRepos = await getReposForAuthenticatedBitbucketCloudUser(client);
+ const bitbucketRepoUuids = bitbucketRepos.map(repo => repo.uuid);
+
+ const repos = await this.db.repo.findMany({
+ where: {
+ external_codeHostType: 'bitbucketCloud',
+ external_id: {
+ in: bitbucketRepoUuids,
+ }
+ }
+ });
+
repos.forEach(repo => aggregatedRepoIds.add(repo.id));
}
@@ -287,6 +309,7 @@ export class AccountPermissionSyncer {
data: repoIds.map(repoId => ({
accountId: account.id,
repoId,
+ source: PermissionSyncSource.ACCOUNT_DRIVEN,
})),
skipDuplicates: true,
})
diff --git a/packages/backend/src/ee/repoPermissionSyncer.ts b/packages/backend/src/ee/repoPermissionSyncer.ts
index 618746d59..dfae24ae9 100644
--- a/packages/backend/src/ee/repoPermissionSyncer.ts
+++ b/packages/backend/src/ee/repoPermissionSyncer.ts
@@ -1,5 +1,5 @@
import * as Sentry from "@sentry/node";
-import { PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
+import { PermissionSyncSource, PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
import { createLogger } from "@sourcebot/shared";
import { env, hasEntitlement } from "@sourcebot/shared";
import { Job, Queue, Worker } from 'bullmq';
@@ -7,8 +7,11 @@ import { Redis } from 'ioredis';
import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js";
import { createOctokitFromToken, getRepoCollaborators, GITHUB_CLOUD_HOSTNAME } from "../github.js";
import { createGitLabFromPersonalAccessToken, getProjectMembers } from "../gitlab.js";
+import { createBitbucketCloudClient, getExplicitUserPermissionsForCloudRepo } from "../bitbucket.js";
+import { repoMetadataSchema } from "@sourcebot/shared";
import { Settings } from "../types.js";
import { getAuthCredentialsForRepo, setIntervalAsync } from "../utils.js";
+import { BitbucketConnectionConfig } from "@sourcebot/schemas/v3/index.type";
type RepoPermissionSyncJob = {
jobId: string;
@@ -181,7 +184,13 @@ export class RepoPermissionSyncer {
throw new Error(`No credentials found for repo ${id}`);
}
- const accountIds = await (async () => {
+ const {
+ accountIds,
+ isPartialSync = false,
+ } = await (async (): Promise<{
+ accountIds: string[],
+ isPartialSync?: boolean
+ }> => {
if (repo.external_codeHostType === 'github') {
const isGitHubCloud = credentials.hostUrl ? new URL(credentials.hostUrl).hostname === GITHUB_CLOUD_HOSTNAME : true;
const { octokit } = await createOctokitFromToken({
@@ -210,7 +219,9 @@ export class RepoPermissionSyncer {
},
});
- return accounts.map(account => account.id);
+ return {
+ accountIds: accounts.map(account => account.id),
+ }
} else if (repo.external_codeHostType === 'gitlab') {
const api = await createGitLabFromPersonalAccessToken({
token: credentials.token,
@@ -234,10 +245,58 @@ export class RepoPermissionSyncer {
},
});
- return accounts.map(account => account.id);
+ return {
+ accountIds: accounts.map(account => account.id),
+ }
+ } else if (repo.external_codeHostType === 'bitbucketCloud') {
+ const config = credentials.connectionConfig as BitbucketConnectionConfig | undefined;
+ if (!config) {
+ throw new Error(`No connection config found for repo ${id}`);
+ }
+
+ const client = createBitbucketCloudClient(config.user, credentials.token);
+
+ const parsedMetadata = repoMetadataSchema.safeParse(repo.metadata);
+ if (!parsedMetadata.success) {
+ throw new Error(`Repo ${id} has invalid metadata: ${JSON.stringify(parsedMetadata.error.errors)}`);
+ }
+ const bitbucketCloudMetadata = parsedMetadata.data.codeHostMetadata?.bitbucketCloud;
+ if (!bitbucketCloudMetadata) {
+ throw new Error(`Repo ${id} is missing required Bitbucket Cloud metadata (workspace/repoSlug)`);
+ }
+
+ const { workspace, repoSlug } = bitbucketCloudMetadata;
+
+ // @note: The Bitbucket Cloud permissions API only returns users who have been *directly*
+ // granted access to this repository. Users who have access via a group added to the repo,
+ // via project-level membership, or via a group in a project are NOT captured here.
+ // These users will still gain access through user-driven syncing (accountPermissionSyncer),
+ // but there may be a delay of up to `experiment_userDrivenPermissionSyncIntervalMs` before
+ // they see the repository in Sourcebot.
+ // @see: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/#api-repositories-workspace-repo-slug-permissions-config-users-get
+ const users = await getExplicitUserPermissionsForCloudRepo(client, workspace, repoSlug);
+ const userAccountIds = users.map(u => u.accountId);
+
+ const accounts = await this.db.account.findMany({
+ where: {
+ provider: 'bitbucket-cloud',
+ providerAccountId: {
+ in: userAccountIds,
+ }
+ },
+ });
+
+ return {
+ accountIds: accounts.map(account => account.id),
+ // Since we only fetch users who have been explicitly granted access to the repo,
+ // this is a partial sync.
+ isPartialSync: true,
+ }
}
- return [];
+ return {
+ accountIds: [],
+ }
})();
await this.db.$transaction([
@@ -247,7 +306,11 @@ export class RepoPermissionSyncer {
},
data: {
permittedAccounts: {
- deleteMany: {},
+ // @note: if this is a partial sync, we only want to delete the repo-driven permissions
+ // since we don't want to overwrite the account-driven permissions.
+ deleteMany: isPartialSync ? {
+ source: PermissionSyncSource.REPO_DRIVEN,
+ } : {},
}
}
}),
@@ -255,7 +318,9 @@ export class RepoPermissionSyncer {
data: accountIds.map(accountId => ({
accountId,
repoId: repo.id,
+ source: PermissionSyncSource.REPO_DRIVEN,
})),
+ skipDuplicates: true,
})
]);
}
diff --git a/packages/backend/src/repoCompileUtils.ts b/packages/backend/src/repoCompileUtils.ts
index a6707b4df..0365758a2 100644
--- a/packages/backend/src/repoCompileUtils.ts
+++ b/packages/backend/src/repoCompileUtils.ts
@@ -510,6 +510,14 @@ export const compileBitbucketConfig = async (
},
branches: config.revisions?.branches ?? undefined,
tags: config.revisions?.tags ?? undefined,
+ ...(codeHostType === 'bitbucketCloud' ? {
+ codeHostMetadata: {
+ bitbucketCloud: {
+ workspace: (repo as BitbucketCloudRepository).full_name!.split('/')[0]!,
+ repoSlug: (repo as BitbucketCloudRepository).full_name!.split('/')[1]!,
+ }
+ }
+ } : {}),
} satisfies RepoMetadata,
};
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 70e16c05d..8803b48b9 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -1,4 +1,5 @@
import { Connection, Repo, RepoToConnection } from "@sourcebot/db";
+import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
import { Settings as SettingsSchema } from "@sourcebot/schemas/v3/index.type";
export type Settings = Required;
@@ -19,4 +20,8 @@ export type RepoAuthCredentials = {
token: string;
cloneUrlWithToken?: string;
authHeader?: string;
+ /** The connection that configured the
+ * credentials for this repo.
+ */
+ connectionConfig?: ConnectionConfig;
}
\ No newline at end of file
diff --git a/packages/backend/src/utils.ts b/packages/backend/src/utils.ts
index f897e6fb7..79b7c8b6c 100644
--- a/packages/backend/src/utils.ts
+++ b/packages/backend/src/utils.ts
@@ -145,6 +145,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logge
password: token,
}
),
+ connectionConfig: config,
}
}
} else if (connection.connectionType === 'gitlab') {
@@ -161,6 +162,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logge
password: token
}
),
+ connectionConfig: config,
}
}
} else if (connection.connectionType === 'gitea') {
@@ -176,6 +178,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logge
password: token
}
),
+ connectionConfig: config,
}
}
} else if (connection.connectionType === 'bitbucket') {
@@ -193,6 +196,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logge
password: token
}
),
+ connectionConfig: config,
}
}
} else if (connection.connectionType === 'azuredevops') {
@@ -223,6 +227,7 @@ export const getAuthCredentialsForRepo = async (repo: RepoWithConnections, logge
password: token
}
),
+ connectionConfig: config,
}
}
}
diff --git a/packages/db/prisma/migrations/20260224202008_add_source_field_to_account_to_repo_permission_table/migration.sql b/packages/db/prisma/migrations/20260224202008_add_source_field_to_account_to_repo_permission_table/migration.sql
new file mode 100644
index 000000000..c61add7ee
--- /dev/null
+++ b/packages/db/prisma/migrations/20260224202008_add_source_field_to_account_to_repo_permission_table/migration.sql
@@ -0,0 +1,5 @@
+-- CreateEnum
+CREATE TYPE "PermissionSyncSource" AS ENUM ('ACCOUNT_DRIVEN', 'REPO_DRIVEN');
+
+-- AlterTable
+ALTER TABLE "AccountToRepoPermission" ADD COLUMN "source" "PermissionSyncSource" NOT NULL DEFAULT 'ACCOUNT_DRIVEN';
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma
index 16c2460eb..6421e7c27 100644
--- a/packages/db/prisma/schema.prisma
+++ b/packages/db/prisma/schema.prisma
@@ -390,6 +390,11 @@ model AccountPermissionSyncJob {
accountId String
}
+enum PermissionSyncSource {
+ ACCOUNT_DRIVEN
+ REPO_DRIVEN
+}
+
model AccountToRepoPermission {
createdAt DateTime @default(now())
@@ -399,6 +404,8 @@ model AccountToRepoPermission {
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
accountId String
+ source PermissionSyncSource @default(ACCOUNT_DRIVEN)
+
@@id([repoId, accountId])
}
diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts
index 2a556491a..4e69c42fc 100644
--- a/packages/shared/src/types.ts
+++ b/packages/shared/src/types.ts
@@ -31,6 +31,16 @@ export const repoMetadataSchema = z.object({
* A list of revisions that were indexed for the repo.
*/
indexedRevisions: z.array(z.string()).optional(),
+
+ /**
+ * Code host specific metadata, keyed by code host type.
+ */
+ codeHostMetadata: z.object({
+ bitbucketCloud: z.object({
+ workspace: z.string(),
+ repoSlug: z.string(),
+ }).optional(),
+ }).optional(),
});
export type RepoMetadata = z.infer;
diff --git a/packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts b/packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
index 6ea5849ac..234a272fa 100644
--- a/packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
+++ b/packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
@@ -31,7 +31,7 @@ export const GET = apiHandler(async () => {
const accounts = await prisma.account.findMany({
where: {
userId: user.id,
- provider: { in: ['github', 'gitlab'] }
+ provider: { in: ['github', 'gitlab', 'bitbucket-cloud'] }
},
include: {
permissionSyncJobs: {