Skip to content

Commit b7ad88c

Browse files
Merge branch 'main' into brendan/associate-sentry-errors-with-users
2 parents 64ed946 + 9371f1d commit b7ad88c

52 files changed

Lines changed: 1895 additions & 249 deletions

Some content is hidden

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

.agents/skills/gh-stack/SKILL.md

Lines changed: 872 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- Added an optional `webUrl` field to the GitLab connection config, used to build links to repositories in the GitLab web UI when the API host differs from the browsable host. [#1457](https://github.com/sourcebot-dev/sourcebot/pull/1457)
12+
13+
### Fixed
14+
- Prevented focus rings in workspace connector dialogs from being clipped. [#1457](https://github.com/sourcebot-dev/sourcebot/pull/1457)
15+
16+
## [5.1.2] - 2026-07-16
17+
18+
### Added
19+
- Added an opt-in `SOURCEBOT_LLM_USER_EMAIL_HEADER_ENABLED` environment variable that sends the authenticated user's lower-cased email to language model providers in the `X-Sourcebot-User-Email` header. [#1455](https://github.com/sourcebot-dev/sourcebot/pull/1455)
20+
21+
### Fixed
22+
- [EE] Verified signed online license assertions before granting paid feature entitlements. [#1442](https://github.com/sourcebot-dev/sourcebot/pull/1442)
23+
- [EE] Fixed worker startup races that could disable GitHub App authentication and permission syncing until restart after an online license refresh. [#1454](https://github.com/sourcebot-dev/sourcebot/pull/1454)
24+
- [EE] Fixed GitHub connection sync jobs to fail safely when GitHub App authentication is configured without the required entitlement. [#1454](https://github.com/sourcebot-dev/sourcebot/pull/1454)
25+
1026
## [5.1.1] - 2026-07-14
1127

1228
- Add book a call button to sidebar. [#1441](https://github.com/sourcebot-dev/sourcebot/pull/1441)

docs/api-reference/sourcebot-public.openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.3",
33
"info": {
44
"title": "Sourcebot Public API",
5-
"version": "v5.1.1",
5+
"version": "v5.1.2",
66
"description": "OpenAPI description for the public Sourcebot REST endpoints used for search, repository listing, and file browsing. Authentication is instance-dependent: API keys are the standard integration mechanism, OAuth bearer tokens are EE-only, and some instances may allow anonymous access."
77
},
88
"tags": [

docs/docs/configuration/environment-variables.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The following environment variables allow you to configure your Sourcebot deploy
4242
| `ALWAYS_INDEX_FILE_PATTERNS` | - | <p>A comma separated list of glob patterns matching file paths that should always be indexed, regardless of size or number of trigrams.</p> |
4343
| `SOURCEBOT_CHAT_ATTACHMENT_MAX_IMAGE_BYTES` | `10485760` (10 MiB) | <p>Maximum size in bytes of a single image attachment uploaded to Ask Sourcebot. Enforced server-side at upload time.</p> |
4444
| `SOURCEBOT_CHAT_ATTACHMENT_ORPHAN_TTL_HOURS` | `24` | <p>How long in hours an uploaded-but-unsent attachment is retained before being deleted by the orphan sweep. Set to `0` to disable the sweep.</p> |
45+
| `SOURCEBOT_LLM_USER_EMAIL_HEADER_ENABLED` | `false` | <p>When enabled, Sourcebot sends the authenticated user's lower-cased email address to configured language model providers in the `X-Sourcebot-User-Email` request header. Anonymous requests omit the header.</p> |
4546
| `NODE_USE_ENV_PROXY` | `0` | <p>Enables Node.js to automatically use `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables for network requests. Set to `1` to enable or `0` to disable. See [this doc](https://nodejs.org/en/learn/http/enterprise-network-configuration) for more info.</p> |
4647
| `HTTP_PROXY` | - | <p>HTTP proxy URL for routing non-SSL requests through a proxy server (e.g., `http://proxy.company.com:8080`). Requires `NODE_USE_ENV_PROXY=1`.</p> |
4748
| `HTTPS_PROXY` | - | <p>HTTPS proxy URL for routing SSL requests through a proxy server (e.g., `http://proxy.company.com:8080`). Requires `NODE_USE_ENV_PROXY=1`.</p> |

docs/snippets/schemas/v3/connection.schema.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@
272272
],
273273
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
274274
},
275+
"webUrl": {
276+
"type": "string",
277+
"format": "url",
278+
"description": "The base URL used to construct links to repositories in the GitLab web UI. Useful when the API is served from a different host than the one users browse (e.g. `https://api.gitlab.example.com` for `url` and `https://gitlab.example.com` for `webUrl`). Defaults to the value of `url`.",
279+
"examples": [
280+
"https://gitlab.example.com"
281+
],
282+
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
283+
},
275284
"all": {
276285
"type": "boolean",
277286
"default": false,

docs/snippets/schemas/v3/gitlab.schema.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@
5151
],
5252
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
5353
},
54+
"webUrl": {
55+
"type": "string",
56+
"format": "url",
57+
"description": "The base URL used to construct links to repositories in the GitLab web UI. Useful when the API is served from a different host than the one users browse (e.g. `https://api.gitlab.example.com` for `url` and `https://gitlab.example.com` for `webUrl`). Defaults to the value of `url`.",
58+
"examples": [
59+
"https://gitlab.example.com"
60+
],
61+
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
62+
},
5463
"all": {
5564
"type": "boolean",
5665
"default": false,

docs/snippets/schemas/v3/index.schema.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,15 @@
763763
],
764764
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
765765
},
766+
"webUrl": {
767+
"type": "string",
768+
"format": "url",
769+
"description": "The base URL used to construct links to repositories in the GitLab web UI. Useful when the API is served from a different host than the one users browse (e.g. `https://api.gitlab.example.com` for `url` and `https://gitlab.example.com` for `webUrl`). Defaults to the value of `url`.",
770+
"examples": [
771+
"https://gitlab.example.com"
772+
],
773+
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
774+
},
766775
"all": {
767776
"type": "boolean",
768777
"default": false,

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,6 @@
5454
"prom-client": "^15.1.3",
5555
"redlock": "5.0.0-beta.2",
5656
"simple-git": "^3.36.0",
57-
"zod": "^3.25.74"
57+
"zod": "^3.25.76"
5858
}
5959
}

packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { PrismaClient, AccountPermissionSyncJobStatus, Account, PermissionSyncSo
33
import { env, createLogger, getIdentityProviderConfig, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared";
44
import { hasEntitlement } from "../entitlements.js";
55
import { ensureFreshAccountToken } from "./tokenRefresh.js";
6-
import { Job, Queue, Worker } from "bullmq";
6+
import { DelayedError, Job, Queue, Worker } from "bullmq";
77
import { Redis } from "ioredis";
88
import {
99
createOctokitFromToken,
@@ -26,6 +26,7 @@ const createJobLogger = (jobId: string) => createLogger(`${LOG_TAG}:job:${jobId}
2626

2727
const QUEUE_NAME = 'accountPermissionSyncQueue';
2828
const POLLING_INTERVAL_MS = 1000;
29+
const ENTITLEMENT_RETRY_DELAY_MS = 30 * 1000;
2930

3031
type AccountPermissionSyncJob = {
3132
jobId: string;
@@ -59,13 +60,13 @@ export class AccountPermissionSyncer {
5960
}
6061

6162
public async startScheduler() {
62-
if (!await hasEntitlement('permission-syncing')) {
63-
throw new Error('Permission syncing is not supported in current plan.');
64-
}
65-
6663
logger.debug('Starting scheduler');
6764

6865
this.interval = setIntervalAsync(async () => {
66+
if (!await hasEntitlement('permission-syncing')) {
67+
return;
68+
}
69+
6970
const thresholdDate = new Date(Date.now() - this.settings.userDrivenPermissionSyncIntervalMs);
7071

7172
const accounts = await this.db.account.findMany({
@@ -168,6 +169,11 @@ export class AccountPermissionSyncer {
168169
}
169170

170171
private async runJob(job: Job<AccountPermissionSyncJob>) {
172+
if (!await hasEntitlement('permission-syncing')) {
173+
await job.moveToDelayed(Date.now() + ENTITLEMENT_RETRY_DELAY_MS, job.token);
174+
throw new DelayedError('Permission syncing entitlement is not currently available.');
175+
}
176+
171177
const id = job.data.jobId;
172178
const logger = createJobLogger(id);
173179

@@ -443,4 +449,4 @@ export class AccountPermissionSyncer {
443449
logger.error(errorMessage('unknown account (id not found)', 'unknown user (id not found)'));
444450
}
445451
}
446-
}
452+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { beforeEach, describe, expect, test, vi } from 'vitest';
2+
3+
const mocks = vi.hoisted(() => ({
4+
loadConfig: vi.fn(),
5+
}));
6+
7+
vi.mock('@sourcebot/shared', () => ({
8+
createLogger: vi.fn(() => ({
9+
debug: vi.fn(),
10+
error: vi.fn(),
11+
info: vi.fn(),
12+
warn: vi.fn(),
13+
})),
14+
env: { CONFIG_PATH: '/tmp/config.json' },
15+
getTokenFromConfig: vi.fn(),
16+
loadConfig: mocks.loadConfig,
17+
}));
18+
19+
vi.mock('@octokit/app', () => ({
20+
App: vi.fn(),
21+
}));
22+
23+
const getManager = async () => {
24+
const { GithubAppManager } = await import('./githubAppManager.js');
25+
return GithubAppManager.getInstance();
26+
};
27+
28+
describe('GithubAppManager.ensureInitialized', () => {
29+
beforeEach(() => {
30+
vi.resetModules();
31+
mocks.loadConfig.mockReset();
32+
});
33+
34+
test('shares initialization across concurrent callers', async () => {
35+
mocks.loadConfig.mockResolvedValue({});
36+
const manager = await getManager();
37+
38+
await Promise.all([
39+
manager.ensureInitialized(),
40+
manager.ensureInitialized(),
41+
]);
42+
43+
expect(mocks.loadConfig).toHaveBeenCalledTimes(1);
44+
});
45+
46+
test('retries initialization after a transient failure', async () => {
47+
mocks.loadConfig
48+
.mockRejectedValueOnce(new Error('GitHub unavailable'))
49+
.mockResolvedValueOnce({});
50+
const manager = await getManager();
51+
52+
await expect(manager.ensureInitialized()).rejects.toThrow('GitHub unavailable');
53+
await expect(manager.ensureInitialized()).resolves.toBeUndefined();
54+
55+
expect(mocks.loadConfig).toHaveBeenCalledTimes(2);
56+
});
57+
});

0 commit comments

Comments
 (0)