Skip to content

Comments

feat(backend): add Bitbucket Cloud permission syncing#925

Merged
brendan-kellam merged 8 commits intomainfrom
brendan/bitbucket-cloud-permission-syncing
Feb 24, 2026
Merged

feat(backend): add Bitbucket Cloud permission syncing#925
brendan-kellam merged 8 commits intomainfrom
brendan/bitbucket-cloud-permission-syncing

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Feb 24, 2026

Summary

  • Implements repo-driven permission syncing for Bitbucket Cloud using the explicit user permissions API
  • Implements user-driven permission syncing for Bitbucket Cloud by fetching all private repos accessible to the authenticated user via their OAuth token (GET /user/permissions/repositories)
  • Adds bitbucketCloud to PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES and bitbucket-cloud to PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS
  • Adds bitbucket-cloud to the permissionSyncStatus route so the UI correctly reflects pending sync state
  • Refactors RepoMetadata.codeHostMetadata from a discriminated union (with a redundant type field) to a provider-keyed object (e.g. codeHostMetadata.bitbucketCloud), making it cleaner to extend for future providers
  • Stores Bitbucket Cloud workspace and repo slug in codeHostMetadata during repo compilation (from full_name) to avoid fragile displayName string parsing
  • Documents the partial coverage limitation in the permission syncing docs: repo-driven syncing only captures direct user grants; users with access via groups or project membership are covered by user-driven syncing (with up to a 24h delay)
  • Adds PermissionSyncSource enum (ACCOUNT_DRIVEN / REPO_DRIVEN) and a non-nullable source field to AccountToRepoPermission, so each permission record tracks which sync path created it
  • Repo-driven syncer uses an isPartialSync flag (true for Bitbucket Cloud) to only wipe REPO_DRIVEN records on cleanup, preserving ACCOUNT_DRIVEN records set by the account syncer — necessary because the Bitbucket Cloud repo permissions API only returns directly-granted users, not those with access via groups or project membership

Known limitation

Bitbucket Cloud's permissions API only returns explicitly granted user permissions. Access granted via:

  • A group added to the repo
  • Project-level membership
  • A group within a project

...is not captured by repo-driven syncing. These users will still gain access via user-driven syncing, but with a delay up to experiment_userDrivenPermissionSyncIntervalMs (default: 24h).

Test plan

  • Configure Bitbucket Cloud as an identity provider
  • Enable EXPERIMENT_EE_PERMISSION_SYNC_ENABLED=true
  • Authenticate with Bitbucket Cloud OAuth
  • Verify repo-driven sync grants access for users with direct repo permissions
  • Verify user-driven sync grants access to all private repos the authenticated user has access to
  • Verify users without access to a private repo cannot see it in search results

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Permission syncing now supports Bitbucket Cloud (account- and repo-level), with partial repo-driven coverage and explicit tracking of permission source (account- vs repo-driven).
    • Repo metadata for Bitbucket Cloud now records workspace and repo slug for improved mapping.
  • Documentation

    • Permission syncing docs updated with Bitbucket Cloud prerequisites, OAuth/token scope guidance, expected coverage, and potential sync delays.
  • Chores

    • Database migration adds a PermissionSyncSource enum to record permission origin.

brendan-kellam and others added 2 commits February 23, 2026 21:44
Implements both repo-driven and user-driven permission syncing for
Bitbucket Cloud repositories. Repo-driven syncing uses the explicit
user permissions API; user-driven syncing fetches all private repos
accessible to the authenticated user via their OAuth token.

Also refactors RepoMetadata.codeHostMetadata to use a provider-keyed
object (e.g. codeHostMetadata.bitbucketCloud) instead of a discriminated
union with a redundant type field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d694641 and 38e192d.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • docs/docs/features/permission-syncing.mdx
  • packages/backend/src/ee/repoPermissionSyncer.ts

Walkthrough

