diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.projects/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.projects/route.tsx index 23a46c65a84..d534d63d8f3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.projects/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.projects/route.tsx @@ -1,4 +1,4 @@ -import { NODE_RUNTIME_UPDATE_MAJOR } from "@trigger.dev/core/v3"; +import { needsNodeRuntimeUpdate } from "@trigger.dev/core/v3"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { resolveOrgIdFromSlugForUser } from "~/models/organization.server"; import { listCurrentProductionProjectRuntimes } from "~/services/projectRuntimeUpdates.server"; @@ -54,7 +54,7 @@ export const loader = dashboardLoader( : null, }; - if (deployment?.nodeMajor === NODE_RUNTIME_UPDATE_MAJOR) { + if (deployment && needsNodeRuntimeUpdate(deployment.runtime, deployment.runtimeVersion)) { needsUpdate.push(row); } else { otherProjects.push(row); diff --git a/apps/webapp/app/services/projectRuntimeUpdates.server.ts b/apps/webapp/app/services/projectRuntimeUpdates.server.ts index 5b571b04dd1..4393fe25344 100644 --- a/apps/webapp/app/services/projectRuntimeUpdates.server.ts +++ b/apps/webapp/app/services/projectRuntimeUpdates.server.ts @@ -85,13 +85,6 @@ export async function listCurrentProductionProjectRuntimes(scope: Scope) { ); } -/** - * Whether any project in the organization runs the reported Node.js major in Production. - * - * The SQL filter mirrors `nodeMajor(runtime, runtimeVersion) === NODE_RUNTIME_UPDATE_MAJOR`, which - * the page applies in JS: keep the two in step. Scoped to the caller's membership so the side menu - * cannot report on an organization the user does not belong to. - */ export async function organizationHasProjectRuntimeUpdate({ organizationSlug, userId, @@ -115,8 +108,20 @@ export async function organizationHasProjectRuntimeUpdate({ some: { label: CURRENT_DEPLOYMENT_LABEL, deployment: { - runtime: { startsWith: "node" }, - runtimeVersion: { startsWith: `${NODE_RUNTIME_UPDATE_MAJOR}.` }, + OR: [ + { + runtimeVersion: { startsWith: `${NODE_RUNTIME_UPDATE_MAJOR}.` }, + OR: [{ runtime: null }, { runtime: { startsWith: "node" } }], + }, + { + runtimeVersion: null, + OR: [ + { runtime: null }, + { runtime: "node" }, + { runtime: `node-${NODE_RUNTIME_UPDATE_MAJOR}` }, + ], + }, + ], }, }, }, diff --git a/packages/cli-v3/src/commands/projects/list.ts b/packages/cli-v3/src/commands/projects/list.ts index 8bd4b8bb033..60f297dc1bb 100644 --- a/packages/cli-v3/src/commands/projects/list.ts +++ b/packages/cli-v3/src/commands/projects/list.ts @@ -1,5 +1,5 @@ import { intro, outro } from "@clack/prompts"; -import { NODE_RUNTIME_UPDATE_MAJOR } from "@trigger.dev/core/v3"; +import { needsNodeRuntimeUpdate, NODE_RUNTIME_UPDATE_MAJOR } from "@trigger.dev/core/v3"; import type { Command } from "commander"; import { z } from "zod"; import { CliApiClient } from "../../apiClient.js"; @@ -70,7 +70,11 @@ async function listProjects(options: ProjectsListCommandOptions) { } const projects = options.needsUpdate - ? response.data.filter((project) => project.deployment?.nodeMajor === NODE_RUNTIME_UPDATE_MAJOR) + ? response.data.filter( + (project) => + project.deployment && + needsNodeRuntimeUpdate(project.deployment.runtime, project.deployment.runtimeVersion) + ) : response.data; if (projects.length === 0) { diff --git a/packages/core/src/v3/schemas/api-type.test.ts b/packages/core/src/v3/schemas/api-type.test.ts index 51a4ac553a1..51d582f934a 100644 --- a/packages/core/src/v3/schemas/api-type.test.ts +++ b/packages/core/src/v3/schemas/api-type.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"; import { BatchItemNDJSON, InitializeDeploymentRequestBody, + needsNodeRuntimeUpdate, nodeMajor, TriggerTaskRequestBody, } from "./api.js"; @@ -21,6 +22,22 @@ describe("nodeMajor", () => { }); }); +describe("needsNodeRuntimeUpdate", () => { + it.each([ + ["node", "21.7.3", true], + [null, "21.7.3", true], + ["node", null, true], + [null, null, true], + ["node-21", null, true], + ["node-22", null, false], + ["node-22", "22.16.0", false], + ["node", "unknown", false], + ["bun", "1.3.3", false], + ])("classifies %s %s", (runtime, runtimeVersion, expected) => { + expect(needsNodeRuntimeUpdate(runtime, runtimeVersion)).toBe(expected); + }); +}); + describe("InitializeDeploymentRequestBody", () => { const base = { contentHash: "abc123" }; diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 44b9a3827b8..bf7a904aac8 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -79,6 +79,24 @@ export function nodeMajor( return match ? Number(match[1]) : undefined; } +export function needsNodeRuntimeUpdate( + runtime: string | null | undefined, + runtimeVersion: string | null | undefined +) { + if (runtime && !runtime.startsWith("node")) return false; + + const versionMatch = runtimeVersion?.match(/^(\d+)(?:\.\d+){1,2}(?:[-+].*)?$/); + if (versionMatch) return Number(versionMatch[1]) === NODE_RUNTIME_UPDATE_MAJOR; + if (runtimeVersion) return false; + + if (!runtime || runtime === "node") return true; + + const configuredMajorMatch = runtime.match(/^node-(\d+)$/); + return configuredMajorMatch + ? Number(configuredMajorMatch[1]) === NODE_RUNTIME_UPDATE_MAJOR + : false; +} + export const GetProjectRuntimesResponseBody = z.array( z.object({ organization: z.object({