Skip to content

Commit 0bc0f42

Browse files
authored
Merge branch 'main' into fix/chat-skip-to-turn-complete-reset
2 parents ffa693e + 11e1cd8 commit 0bc0f42

30 files changed

Lines changed: 1799 additions & 54 deletions

.changeset/local-bundle-deploy.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/core": patch
3+
"trigger.dev": patch
4+
---
5+
6+
Add an experimental `--local-bundle` deploy flag that runs the install and bundling steps on your machine and uploads only the build output; the image is still built remotely. Useful when your project's install step needs tooling or credentials that only exist locally.

.github/workflows/codeql.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions: {}
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
analyze:
16+
name: Analyze (${{ matrix.language }})
17+
if: github.repository == 'triggerdotdev/trigger.dev'
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
security-events: write # Upload SARIF to GitHub Security tab
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
language: [actions, javascript-typescript]
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
29+
with:
30+
persist-credentials: false
31+
32+
- name: Initialize CodeQL
33+
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
34+
with:
35+
languages: ${{ matrix.language }}
36+
37+
- name: Perform CodeQL Analysis
38+
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
39+
with:
40+
category: /language:${{ matrix.language }}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Improved the performance and reliability of the runs list and the runs.list API, especially for large projects and filtered views.

apps/webapp/app/env.server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,19 @@ const EnvironmentSchema = z
816816
.number()
817817
.int()
818818
.default(60 * 1000 * 15), // 15 minutes
819+
DEPLOYMENT_CONTEXT_ARTIFACT_SIZE_LIMIT_BYTES: z.coerce
820+
.number()
821+
.int()
822+
.default(100 * 1024 * 1024), // 100MB
823+
DEPLOYMENT_BUNDLE_ARTIFACT_SIZE_LIMIT_BYTES: z.coerce
824+
.number()
825+
.int()
826+
.default(100 * 1024 * 1024), // 100MB
827+
DEPLOYMENT_BUILD_ENV_VARS_SIZE_LIMIT_BYTES: z.coerce
828+
.number()
829+
.int()
830+
.default(128 * 1024), // 128KB
831+
DEPLOYMENT_BUILD_ENV_VARS_MAX_KEYS: z.coerce.number().int().default(400),
819832

820833
// When enabled, reject deploys made by v3 CLI versions (i.e. payloads that
821834
// omit the `type` field). v4 CLI versions always send `type` ("MANAGED" or "V1"),
@@ -2233,6 +2246,15 @@ const EnvironmentSchema = z
22332246
.enum(["log", "error", "warn", "info", "debug"])
22342247
.default("info"),
22352248
RUNS_LIST_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
2249+
RUNS_LIST_CLICKHOUSE_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(40_000),
2250+
RUNS_LIST_CLICKHOUSE_MAX_EXECUTION_TIME: z.coerce.number().int().positive().default(35),
2251+
RUNS_LIST_CLICKHOUSE_MAX_THREADS: z.coerce.number().int().positive().default(4),
2252+
RUNS_LIST_CLICKHOUSE_MAX_MEMORY_USAGE: z.coerce
2253+
.number()
2254+
.int()
2255+
.positive()
2256+
.default(1_073_741_824),
2257+
RUNS_LIST_CLICKHOUSE_READONLY: z.enum(["0", "1", "2"]).default("2"),
22362258
/**
22372259
* Dedicated ClickHouse service for queue metrics: the ingestion consumer's inserts and every
22382260
* queue-metrics read (dashboards, queue pages, run inspector, health report) go through it, so

apps/webapp/app/presenters/v3/CreateBulkActionPresenter.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class CreateBulkActionPresenter extends BasePresenter {
2626

2727
const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
2828
organizationId,
29-
"standard"
29+
"runsList"
3030
);
3131
const runsRepository = new RunsRepository({
3232
clickhouse,

apps/webapp/app/routes/api.v1.artifacts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export async function action({ request }: ActionFunctionArgs) {
6464
case "deployment_context":
6565
errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Make sure you are in the correct directory of your Trigger.dev project. Reach out to us if you are seeing this error consistently.`;
6666
break;
67+
case "deployment_bundle":
68+
errorMessage = `Bundle size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB. Reach out to us if you are seeing this error consistently.`;
69+
break;
6770
default:
6871
body.data.type satisfies never;
6972
errorMessage = `Artifact size (${sizeMB} MB) exceeds the allowed limit of ${limitMB} MB`;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { type GetDeploymentBuildEnvVarsResponseBody } from "@trigger.dev/core/v3";
3+
import { z } from "zod";
4+
import { prisma } from "~/db.server";
5+
import { env } from "~/env.server";
6+
import { authenticateApiKeyWithScope } from "~/services/apiAuth.server";
7+
import { logger } from "~/services/logger.server";
8+
import { decryptSecret, EncryptedSecretValueSchema } from "~/services/secrets/secretStore.server";
9+
import { FINAL_DEPLOYMENT_STATUSES } from "~/v3/services/failDeployment.server";
10+
11+
const ParamsSchema = z.object({
12+
deploymentId: z.string(),
13+
});
14+
15+
// Secret material, deliberately separate from the main GET deployment endpoint.
16+
export async function loader({ request, params }: LoaderFunctionArgs) {
17+
const parsedParams = ParamsSchema.safeParse(params);
18+
19+
if (!parsedParams.success) {
20+
return json({ error: "Invalid params" }, { status: 400 });
21+
}
22+
23+
try {
24+
const authResult = await authenticateApiKeyWithScope(request, {
25+
action: "read",
26+
resource: { type: "deployments" },
27+
});
28+
29+
if (!authResult.ok) {
30+
logger.info("Invalid or missing api key", { url: request.url });
31+
return json({ error: authResult.error }, { status: authResult.status });
32+
}
33+
34+
const authenticatedEnv = authResult.authentication.environment;
35+
36+
const { deploymentId } = parsedParams.data;
37+
38+
const deployment = await prisma.workerDeployment.findFirst({
39+
where: {
40+
friendlyId: deploymentId,
41+
environmentId: authenticatedEnv.id,
42+
},
43+
select: {
44+
id: true,
45+
status: true,
46+
buildEnvVars: true,
47+
},
48+
});
49+
50+
if (!deployment) {
51+
return json({ error: "Deployment not found" }, { status: 404 });
52+
}
53+
54+
logger.info("Build env vars read", {
55+
deploymentId,
56+
environmentId: authenticatedEnv.id,
57+
projectId: authenticatedEnv.projectId,
58+
status: deployment.status,
59+
hasVars: deployment.buildEnvVars !== null,
60+
});
61+
62+
// Never serve secrets for a build that is no longer active, even if a clear is still in flight
63+
if (FINAL_DEPLOYMENT_STATUSES.includes(deployment.status)) {
64+
return json({ variables: {} } satisfies GetDeploymentBuildEnvVarsResponseBody, {
65+
status: 200,
66+
});
67+
}
68+
69+
if (!deployment.buildEnvVars) {
70+
return json({ variables: {} } satisfies GetDeploymentBuildEnvVarsResponseBody, {
71+
status: 200,
72+
});
73+
}
74+
75+
// Present-but-unreadable must fail loud: an empty record would let the build run without its secrets
76+
const envelope = EncryptedSecretValueSchema.safeParse(deployment.buildEnvVars);
77+
78+
if (!envelope.success) {
79+
logger.error("Stored build env vars are not a valid encrypted envelope", {
80+
deploymentId,
81+
environmentId: authenticatedEnv.id,
82+
});
83+
return json(
84+
{ error: "The stored build environment variables could not be read. Retry the deploy." },
85+
{ status: 500 }
86+
);
87+
}
88+
89+
const decrypted = await decryptSecret(env.ENCRYPTION_KEY, envelope.data);
90+
const variables = z.record(z.string()).parse(JSON.parse(decrypted));
91+
92+
return json({ variables } satisfies GetDeploymentBuildEnvVarsResponseBody, { status: 200 });
93+
} catch (error) {
94+
if (error instanceof Response) throw error;
95+
logger.error("Failed to load deployment build env vars", { error });
96+
return json({ error: "Internal Server Error" }, { status: 500 });
97+
}
98+
}

apps/webapp/app/services/clickhouse/clickhouseFactory.server.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ClickHouse } from "@internal/clickhouse";
1+
import { ClickHouse, type ClickHouseSettings } from "@internal/clickhouse";
22
import { createHash } from "crypto";
33
import { ClickhouseEventRepository } from "~/v3/eventRepository/clickhouseEventRepository.server";
44
import { env } from "~/env.server";
@@ -292,6 +292,45 @@ function initializeRealtimeClickhouseClient(): ClickHouse {
292292
});
293293
}
294294

295+
/**
296+
* Server-side query protection for the runs-list read pool. Every setting here is PER-QUERY, so a
297+
* pathological query only ever kills itself: a slow one hits `max_execution_time`, a memory-hungry
298+
* one hits `max_memory_usage`, a thread-hungry one hits `max_threads`. Per-USER limits
299+
* (`max_*_for_user`) are deliberately NOT used: everything connects as `default`, so a per-user cap
300+
* would reject whichever query arrives once the shared budget is hit, punishing innocent tenants
301+
* for a noisy one. The node itself is protected by the server-level `max_server_memory_usage`.
302+
* Safe as client-level settings ONLY because this pool is read-only; on a mixed read+write pool a
303+
* client-level `max_execution_time` would also kill slow inserts. `readonly=2` enforces read-only
304+
* while still allowing these settings to apply (`readonly=1` rejects them).
305+
*/
306+
/**
307+
* Client request timeout for the runs-list pool, forced above the server-side `max_execution_time`
308+
* so the server cap is what stops a slow query and the client stays connected to receive that
309+
* error. If the client timed out first, it would abort while ClickHouse kept executing, which is
310+
* the abandoned-query behaviour this pool is trying to prevent.
311+
*/
312+
function getRunsListRequestTimeoutMs() {
313+
return Math.max(
314+
env.RUNS_LIST_CLICKHOUSE_REQUEST_TIMEOUT_MS,
315+
(env.RUNS_LIST_CLICKHOUSE_MAX_EXECUTION_TIME + 5) * 1000
316+
);
317+
}
318+
319+
function getRunsListClickhouseSettings(): ClickHouseSettings {
320+
const settings: ClickHouseSettings = {
321+
max_execution_time: env.RUNS_LIST_CLICKHOUSE_MAX_EXECUTION_TIME,
322+
timeout_before_checking_execution_speed: 0,
323+
max_threads: env.RUNS_LIST_CLICKHOUSE_MAX_THREADS,
324+
max_memory_usage: env.RUNS_LIST_CLICKHOUSE_MAX_MEMORY_USAGE.toString(),
325+
};
326+
327+
if (env.RUNS_LIST_CLICKHOUSE_READONLY !== "0") {
328+
settings.readonly = env.RUNS_LIST_CLICKHOUSE_READONLY;
329+
}
330+
331+
return settings;
332+
}
333+
295334
/** Runs list reads — dashboard + API (`RUNS_LIST_CLICKHOUSE_URL`);
296335
* falls back to the default client if unset. */
297336
const defaultRunsListClickhouseClient = singleton(
@@ -319,6 +358,8 @@ function initializeRunsListClickhouseClient(): ClickHouse {
319358
request: env.RUNS_LIST_CLICKHOUSE_COMPRESSION_REQUEST === "1",
320359
},
321360
maxOpenConnections: env.RUNS_LIST_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
361+
requestTimeoutMs: getRunsListRequestTimeoutMs(),
362+
clickhouseSettings: getRunsListClickhouseSettings(),
322363
});
323364
}
324365

