Skip to content

Commit d694641

Browse files
feat(backend): add PermissionSyncSource to AccountToRepoPermission
- 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>
1 parent adceff1 commit d694641

File tree

4 files changed

+44
-8
lines changed

4 files changed

+44
-8
lines changed

packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Sentry from "@sentry/node";
2-
import { PrismaClient, AccountPermissionSyncJobStatus, Account} from "@sourcebot/db";
2+
import { PrismaClient, AccountPermissionSyncJobStatus, Account, PermissionSyncSource} from "@sourcebot/db";
33
import { env, hasEntitlement, createLogger, loadConfig, decryptOAuthToken } from "@sourcebot/shared";
44
import { Job, Queue, Worker } from "bullmq";
55
import { Redis } from "ioredis";
@@ -309,6 +309,7 @@ export class AccountPermissionSyncer {
309309
data: repoIds.map(repoId => ({
310310
accountId: account.id,
311311
repoId,
312+
source: PermissionSyncSource.ACCOUNT_DRIVEN,
312313
})),
313314
skipDuplicates: true,
314315
})

packages/backend/src/ee/repoPermissionSyncer.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Sentry from "@sentry/node";
2-
import { PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
2+
import { PermissionSyncSource, PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
33
import { createLogger } from "@sourcebot/shared";
44
import { env, hasEntitlement } from "@sourcebot/shared";
55
import { Job, Queue, Worker } from 'bullmq';
@@ -184,7 +184,13 @@ export class RepoPermissionSyncer {
184184
throw new Error(`No credentials found for repo ${id}`);
185185
}
186186

187-
const accountIds = await (async () => {
187+
const {
188+
accountIds,
189+
isPartialSync = false,
190+
} = await (async (): Promise<{
191+
accountIds: string[],
192+
isPartialSync?: boolean
193+
}> => {
188194
if (repo.external_codeHostType === 'github') {
189195
const isGitHubCloud = credentials.hostUrl ? new URL(credentials.hostUrl).hostname === GITHUB_CLOUD_HOSTNAME : true;
190196
const { octokit } = await createOctokitFromToken({
@@ -213,7 +219,9 @@ export class RepoPermissionSyncer {
213219
},
214220
});
215221

216-
return accounts.map(account => account.id);
222+
return {
223+
accountIds: accounts.map(account => account.id),
224+
}
217225
} else if (repo.external_codeHostType === 'gitlab') {
218226
const api = await createGitLabFromPersonalAccessToken({
219227
token: credentials.token,
@@ -237,7 +245,9 @@ export class RepoPermissionSyncer {
237245
},
238246
});
239247

240-
return accounts.map(account => account.id);
248+
return {
249+
accountIds: accounts.map(account => account.id),
250+
}
241251
} else if (repo.external_codeHostType === 'bitbucketCloud') {
242252
const config = credentials.connectionConfig as BitbucketConnectionConfig | undefined;
243253
if (!config) {
@@ -273,10 +283,17 @@ export class RepoPermissionSyncer {
273283
},
274284
});
275285

276-
return accounts.map(account => account.id);
286+
return {
287+
accountIds: accounts.map(account => account.id),
288+
// Since we only fetch users who have been explicitly granted accesss to the repo,
289+
// this is a partial sync.
290+
isPartialSync: true,
291+
}
277292
}
278293

279-
return [];
294+
return {
295+
accountIds: [],
296+
}
280297
})();
281298

282299
await this.db.$transaction([
@@ -286,15 +303,21 @@ export class RepoPermissionSyncer {
286303
},
287304
data: {
288305
permittedAccounts: {
289-
deleteMany: {},
306+
// @note: if this is a partial sync, we only want to delete the repo-driven permissions
307+
// since we don't want to overwrite the account-driven permissions.
308+
deleteMany: isPartialSync ? {
309+
source: PermissionSyncSource.REPO_DRIVEN,
310+
} : {},
290311
}
291312
}
292313
}),
293314
this.db.accountToRepoPermission.createMany({
294315
data: accountIds.map(accountId => ({
295316
accountId,
296317
repoId: repo.id,
318+
source: PermissionSyncSource.REPO_DRIVEN,
297319
})),
320+
skipDuplicates: true,
298321
})
299322
]);
300323
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- CreateEnum
2+
CREATE TYPE "PermissionSyncSource" AS ENUM ('ACCOUNT_DRIVEN', 'REPO_DRIVEN');
3+
4+
-- AlterTable
5+
ALTER TABLE "AccountToRepoPermission" ADD COLUMN "source" "PermissionSyncSource" NOT NULL DEFAULT 'ACCOUNT_DRIVEN';

packages/db/prisma/schema.prisma

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ model AccountPermissionSyncJob {
390390
accountId String
391391
}
392392

393+
enum PermissionSyncSource {
394+
ACCOUNT_DRIVEN
395+
REPO_DRIVEN
396+
}
397+
393398
model AccountToRepoPermission {
394399
createdAt DateTime @default(now())
395400
@@ -399,6 +404,8 @@ model AccountToRepoPermission {
399404
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
400405
accountId String
401406
407+
source PermissionSyncSource @default(ACCOUNT_DRIVEN)
408+
402409
@@id([repoId, accountId])
403410
}
404411

0 commit comments

Comments
 (0)