diff --git a/.changeset/experimental-node-runtimes.md b/.changeset/experimental-node-runtimes.md new file mode 100644 index 000000000..39ec16321 --- /dev/null +++ b/.changeset/experimental-node-runtimes.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/core": patch +"trigger.dev": patch +--- + +Add experimental Node.js 24 and 26 task runtimes. Set `runtime` to `experimental-node-24` or `experimental-node-26` in `trigger.config.ts`. diff --git a/.changeset/project-default-region-response.md b/.changeset/project-default-region-response.md new file mode 100644 index 000000000..86d1856ac --- /dev/null +++ b/.changeset/project-default-region-response.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Add `defaultRegion` to the project GET and list API responses; null when unset. diff --git a/.claude/rules/server-apps.md b/.claude/rules/server-apps.md index 9229a0b2f..3bc4c0b2b 100644 --- a/.claude/rules/server-apps.md +++ b/.claude/rules/server-apps.md @@ -14,10 +14,12 @@ area: webapp type: fix --- -Brief description of what changed and why. +Fix pages occasionally loading unstyled during deploys. The dashboard now recovers automatically. EOF ``` - **area**: `webapp` | `supervisor` - **type**: `feature` | `fix` | `improvement` | `breaking` - If the PR also touches `packages/`, just the changeset is sufficient (no `.server-changes/` needed). + +The body ships **verbatim in user-facing release notes**. Keep it to 1–2 short sentences, non-technical, written for a dashboard user: describe what changed for them, never the implementation (no header names, endpoints, middleware, storage mechanisms, internal tools). See `.server-changes/README.md` for full guidance. diff --git a/.env.example b/.env.example index 37ffa4220..f37a1a519 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ LOGIN_ORIGIN=http://localhost:3030 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public # This sets the URL used for direct connections to the database and should only be needed in limited circumstances # See: https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#fields:~:text=the%20shadow%20database.-,directUrl,-No -DIRECT_URL=${DATABASE_URL} +DIRECT_URL=postgresql://postgres:postgres@localhost:5432/postgres?schema=public # Dedicated run-ops database (@internal/run-ops-database). Only needed to run prisma commands # against it or to enable the run-ops split; start it with `docker compose --profile runops up`. RUN_OPS_DATABASE_URL=postgresql://postgres:postgres@localhost:5434/postgres?schema=public @@ -166,4 +166,4 @@ POSTHOG_PROJECT_KEY= # Uncomment these to send metrics to the local Prometheus via OTEL Collector: # INTERNAL_OTEL_METRIC_EXPORTER_ENABLED=1 # INTERNAL_OTEL_METRIC_EXPORTER_URL=http://localhost:4318/v1/metrics -# INTERNAL_OTEL_METRIC_EXPORTER_INTERVAL_MS=15000 \ No newline at end of file +# INTERNAL_OTEL_METRIC_EXPORTER_INTERVAL_MS=15000 diff --git a/.github/workflows/changesets-pr.yml b/.github/workflows/changesets-pr.yml index e329d3a2f..851c5341d 100644 --- a/.github/workflows/changesets-pr.yml +++ b/.github/workflows/changesets-pr.yml @@ -36,7 +36,7 @@ jobs: - name: Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 49d5585e7..7a278672a 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -44,7 +44,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: šŸ“„ Download deps diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 894eba6e3..48575827b 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -25,7 +25,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: šŸ“„ Download deps diff --git a/.github/workflows/dashboard-agent-deploy.yml b/.github/workflows/dashboard-agent-deploy.yml index 04dd78d6f..e5ea85606 100644 --- a/.github/workflows/dashboard-agent-deploy.yml +++ b/.github/workflows/dashboard-agent-deploy.yml @@ -51,7 +51,7 @@ jobs: - name: Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: Install + build the CLI and the agent's deps diff --git a/.github/workflows/e2e-webapp-auth-full.yml b/.github/workflows/e2e-webapp-auth-full.yml index 2e47851ab..e88429a34 100644 --- a/.github/workflows/e2e-webapp-auth-full.yml +++ b/.github/workflows/e2e-webapp-auth-full.yml @@ -85,7 +85,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: 🐳 Login to DockerHub diff --git a/.github/workflows/e2e-webapp.yml b/.github/workflows/e2e-webapp.yml index c795b3e64..1931cfdd3 100644 --- a/.github/workflows/e2e-webapp.yml +++ b/.github/workflows/e2e-webapp.yml @@ -59,7 +59,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" # ..to avoid rate limits when pulling images diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f2015e0d3..4584ea69c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,7 +37,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 - name: šŸ“„ Download deps run: pnpm install --frozen-lockfile --filter trigger.dev... diff --git a/.github/workflows/pr_checks.yml b/.github/workflows/pr_checks.yml index 2629ff824..b0fbd6ac0 100644 --- a/.github/workflows/pr_checks.yml +++ b/.github/workflows/pr_checks.yml @@ -56,6 +56,7 @@ jobs: - '.github/workflows/pr_checks.yml' - '.github/workflows/unit-tests-webapp.yml' - '.github/workflows/e2e-webapp.yml' + - '.github/workflows/runops-guard.yml' - '.configs/**' - 'package.json' - 'pnpm-lock.yaml' @@ -111,6 +112,11 @@ jobs: if: needs.changes.outputs.code == 'true' || needs.changes.outputs.typecheck_self == 'true' uses: ./.github/workflows/typecheck.yml + runops-guard: + needs: changes + if: needs.changes.outputs.webapp == 'true' + uses: ./.github/workflows/runops-guard.yml + webapp: needs: changes if: needs.changes.outputs.webapp == 'true' @@ -161,6 +167,7 @@ jobs: - changes - code-quality - typecheck + - runops-guard - webapp - e2e-webapp - packages diff --git a/.github/workflows/preview-packages.yml b/.github/workflows/preview-packages.yml index 15d8b1538..b5bdbb313 100644 --- a/.github/workflows/preview-packages.yml +++ b/.github/workflows/preview-packages.yml @@ -59,7 +59,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: šŸ“„ Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7f8b14c9..529290356 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,7 @@ jobs: - name: Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" # npm v11.5.1 or newer is required for OIDC support @@ -303,7 +303,7 @@ jobs: - name: Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" # npm v11.5.1 or newer is required for OIDC support diff --git a/.github/workflows/runops-guard.yml b/.github/workflows/runops-guard.yml new file mode 100644 index 000000000..e496db2a9 --- /dev/null +++ b/.github/workflows/runops-guard.yml @@ -0,0 +1,38 @@ +name: "šŸ›”ļø Run-ops Legacy Guard" + +on: + workflow_call: + +permissions: + contents: read + +jobs: + runops-guard: + runs-on: warp-ubuntu-latest-x64-16x + + steps: + - name: ā¬‡ļø Checkout repo + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: āŽ” Setup pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 10.33.2 + + - name: āŽ” Setup node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24.18.0 + cache: "pnpm" + + - name: šŸ“„ Download deps + run: pnpm install --frozen-lockfile + + - name: šŸ“€ Generate Prisma Client + run: pnpm run generate + + - name: šŸ›”ļø Run-ops legacy guard + run: pnpm --filter webapp run guard:runops-legacy -- --check diff --git a/.github/workflows/sdk-compat.yml b/.github/workflows/sdk-compat.yml index 7d89046c8..ddfb4731d 100644 --- a/.github/workflows/sdk-compat.yml +++ b/.github/workflows/sdk-compat.yml @@ -70,7 +70,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: 🄟 Setup Bun @@ -112,7 +112,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: šŸ¦• Setup Deno @@ -158,7 +158,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: šŸ“„ Download deps diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index d4c6cd2a5..d8521f393 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -25,7 +25,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" - name: šŸ“„ Download deps diff --git a/.github/workflows/unit-tests-internal.yml b/.github/workflows/unit-tests-internal.yml index 068d9e47a..1a398af9c 100644 --- a/.github/workflows/unit-tests-internal.yml +++ b/.github/workflows/unit-tests-internal.yml @@ -63,7 +63,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" # ..to avoid rate limits when pulling images diff --git a/.github/workflows/unit-tests-packages.yml b/.github/workflows/unit-tests-packages.yml index 5802bba77..465174fb9 100644 --- a/.github/workflows/unit-tests-packages.yml +++ b/.github/workflows/unit-tests-packages.yml @@ -66,7 +66,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" # ..to avoid rate limits when pulling images @@ -146,7 +146,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 # no cache enabled, we're not installing deps - name: Download blob reports from GitHub Actions Artifacts diff --git a/.github/workflows/unit-tests-webapp.yml b/.github/workflows/unit-tests-webapp.yml index 0ba248ca7..680eafd9a 100644 --- a/.github/workflows/unit-tests-webapp.yml +++ b/.github/workflows/unit-tests-webapp.yml @@ -71,7 +71,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 cache: "pnpm" # ..to avoid rate limits when pulling images @@ -160,7 +160,7 @@ jobs: - name: āŽ” Setup node uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22.23.1 + node-version: 24.18.0 # no cache enabled, we're not installing deps - name: Download blob reports from GitHub Actions Artifacts diff --git a/.nvmrc b/.nvmrc index 279de6db0..5bcf9c6e6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.23.1 +v24.18.0 diff --git a/.oxlintrc.json b/.oxlintrc.json index e32d206dc..d9b4cb217 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,7 +1,10 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["typescript", "import", "react"], - "jsPlugins": ["./oxlint-plugins/no-thrown-unawaited-redirect.mjs"], + "jsPlugins": [ + "./oxlint-plugins/no-thrown-unawaited-redirect.mjs", + "./oxlint-plugins/runops-residency.mjs" + ], "ignorePatterns": [ "**/dist/**", "**/build/**", @@ -34,5 +37,21 @@ "react-hooks/exhaustive-deps": "off", "react-hooks/rules-of-hooks": "off", "trigger/no-thrown-unawaited-redirect": "error" - } + }, + "overrides": [ + { + "files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"], + "rules": { + "trigger-runops/no-control-plane-run-graph-access": "error", + "trigger-runops/no-control-plane-in-runops-slot": "error" + } + }, + { + "files": ["apps/webapp/app/**/*.test.ts", "apps/webapp/app/**/*.test.tsx"], + "rules": { + "trigger-runops/no-control-plane-run-graph-access": "off", + "trigger-runops/no-control-plane-in-runops-slot": "off" + } + } + ] } diff --git a/.server-changes/attio-sync-transient-retry.md b/.server-changes/attio-sync-transient-retry.md new file mode 100644 index 000000000..9c54416a4 --- /dev/null +++ b/.server-changes/attio-sync-transient-retry.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Transient internal sync failures are now retried quietly instead of surfacing as errors. diff --git a/.server-changes/clickhouse-read-replica.md b/.server-changes/clickhouse-read-replica.md new file mode 100644 index 000000000..334b10d3c --- /dev/null +++ b/.server-changes/clickhouse-read-replica.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Optionally route ClickHouse read traffic to a read replica while writes stay on the primary. Set `CLICKHOUSE_READER_URL` to move all reads, or target the busiest paths with `RUNS_LIST_CLICKHOUSE_URL` (runs list) and `EVENTS_READER_CLICKHOUSE_URL` (traces, spans, logs). All optional; unset keeps current behavior. diff --git a/.server-changes/fix-batch-idempotency-per-item-keys.md b/.server-changes/fix-batch-idempotency-per-item-keys.md new file mode 100644 index 000000000..9e84d474c --- /dev/null +++ b/.server-changes/fix-batch-idempotency-per-item-keys.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Fix batchTrigger requests that set a per-item idempotency key failing with an error instead of creating and deduplicating the runs diff --git a/.server-changes/fix-batch-idempotency-point-lookups.md b/.server-changes/fix-batch-idempotency-point-lookups.md new file mode 100644 index 000000000..1c3a4aab7 --- /dev/null +++ b/.server-changes/fix-batch-idempotency-point-lookups.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Speed up idempotency checks on `batchTrigger` calls that use idempotency keys. Large batches against a task with a big run history no longer degrade to multi-second lookups. diff --git a/.server-changes/remove-deprecated-realtime-stream-action.md b/.server-changes/remove-deprecated-realtime-stream-action.md new file mode 100644 index 000000000..3ad4f19d7 --- /dev/null +++ b/.server-changes/remove-deprecated-realtime-stream-action.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Remove the deprecated realtime stream write endpoint used by retired v3 task clients. diff --git a/.server-changes/stale-deploy-asset-recovery.md b/.server-changes/stale-deploy-asset-recovery.md new file mode 100644 index 000000000..ae34351fb --- /dev/null +++ b/.server-changes/stale-deploy-asset-recovery.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Fix pages occasionally loading unstyled or failing to load during a deploy. The dashboard now reloads automatically to recover. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1d0a6871..fb41c308f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,7 +29,7 @@ branch are tagged into a release periodically. ### Prerequisites -- [Node.js](https://nodejs.org/en) version 22.23.1 +- [Node.js](https://nodejs.org/en) version 24.18.0 - [pnpm package manager](https://pnpm.io/installation) version 10.33.2 - [Docker](https://www.docker.com/get-started/) - [protobuf](https://github.com/protocolbuffers/protobuf) @@ -49,7 +49,7 @@ branch are tagged into a release periodically. ``` cd trigger.dev ``` -3. Ensure you are on the correct version of Node.js (22.23.1). If you are using `nvm`, there is an `.nvmrc` file that will automatically select the correct version of Node.js when you navigate to the repository. +3. Ensure you are on the correct version of Node.js (24.18.0). If you are using `nvm`, there is an `.nvmrc` file that will automatically select the correct version of Node.js when you navigate to the repository. 4. Run `corepack enable` to use the correct version of pnpm (`10.33.2`) as specified in the root `package.json` file. diff --git a/apps/supervisor/.nvmrc b/apps/supervisor/.nvmrc index 279de6db0..5bcf9c6e6 100644 --- a/apps/supervisor/.nvmrc +++ b/apps/supervisor/.nvmrc @@ -1 +1 @@ -v22.23.1 +v24.18.0 diff --git a/apps/supervisor/Containerfile b/apps/supervisor/Containerfile index 9ca205e37..edc6ba2ee 100644 --- a/apps/supervisor/Containerfile +++ b/apps/supervisor/Containerfile @@ -1,13 +1,13 @@ -FROM node:22-alpine@sha256:9bef0ef1e268f60627da9ba7d7605e8831d5b56ad07487d24d1aa386336d1944 AS node-22-alpine +FROM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS node-24-alpine WORKDIR /app -FROM node-22-alpine AS pruner +FROM node-24-alpine AS pruner COPY --chown=node:node . . RUN npx -q turbo@2.10.0 prune --scope=supervisor --docker -FROM node-22-alpine AS base +FROM node-24-alpine AS base RUN apk add --no-cache dumb-init diff --git a/apps/supervisor/src/index.ts b/apps/supervisor/src/index.ts index aeda154c3..701b8a25e 100644 --- a/apps/supervisor/src/index.ts +++ b/apps/supervisor/src/index.ts @@ -120,6 +120,7 @@ class ManagedSupervisor { snapshotPollIntervalSeconds: env.RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS, additionalEnvVars: env.RUNNER_ADDITIONAL_ENV_VARS, dockerAutoremove: env.DOCKER_AUTOREMOVE_EXITED_CONTAINERS, + checkpointsEnabled: !!env.TRIGGER_CHECKPOINT_URL, } satisfies WorkloadManagerOptions; this.resourceMonitor = env.RESOURCE_MONITOR_ENABLED @@ -615,6 +616,7 @@ class ManagedSupervisor { projectId: message.project.id, deploymentFriendlyId: message.deployment.friendlyId, deploymentVersion: message.backgroundWorker.version, + runtime: message.backgroundWorker.runtime, runId: message.run.id, runFriendlyId: message.run.friendlyId, version: message.version, diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts new file mode 100644 index 000000000..85ad3cbeb --- /dev/null +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { + BLOCK_IO_URING_SECCOMP_PROFILE, + withBlockIoUringSeccompProfile, +} from "./kubernetesPodSpec.js"; + +const basePodSpec = { + restartPolicy: "Never" as const, + automountServiceAccountToken: false, + securityContext: { + runAsNonRoot: true, + runAsUser: 1000, + fsGroup: 1000, + }, +}; + +describe("withBlockIoUringSeccompProfile", () => { + it("adds the Localhost io_uring profile for node-24 and above, preserving pod security defaults", () => { + for (const runtime of ["node-24", "node-26", "node-30", "experimental-node-24"]) { + const podSpec = withBlockIoUringSeccompProfile(basePodSpec, runtime); + + expect(podSpec).toMatchObject({ + ...basePodSpec, + securityContext: { + ...basePodSpec.securityContext, + seccompProfile: { + type: "Localhost", + localhostProfile: BLOCK_IO_URING_SECCOMP_PROFILE, + }, + }, + }); + } + }); + + it("leaves the pod spec unchanged for runtimes that do not create io_uring fds", () => { + for (const runtime of ["node", "node-22", "bun", undefined, null, ""]) { + expect(withBlockIoUringSeccompProfile(basePodSpec, runtime)).toEqual(basePodSpec); + } + }); +}); diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 762860104..282ac80b4 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -14,6 +14,7 @@ import { PlacementTagProcessor } from "@trigger.dev/core/v3/serverOnly"; import { env } from "../env.js"; import { type K8sApi, createK8sApi, type k8s } from "../clients/kubernetes.js"; import { getRunnerId } from "../util.js"; +import { withBlockIoUringSeccompProfile } from "./kubernetesPodSpec.js"; type ResourceQuantities = { [K in "cpu" | "memory" | "ephemeral-storage"]?: string; @@ -105,6 +106,11 @@ export class KubernetesWorkloadManager implements WorkloadManager { const runnerId = getRunnerId(opts.runFriendlyId, opts.nextAttemptNumber); try { + const basePodSpec = this.addPlacementTags(this.#defaultPodSpec, opts.placementTags); + const podSpec = this.opts.checkpointsEnabled + ? withBlockIoUringSeccompProfile(basePodSpec, opts.runtime) + : basePodSpec; + await this.k8s.core.createNamespacedPod({ namespace: this.namespace, body: { @@ -119,7 +125,7 @@ export class KubernetesWorkloadManager implements WorkloadManager { }, }, spec: { - ...this.addPlacementTags(this.#defaultPodSpec, opts.placementTags), + ...podSpec, affinity: this.#getAffinity(opts), tolerations: this.#getScheduleTolerations(this.#isScheduledRun(opts)), terminationGracePeriodSeconds: 60 * 60, diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts new file mode 100644 index 000000000..32c4410ca --- /dev/null +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -0,0 +1,33 @@ +import type { k8s } from "../clients/kubernetes.js"; + +/** + * Relative path (kubelet seccomp root) of the profile blocking only io_uring + * syscalls. Must match the profile deployed to worker nodes. + */ +export const BLOCK_IO_URING_SECCOMP_PROFILE = "profiles/block-io-uring.json"; + +/** + * Node >= 24 always creates io_uring fds, which can't be checkpointed. Blocking + * io_uring_setup makes libuv fall back to epoll. Other runtimes don't need this, + * so the profile is only applied for node-24+. Tolerates an "experimental-" prefix. + */ +export function withBlockIoUringSeccompProfile( + podSpec: Omit, + runtime: string | null | undefined +): Omit { + const match = runtime ? /^(?:experimental-)?node-(\d+)$/.exec(runtime) : null; + if (!match || Number(match[1]) < 24) { + return podSpec; + } + + return { + ...podSpec, + securityContext: { + ...podSpec.securityContext, + seccompProfile: { + type: "Localhost", + localhostProfile: BLOCK_IO_URING_SECCOMP_PROFILE, + }, + }, + }; +} diff --git a/apps/supervisor/src/workloadManager/types.ts b/apps/supervisor/src/workloadManager/types.ts index ee759cb8a..4945b1382 100644 --- a/apps/supervisor/src/workloadManager/types.ts +++ b/apps/supervisor/src/workloadManager/types.ts @@ -16,6 +16,8 @@ export interface WorkloadManagerOptions { snapshotPollIntervalSeconds?: number; additionalEnvVars?: Record; dockerAutoremove?: boolean; + // Whether CRIU checkpoint/restore is enabled for this deployment + checkpointsEnabled?: boolean; } export interface WorkloadManager { @@ -40,6 +42,8 @@ export interface WorkloadManagerCreateOptions { projectId: string; deploymentFriendlyId: string; deploymentVersion: string; + // Canonical runtime identifier (e.g. "node", "node-22", "node-24") + runtime?: string; runId: string; runFriendlyId: string; snapshotId: string; diff --git a/apps/webapp/CLAUDE.md b/apps/webapp/CLAUDE.md index bf30ee756..b65482fb7 100644 --- a/apps/webapp/CLAUDE.md +++ b/apps/webapp/CLAUDE.md @@ -115,6 +115,16 @@ The `triggerTask.server.ts` service is the **highest-throughput code path** in t - **Always use `findFirst` instead of `findUnique`.** Prisma's `findUnique` has an implicit DataLoader that batches concurrent calls into a single `IN` query. This batching cannot be disabled and has active bugs even in Prisma 6.x: uppercase UUIDs returning null (#25484, confirmed 6.4.1), composite key SQL correctness issues (#22202), and 5-10x worse performance than manual DataLoader (#6573, open since 2021). `findFirst` is never batched and avoids this entire class of issues. +## Transactions + +- **Always use the `$transaction` helper from `~/db.server`, never `prisma.$transaction` (or `$replica.$transaction`) directly.** The helper wraps the raw call with tracing (an OTEL span + an `isolation_level` attribute) and boundary logging for infrastructure errors (e.g. `PrismaClientInitializationError`) that the raw client swallows. Signature: `$transaction(prisma, name?, async (tx) => { ... }, options?)`. +- Pass the isolation level via options as a string: `{ isolationLevel: "Serializable" }`. Reach for `Serializable` when a read-then-write must be atomic against concurrent transactions (e.g. a count-then-delete invariant); the loser of a race fails and can retry, which is the right trade for rare, correctness-critical paths. +- The helper returns `R | undefined` — guard the result (`if (!result) throw ...`) when callers need a definite value. + +## PAT-authenticated API routes + +- **A PAT route must resolve its target org/project scoped to the caller's membership** (`members: { some: { userId } }`, or a helper like `findProjectByRef` / `resolveOrganizationForApiUser`). A PAT is user-scoped and can name any org/project by id/slug, and the OSS RBAC fallback ability is permissive — so `ability.can(...)` alone does NOT reject a non-member on self-hosted. The RBAC `authorization` gate enforces the *role*; the membership-scoped query is the *tenant* floor. Skipping it opens cross-org access on OSS. + ## React Patterns - Only use `useCallback`/`useMemo` for context provider values, expensive derived data that is a dependency elsewhere, or stable refs required by a dependency array. Don't wrap ordinary event handlers or trivial computations. diff --git a/apps/webapp/app/components/StaleAssetRecovery.test.ts b/apps/webapp/app/components/StaleAssetRecovery.test.ts new file mode 100644 index 000000000..0586189e1 --- /dev/null +++ b/apps/webapp/app/components/StaleAssetRecovery.test.ts @@ -0,0 +1,56 @@ +// @vitest-environment jsdom +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { staleAssetRecoveryScript } from "./StaleAssetRecovery"; + +// Each staleAssetRecoveryScript() call models a fresh page load: it reads the shared +// sessionStorage budget and returns its own `recover`. We drive recover() directly rather +// than dispatching resource-error events, so accumulated window listeners never fire. +describe("staleAssetRecoveryScript", () => { + let reload: ReturnType; + + beforeEach(() => { + sessionStorage.clear(); + reload = vi.fn(); + vi.stubGlobal("location", { reload }); + vi.stubGlobal("navigator", { onLine: true }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it("reloads on a recovery", () => { + staleAssetRecoveryScript().recover(); + expect(reload).toHaveBeenCalledTimes(1); + }); + + it("reloads only once per page even if several assets fail (re-entrancy guard)", () => { + const { recover } = staleAssetRecoveryScript(); + recover(); + recover(); + recover(); + expect(reload).toHaveBeenCalledTimes(1); + }); + + it("stops reloading once the budget is spent across reloads", () => { + staleAssetRecoveryScript().recover(); // reload 1 + staleAssetRecoveryScript().recover(); // reload 2 + staleAssetRecoveryScript().recover(); // budget spent -> no reload + expect(reload).toHaveBeenCalledTimes(2); + }); + + it("does not reload when offline", () => { + vi.stubGlobal("navigator", { onLine: false }); + staleAssetRecoveryScript().recover(); + expect(reload).not.toHaveBeenCalled(); + }); + + it("does not reload when sessionStorage is unavailable", () => { + vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { + throw new Error("blocked"); + }); + staleAssetRecoveryScript().recover(); + expect(reload).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/webapp/app/components/StaleAssetRecovery.tsx b/apps/webapp/app/components/StaleAssetRecovery.tsx new file mode 100644 index 000000000..7827d9ac8 --- /dev/null +++ b/apps/webapp/app/components/StaleAssetRecovery.tsx @@ -0,0 +1,94 @@ +// Recovers from a rolling deploy rotating the content-hashed /build assets out from +// under a page. Each image serves only its own build and hard-404s unknown hashes, so +// a client can request a hash the serving replica doesn't have and get missing styles +// or a failed asset load. On such a /build load failure we do a bounded full document +// reload: the fresh document (and, under sticky routing, all of its assets) lands on a +// single live build, so the asset resolves. Bounded via sessionStorage so it can never +// loop; when the budget is spent it stops rather than reloading forever. +// +// Deliberately minimal — no fetch interception, no build-version polling, no server +// build-id contract, no form snapshot, no blocking overlay. + +// The recovery logic runs as an inline