Skip to content

Commit 40e07f5

Browse files
authored
Merge branch 'main' into sentry-db-unreachable-fingerprint
2 parents d27ee9d + 55fa2d4 commit 40e07f5

9 files changed

Lines changed: 116 additions & 15 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Add `TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP=1` escape hatch for local self-hosted builds whose buildx driver doesn't support `rewrite-timestamp` alongside push (e.g. orbstack's default `docker` driver).

.claude/REVIEW.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Reserve 🔴 for things that would page someone or block a rollback. In this cod
1111
- A Redis data shape used by both versions changes in place. New shapes need a new key namespace.
1212
- A migration is not backward-compatible with the prior image.
1313
- **Schema / migration safety.** Prisma migrations must be backward-compatible with the prior deploy. Adding NOT NULL without a default, dropping a column an old image still reads, renaming a column — all 🔴.
14+
- **ClickHouse migration ordering + idempotency.** Goose runs in strict mode in the deploy pipeline and refuses to apply a missing version below the current version — slotting a new file in below the latest already-applied version blocks the deploy. New ClickHouse migration files MUST use the next available number (`max(files in internal-packages/clickhouse/schema/) + 1`); if main has added migrations while you've been on a branch, renumber yours. DDL must also be idempotent (`ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS`, `CREATE TABLE IF NOT EXISTS`, `ADD INDEX IF NOT EXISTS`) so a partial / `--allow-missing` apply elsewhere doesn't fail on retry. Either fault is 🔴 — both break test/prod deploys. Rules live in `internal-packages/clickhouse/CLAUDE.md`.
1415
- **Queue / concurrency correctness.** RunQueue, MarQS (V1, legacy), redis-worker — any change to enqueue / dequeue / locking semantics. Re-derive the invariant on paper before flagging or accepting.
1516
- **Missing index on a hot table.** New Prisma queries against `TaskRun`, `TaskRunExecutionSnapshot`, `JobRun`, `Project`, etc. must use an existing index. Check `internal-packages/database/prisma/schema.prisma` for the relevant `@@index` lines — don't guess and don't propose `EXPLAIN`.
1617
- **Recovery-path queries.** Any `TaskRun.findFirst` / `findMany` added to a schedule, run-recovery, or restart loop. Recovery fan-outs (Redis crash, restart storms) turn "rare indexed query" into a DB incident. 🔴 even if indexed.

.github/workflows/pr_checks.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@ jobs:
1717
name: Detect changes
1818
runs-on: ubuntu-latest
1919
outputs:
20-
code: ${{ steps.filter.outputs.code }}
20+
code: ${{ steps.code_filter.outputs.code }}
21+
typecheck_self: ${{ steps.filter.outputs.typecheck_self }}
2122
webapp: ${{ steps.filter.outputs.webapp }}
2223
packages: ${{ steps.filter.outputs.packages }}
2324
internal: ${{ steps.filter.outputs.internal }}
2425
cli: ${{ steps.filter.outputs.cli }}
2526
sdk: ${{ steps.filter.outputs.sdk }}
2627
steps:
28+
# `code` uses `every` semantics so the negation patterns actually subtract.
29+
# With the default `some` quantifier, `**` matches every file and the
30+
# subsequent `!...` patterns are no-ops (each pattern is OR'd, not AND'd).
2731
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
28-
id: filter
32+
id: code_filter
2933
with:
34+
predicate-quantifier: every
3035
filters: |
3136
code:
3237
- '**'
@@ -37,6 +42,11 @@ jobs:
3742
- '!references/**'
3843
- '!**/*.md'
3944
- '!**/.env.example'
45+
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
46+
id: filter
47+
with:
48+
filters: |
49+
typecheck_self:
4050
- '.github/workflows/pr_checks.yml'
4151
- '.github/workflows/typecheck.yml'
4252
webapp:
@@ -95,7 +105,7 @@ jobs:
95105
96106
typecheck:
97107
needs: changes
98-
if: needs.changes.outputs.code == 'true'
108+
if: needs.changes.outputs.code == 'true' || needs.changes.outputs.typecheck_self == 'true'
99109
uses: ./.github/workflows/typecheck.yml
100110

101111
webapp:

internal-packages/clickhouse/CLAUDE.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,46 @@
44

55
## Migrations
66

7-
Goose-format SQL migrations live in `schema/`. Create new numbered files:
7+
Goose-format SQL migrations live in `schema/`. Two rules below are load-bearing — both can block a deploy.
8+
9+
### Rule 1: number to `max + 1`, never slot in
10+
11+
Goose runs in strict mode in the deploy pipeline. If a migration file numbered *below* the version currently recorded in `goose_db_version` ever shows up, goose refuses to apply it and the deploy fails:
12+
13+
```
14+
goose run: error: found 1 missing migrations before current version 30:
15+
version 29: 029_add_task_kind_to_task_runs_v2.sql
16+
```
17+
18+
When adding a migration:
19+
20+
1. Look at `schema/` and take the largest existing number, call it `N`.
21+
2. Name your file `0(N+1)_descriptive_name.sql`.
22+
3. If you've been on a branch while main added migrations, **rebase and renumber** before opening the PR — a file numbered below the new max will block the next deploy after your PR merges.
23+
24+
### Rule 2: DDL must be idempotent
25+
26+
Migrations can be applied out of order in some environments (`goose up --allow-missing` for local recovery, manual fixups, etc.) and may be retried. Always use idempotent forms so a re-apply is a no-op:
827

928
```sql
1029
-- +goose Up
1130
ALTER TABLE trigger_dev.your_table
12-
ADD COLUMN new_column String DEFAULT '';
31+
ADD COLUMN IF NOT EXISTS new_column String DEFAULT '';
1332

1433
-- +goose Down
1534
ALTER TABLE trigger_dev.your_table
16-
DROP COLUMN new_column;
35+
DROP COLUMN IF EXISTS new_column;
1736
```
1837

38+
Equivalent forms for other DDL:
39+
40+
- `CREATE TABLE IF NOT EXISTS …`
41+
- `DROP TABLE IF EXISTS …`
42+
- `ADD INDEX IF NOT EXISTS …` / `DROP INDEX IF EXISTS …`
43+
- `CREATE MATERIALIZED VIEW IF NOT EXISTS …` / `DROP VIEW IF EXISTS …`
44+
45+
ClickHouse supports `IF [NOT] EXISTS` on all of the above. Older migrations in this directory predate the rule and are not idempotent — leave them as-is unless you're explicitly hardening one.
46+
1947
## Naming Conventions
2048

2149
- `raw_` prefix for input tables (where data lands first)

internal-packages/clickhouse/schema/029_add_task_kind_to_task_runs_v2.sql

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- +goose Up
2+
-- IF NOT EXISTS is required because this migration was previously numbered
3+
-- 029 and may have been applied in environments where goose accepted it
4+
-- before 030_create_sessions_v1 advanced the version counter. Renaming to
5+
-- 031 makes goose treat this as new everywhere, so the DDL must tolerate
6+
-- the column already being present.
7+
ALTER TABLE trigger_dev.task_runs_v2
8+
ADD COLUMN IF NOT EXISTS task_kind LowCardinality(String) DEFAULT '';
9+
10+
-- +goose Down
11+
ALTER TABLE trigger_dev.task_runs_v2
12+
DROP COLUMN IF EXISTS task_kind;

packages/cli-v3/src/deploy/buildImage.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,14 @@ function getOutputOptions({
11521152
return outputOptions;
11531153
}
11541154

1155-
const outputOptions: string[] = ["type=image", "oci-mediatypes=true", "rewrite-timestamp=true"];
1155+
// `rewrite-timestamp` is incompatible with the buildx docker driver's
1156+
// implicit `unpack=true` on push (used by e.g. orbstack's default builder).
1157+
// Provide an env-var escape hatch so local-dev deploys can opt out.
1158+
const skipRewriteTimestamp = process.env.TRIGGER_BUILD_SKIP_REWRITE_TIMESTAMP === "1";
1159+
const outputOptions: string[] = ["type=image", "oci-mediatypes=true"];
1160+
if (!skipRewriteTimestamp) {
1161+
outputOptions.push("rewrite-timestamp=true");
1162+
}
11561163

11571164
if (imageTag) {
11581165
outputOptions.push(`name=${imageTag}`);

packages/trigger-sdk/src/v3/chat.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,40 @@ describe("TriggerChatTransport", () => {
568568
expect(subscribe!).toContain("/realtime/v1/sessions/chat-by-chatid/out");
569569
});
570570

571+
it("routes .out SSE through streamBaseURL while appends stay on baseURL", async () => {
572+
const requests: string[] = [];
573+
global.fetch = vi.fn().mockImplementation(async (url: string | URL) => {
574+
const urlStr = typeof url === "string" ? url : url.toString();
575+
requests.push(urlStr);
576+
if (isSessionStreamAppendUrl(urlStr)) return defaultAppendResponse();
577+
if (isSessionOutSubscribeUrl(urlStr)) return defaultSseResponse();
578+
throw new Error(`Unexpected URL: ${urlStr}`);
579+
});
580+
581+
const transport = new TriggerChatTransport({
582+
task: "my-chat-task",
583+
accessToken: () => "pat",
584+
baseURL: "https://api.test.trigger.dev",
585+
streamBaseURL: "https://chat-proxy.example.com",
586+
sessions: { "chat-split": { publicAccessToken: "p" } },
587+
});
588+
589+
const stream = await transport.sendMessages({
590+
trigger: "submit-message",
591+
chatId: "chat-split",
592+
messageId: undefined,
593+
messages: [createUserMessage("Hi")],
594+
abortSignal: undefined,
595+
});
596+
await drainChunks(stream);
597+
598+
const append = requests.find(isSessionStreamAppendUrl);
599+
const subscribe = requests.find(isSessionOutSubscribeUrl);
600+
expect(append!.startsWith("https://api.test.trigger.dev/")).toBe(true);
601+
expect(subscribe!.startsWith("https://chat-proxy.example.com/")).toBe(true);
602+
expect(subscribe!).toContain("/realtime/v1/sessions/chat-split/out");
603+
});
604+
571605
it("for submit-message, only the latest message is delivered to .in", async () => {
572606
// Slim wire: each `.in/append` carries at most ONE new message in
573607
// `payload.message` (singular). Even if the caller hands sendMessages

packages/trigger-sdk/src/v3/chat.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ export type TriggerChatTransportOptions<TClientData = unknown> = {
225225
/** Base URL for the Trigger.dev API. @default "https://api.trigger.dev" */
226226
baseURL?: string;
227227

228+
/**
229+
* Base URL for the SSE stream subscription only (`GET .../sessions/{chatId}/out`).
230+
* Falls back to `baseURL` when unset. Set this to route the long-lived
231+
* stream through a custom proxy (e.g. a Cloudflare worker capturing JA4
232+
* fingerprints for bot detection) while keeping append POSTs direct to
233+
* `baseURL` to avoid an extra hop on every user message.
234+
*/
235+
streamBaseURL?: string;
236+
228237
/** Additional headers included in every API request. */
229238
headers?: Record<string, string>;
230239

@@ -346,6 +355,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
346355
| ((params: StartSessionParams<Record<string, unknown>>) => Promise<StartSessionResult>)
347356
| undefined;
348357
private readonly baseURL: string;
358+
private readonly streamBaseURL: string;
349359
private readonly extraHeaders: Record<string, string>;
350360
private readonly streamTimeoutSeconds: number;
351361
private defaultMetadata: Record<string, unknown> | undefined;
@@ -367,6 +377,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
367377
| ((params: StartSessionParams<Record<string, unknown>>) => Promise<StartSessionResult>)
368378
| undefined;
369379
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
380+
this.streamBaseURL = options.streamBaseURL ?? this.baseURL;
370381
this.extraHeaders = options.headers ?? {};
371382
this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS;
372383
this.defaultMetadata = options.clientData;
@@ -1021,7 +1032,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
10211032
);
10221033
}
10231034

1024-
const streamUrl = `${this.baseURL}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`;
1035+
const streamUrl = `${this.streamBaseURL}/realtime/v1/sessions/${encodeURIComponent(chatId)}/out`;
10251036

10261037
return new ReadableStream<UIMessageChunk>({
10271038
start: async (controller) => {

0 commit comments

Comments
 (0)