Adds Bitbucket Cloud permission-syncing: new Bitbucket client APIs and helpers, account- and repo-level syncer branches, repo metadata and types for Bitbucket, DB enum/column for permission source, constants and route updates, and documentation marking partial support and prerequisites.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md, docs/docs/features/permission-syncing.mdx
Changelog and docs updated to mark Bitbucket Cloud as partial support and add prerequisites, OAuth scopes, behavior notes, and limitations.
Bitbucket client & APIs
packages/backend/src/bitbucket.ts
Introduced base client imports, exported createBitbucketCloudClient, and added getExplicitUserPermissionsForCloudRepo(...) and getReposForAuthenticatedBitbucketCloudUser(...) with paginated fetch and error handling.
Permission sync config & routes
packages/backend/src/constants.ts, packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
Added bitbucketCloud/bitbucket-cloud to supported code host types and identity providers; included Bitbucket Cloud in account-provider filters for sync-status checks.
Permission syncer logic
packages/backend/src/ee/accountPermissionSyncer.ts, packages/backend/src/ee/repoPermissionSyncer.ts
Account- and repo-driven syncer branches for Bitbucket Cloud: validate tokens, create client, fetch user repos and explicit repo permissions, map Bitbucket accounts to local accounts, mark partial repo-driven syncs, and record permission source.
Repo metadata & shared types
packages/backend/src/repoCompileUtils.ts, packages/shared/src/types.ts
Added optional codeHostMetadata.bitbucketCloud.{workspace,repoSlug} to RepoMetadata schema and populate it for Bitbucket Cloud repos.
Auth credentials plumbing & types
packages/backend/src/utils.ts, packages/backend/src/types.ts
getAuthCredentialsForRepo now includes connectionConfig in returned credentials; RepoAuthCredentials gains optional connectionConfig?: ConnectionConfig.
DB schema / migration
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/.../migration.sql
Added PermissionSyncSource enum (ACCOUNT_DRIVEN, REPO_DRIVEN) and a non-null source column on AccountToRepoPermission defaulting to ACCOUNT_DRIVEN.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Syncer as Permission Syncer
    participant BBAPI as Bitbucket Cloud API
    participant DB as Database
    participant Accounts as Account Store

    User->>Syncer: Trigger permission sync (bitbucket-cloud)
    Syncer->>Syncer: Validate OAuth token / create client
    Syncer->>BBAPI: Fetch authenticated user's repo UUIDs
    BBAPI-->>Syncer: Return repo UUIDs
    Syncer->>DB: Query local repos where external_codeHostType='bitbucketCloud' and external_id IN UUIDs
    DB-->>Syncer: Return local repo IDs
    Syncer->>BBAPI: For each repo, fetch explicit user permissions (paginated)
    BBAPI-->>Syncer: Return permission entries
    Syncer->>Accounts: Map Bitbucket account UUIDs -> local account IDs
    Accounts-->>Syncer: Return account records
    Syncer->>DB: Create/Update AccountToRepoPermission rows with source (ACCOUNT_DRIVEN or REPO_DRIVEN)
    DB-->>Syncer: Acknowledge updates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

sourcebot-team

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: adding Bitbucket Cloud permission syncing support to the backend, which is reflected across the entire changeset including new syncing logic, API additions, and database schema updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan/bitbucket-cloud-permission-syncing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/backend/src/repoCompileUtils.ts (1)

513-520: full_name is split and the repo is cast twice — consider extracting variables.

full_name!.split('/') is called separately for workspace and repoSlug, and repo as BitbucketCloudRepository is repeated. Extracting these avoids the duplication and makes the intent clearer.

