feat(runtime): add experimental Node.js 24 and 26 task runtimes (#4085)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled

## Summary

Adds experimental Node.js 24 and 26 task runtimes through the
`experimental-node-24` and `experimental-node-26` config values.

Existing runtime defaults and the `node`, `node-22`, and `bun` behavior
remain unchanged. The unprefixed `node-24` and `node-26` config values
remain unavailable until the runtimes are ready for general use.

## Design

Experimental config values normalize to canonical runtime identifiers
before build manifests are created, keeping deployment metadata and
execution behavior consistent. Kubernetes task pods also use the
runtime-default seccomp profile so modern Node.js versions fall back
from io_uring to checkpoint-compatible system calls.
This commit is contained in:
Chris Arderne
2026-07-16 12:19:03 +01:00
committed by GitHub
parent 43250522a5
commit d7ec75d5ad
18 changed files with 337 additions and 39 deletions
+2
View File
@@ -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,
@@ -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);
}
});
});
@@ -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,
@@ -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<k8s.V1PodSpec, "containers">,
runtime: string | null | undefined
): Omit<k8s.V1PodSpec, "containers"> {
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,
},
},
};
}
@@ -16,6 +16,8 @@ export interface WorkloadManagerOptions {
snapshotPollIntervalSeconds?: number;
additionalEnvVars?: Record<string, string>;
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;