Skip to content

Commit 3cb2ff0

Browse files
committed
feat(runtime): add experimental Node.js 24 and 26 runtimes
1 parent caf4b63 commit 3cb2ff0

14 files changed

Lines changed: 283 additions & 39 deletions

File tree

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 experimental Node.js 24 and 26 task runtimes. Set `runtime` to `experimental-node-24` or `experimental-node-26` in `trigger.config.ts`; the forward-compatible `node-24` and `node-26` keys are also accepted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "vitest";
2+
import { withRuntimeDefaultSeccompProfile } from "./kubernetesPodSpec.js";
3+
4+
describe("withRuntimeDefaultSeccompProfile", () => {
5+
it("adds RuntimeDefault seccomp while preserving pod security defaults", () => {
6+
const podSpec = withRuntimeDefaultSeccompProfile({
7+
restartPolicy: "Never",
8+
automountServiceAccountToken: false,
9+
securityContext: {
10+
runAsNonRoot: true,
11+
runAsUser: 1000,
12+
fsGroup: 1000,
13+
},
14+
});
15+
16+
expect(podSpec).toMatchObject({
17+
restartPolicy: "Never",
18+
automountServiceAccountToken: false,
19+
securityContext: {
20+
runAsNonRoot: true,
21+
runAsUser: 1000,
22+
fsGroup: 1000,
23+
seccompProfile: {
24+
type: "RuntimeDefault",
25+
},
26+
},
27+
});
28+
});
29+
});

apps/supervisor/src/workloadManager/kubernetes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { PlacementTagProcessor } from "@trigger.dev/core/v3/serverOnly";
1414
import { env } from "../env.js";
1515
import { type K8sApi, createK8sApi, type k8s } from "../clients/kubernetes.js";
1616
import { getRunnerId } from "../util.js";
17+
import { withRuntimeDefaultSeccompProfile } from "./kubernetesPodSpec.js";
1718

1819
type ResourceQuantities = {
1920
[K in "cpu" | "memory" | "ephemeral-storage"]?: string;
@@ -309,7 +310,7 @@ export class KubernetesWorkloadManager implements WorkloadManager {
309310
}
310311

311312
get #defaultPodSpec(): Omit<k8s.V1PodSpec, "containers"> {
312-
return {
313+
return withRuntimeDefaultSeccompProfile({
313314
restartPolicy: "Never",
314315
automountServiceAccountToken: false,
315316
imagePullSecrets: this.getImagePullSecrets(),
@@ -332,7 +333,7 @@ export class KubernetesWorkloadManager implements WorkloadManager {
332333
},
333334
}
334335
: {}),
335-
};
336+
});
336337
}
337338

338339
get #defaultResourceRequests(): ResourceQuantities {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { k8s } from "../clients/kubernetes.js";
2+
3+
export function withRuntimeDefaultSeccompProfile(
4+
podSpec: Omit<k8s.V1PodSpec, "containers">
5+
): Omit<k8s.V1PodSpec, "containers"> {
6+
return {
7+
...podSpec,
8+
securityContext: {
9+
...podSpec.securityContext,
10+
seccompProfile: {
11+
type: "RuntimeDefault",
12+
},
13+
},
14+
};
15+
}

packages/cli-v3/src/commands/init.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Examples:
9494
)
9595
.option(
9696
"-r, --runtime <runtime>",
97-
"Which runtime to use for the project. Supported: node, node-22, node-24, node-26, bun",
97+
"Which runtime to use for the project. Supported: node, node-22, bun",
9898
"node"
9999
)
100100
.option("--skip-package-install", "Skip installing the @trigger.dev/sdk package")

