Skip to content

Commit 0df09b7

Browse files
Merge branch 'main' into linear/sou-1410-sourcebot-devsourcebot-GHSA-p6gq-j5cr-w38f-nodemailer-ab79
2 parents 9863a6a + e706330 commit 0df09b7

217 files changed

Lines changed: 15667 additions & 3193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
name: Check Prisma Migrations
2+
description: >-
3+
Verify Prisma migrations apply cleanly in order and reproduce schema.prisma
4+
(drift check), and that no new migration predates the latest on the base
5+
branch (ordering check). Designed to be embedded in an existing job so its
6+
failure turns that job's status red.
7+
8+
inputs:
9+
base-ref:
10+
description: >-
11+
Base git ref to diff migrations against (e.g. a PR's base branch). When
12+
set, the action skips work on PRs that don't touch migrations and runs the
13+
ordering check. When empty (release builds), the drift check always runs
14+
and the ordering check is skipped.
15+
required: false
16+
default: ""
17+
18+
runs:
19+
using: composite
20+
steps:
21+
- name: Detect Prisma changes
22+
id: detect
23+
shell: bash
24+
run: |
25+
if [ -z "${{ inputs.base-ref }}" ]; then
26+
echo "changed=true" >> "$GITHUB_OUTPUT"
27+
echo "No base-ref provided — running drift check unconditionally."
28+
exit 0
29+
fi
30+
git fetch --no-tags --depth=1 origin "+refs/heads/${{ inputs.base-ref }}:refs/remotes/origin/${{ inputs.base-ref }}"
31+
if git diff --name-only "origin/${{ inputs.base-ref }}" HEAD | grep -q '^packages/db/prisma/'; then
32+
echo "changed=true" >> "$GITHUB_OUTPUT"
33+
echo "Prisma changes detected — running migration checks."
34+
else
35+
echo "changed=false" >> "$GITHUB_OUTPUT"
36+
echo "No Prisma changes — skipping migration checks."
37+
fi
38+
39+
- name: Start Postgres
40+
if: steps.detect.outputs.changed == 'true'
41+
shell: bash
42+
run: |
43+
docker run -d --name prisma-check-pg \
44+
-e POSTGRES_USER=postgres \
45+
-e POSTGRES_PASSWORD=postgres \
46+
-e POSTGRES_DB=sourcebot \
47+
-p 5432:5432 postgres:16
48+
for i in $(seq 1 30); do
49+
if docker exec prisma-check-pg pg_isready -U postgres -q; then
50+
echo "Postgres ready."
51+
exit 0
52+
fi
53+
sleep 2
54+
done
55+
echo "Postgres failed to become ready." && exit 1
56+
57+
- name: Use Node.js
58+
if: steps.detect.outputs.changed == 'true'
59+
uses: actions/setup-node@v4
60+
with:
61+
node-version: "20.x"
62+
63+
- name: Install
64+
if: steps.detect.outputs.changed == 'true'
65+
shell: bash
66+
run: yarn install --frozen-lockfile
67+
68+
# Check 1: migrations apply cleanly in order AND reproduce schema.prisma.
69+
# `migrate deploy` fails if a migration is broken or applies out of sequence;
70+
# `migrate diff` exits 2 when the applied history drifts from the schema.
71+
- name: Apply migrations
72+
if: steps.detect.outputs.changed == 'true'
73+
shell: bash
74+
working-directory: packages/db
75+
env:
76+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot
77+
run: yarn prisma migrate deploy
78+
79+
- name: Check for schema drift
80+
if: steps.detect.outputs.changed == 'true'
81+
shell: bash
82+
working-directory: packages/db
83+
env:
84+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot
85+
run: |
86+
yarn prisma migrate diff \
87+
--from-url "$DATABASE_URL" \
88+
--to-schema-datamodel prisma/schema.prisma \
89+
--exit-code \
90+
&& echo "✅ No drift: migrations reproduce schema.prisma" \
91+
|| (echo "❌ schema.prisma has changes not captured in a migration. Run: yarn dev:prisma:migrate:dev --name <name>" && exit 1)
92+
93+
# Check 2 (PRs only): no new migration predates the latest on the base branch.
94+
- name: Check migration ordering
95+
if: steps.detect.outputs.changed == 'true' && inputs.base-ref != ''
96+
shell: bash
97+
run: |
98+
MIG_DIR=packages/db/prisma/migrations
99+
BASE="origin/${{ inputs.base-ref }}"
100+
LATEST_ON_BASE=$(git ls-tree -r --name-only "$BASE" -- "$MIG_DIR" \
101+
| sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort | tail -1)
102+
echo "Latest migration on ${{ inputs.base-ref }}: ${LATEST_ON_BASE:-<none>}"
103+
NEW=$(comm -23 \
104+
<(ls "$MIG_DIR" | sed -n 's/^\([0-9]\{14\}\)_.*/\1/p' | sort -u) \
105+
<(git ls-tree -r --name-only "$BASE" -- "$MIG_DIR" | sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort -u))
106+
FAIL=0
107+
for ts in $NEW; do
108+
if [ -n "$LATEST_ON_BASE" ] && [ "$ts" -lt "$LATEST_ON_BASE" ]; then
109+
echo "❌ New migration $ts predates latest migration on ${{ inputs.base-ref }} ($LATEST_ON_BASE). Rename it with a current timestamp."
110+
FAIL=1
111+
fi
112+
done
113+
[ "$FAIL" -eq 0 ] && echo "✅ Migration ordering OK"
114+
exit $FAIL

.github/workflows/_build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ jobs:
7676
fetch-depth: 0
7777
token: ${{ inputs.use_app_token && steps.generate_token.outputs.token || github.token }}
7878

79+
# Release backstop: fail the build if migrations drift from schema.prisma.
80+
# Runs once (amd64 only) since the check is platform-independent. base-ref
81+
# is omitted, so the drift check always runs and the (PR-only) ordering
82+
# check is skipped.
83+
- name: Check Prisma migrations
84+
if: matrix.platform == 'linux/amd64'
85+
uses: ./.github/actions/check-prisma-migrations
86+
7987
# Extract metadata (tags, labels) for Docker
8088
# https://github.com/docker/metadata-action
8189
- name: Extract Docker metadata
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Generate OpenAPI Spec
2+
3+
on:
4+
pull_request:
5+
branches: ["main"]
6+
paths:
7+
- "packages/web/**"
8+
- "packages/shared/src/version.ts"
9+
10+
jobs:
11+
generate-openapi:
12+
runs-on: ubuntu-latest
13+
# Skip forks: the default GITHUB_TOKEN can't push back to a fork's branch.
14+
if: github.event.pull_request.head.repo.full_name == github.repository
15+
permissions:
16+
contents: write
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
with:
21+
submodules: "true"
22+
ref: ${{ github.head_ref }}
23+
token: ${{ secrets.GITHUB_TOKEN }}
24+
25+
- name: Use Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: '20.x'
29+
cache: 'yarn'
30+
cache-dependency-path: '**/yarn.lock'
31+
32+
- name: Install
33+
run: yarn install --frozen-lockfile
34+
35+
- name: Generate OpenAPI spec
36+
run: yarn workspace @sourcebot/web openapi:generate
37+
38+
- name: Commit regenerated spec if changed
39+
run: |
40+
SPEC=docs/api-reference/sourcebot-public.openapi.json
41+
if [ -z "$(git status --porcelain "$SPEC")" ]; then
42+
echo "OpenAPI spec is up to date."
43+
exit 0
44+
fi
45+
git config user.name "github-actions[bot]"
46+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
47+
git add "$SPEC"
48+
git commit -m "chore: regenerate OpenAPI spec"
49+
git push

.github/workflows/pr-gate.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: PR Gate
22

3-
# This gate simply validates that we can build the docker container.
3+
# This gate validates that Prisma migrations are in order and that we can build
4+
# the docker container.
45

56
on:
67
pull_request:
@@ -16,6 +17,15 @@ jobs:
1617
uses: actions/checkout@v4
1718
with:
1819
submodules: "true"
20+
# full history so migration checks can diff against the base branch
21+
fetch-depth: 0
22+
23+
# Fails fast (before the docker build) when migrations drift from
24+
# schema.prisma or a new migration is added out of timestamp order.
25+
- name: Check Prisma migrations
26+
uses: ./.github/actions/check-prisma-migrations
27+
with:
28+
base-ref: ${{ github.base_ref }}
1929

2030
- name: Build Docker image
2131
id: build

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Changed
11+
- [EE] Improved Ask Sourcebot prompt caching by splitting static and dynamic prompt sections and advancing cache breakpoints after every agent step instead of only after each message. [#1366](https://github.com/sourcebot-dev/sourcebot/pull/1366)
12+
- Refactored Ask Sourcebot user message text extraction into a shared helper that robustly handles non-text message parts. [#1371](https://github.com/sourcebot-dev/sourcebot/pull/1371)
13+
- Made the backend worker API address configurable via the `WORKER_API_URL` environment variable (default `http://localhost:3060`) instead of being hardcoded. [#1409](https://github.com/sourcebot-dev/sourcebot/pull/1409)
14+
1015
### Added
1116
- Added per-step token cost tracking and estimated tool call token usage to Ask Sourcebot chat history. [#1353](https://github.com/sourcebot-dev/sourcebot/pull/1353)
17+
- [EE] Added mermaid diagram rendering to Ask Sourcebot answers, with pan/zoom, copy/export, in-thread deep links, and an interleaved right-panel view. [#1369](https://github.com/sourcebot-dev/sourcebot/pull/1369)
18+
- [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370)
19+
- Added language model input-modality and document capability resolution, automatically resolved from the models.dev catalog (falls back to text-only for uncatalogued/self-hosted models). [#1372](https://github.com/sourcebot-dev/sourcebot/pull/1372)
20+
- [EE] Added DPoP sender-constrained OAuth tokens for MCP clients. [#1395](https://github.com/sourcebot-dev/sourcebot/pull/1395)
21+
- [EE] Added text file attachments to Ask Sourcebot, letting users attach text/code/config files to a chat message via the paperclip button, drag-and-drop, or paste, with large pastes auto-converted to attachments. [#1374](https://github.com/sourcebot-dev/sourcebot/pull/1374)
22+
- [EE] Added image attachments to Ask Sourcebot, letting users attach images to a chat message when the selected model supports image input. [#1375](https://github.com/sourcebot-dev/sourcebot/pull/1375)
23+
24+
### Fixed
25+
- Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367)
26+
- [EE] Fixed Ask Sourcebot mermaid diagrams overflowing their container by contain-fitting them to both width and height, and made revealing a diagram from the answer jump it into view instantly to avoid over/undershooting. [#1373](https://github.com/sourcebot-dev/sourcebot/pull/1373)
27+
- Verified GitHub review webhook deliveries before processing them. [#1378](https://github.com/sourcebot-dev/sourcebot/pull/1378)
28+
- Passed Zoekt index parameters via argv to preserve revision names with punctuation. [#1376](https://github.com/sourcebot-dev/sourcebot/pull/1376)
29+
- [EE] Validated OAuth bearer token scopes before allowing access to the Sourcebot MCP resource server. [#1396](https://github.com/sourcebot-dev/sourcebot/pull/1396)
30+
- Added HTTP security headers to all web app responses. [#1407](https://github.com/sourcebot-dev/sourcebot/pull/1407)
1231

1332
### Fixed
1433
- Upgraded `nodemailer` to `^9.0.1`. [#1356](https://github.com/sourcebot-dev/sourcebot/pull/1356)
@@ -45,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4564
### Added
4665
- Added the ability to configure email code and credentials login from the security settings. [#1303](https://github.com/sourcebot-dev/sourcebot/pull/1303)
4766
- Added a list of configured SSO providers from the security settings. [#1303](https://github.com/sourcebot-dev/sourcebot/pull/1303)
67+
- [EE] Added a SCIM 2.0 server for automated user provisioning and deprovisioning from identity providers (Okta, Entra). [#1306](https://github.com/sourcebot-dev/sourcebot/pull/1306)
4868

4969
### Fixed
5070
- Validated that `SOURCEBOT_ENCRYPTION_KEY` is exactly 32 characters at startup, failing fast with an actionable message instead of a runtime encryption error. [#1305](https://github.com/sourcebot-dev/sourcebot/pull/1305)

CLAUDE.md

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ When implementing a new API route, ask the user whether it should be part of the
9696
3. Add the endpoint to the relevant group in the `API Reference` tab of `docs/docs.json`.
9797
4. Regenerate the OpenAPI spec by running `yarn workspace @sourcebot/web openapi:generate`.
9898

99-
Route handlers should validate inputs using Zod schemas.
99+
Route handlers should validate inputs using Zod schemas. Put coercion, defaults, minimums, maximums, and cross-field validation in the schema instead of scattering parsing logic through the handler.
100100

101101
**Query parameters** (GET requests):
102102

@@ -105,8 +105,9 @@ import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/se
105105
import { z } from "zod";
106106

107107
const myQueryParamsSchema = z.object({
108-
q: z.string().default(''),
108+
repo: z.string(),
109109
page: z.coerce.number().int().positive().default(1),
110+
perPage: z.coerce.number().int().positive().max(100).default(50),
110111
});
111112

112113
export const GET = apiHandler(async (request: NextRequest) => {
@@ -124,11 +125,42 @@ export const GET = apiHandler(async (request: NextRequest) => {
124125
);
125126
}
126127

127-
const { q, page } = parsed.data;
128+
const { page, perPage, ...searchParams } = parsed.data;
129+
const skip = (page - 1) * perPage;
128130
// ... rest of handler
129131
});
130132
```
131133

134+
**Search query parameters**:
135+
136+
```ts
137+
import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
138+
import { z } from "zod";
139+
140+
const searchMembersQueryParamsSchema = z.object({
141+
query: z.string().default(''),
142+
});
143+
144+
export const GET = apiHandler(async (request: NextRequest) => {
145+
const rawParams = Object.fromEntries(
146+
Object.keys(searchMembersQueryParamsSchema.shape).map(key => [
147+
key,
148+
request.nextUrl.searchParams.get(key) ?? undefined
149+
])
150+
);
151+
const parsed = searchMembersQueryParamsSchema.safeParse(rawParams);
152+
153+
if (!parsed.success) {
154+
return serviceErrorResponse(
155+
queryParamsSchemaValidationError(parsed.error)
156+
);
157+
}
158+
159+
const { query } = parsed.data;
160+
// ... use query in the database/search call
161+
});
162+
```
163+
132164
**Request body** (POST/PUT/PATCH requests):
133165

134166
```ts
@@ -155,6 +187,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
155187
});
156188
```
157189

190+
Use `safeParseAsync` when the schema has async refinements, as in search routes:
191+
192+
```ts
193+
const body = await request.json();
194+
const parsed = await searchRequestSchema.safeParseAsync(body);
195+
```
196+
158197
## Data Fetching
159198

160199
For GET requests, prefer using API routes with react-query over server actions. This provides caching benefits and better control over data refetching.
@@ -226,6 +265,41 @@ export const GET = apiHandler(async (request: NextRequest) => {
226265
});
227266
```
228267

268+
## Membership States
269+
270+
Organization membership state is derived from fields on `UserToOrg`:
271+
272+
- **Active**: `suspendedAt` is `null` and `lastActiveAt` is not `null`. Active members can access the org and count as billable seats.
273+
- **Pending**: `suspendedAt` is `null` and `lastActiveAt` is `null`. Pending users can access the org, but are not billable yet.
274+
- **Suspended**: `suspendedAt` is not `null`. Suspended users cannot access the org and are not billable.
275+
276+
When filtering memberships, use the helper predicates from `packages/web/src/features/membership/utils.ts` instead of writing these conditions inline. This keeps auth, billing, SCIM, and UI queries aligned as the state rules evolve.
277+
278+
```ts
279+
import {
280+
activeMembershipWhere,
281+
activeOrPendingMembershipWhere,
282+
pendingMembershipWhere,
283+
suspendedMembershipWhere,
284+
} from "@/features/membership/utils";
285+
286+
// Billable seat count.
287+
await prisma.userToOrg.count({
288+
where: {
289+
orgId,
290+
...activeMembershipWhere(),
291+
},
292+
});
293+
294+
// Users who should be able to access the org.
295+
await prisma.userToOrg.findMany({
296+
where: {
297+
orgId,
298+
...activeOrPendingMembershipWhere(),
299+
},
300+
});
301+
```
302+
229303
## Next.js Router Navigation
230304

231305
Do NOT call `router.refresh()` immediately after `router.push()`. In Next.js 16, the prefetch cache and navigation system was completely rewritten, and calling `router.refresh()` right after `router.push()` creates a race condition. The refresh invalidates the cache and can interrupt the in-flight navigation, leaving the page stuck or not loading.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,11 @@
11481148
"MEMBER"
11491149
]
11501150
},
1151+
"suspendedAt": {
1152+
"type": "string",
1153+
"nullable": true,
1154+
"format": "date-time"
1155+
},
11511156
"createdAt": {
11521157
"type": "string",
11531158
"format": "date-time"
@@ -1163,6 +1168,7 @@
11631168
"name",
11641169
"email",
11651170
"role",
1171+
"suspendedAt",
11661172
"createdAt",
11671173
"lastActivityAt"
11681174
]

0 commit comments

Comments
 (0)