Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/node-24-project-default.md
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions apps/webapp/app/models/project.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/models/runtimeEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/routes/api.v1.projects.$projectRef.$env.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/routes/api.v1.projects.$projectRef.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/v3/services/initializeDeployment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
carderne marked this conversation as resolved.
Comment thread
carderne marked this conversation as resolved.
triggeredVia: payload.triggeredVia ?? undefined,
startedAt: initialStatus === "BUILDING" ? new Date() : undefined,
};
Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/test/projectEnvironmentCredentialRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const environment = {
project: {
id: "proj_123",
name: "Example project",
defaultRuntime: "node-24",
},
};

Expand Down Expand Up @@ -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();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "defaultRuntime" TEXT;
27 changes: 15 additions & 12 deletions internal-packages/database/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -790,26 +793,26 @@ 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"

/// 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])
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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?
Expand Down
6 changes: 5 additions & 1 deletion packages/cli-v3/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions packages/cli-v3/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Comment thread
carderne marked this conversation as resolved.
pkgArgs: z.string().optional(),
gitRef: z.string().default("main"),
javascript: z.boolean().default(false),
Expand Down Expand Up @@ -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")
Expand Down
20 changes: 19 additions & 1 deletion packages/cli-v3/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down
27 changes: 19 additions & 8 deletions packages/cli-v3/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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>;
};
Expand Down Expand Up @@ -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.
Expand All @@ -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)) {
Expand Down Expand Up @@ -224,7 +228,7 @@ async function resolveConfig(
config,
{
dirs,
runtime: defaultRuntime,
runtime: legacyDefaultRuntime,
tsconfig: tsconfigPath,
build: {
jsx: {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/cli-v3/src/utilities/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export async function getProjectClient(options: GetEnvOptions) {
return {
id: projectEnv.data.projectId,
name: projectEnv.data.name,
defaultRuntime: projectEnv.data.defaultRuntime,
client,
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/v3/auth/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/v3/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")]);

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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>;
Expand Down