packages/cli-v3/src/config.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, describe, expect, it } from "vitest";
5+
import { loadConfig } from "./config.js";
6+
7+
const projectDirs: string[] = [];
8+
9+
afterEach(async () => {
10+
await Promise.all(projectDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
11+
});
12+
13+
async function createProject(runtime?: string) {
14+
const cwd = await mkdtemp(join(tmpdir(), "trigger-runtime-config-"));
15+
projectDirs.push(cwd);
16+
17+
await mkdir(join(cwd, "trigger"));
18+
await writeFile(join(cwd, "package.json"), JSON.stringify({ name: "runtime-config-test" }));
19+
await writeFile(join(cwd, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
20+
await writeFile(
21+
join(cwd, "trigger.config.ts"),
22+
`export default {
23+
project: "proj_runtime_config_test",
24+
maxDuration: 60,
25+
dirs: ["./trigger"],
26+
${runtime === undefined ? "" : `runtime: ${JSON.stringify(runtime)},`}
27+
};
28+
`
29+
);
30+
31+
return cwd;
32+
}
33+
34+
describe("loadConfig runtime", () => {
35+
it.each([
36+
["experimental-node-24", "node-24"],
37+
["experimental-node-26", "node-26"],
38+
["node-24", "node-24"],
39+
["node-26", "node-26"],
40+
] as const)("normalizes %s before returning the resolved config", async (runtime, expected) => {
41+
const cwd = await createProject(runtime);
42+
43+
await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ runtime: expected });
44+
});
45+
46+
it("keeps node as the default", async () => {
47+
const cwd = await createProject();
48+
49+
await expect(loadConfig({ cwd, warn: false })).resolves.toMatchObject({ runtime: "node" });
50+
});
51+
52+
it("rejects unsupported runtimes while loading config", async () => {
53+
const cwd = await createProject("node-23");
54+
55+
await expect(loadConfig({ cwd, warn: false })).rejects.toThrowError(
56+
/Unsupported runtime "node-23" in trigger\.config/
57+
);
58+
});
59+
});

packages/cli-v3/src/config.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import type {
66
TriggerConfig,
77
} from "@trigger.dev/core/v3";
88
import type { ResolvedConfig } from "@trigger.dev/core/v3/build";
9-
import { DEFAULT_RUNTIME } from "@trigger.dev/core/v3/build";
9+
import {
10+
DEFAULT_RUNTIME,
11+
isExperimentalConfigRuntime,
12+
resolveBuildRuntime,
13+
} from "@trigger.dev/core/v3/build";
1014
import * as c12 from "c12";
1115
import { defu } from "defu";
1216
import type * as esbuild from "esbuild";
@@ -170,6 +174,24 @@ async function resolveConfig(
170174
);
171175
}
172176

177+
const config =
178+
"config" in result.config ? (result.config.config as TriggerConfig) : result.config;
179+
180+
const features = featuresFromCompatibilityFlags(
181+
["run_engine_v2" as const].concat(config.compatibilityFlags ?? [])
182+
);
183+
const defaultRuntime: BuildRuntime = features.run_engine_v2 ? "node" : DEFAULT_RUNTIME;
184+
const configuredRuntime = overrides?.runtime ?? config.runtime ?? defaultRuntime;
185+
const runtime = resolveBuildRuntime(configuredRuntime);
186+
187+
if (warn && isExperimentalConfigRuntime(configuredRuntime)) {
188+
prettyWarning(
189+
`The "${configuredRuntime}" runtime is experimental and may change before general availability.`
190+
);
191+
}
192+
193+
validateConfig(config, warn);
194+
173195
const packageJsonPath = await resolvePackageJSON(cwd);
174196
const tsconfigPath = await safeResolveTsConfig(cwd);
175197
const lockfilePath = await resolveLockfile(cwd);
@@ -181,24 +203,14 @@ async function resolveConfig(
181203
? dirname(packageJsonPath)
182204
: cwd;
183205

184-
const config =
185-
"config" in result.config ? (result.config.config as TriggerConfig) : result.config;
186-
187-
validateConfig(config, warn);
188-
189206
let dirs = config.dirs ? config.dirs : await autoDetectDirs(workingDir);
190207

191208
dirs = dirs.map((dir) => resolveTriggerDir(dir, workingDir));
192209

193-
const features = featuresFromCompatibilityFlags(
194-
["run_engine_v2" as const].concat(config.compatibilityFlags ?? [])
195-
);
196-
197-
const defaultRuntime: BuildRuntime = features.run_engine_v2 ? "node" : DEFAULT_RUNTIME;
198-
199210
const mergedConfig = defu(
200211
{
201212
workingDir,
213+
runtime,
202214
configFile: result.configFile,
203215
packageJsonPath,
204216
tsconfigPath,
@@ -230,7 +242,7 @@ async function resolveConfig(
230242
...mergedConfig,
231243
dirs: Array.from(new Set(dirs)),
232244
instrumentedPackageNames: getInstrumentedPackageNames(mergedConfig),
233-
runtime: mergedConfig.runtime,
245+
runtime,
234246
};
235247
}
236248

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { BuildRuntime } from "@trigger.dev/core/v3/schemas";
2+
import { describe, expect, it } from "vitest";
3+
import { generateContainerfile } from "./buildImage.js";
4+
5+
const nodeImages: Array<[BuildRuntime, string]> = [
6+
[
7+
"node-24",
8+
"node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d",
9+
],
10+
[
11+
"node-26",
12+
"node:26.4.0-bookworm-slim@sha256:ec82d089a8ae2cf02628da7b34ea57dc357b24db724d557fe2d240e6beb659c1",
13+
],
14+
];
15+
16+
describe("generateContainerfile", () => {
17+
it.each(nodeImages)("selects the pinned multiplatform image for %s", async (runtime, image) => {
18+
const containerfile = await generateContainerfile({
19+
runtime,
20+
build: {},
21+
image: undefined,
22+
indexScript: "index.js",
23+
entrypoint: "entrypoint.js",
24+
});
25+
26+
expect(containerfile).toContain(`FROM ${image} AS base`);
27+
});
28+
});

packages/cli-v3/src/deploy/buildImage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,8 +692,10 @@ const BASE_IMAGE: Record<BuildRuntime, string> = {
692692
node: "node:21.7.3-bookworm-slim@sha256:dfc05dee209a1d7adf2ef189bd97396daad4e97c6eaa85778d6f75205ba1b0fb",
693693
"node-22":
694694
"node:22.16.0-bookworm-slim@sha256:048ed02c5fd52e86fda6fbd2f6a76cf0d4492fd6c6fee9e2c463ed5108da0e34",
695-
"node-24": "node:24.18.0-bookworm-slim",
696-
"node-26": "node:26.4.0-bookworm-slim",
695+
"node-24":
696+
"node:24.18.0-bookworm-slim@sha256:6f7b03f7c2c8e2e784dcf9295400527b9b1270fd37b7e9a7285cf83b6951452d",
697+
"node-26":
698+
"node:26.4.0-bookworm-slim@sha256:ec82d089a8ae2cf02628da7b34ea57dc357b24db724d557fe2d240e6beb659c1",
697699
};
698700

699701
const DEFAULT_PACKAGES = ["busybox", "ca-certificates", "dumb-init", "git", "openssl"];

packages/core/src/v3/build/resolvedConfig.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
import { type Defu } from "defu";
22
import type { Prettify } from "ts-essentials";
3-
import { CompatibilityFlag, CompatibilityFlagFeatures, TriggerConfig } from "../config.js";
4-
import { BuildRuntime } from "../schemas/build.js";
5-
import { ResolveEnvironmentVariablesFunction } from "../types/index.js";
3+
import type { CompatibilityFlag, CompatibilityFlagFeatures, TriggerConfig } from "../config.js";
4+
import type { BuildRuntime } from "../schemas/build.js";
5+
import type { ResolveEnvironmentVariablesFunction } from "../types/index.js";
66

77
export type ResolvedConfig = Prettify<
8-
Defu<
9-
TriggerConfig,
10-
[
11-
{},
12-
{
13-
runtime: BuildRuntime;
14-
dirs: string[];
15-
tsconfig: string;
16-
build: {
17-
jsx: { factory: string; fragment: string; automatic: true };
18-
} & Omit<NonNullable<TriggerConfig["build"]>, "jsx">;
19-
compatibilityFlags: CompatibilityFlag[];
20-
features: CompatibilityFlagFeatures;
21-
},
22-
]
8+
Omit<
9+
Defu<
10+
TriggerConfig,
11+
[
12+
{},
13+
{
14+
runtime: BuildRuntime;
15+
dirs: string[];
16+
tsconfig: string;
17+
build: {
18+
jsx: { factory: string; fragment: string; automatic: true };
19+
} & Omit<NonNullable<TriggerConfig["build"]>, "jsx">;
20+
compatibilityFlags: CompatibilityFlag[];
21+
features: CompatibilityFlagFeatures;
22+
},
23+
]
24+
>,
25+
"runtime"
2326
> & {
27+
runtime: BuildRuntime;
2428
workingDir: string;
2529
workspaceDir: string;
2630
packageJsonPath: string;

0 commit comments

Comments
 (0)