♻️ Proposed refactor
-...(codeHostType === 'bitbucketCloud' ? {
-    codeHostMetadata: {
-        bitbucketCloud: {
-            workspace: (repo as BitbucketCloudRepository).full_name!.split('/')[0]!,
-            repoSlug: (repo as BitbucketCloudRepository).full_name!.split('/')[1]!,
-        }
-    }
-} : {}),
+...(codeHostType === 'bitbucketCloud' ? (() => {
+    const [workspace, repoSlug] = (repo as BitbucketCloudRepository).full_name!.split('/');
+    return { codeHostMetadata: { bitbucketCloud: { workspace: workspace!, repoSlug: repoSlug! } } };
+})() : {}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/repoCompileUtils.ts` around lines 513 - 520, The
bitbucket Cloud branch currently calls (repo as
BitbucketCloudRepository).full_name!.split('/') twice and repeats the cast;
refactor by extracting the full_name once and splitting it into workspace and
repoSlug variables before building codeHostMetadata. Concretely, inside the
codeHostType === 'bitbucketCloud' branch get const fullName = (repo as
BitbucketCloudRepository).full_name! and const [workspace, repoSlug] =
fullName.split('/'), then use workspace and repoSlug when constructing
codeHostMetadata to avoid duplicate casts and splits.
packages/backend/src/ee/accountPermissionSyncer.ts (1)

271-288: Add upfront OAuth scope validation for Bitbucket Cloud, using the x-oauth-scopes response header.

The Bitbucket Cloud branch lacks the upfront scope validation present in the GitHub and GitLab branches. Unlike those providers, getReposForAuthenticatedBitbucketCloudUser currently makes an immediate API call without checking whether the token has the required repository scope. If the scope is missing, users see a raw API error that is difficult to diagnose.

Bitbucket Cloud does expose OAuth scopes via the x-oauth-scopes response header (similar to GitHub's X-OAuth-Scopes), available on any authenticated API request. Consider adding a lightweight getBitbucketCloudOAuthScopes utility to bitbucket.ts that makes an initial call to check the header, validates the presence of the repository scope, and throws a clear, actionable error message if it is missing—before attempting the full repository permission lookup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/ee/accountPermissionSyncer.ts` around lines 271 - 288,
Add an upfront Bitbucket Cloud OAuth scope validation before calling
getReposForAuthenticatedBitbucketCloudUser: implement a lightweight helper
getBitbucketCloudOAuthScopes in bitbucket.ts that issues a minimal authenticated
request, reads the x-oauth-scopes header, verifies the presence of the
repository scope, and throws a clear actionable Error if missing; then call
getBitbucketCloudOAuthScopes(accessToken) in accountPermissionSyncer.ts (inside
the bitbucket-cloud branch, before getReposForAuthenticatedBitbucketCloudUser)
and only proceed to fetch repos and add repo.id to aggregatedRepoIds when the
scope check passes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/backend/src/bitbucket.ts`:
- Around line 640-673: The implementation of
getReposForAuthenticatedBitbucketCloudUser currently returns all accessible
repos but the JSDoc promises "private repositories"; modify the function to
request repository.is_private from the Bitbucket Cloud API (add the fields
parameter to the apiClient.GET call inside the getPaginatedCloud call, e.g.
include "repository.is_private") and then filter the resulting permissions array
to only include entries where p.repository?.is_private === true before mapping
to { uuid: p.repository!.uuid }, keeping the return type the same;
alternatively, if you prefer not to filter, update the JSDoc to state that all
accessible repositories are returned (but do one or the other so implementation
and docs match).

In `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Line 270: The forEach callbacks that call aggregatedRepoIds.add(repo.id) are
written with concise arrow bodies which implicitly return the Set (causing
Biome's useIterableCallbackReturn error); update each repos.forEach(...)
callback (the occurrences around aggregatedRepoIds.add at the three sites) to
use a block body with an explicit statement (e.g. repos.forEach(repo => {
aggregatedRepoIds.add(repo.id); });) so the callback returns void—apply the same
change for the other two occurrences referenced in the comment.

In `@packages/backend/src/ee/repoPermissionSyncer.ts`:
- Around line 241-245: The current logic around repoMetadataSchema.safeParse in
repoPermissionSyncer.ts masks Zod parsing failures and throws a misleading
"missing required Bitbucket Cloud metadata" error; update the handling in the
block that computes parsedMetadata and bitbucketCloudMetadata so that if
parsedMetadata.success is false you capture and surface the parsing errors
(e.g., include parsedMetadata.error.format() or parsedMetadata.error.errors) in
the thrown Error or processLogger.error before throwing, and only throw the
existing workspace/repoSlug missing message when parsing succeeded but
codeHostMetadata?.bitbucketCloud is actually absent; modify the code around
parsedMetadata, bitbucketCloudMetadata, and the throw to distinguish parse
failures from missing fields.

---

Nitpick comments:
In `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Around line 271-288: Add an upfront Bitbucket Cloud OAuth scope validation
before calling getReposForAuthenticatedBitbucketCloudUser: implement a
lightweight helper getBitbucketCloudOAuthScopes in bitbucket.ts that issues a
minimal authenticated request, reads the x-oauth-scopes header, verifies the
presence of the repository scope, and throws a clear actionable Error if
missing; then call getBitbucketCloudOAuthScopes(accessToken) in
accountPermissionSyncer.ts (inside the bitbucket-cloud branch, before
getReposForAuthenticatedBitbucketCloudUser) and only proceed to fetch repos and
add repo.id to aggregatedRepoIds when the scope check passes.

In `@packages/backend/src/repoCompileUtils.ts`:
- Around line 513-520: The bitbucket Cloud branch currently calls (repo as
BitbucketCloudRepository).full_name!.split('/') twice and repeats the cast;
refactor by extracting the full_name once and splitting it into workspace and
repoSlug variables before building codeHostMetadata. Concretely, inside the
codeHostType === 'bitbucketCloud' branch get const fullName = (repo as
BitbucketCloudRepository).full_name! and const [workspace, repoSlug] =
fullName.split('/'), then use workspace and repoSlug when constructing
codeHostMetadata to avoid duplicate casts and splits.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d171f6 and 5ab973b.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • docs/docs/features/permission-syncing.mdx
  • packages/backend/src/bitbucket.ts
  • packages/backend/src/constants.ts
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/backend/src/ee/repoPermissionSyncer.ts
  • packages/backend/src/repoCompileUtils.ts
  • packages/shared/src/types.ts
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/backend/src/utils.ts (1)

134-233: ⚠️ Potential issue | 🟡 Minor

Remove unnecessary connectionConfig from RepoAuthCredentials return type.

connectionConfig contains credentials and should not be included in the return object since callers only need authentication tokens/headers. Currently, it's unused by repoIndexManager.ts and only accessed internally by repoPermissionSyncer.ts. Pass connectionConfig separately to the permission syncer or retrieve it on-demand instead of embedding it in RepoAuthCredentials.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/utils.ts` around lines 134 - 233, The function returning
RepoAuthCredentials currently includes a sensitive connectionConfig in each
branch (github/gitlab/gitea/bitbucket/azuredevops) — remove connectionConfig
from all returned objects so RepoAuthCredentials only contains hostUrl, token,
cloneUrlWithToken and/or authHeader; keep the logic that computes token
(getTokenFromConfig) and clone URL (createGitCloneUrlWithToken) but stop
embedding connection.config. Update callers: ensure repoIndexManager.ts does not
rely on connectionConfig (it currently doesn't) and change
repoPermissionSyncer.ts to accept the connectionConfig separately (pass
connection.config from the caller or fetch it on-demand) so permission syncing
still has access without leaking credentials in RepoAuthCredentials.
♻️ Duplicate comments (2)
packages/backend/src/ee/accountPermissionSyncer.ts (1)

270-270: ⚠️ Potential issue | 🟡 Minor

Biome lint: forEach callback should not return a value.
Use a block body to avoid the implicit return from Set.prototype.add().

🐛 Proposed fix
-repos.forEach(repo => aggregatedRepoIds.add(repo.id));
+repos.forEach(repo => { aggregatedRepoIds.add(repo.id); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/ee/accountPermissionSyncer.ts` at line 270, The forEach
callback in accountPermissionSyncer.ts currently uses a concise arrow body that
implicitly returns the value from Set.prototype.add(); change the callback to a
block body so it does not return a value: replace the concise arrow in the
repos.forEach(...) call (the one referencing repos and
aggregatedRepoIds.add(repo.id)) with a block arrow that calls
aggregatedRepoIds.add(repo.id); inside braces and no return.
packages/backend/src/bitbucket.ts (1)

631-655: ⚠️ Potential issue | 🟡 Minor

Align “private repos only” promise with implementation.
The JSDoc says private repos only, but the function returns all accessible repos. Either filter to private repos (request repository.is_private) or update the doc to reflect the actual behavior.

✅ Possible fix (filter to private)
-    const permissions = await getPaginatedCloud<CloudRepositoryPermission>(path, async (p, query) => {
+    const permissions = await getPaginatedCloud<CloudRepositoryPermission>(path, async (p, query) => {
         const response = await client.apiClient.GET(p, {
-            params: { query },
+            params: { query: { ...query, fields: "values.repository.uuid,values.repository.is_private" } },
         });
         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)
+        .filter(p => p.repository?.uuid != null && p.repository?.is_private === true)
         .map(p => ({ uuid: p.repository!.uuid as string }));

Please verify the Bitbucket Cloud API supports fields and repository.is_private for this endpoint:

Bitbucket Cloud REST API /user/permissions/repositories fields parameter repository.is_private
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/bitbucket.ts` around lines 631 - 655, The JSDoc for
getReposForAuthenticatedBitbucketCloudUser claims it returns private repos only
but the implementation returns all accessible repos; update the function to
filter for private repos by either requesting the is_private field from the API
and then filtering (use the CloudRepositoryPermission.repository.is_private
property) or change the JSDoc to accurately state it returns all accessible
repos—preferably implement the filter: include repository.is_private in the GET
(via fields or params if supported) and then replace the final return with
permissions.filter(p => p.repository?.is_private).map(p => ({ uuid:
p.repository!.uuid as string })).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/backend/src/utils.ts`:
- Around line 134-233: The function returning RepoAuthCredentials currently
includes a sensitive connectionConfig in each branch
(github/gitlab/gitea/bitbucket/azuredevops) — remove connectionConfig from all
returned objects so RepoAuthCredentials only contains hostUrl, token,
cloneUrlWithToken and/or authHeader; keep the logic that computes token
(getTokenFromConfig) and clone URL (createGitCloneUrlWithToken) but stop
embedding connection.config. Update callers: ensure repoIndexManager.ts does not
rely on connectionConfig (it currently doesn't) and change
repoPermissionSyncer.ts to accept the connectionConfig separately (pass
connection.config from the caller or fetch it on-demand) so permission syncing
still has access without leaking credentials in RepoAuthCredentials.

---

Duplicate comments:
In `@packages/backend/src/bitbucket.ts`:
- Around line 631-655: The JSDoc for getReposForAuthenticatedBitbucketCloudUser
claims it returns private repos only but the implementation returns all
accessible repos; update the function to filter for private repos by either
requesting the is_private field from the API and then filtering (use the
CloudRepositoryPermission.repository.is_private property) or change the JSDoc to
accurately state it returns all accessible repos—preferably implement the
filter: include repository.is_private in the GET (via fields or params if
supported) and then replace the final return with permissions.filter(p =>
p.repository?.is_private).map(p => ({ uuid: p.repository!.uuid as string })).

In `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Line 270: The forEach callback in accountPermissionSyncer.ts currently uses a
concise arrow body that implicitly returns the value from Set.prototype.add();
change the callback to a block body so it does not return a value: replace the
concise arrow in the repos.forEach(...) call (the one referencing repos and
aggregatedRepoIds.add(repo.id)) with a block arrow that calls
aggregatedRepoIds.add(repo.id); inside braces and no return.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ab973b and adceff1.

📒 Files selected for processing (5)
  • packages/backend/src/bitbucket.ts
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/backend/src/ee/repoPermissionSyncer.ts
  • packages/backend/src/types.ts
  • packages/backend/src/utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/backend/src/ee/repoPermissionSyncer.ts

- Adds `PermissionSyncSource` enum (`ACCOUNT_DRIVEN` / `REPO_DRIVEN`) and `source` field to `AccountToRepoPermission` table (non-nullable, defaults to `ACCOUNT_DRIVEN`)
- Both syncers now set `source` on all created permission records
- Repo-driven syncer uses `isPartialSync` flag (true for Bitbucket Cloud) to only delete `REPO_DRIVEN` records on cleanup, preserving `ACCOUNT_DRIVEN` records from the account syncer
- Adds `skipDuplicates: true` to repo-driven `createMany` to handle overlap between the two sync paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/backend/src/ee/repoPermissionSyncer.ts (1)

259-263: Silent safeParse failure still produces a misleading error (lines 259-263).

parsedMetadata.success === false collapses silently — bitbucketCloudMetadata becomes undefined and the thrown error incorrectly implies the field was never set rather than that parsing failed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/ee/repoPermissionSyncer.ts` around lines 259 - 263, The
current block uses repoMetadataSchema.safeParse and then treats missing
bitbucketCloudMetadata the same as a parse failure, producing a misleading
error; update the logic in repoPermissionSyncer.ts to first check
parsedMetadata.success and if false throw or log a descriptive error using
parsedMetadata.error (from repoMetadataSchema.safeParse) before accessing
parsedMetadata.data, otherwise extract codeHostMetadata.bitbucketCloud into
bitbucketCloudMetadata and only then throw the existing "missing required
Bitbucket Cloud metadata" error if that specific field is absent; reference
parsedMetadata, repoMetadataSchema.safeParse, bitbucketCloudMetadata, and the
existing throw new Error call when making this change.
packages/backend/src/ee/accountPermissionSyncer.ts (1)

270-270: Biome lint error: forEach callback returns a non-void value (line 270).

Set.prototype.add() returns the Set, so the arrow-function body has an implicit non-void return. Biome flags this as useIterableCallbackReturn. Use a block body to suppress the implicit return.

🐛 Proposed fix
-repos.forEach(repo => aggregatedRepoIds.add(repo.id));
+repos.forEach(repo => { aggregatedRepoIds.add(repo.id); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/ee/accountPermissionSyncer.ts` at line 270, The forEach
callback in accountPermissionSyncer.ts is implicitly returning the Set because
it uses a concise arrow body (repos.forEach(repo =>
aggregatedRepoIds.add(repo.id))), triggering Biome's useIterableCallbackReturn
lint; change the callback to a block body so it returns void (e.g.,
repos.forEach(repo => { aggregatedRepoIds.add(repo.id); })), or replace the
forEach with an explicit loop (for (const repo of repos)
aggregatedRepoIds.add(repo.id);) so aggregatedRepoIds.add(repo.id) does not
produce an implicit return.
🧹 Nitpick comments (1)
packages/backend/src/ee/repoPermissionSyncer.ts (1)

252-255: Unsafe as assertion on connectionConfig — prefer a runtime guard.

credentials.connectionConfig as BitbucketConnectionConfig | undefined skips any runtime check. If the credential object carries a config for a different connection type (or a future refactor changes the shape), this cast silently succeeds and fails late with an opaque error. A discriminant check on config.type before use would be safer.

🛡️ Proposed defensive guard
-const config = credentials.connectionConfig as BitbucketConnectionConfig | undefined;
-if (!config) {
-    throw new Error(`No connection config found for repo ${id}`);
-}
+const rawConfig = credentials.connectionConfig;
+if (!rawConfig || (rawConfig as BitbucketConnectionConfig).type !== 'bitbucket') {
+    throw new Error(`No valid Bitbucket connection config found for repo ${id}`);
+}
+const config = rawConfig as BitbucketConnectionConfig;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/ee/repoPermissionSyncer.ts` around lines 252 - 255,
Replace the unsafe cast of credentials.connectionConfig to
BitbucketConnectionConfig with a runtime discriminant guard: check that
credentials.connectionConfig exists and that its type/discriminant equals the
Bitbucket connection type (e.g. config.type === 'bitbucket') before treating it
as BitbucketConnectionConfig; if the check fails throw the existing error for
repo id. Update the const named config (from credentials.connectionConfig) to be
assigned only after this guard so downstream usage in repoPermissionSyncer (the
variable config and the surrounding block that throws for missing config using
id) operates on a properly validated BitbucketConnectionConfig.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Around line 271-292: The new concise-arrow forEach in the Bitbucket Cloud
branch causes the same Biome lint error; replace the repos.forEach(repo =>
aggregatedRepoIds.add(repo.id)) use with an explicit loop (e.g., for (const repo
of repos) { aggregatedRepoIds.add(repo.id); }) inside the
accountPermissionSyncer logic so the callback does not return a value; update
the block that calls createBitbucketCloudClient and
getReposForAuthenticatedBitbucketCloudUser and references bitbucketRepoUuids,
repos, and aggregatedRepoIds accordingly.

In `@packages/backend/src/ee/repoPermissionSyncer.ts`:
- Around line 286-291: Fix the typo in the comment inside
repoPermissionSyncer.ts where the returned object (accountIds and isPartialSync)
is documented; change "explicitly granted accesss to the repo" to "explicitly
granted access to the repo" (locate the comment near the return that constructs
accountIds from accounts and sets isPartialSync: true).

---

Duplicate comments:
In `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Line 270: The forEach callback in accountPermissionSyncer.ts is implicitly
returning the Set because it uses a concise arrow body (repos.forEach(repo =>
aggregatedRepoIds.add(repo.id))), triggering Biome's useIterableCallbackReturn
lint; change the callback to a block body so it returns void (e.g.,
repos.forEach(repo => { aggregatedRepoIds.add(repo.id); })), or replace the
forEach with an explicit loop (for (const repo of repos)
aggregatedRepoIds.add(repo.id);) so aggregatedRepoIds.add(repo.id) does not
produce an implicit return.

In `@packages/backend/src/ee/repoPermissionSyncer.ts`:
- Around line 259-263: The current block uses repoMetadataSchema.safeParse and
then treats missing bitbucketCloudMetadata the same as a parse failure,
producing a misleading error; update the logic in repoPermissionSyncer.ts to
first check parsedMetadata.success and if false throw or log a descriptive error
using parsedMetadata.error (from repoMetadataSchema.safeParse) before accessing
parsedMetadata.data, otherwise extract codeHostMetadata.bitbucketCloud into
bitbucketCloudMetadata and only then throw the existing "missing required
Bitbucket Cloud metadata" error if that specific field is absent; reference
parsedMetadata, repoMetadataSchema.safeParse, bitbucketCloudMetadata, and the
existing throw new Error call when making this change.

---

Nitpick comments:
In `@packages/backend/src/ee/repoPermissionSyncer.ts`:
- Around line 252-255: Replace the unsafe cast of credentials.connectionConfig
to BitbucketConnectionConfig with a runtime discriminant guard: check that
credentials.connectionConfig exists and that its type/discriminant equals the
Bitbucket connection type (e.g. config.type === 'bitbucket') before treating it
as BitbucketConnectionConfig; if the check fails throw the existing error for
repo id. Update the const named config (from credentials.connectionConfig) to be
assigned only after this guard so downstream usage in repoPermissionSyncer (the
variable config and the surrounding block that throws for missing config using
id) operates on a properly validated BitbucketConnectionConfig.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between adceff1 and d694641.

📒 Files selected for processing (4)
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/backend/src/ee/repoPermissionSyncer.ts
  • packages/db/prisma/migrations/20260224202008_add_source_field_to_account_to_repo_permission_table/migration.sql
  • packages/db/prisma/schema.prisma

@brendan-kellam brendan-kellam merged commit 649671b into main Feb 24, 2026
9 checks passed
@brendan-kellam brendan-kellam deleted the brendan/bitbucket-cloud-permission-syncing branch February 24, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant