Skip to content

Commit 22d548e

Browse files
fix(search-contexts): Fix issue where a repository would not appear in a search context if it was created after the search context was created (#354)
## Problem If a repository is added **after** a search context (e.g., a new repository is synced from the code host), then it will never be added to the context even if it should be included. The workaround is to restart the instance. ## Solution This PR adds a call to re-sync all search contexts whenever a connection is successfully synced. This PR adds the `@sourcebot/shared` package that contains `syncSearchContexts.ts` (previously in web) and it's dependencies (namely the entitlements system). ## Why another package? Because the `syncSearchContexts` call is now called from: 1. `initialize.ts` in **web** - handles syncing search contexts on startup and whenever the config is modified in watch mode. This is the same as before. 2. `connectionManager.ts` in **backend** - syncs the search contexts whenever a connection is successfully synced. ## Follow-up devex work Two things: 1. We have several very thin shared packages (i.e., `crypto`, `error`, and `logger`) that we can probably fold into this "general" shared package. `schemas` and `db` _feels_ like they should remain separate (mostly because they are "code-gen" packages). 2. When running `yarn dev`, any changes made to the shared package will only get picked if you `ctrl+c` and restart the instance. Would be nice if we have watch mode work across package dependencies in the monorepo.
1 parent c0caa5a commit 22d548e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+321
-172
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111
- Delete account join request when redeeming an invite. [#352](https://github.com/sourcebot-dev/sourcebot/pull/352)
12+
- Fix issue where a repository would not be included in a search context if the context was created before the repository. [#354](https://github.com/sourcebot-dev/sourcebot/pull/354)
1213

1314
## [4.3.0] - 2025-06-11
1415

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ COPY ./packages/schemas ./packages/schemas
4343
COPY ./packages/crypto ./packages/crypto
4444
COPY ./packages/error ./packages/error
4545
COPY ./packages/logger ./packages/logger
46+
COPY ./packages/shared ./packages/shared
4647

4748
RUN yarn workspace @sourcebot/db install
4849
RUN yarn workspace @sourcebot/schemas install
4950
RUN yarn workspace @sourcebot/crypto install
5051
RUN yarn workspace @sourcebot/error install
5152
RUN yarn workspace @sourcebot/logger install
53+
RUN yarn workspace @sourcebot/shared install
5254
# ------------------------------------
5355

5456
# ------ Build Web ------
@@ -92,6 +94,7 @@ COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
9294
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
9395
COPY --from=shared-libs-builder /app/packages/error ./packages/error
9496
COPY --from=shared-libs-builder /app/packages/logger ./packages/logger
97+
COPY --from=shared-libs-builder /app/packages/shared ./packages/shared
9598

9699
# Fixes arm64 timeouts
97100
RUN yarn workspace @sourcebot/web install
@@ -132,6 +135,7 @@ COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
132135
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
133136
COPY --from=shared-libs-builder /app/packages/error ./packages/error
134137
COPY --from=shared-libs-builder /app/packages/logger ./packages/logger
138+
COPY --from=shared-libs-builder /app/packages/shared ./packages/shared
135139
RUN yarn workspace @sourcebot/backend install
136140
RUN yarn workspace @sourcebot/backend build
137141

@@ -215,6 +219,7 @@ COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
215219
COPY --from=shared-libs-builder /app/packages/crypto ./packages/crypto
216220
COPY --from=shared-libs-builder /app/packages/error ./packages/error
217221
COPY --from=shared-libs-builder /app/packages/logger ./packages/logger
222+
COPY --from=shared-libs-builder /app/packages/shared ./packages/shared
218223

219224
# Configure dependencies
220225
RUN apk add --no-cache git ca-certificates bind-tools tini jansson wget supervisor uuidgen curl perl jq redis postgresql postgresql-contrib openssl util-linux unzip

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Copyright (c) 2025 Taqla Inc.
22

33
Portions of this software are licensed as follows:
44

5-
- All content that resides under the "ee/" and "packages/web/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
5+
- All content that resides under the "ee/", "packages/web/src/ee/", and "packages/shared/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
66
- All third party components incorporated into the Sourcebot Software are licensed under the original license provided by the owner of the applicable component.
77
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
88

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ clean:
3434
packages/error/dist \
3535
packages/mcp/node_modules \
3636
packages/mcp/dist \
37+
packages/shared/node_modules \
38+
packages/shared/dist \
3739
.sourcebot
3840

3941
soft-reset:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"dev:prisma:migrate:dev": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:dev",
1717
"dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio",
1818
"dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset",
19-
"build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db}' run build"
19+
"build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build"
2020
},
2121
"devDependencies": {
2222
"cross-env": "^7.0.3",

packages/backend/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
"@sourcebot/error": "workspace:*",
3434
"@sourcebot/logger": "workspace:*",
3535
"@sourcebot/schemas": "workspace:*",
36+
"@sourcebot/shared": "workspace:*",
3637
"@t3-oss/env-core": "^0.12.0",
3738
"@types/express": "^5.0.0",
38-
"ajv": "^8.17.1",
3939
"argparse": "^2.0.1",
4040
"bullmq": "^5.34.10",
4141
"cross-fetch": "^4.0.0",
@@ -50,7 +50,6 @@
5050
"posthog-node": "^4.2.1",
5151
"prom-client": "^15.1.3",
5252
"simple-git": "^3.27.0",
53-
"strip-json-comments": "^5.0.1",
5453
"zod": "^3.24.3"
5554
}
5655
}

packages/backend/src/connectionManager.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { BackendError, BackendException } from "@sourcebot/error";
99
import { captureEvent } from "./posthog.js";
1010
import { env } from "./env.js";
1111
import * as Sentry from "@sentry/node";
12+
import { loadConfig, syncSearchContexts } from "@sourcebot/shared";
1213

1314
interface IConnectionManager {
1415
scheduleConnectionSync: (connection: Connection) => Promise<void>;
@@ -264,7 +265,7 @@ export class ConnectionManager implements IConnectionManager {
264265

265266
private async onSyncJobCompleted(job: Job<JobPayload>, result: JobResult) {
266267
this.logger.info(`Connection sync job for connection ${job.data.connectionName} (id: ${job.data.connectionId}, jobId: ${job.id}) completed`);
267-
const { connectionId } = job.data;
268+
const { connectionId, orgId } = job.data;
268269

269270
let syncStatusMetadata: Record<string, unknown> = (await this.db.connection.findUnique({
270271
where: { id: connectionId },
@@ -289,7 +290,25 @@ export class ConnectionManager implements IConnectionManager {
289290
notFound.repos.length > 0 ? ConnectionSyncStatus.SYNCED_WITH_WARNINGS : ConnectionSyncStatus.SYNCED,
290291
syncedAt: new Date()
291292
}
292-
})
293+
});
294+
295+
// After a connection has synced, we need to re-sync the org's search contexts as
296+
// there may be new repos that match the search context's include/exclude patterns.
297+
if (env.CONFIG_PATH) {
298+
try {
299+
const config = await loadConfig(env.CONFIG_PATH);
300+
301+
await syncSearchContexts({
302+
db: this.db,
303+
orgId,
304+
contexts: config.contexts,
305+
});
306+
} catch (err) {
307+
this.logger.error(`Failed to sync search contexts for connection ${connectionId}: ${err}`);
308+
Sentry.captureException(err);
309+
}
310+
}
311+
293312

294313
captureEvent('backend_connection_sync_job_completed', {
295314
connectionId: connectionId,

packages/backend/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ export const DEFAULT_SETTINGS: Settings = {
1616
repoGarbageCollectionGracePeriodMs: 10 * 1000, // 10 seconds
1717
repoIndexTimeoutMs: 1000 * 60 * 60 * 2, // 2 hours
1818
enablePublicAccess: false,
19-
}
19+
}

packages/backend/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { PrismaClient } from "@sourcebot/db";
1010
import { env } from "./env.js";
1111
import { createLogger } from "@sourcebot/logger";
1212

13-
const logger = createLogger('index');
13+
const logger = createLogger('backend-entrypoint');
14+
1415

1516
// Register handler for normal exit
1617
process.on('exit', (code) => {
@@ -72,3 +73,4 @@ main(prisma, context)
7273
.finally(() => {
7374
logger.info("Shutting down...");
7475
});
76+

packages/backend/src/main.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,16 @@ import { ConnectionManager } from './connectionManager.js';
77
import { RepoManager } from './repoManager.js';
88
import { env } from './env.js';
99
import { PromClient } from './promClient.js';
10-
import { isRemotePath } from './utils.js';
11-
import { readFile } from 'fs/promises';
12-
import stripJsonComments from 'strip-json-comments';
13-
import { SourcebotConfig } from '@sourcebot/schemas/v3/index.type';
14-
import { indexSchema } from '@sourcebot/schemas/v3/index.schema';
15-
import { Ajv } from "ajv";
10+
import { loadConfig } from '@sourcebot/shared';
1611

1712
const logger = createLogger('backend-main');
18-
const ajv = new Ajv({
19-
validateFormats: false,
20-
});
2113

2214
const getSettings = async (configPath?: string) => {
2315
if (!configPath) {
2416
return DEFAULT_SETTINGS;
2517
}
2618

27-
const configContent = await (async () => {
28-
if (isRemotePath(configPath)) {
29-
const response = await fetch(configPath);
30-
if (!response.ok) {
31-
throw new Error(`Failed to fetch config file ${configPath}: ${response.statusText}`);
32-
}
33-
return response.text();
34-
} else {
35-
return readFile(configPath, { encoding: 'utf-8' });
36-
}
37-
})();
38-
39-
const config = JSON.parse(stripJsonComments(configContent)) as SourcebotConfig;
40-
const isValidConfig = ajv.validate(indexSchema, config);
41-
if (!isValidConfig) {
42-
throw new Error(`Config file '${configPath}' is invalid: ${ajv.errorsText(ajv.errors)}`);
43-
}
19+
const config = await loadConfig(configPath);
4420

4521
return {
4622
...DEFAULT_SETTINGS,

packages/backend/src/utils.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from 'vitest';
2-
import { arraysEqualShallow, isRemotePath } from './utils';
2+
import { arraysEqualShallow } from './utils';
3+
import { isRemotePath } from '@sourcebot/shared';
34

45
test('should return true for identical arrays', () => {
56
expect(arraysEqualShallow([1, 2, 3], [1, 2, 3])).toBe(true);

packages/backend/src/utils.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ export const marshalBool = (value?: boolean) => {
2020
return !!value ? '1' : '0';
2121
}
2222

23-
export const isRemotePath = (path: string) => {
24-
return path.startsWith('https://') || path.startsWith('http://');
25-
}
26-
2723
export const getTokenFromConfig = async (token: any, orgId: number, db: PrismaClient, logger?: Logger) => {
2824
try {
2925
return await getTokenFromConfigBase(token, orgId, db);

packages/shared/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist/
2+
*.tsbuildinfo

packages/shared/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
This package contains shared code between the backend & webapp packages.
2+
3+
### Why two index files?
4+
5+
This package contains two index files: `index.server.ts` and `index.client.ts`. There is some code in this package that will only work in a Node.JS runtime (e.g., because it depends on the `fs` pacakge. Entitlements are a good example of this), and other code that is runtime agnostic (e.g., `constants.ts`). To deal with this, we these two index files export server code and client code, respectively.
6+
7+
For package consumers, the usage would look like the following:
8+
- Server: `import { ... } from @sourcebot/shared`
9+
- Client: `import { ... } from @sourcebot/shared/client`

packages/shared/package.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@sourcebot/shared",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"private": true,
6+
"scripts": {
7+
"build": "tsc",
8+
"build:watch": "tsc-watch --preserveWatchOutput",
9+
"postinstall": "yarn build"
10+
},
11+
"dependencies": {
12+
"@sourcebot/crypto": "workspace:*",
13+
"@sourcebot/db": "workspace:*",
14+
"@sourcebot/logger": "workspace:*",
15+
"@sourcebot/schemas": "workspace:*",
16+
"@t3-oss/env-core": "^0.12.0",
17+
"ajv": "^8.17.1",
18+
"micromatch": "^4.0.8",
19+
"strip-json-comments": "^5.0.1",
20+
"zod": "^3.24.3"
21+
},
22+
"devDependencies": {
23+
"@types/micromatch": "^4.0.9",
24+
"@types/node": "^22.7.5",
25+
"tsc-watch": "6.2.1",
26+
"typescript": "^5.7.3"
27+
},
28+
"exports": {
29+
".": "./dist/index.server.js",
30+
"./client": "./dist/index.client.js"
31+
}
32+
}

packages/shared/src/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
export const SOURCEBOT_SUPPORT_EMAIL = '[email protected]';
3+
4+
export const SOURCEBOT_CLOUD_ENVIRONMENT = [
5+
"dev",
6+
"demo",
7+
"staging",
8+
"prod",
9+
] as const;
10+
11+
export const SOURCEBOT_UNLIMITED_SEATS = -1;

packages/web/src/ee/features/searchContexts/syncSearchContexts.ts renamed to packages/shared/src/ee/syncSearchContexts.ts

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,34 @@
1-
import { env } from "@/env.mjs";
2-
import { getPlan, hasEntitlement } from "@/features/entitlements/server";
3-
import { SINGLE_TENANT_ORG_ID, SOURCEBOT_SUPPORT_EMAIL } from "@/lib/constants";
4-
import { prisma } from "@/prisma";
5-
import { SearchContext } from "@sourcebot/schemas/v3/index.type";
61
import micromatch from "micromatch";
72
import { createLogger } from "@sourcebot/logger";
3+
import { PrismaClient } from "@sourcebot/db";
4+
import { getPlan, hasEntitlement } from "../entitlements.js";
5+
import { SOURCEBOT_SUPPORT_EMAIL } from "../constants.js";
6+
import { SearchContext } from "@sourcebot/schemas/v3/index.type";
87

98
const logger = createLogger('sync-search-contexts');
109

11-
export const syncSearchContexts = async (contexts?: { [key: string]: SearchContext }) => {
12-
if (env.SOURCEBOT_TENANCY_MODE !== 'single') {
13-
throw new Error("Search contexts are not supported in this tenancy mode. Set SOURCEBOT_TENANCY_MODE=single in your environment variables.");
14-
}
10+
interface SyncSearchContextsParams {
11+
contexts?: { [key: string]: SearchContext } | undefined;
12+
orgId: number;
13+
db: PrismaClient;
14+
}
15+
16+
export const syncSearchContexts = async (params: SyncSearchContextsParams) => {
17+
const { contexts, orgId, db } = params;
1518

1619
if (!hasEntitlement("search-contexts")) {
1720
if (contexts) {
1821
const plan = getPlan();
19-
logger.error(`Search contexts are not supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
22+
logger.warn(`Skipping search context sync. Reason: "Search contexts are not supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}."`);
2023
}
21-
return;
24+
return false;
2225
}
2326

2427
if (contexts) {
2528
for (const [key, newContextConfig] of Object.entries(contexts)) {
26-
const allRepos = await prisma.repo.findMany({
29+
const allRepos = await db.repo.findMany({
2730
where: {
28-
orgId: SINGLE_TENANT_ORG_ID,
31+
orgId,
2932
},
3033
select: {
3134
id: true,
@@ -44,23 +47,23 @@ export const syncSearchContexts = async (contexts?: { [key: string]: SearchConte
4447
});
4548
}
4649

47-
const currentReposInContext = (await prisma.searchContext.findUnique({
50+
const currentReposInContext = (await db.searchContext.findUnique({
4851
where: {
4952
name_orgId: {
5053
name: key,
51-
orgId: SINGLE_TENANT_ORG_ID,
54+
orgId,
5255
}
5356
},
5457
include: {
5558
repos: true,
5659
}
5760
}))?.repos ?? [];
5861

59-
await prisma.searchContext.upsert({
62+
await db.searchContext.upsert({
6063
where: {
6164
name_orgId: {
6265
name: key,
63-
orgId: SINGLE_TENANT_ORG_ID,
66+
orgId,
6467
}
6568
},
6669
update: {
@@ -81,7 +84,7 @@ export const syncSearchContexts = async (contexts?: { [key: string]: SearchConte
8184
description: newContextConfig.description,
8285
org: {
8386
connect: {
84-
id: SINGLE_TENANT_ORG_ID,
87+
id: orgId,
8588
}
8689
},
8790
repos: {
@@ -94,21 +97,23 @@ export const syncSearchContexts = async (contexts?: { [key: string]: SearchConte
9497
}
9598
}
9699

97-
const deletedContexts = await prisma.searchContext.findMany({
100+
const deletedContexts = await db.searchContext.findMany({
98101
where: {
99102
name: {
100103
notIn: Object.keys(contexts ?? {}),
101104
},
102-
orgId: SINGLE_TENANT_ORG_ID,
105+
orgId,
103106
}
104107
});
105108

106109
for (const context of deletedContexts) {
107110
logger.info(`Deleting search context with name '${context.name}'. ID: ${context.id}`);
108-
await prisma.searchContext.delete({
111+
await db.searchContext.delete({
109112
where: {
110113
id: context.id,
111114
}
112115
})
113116
}
117+
118+
return true;
114119
}

0 commit comments

Comments
 (0)