diff --git a/.changeset/node-24-project-default.md b/.changeset/node-24-project-default.md new file mode 100644 index 00000000000..551a7d5a23f --- /dev/null +++ b/.changeset/node-24-project-default.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +New projects created with `trigger init` use Node.js 24 by default. Deployments without explicit `runtime` now use their project's configured default runtime. diff --git a/apps/webapp/app/models/project.server.ts b/apps/webapp/app/models/project.server.ts index 2ed317fe879..f309ae3212d 100644 --- a/apps/webapp/app/models/project.server.ts +++ b/apps/webapp/app/models/project.server.ts @@ -113,6 +113,7 @@ export async function createProject( // for historical rows; the V1->V2 upgrade guards on worker-register / deploy // stay in place to migrate existing legacy projects. engine: "V2", + defaultRuntime: "node-24", onboardingData, }, include: { diff --git a/apps/webapp/app/models/runtimeEnvironment.server.ts b/apps/webapp/app/models/runtimeEnvironment.server.ts index 8dc5a68b63f..e7cf10f3e02 100644 --- a/apps/webapp/app/models/runtimeEnvironment.server.ts +++ b/apps/webapp/app/models/runtimeEnvironment.server.ts @@ -6,6 +6,7 @@ import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver. import { logger } from "~/services/logger.server"; import { getUsername } from "~/utils/username"; import { hashApiKey } from "~/utils/apiKeys"; +import { BuildRuntime } from "@trigger.dev/core/v3"; import { isAdditionalApiKey } from "@trigger.dev/core/v3/apiKeys"; import { isDefaultDevBranch, sanitizeBranchName } from "@trigger.dev/core/v3/utils/gitBranch"; import { scopesGrantFullAccess } from "@trigger.dev/rbac"; @@ -77,6 +78,7 @@ export function toAuthenticated( defaultWorkerGroupId: env.project.defaultWorkerGroupId, organizationId: env.project.organizationId, builderProjectId: env.project.builderProjectId, + defaultRuntime: BuildRuntime.nullable().safeParse(env.project.defaultRuntime).data ?? null, }, organization: { id: env.organization.id, diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts index c7090ce4721..8083ba1a0a4 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts @@ -1,5 +1,5 @@ import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { type GetProjectEnvResponse } from "@trigger.dev/core/v3"; +import { BuildRuntime, type GetProjectEnvResponse } from "@trigger.dev/core/v3"; import { z } from "zod"; import { env as processEnv } from "~/env.server"; import { @@ -65,6 +65,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { name: environment.project.name, apiUrl: processEnv.API_ORIGIN ?? processEnv.APP_ORIGIN, projectId: environment.project.id, + defaultRuntime: + BuildRuntime.nullable().safeParse(environment.project.defaultRuntime ?? null).data ?? null, }; return json(result); diff --git a/apps/webapp/app/routes/api.v1.projects.$projectRef.ts b/apps/webapp/app/routes/api.v1.projects.$projectRef.ts index c1fa0acc917..824ab1199a8 100644 --- a/apps/webapp/app/routes/api.v1.projects.$projectRef.ts +++ b/apps/webapp/app/routes/api.v1.projects.$projectRef.ts @@ -1,5 +1,5 @@ import { json } from "@remix-run/server-runtime"; -import type { GetProjectResponseBody } from "@trigger.dev/core/v3"; +import { BuildRuntime, type GetProjectResponseBody } from "@trigger.dev/core/v3"; import { z } from "zod"; import { prisma } from "~/db.server"; import { DeleteProjectService } from "~/services/deleteProject.server"; @@ -53,6 +53,8 @@ export const loader = createLoaderPATApiRoute( slug: project.slug, createdAt: project.createdAt, defaultRegion: project.defaultWorkerGroup?.name ?? null, + defaultRuntime: + BuildRuntime.nullable().safeParse(project.defaultRuntime ?? null).data ?? null, organization: { id: project.organization.id, title: project.organization.title, diff --git a/apps/webapp/app/v3/services/initializeDeployment.server.ts b/apps/webapp/app/v3/services/initializeDeployment.server.ts index abb99082dd6..fb8ffd259e4 100644 --- a/apps/webapp/app/v3/services/initializeDeployment.server.ts +++ b/apps/webapp/app/v3/services/initializeDeployment.server.ts @@ -253,7 +253,7 @@ export class InitializeDeploymentService extends BaseService { imagePlatform: env.DEPLOY_IMAGE_PLATFORM, git: payload.gitMeta ?? undefined, commitSHA: payload.gitMeta?.commitSha ?? undefined, - runtime: payload.runtime ?? undefined, + runtime: payload.runtime ?? environment.project.defaultRuntime ?? undefined, triggeredVia: payload.triggeredVia ?? undefined, startedAt: initialStatus === "BUILDING" ? new Date() : undefined, }; diff --git a/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts b/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts index 9a24883a85a..e50363c71dc 100644 --- a/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts +++ b/apps/webapp/test/projectEnvironmentCredentialRoute.test.ts @@ -37,6 +37,7 @@ const environment = { project: { id: "proj_123", name: "Example project", + defaultRuntime: "node-24", }, }; @@ -82,6 +83,7 @@ describe("project environment credential response", () => { await expect(responseJson(response)).resolves.toMatchObject({ apiKey: "tr_prod_sk_presented", projectId: "proj_123", + defaultRuntime: "node-24", }); expect(mocks.authorizePatEnvironmentAccess).not.toHaveBeenCalled(); }); diff --git a/internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql b/internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql new file mode 100644 index 00000000000..899dd5921a4 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260817111521_add_project_default_runtime/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "defaultRuntime" TEXT; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 44392406aae..9c45d7fa7c0 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -474,6 +474,9 @@ model Project { /// Set the first time the CLI `init` command completes against this project. Drives the dev onboarding progress. initializedAt DateTime? + /// Runtime used when a deployment config does not specify one. Null preserves the legacy Node 20 fallback. + defaultRuntime String? + version ProjectVersion @default(V2) engine RunEngineVersion @default(V1) @@ -790,12 +793,12 @@ model WebhookEndpoint { source String // provider tag e.g. "stripe","slack","github" handlerWebhookId String // declared webhook() id (string ref, GOLDEN LAW, no relation) - routingTarget Json // RoutingTarget tagged union ({ type: "task" } | { type: "session" }) - verifierArtifact Json // VerifierArtifact tagged union (config|preset in P1) + routingTarget Json // RoutingTarget tagged union ({ type: "task" } | { type: "session" }) + verifierArtifact Json // VerifierArtifact tagged union (config|preset in P1) filter String? // source filter DSL string (display/round-trip) - filterAst Json? // compiled FilterAst, evaluated at ingest; null = route all - filterAstVersion Int? // re-parse `filter` on a format bump - metadata Json @default("{}") // arbitrary user metadata; flows into the webhook task + filterAst Json? // compiled FilterAst, evaluated at ingest; null = route all + filterAstVersion Int? // re-parse `filter` on a format bump + metadata Json @default("{}") // arbitrary user metadata; flows into the webhook task // who supplies the secret/key; drives the Connect UI (paste vs generate). From the source. secretProvisioning String @default("either") // "provider" | "integrator" | "either" @@ -803,13 +806,13 @@ model WebhookEndpoint { /// SecretReference.key string. Plain String, NO @relation -> no FK to SecretReference. signingSecretKey String? - status WebhookEndpointStatus @default(ACTIVE) + status WebhookEndpointStatus @default(ACTIVE) /// When an operator disabled the endpoint via the dashboard/API. Null means the declarative sync /// owns the status: a redeploy that re-declares a previously-removed (auto-deactivated) webhook /// reactivates it. Non-null means the operator disabled it, so the sync leaves the status alone. manuallyDeactivatedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@unique([runtimeEnvironmentId, handlerWebhookId, endpointTenantId, endpointExternalRef]) // deploy-sync key @@index([runtimeEnvironmentId, source]) @@ -842,8 +845,8 @@ model WebhookDelivery { /// Set from the x-trigger-test ingress header; marks console/test-send deliveries so the list can filter them. isTest Boolean @default(false) - parsedEvent Json? // size-capped snapshot of the verified event (full event lives in ClickHouse) - headers Json? // inbound request headers, surfaced to the webhook task via onEvent({ headers }) + parsedEvent Json? // size-capped snapshot of the verified event (full event lives in ClickHouse) + headers Json? // inbound request headers, surfaced to the webhook task via onEvent({ headers }) rawBodyHash String? // sha256 of raw bytes; cheap P2 replay anchor errorMessage String? filterReason String? // why a FILTERED delivery was not routed (failing clause + actual value) @@ -2370,8 +2373,8 @@ model TaskSchedule { timezone String @default("UTC") // Cron spread - windowDurationSeconds Int? - windowPercentage Int? + windowDurationSeconds Int? + windowPercentage Int? ///Can be provided by the user then accessed inside a run externalId String? diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index f82942d4e43..41c48330836 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -300,7 +300,7 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { logger.debug("Using project ref from env", { ref: envVars.TRIGGER_PROJECT_REF }); } - const resolvedConfig = await loadConfig({ + let resolvedConfig = await loadConfig({ cwd: projectPath, overrides: { project: options.projectRef ?? envVars.TRIGGER_PROJECT_REF }, configFile: options.config, @@ -364,6 +364,10 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) { throw new Error("Failed to get project client"); } + if (!resolvedConfig.runtimeWasExplicit && projectClient.defaultRuntime) { + resolvedConfig.runtime = projectClient.defaultRuntime; + } + if (options.nativeBuildServer) { await handleNativeBuildServerDeploy({ apiClient: projectClient.client, diff --git a/packages/cli-v3/src/commands/init.ts b/packages/cli-v3/src/commands/init.ts index a6ae5af60cb..46aeee5f5e0 100644 --- a/packages/cli-v3/src/commands/init.ts +++ b/packages/cli-v3/src/commands/init.ts @@ -50,7 +50,7 @@ const InitCommandOptions = CommonCommandOptions.extend({ overrideConfig: z.boolean().default(false), tag: z.string().default(cliVersion), skipPackageInstall: z.boolean().default(false), - runtime: z.string().default("node"), + runtime: z.string().default("node-24"), pkgArgs: z.string().optional(), gitRef: z.string().default("main"), javascript: z.boolean().default(false), @@ -94,8 +94,8 @@ Examples: ) .option( "-r, --runtime ", - "Which runtime to use for the project. Supported: node, node-22, bun", - "node" + "Which runtime to use for the project. Supported: node, node-22, node-24, node-26, bun", + "node-24" ) .option("--skip-package-install", "Skip installing the @trigger.dev/sdk package") .option("--override-config", "Override the existing config file if it exists") diff --git a/packages/cli-v3/src/config.test.ts b/packages/cli-v3/src/config.test.ts index cafee4b9a3d..6a77348f876 100644 --- a/packages/cli-v3/src/config.test.ts +++ b/packages/cli-v3/src/config.test.ts @@ -45,7 +45,25 @@ describe("loadConfig runtime", () => { await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ runtime: expected }); }); - it("keeps node as the default", async () => { + it("tracks whether runtime was explicitly configured", async () => { + const cwd = await createProject("node-22"); + + await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ + runtime: "node-22", + runtimeWasExplicit: true, + }); + }); + + it("tracks an omitted runtime separately from the legacy default", async () => { + const cwd = await createProject(); + + await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ + runtime: "node", + runtimeWasExplicit: false, + }); + }); + + it("keeps node as the legacy default when runtime is omitted", async () => { const cwd = await createProject(); await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ runtime: "node" }); diff --git a/packages/cli-v3/src/config.ts b/packages/cli-v3/src/config.ts index 2e7ffdee509..9f3c13413bf 100644 --- a/packages/cli-v3/src/config.ts +++ b/packages/cli-v3/src/config.ts @@ -37,12 +37,16 @@ export type ResolveConfigOptions = { warn?: boolean; }; +export type LoadedConfig = ResolvedConfig & { + runtimeWasExplicit: boolean; +}; + export async function loadConfig({ cwd = process.cwd(), overrides, configFile, warn = true, -}: ResolveConfigOptions = {}): Promise { +}: ResolveConfigOptions = {}): Promise { const result = await c12.loadConfig({ name: "trigger", cwd, @@ -54,13 +58,13 @@ export async function loadConfig({ } type ResolveWatchConfigOptions = ResolveConfigOptions & { - onUpdate: (config: ResolvedConfig) => void; + onUpdate: (config: LoadedConfig) => void; debounce?: number; ignoreInitial?: boolean; }; type ResolveWatchConfigResult = { - config: ResolvedConfig; + config: LoadedConfig; files: string[]; stop: () => Promise; }; @@ -157,7 +161,7 @@ async function resolveConfig( result: c12.ResolvedConfig, overrides?: Partial, warn = true -): Promise { +): Promise { // `trigger.config` is the fallback value set by c12. Bail out with actionable guidance before // touching the filesystem: the pkg-types resolvers below throw raw errors when run outside a // project (e.g. `dev` before `init`), which would mask this message. @@ -181,8 +185,8 @@ async function resolveConfig( const features = featuresFromCompatibilityFlags( ["run_engine_v2" as const].concat(config.compatibilityFlags ?? []) ); - const defaultRuntime: BuildRuntime = features.run_engine_v2 ? "node" : DEFAULT_RUNTIME; - const configuredRuntime = overrides?.runtime ?? config.runtime ?? defaultRuntime; + const legacyDefaultRuntime: BuildRuntime = features.run_engine_v2 ? "node" : DEFAULT_RUNTIME; + const configuredRuntime = overrides?.runtime ?? config.runtime ?? legacyDefaultRuntime; const runtime = resolveBuildRuntime(configuredRuntime); if (warn && isDeprecatedConfigRuntime(configuredRuntime)) { @@ -224,7 +228,7 @@ async function resolveConfig( config, { dirs, - runtime: defaultRuntime, + runtime: legacyDefaultRuntime, tsconfig: tsconfigPath, build: { jsx: { @@ -241,12 +245,19 @@ async function resolveConfig( } ) as ResolvedConfig; // TODO: For some reason, without this, there is a weird type error complaining about tsconfigPath being string | nullish, which can't be assigned to string | undefined - return { + const resolvedConfig = { ...mergedConfig, dirs: Array.from(new Set(dirs)), instrumentedPackageNames: getInstrumentedPackageNames(mergedConfig), runtime, }; + + Object.defineProperty(resolvedConfig, "runtimeWasExplicit", { + value: overrides?.runtime !== undefined || config.runtime !== undefined, + enumerable: false, + }); + + return resolvedConfig as LoadedConfig; } function resolveTriggerDir(dir: string, workingDir: string): string { diff --git a/packages/cli-v3/src/utilities/session.ts b/packages/cli-v3/src/utilities/session.ts index b7583f83c9f..2500cf1f368 100644 --- a/packages/cli-v3/src/utilities/session.ts +++ b/packages/cli-v3/src/utilities/session.ts @@ -112,6 +112,7 @@ export async function getProjectClient(options: GetEnvOptions) { return { id: projectEnv.data.projectId, name: projectEnv.data.name, + defaultRuntime: projectEnv.data.defaultRuntime, client, }; } diff --git a/packages/core/src/v3/auth/environment.ts b/packages/core/src/v3/auth/environment.ts index 498393722fe..f61a23be28d 100644 --- a/packages/core/src/v3/auth/environment.ts +++ b/packages/core/src/v3/auth/environment.ts @@ -67,6 +67,7 @@ export type AuthenticatedEnvironment = { // Build-server bookkeeping. Read by remote-image-builder when // creating Depot builds. builderProjectId: string | null; + defaultRuntime?: string | null; }; organization: { diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 6cd100f7c3c..12e991cfef9 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -11,6 +11,7 @@ import { BackgroundWorkerMetadata } from "./resources.js"; import { DequeuedMessage, MachineResources } from "./runEngine.js"; import { QueueTypeName } from "./queues.js"; import { ScheduleWindow } from "./schemas.js"; +import { BuildRuntime } from "./build.js"; export const RunEngineVersion = z.union([z.literal("V1"), z.literal("V2")]); @@ -43,6 +44,7 @@ export const GetProjectResponseBody = z.object({ // (the project falls back to the global platform default). Optional so a // newer client still parses responses from an older server that omits it. defaultRegion: z.string().nullable().optional(), + defaultRuntime: BuildRuntime.nullable().optional(), organization: z.object({ id: z.string(), title: z.string(), @@ -98,6 +100,7 @@ export const GetProjectEnvResponse = z.object({ name: z.string(), apiUrl: z.string(), projectId: z.string(), + defaultRuntime: BuildRuntime.nullable().optional(), }); export type GetProjectEnvResponse = z.infer;