feat(cli,webapp): default new projects to node-24 (#4649)

This commit is contained in:
Chris Arderne
2026-08-18 07:23:52 +01:00
committed by GitHub
parent 7d9f1a3268
commit 99f0787148
16 changed files with 85 additions and 28 deletions
+5
View File
@@ -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.
+1
View File
@@ -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: {
@@ -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,
@@ -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);
@@ -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,
@@ -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,
};
@@ -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();
});
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "defaultRuntime" TEXT;
+15 -12
View File
@@ -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?
+5 -1
View File
@@ -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,
+3 -3
View File
@@ -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 <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")
+19 -1
View File
@@ -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" });
+19 -8
View File
@@ -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<ResolvedConfig> {
}: ResolveConfigOptions = {}): Promise<LoadedConfig> {
const result = await c12.loadConfig<TriggerConfig>({
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<void>;
};
@@ -157,7 +161,7 @@ async function resolveConfig(
result: c12.ResolvedConfig<TriggerConfig>,
overrides?: Partial<TriggerConfig>,
warn = true
): Promise<ResolvedConfig> {
): Promise<LoadedConfig> {
// `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 {
+1
View File
@@ -112,6 +112,7 @@ export async function getProjectClient(options: GetEnvOptions) {
return {
id: projectEnv.data.projectId,
name: projectEnv.data.name,
defaultRuntime: projectEnv.data.defaultRuntime,
client,
};
}
+1
View File
@@ -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: {
+3
View File
@@ -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<typeof GetProjectEnvResponse>;