@@ -550,10 +591,25 @@ function buildOrgClickhouseClient(url: string, clientType: ClientType): ClickHou
550591
},
551592
maxOpenConnections: env.REALTIME_BACKEND_NATIVE_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
552593
});
594+
case "runsList":
595+
return new ClickHouse({
596+
url: parsed.toString(),
597+
name,
598+
keepAlive: {
599+
enabled: env.RUNS_LIST_CLICKHOUSE_KEEP_ALIVE_ENABLED === "1",
600+
idleSocketTtl: env.RUNS_LIST_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS,
601+
},
602+
logLevel: env.RUNS_LIST_CLICKHOUSE_LOG_LEVEL,
603+
compression: {
604+
request: env.RUNS_LIST_CLICKHOUSE_COMPRESSION_REQUEST === "1",
605+
},
606+
maxOpenConnections: env.RUNS_LIST_CLICKHOUSE_MAX_OPEN_CONNECTIONS,
607+
requestTimeoutMs: getRunsListRequestTimeoutMs(),
608+
clickhouseSettings: getRunsListClickhouseSettings(),
609+
});
553610
case "standard":
554611
case "query":
555612
case "admin":
556-
case "runsList":
557613
return new ClickHouse({
558614
url: parsed.toString(),
559615
name,

apps/webapp/app/services/platform.v3.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,7 @@ export async function enqueueBuild(
10951095
options: {
10961096
skipPromotion?: boolean;
10971097
configFilePath?: string;
1098+
fromBundle?: boolean;
10981099
}
10991100
) {
11001101
if (!client) return undefined;
@@ -1235,6 +1236,10 @@ export function isCloud(): boolean {
12351236
return true;
12361237
}
12371238

1239+
if (env.LOGIN_ORIGIN?.endsWith(".triggerlabs.dev")) {
1240+
return true;
1241+
}
1242+
12381243
if (process.env.CLOUD_ENV === "development" && process.env.NODE_ENV === "development") {
12391244
return true;
12401245
}

0 commit comments

Comments
 (0)