Skip to content

Commit 053b0b4

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
feat(cli,webapp): warn on node-21 deploy
Mono-RevId: f38e17c1e25cc4c4a6d2f38ee38cac7e2722e50b
1 parent 71598e9 commit 053b0b4

10 files changed

Lines changed: 824 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Warn when a deployment uses the deprecated Node.js 21 runtime. Deployment logs now include upgrade guidance, and the account associated with the deployment receives an email notification.

‎apps/webapp/app/v3/services/createDeploymentBackgroundWorkerV4.server.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CreateBackgroundWorkerRequestBody } from "@trigger.dev/core/v3";
2-
import { logger, tryCatch } from "@trigger.dev/core/v3";
2+
import { logger, needsNodeRuntimeUpdate, tryCatch } from "@trigger.dev/core/v3";
33
import {
44
Prisma,
55
type BackgroundWorker,
@@ -21,6 +21,8 @@ import { TimeoutDeploymentService } from "./timeoutDeployment.server";
2121
import { recordDeploymentFinished } from "./recordDeploymentFinished.server";
2222
import { env } from "~/env.server";
2323
import { webhookPrisma } from "~/db.server";
24+
import { scheduleNodeRuntimeDeprecationEmail } from "./nodeRuntimeDeprecationEmail.server";
25+
import { DeploymentService } from "./deployment.server";
2426

2527
export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
2628
private readonly _taskMetaCache: TaskMetadataCache;
@@ -289,6 +291,36 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
289291
new Date(Date.now() + env.DEPLOY_TIMEOUT_MS)
290292
);
291293

294+
if (needsNodeRuntimeUpdate(body.metadata.runtime, body.metadata.runtimeVersion)) {
295+
await new DeploymentService(this._prisma, this._replica)
296+
.appendToEventLog(environment.project, deployment, [
297+
{
298+
type: "log",
299+
data: {
300+
level: "warn",
301+
message:
302+
'This deployment uses Node.js 21, which is deprecated. Set runtime: "node-24" in trigger.config.ts and deploy again.',
303+
},
304+
},
305+
])
306+
.orTee((error) => {
307+
logger.error("Failed to append Node.js runtime deprecation warning", {
308+
error,
309+
deploymentId: deployment.id,
310+
environmentId: environment.id,
311+
projectId: environment.projectId,
312+
});
313+
});
314+
315+
await scheduleNodeRuntimeDeprecationEmail({
316+
prisma: this._prisma,
317+
deployment,
318+
environment,
319+
runtime: body.metadata.runtime,
320+
runtimeVersion: body.metadata.runtimeVersion,
321+
});
322+
}
323+
292324
return backgroundWorker;
293325
});
294326
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { GitMeta, needsNodeRuntimeUpdate } from "@trigger.dev/core/v3";
2+
import type { PrismaClientOrTransaction, WorkerDeployment } from "@trigger.dev/database";
3+
import { env } from "~/env.server";
4+
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
5+
import { logger } from "~/services/logger.server";
6+
import { rbac } from "~/services/rbac.server";
7+
import { scheduleEmail } from "~/services/scheduleEmail.server";
8+
9+
export async function scheduleNodeRuntimeDeprecationEmail({
10+
prisma,
11+
deployment,
12+
environment,
13+
runtime,
14+
runtimeVersion,
15+
}: {
16+
prisma: PrismaClientOrTransaction;
17+
deployment: WorkerDeployment;
18+
environment: AuthenticatedEnvironment;
19+
runtime: string | null | undefined;
20+
runtimeVersion: string | null | undefined;
21+
}) {
22+
if (!needsNodeRuntimeUpdate(runtime, runtimeVersion)) {
23+
return;
24+
}
25+
26+
try {
27+
const recipients = await resolveRecipients(prisma, deployment, environment);
28+
29+
if (recipients.length === 0) {
30+
logger.warn("No recipient found for Node.js runtime deprecation email", {
31+
deploymentId: deployment.id,
32+
environmentId: environment.id,
33+
projectId: environment.projectId,
34+
});
35+
return;
36+
}
37+
38+
for (const recipient of recipients) {
39+
try {
40+
await scheduleEmail({
41+
email: "node-runtime-deprecation",
42+
to: recipient,
43+
organization: environment.organization.title,
44+
project: environment.project.name,
45+
environment: environment.slug,
46+
version: deployment.version,
47+
runtimeVersion: runtimeVersion ?? "21",
48+
deploymentLink: `${env.APP_ORIGIN}/projects/v3/${environment.project.externalRef}/deployments/${deployment.shortCode}`,
49+
projectsLink: `${env.APP_ORIGIN}/orgs/${encodeURIComponent(environment.organization.slug)}/settings/projects`,
50+
});
51+
} catch (error) {
52+
logger.error("Failed to enqueue Node.js runtime deprecation email", {
53+
error,
54+
recipient,
55+
deploymentId: deployment.id,
56+
});
57+
}
58+
}
59+
} catch (error) {
60+
logger.error("Failed to schedule Node.js runtime deprecation email", {
61+
error,
62+
deploymentId: deployment.id,
63+
environmentId: environment.id,
64+
projectId: environment.projectId,
65+
});
66+
}
67+
}
68+
69+
async function resolveRecipients(
70+
prisma: PrismaClientOrTransaction,
71+
deployment: WorkerDeployment,
72+
environment: AuthenticatedEnvironment
73+
): Promise<string[]> {
74+
if (deployment.triggeredById) {
75+
const triggeredBy = await findOrganizationMemberEmail(
76+
prisma,
77+
deployment.triggeredById,
78+
environment.organizationId
79+
);
80+
81+
if (triggeredBy) {
82+
return [triggeredBy];
83+
}
84+
}
85+
86+
const git = GitMeta.safeParse(deployment.git);
87+
if (git.success && git.data.source === "trigger_github_app") {
88+
const connection = await prisma.connectedGithubRepository.findFirst({
89+
where: { projectId: environment.projectId },
90+
select: {
91+
repository: {
92+
select: {
93+
installation: {
94+
select: { installedByUserId: true },
95+
},
96+
},
97+
},
98+
},
99+
});
100+
const installedByUserId = connection?.repository.installation.installedByUserId;
101+
const installedBy = installedByUserId
102+
? await findOrganizationMemberEmail(prisma, installedByUserId, environment.organizationId)
103+
: undefined;
104+
105+
if (installedBy) {
106+
return [installedBy];
107+
}
108+
}
109+
110+
const members = await prisma.orgMember.findMany({
111+
where: { organizationId: environment.organizationId },
112+
select: {
113+
role: true,
114+
userId: true,
115+
user: { select: { email: true } },
116+
},
117+
});
118+
const roles = await rbac.getUserRoles(
119+
members.map((member) => member.userId),
120+
environment.organizationId
121+
);
122+
const adminEmails = members.flatMap((member) => {
123+
const role = roles.get(member.userId);
124+
const hasEffectiveAdminRole =
125+
role?.isSystem === true && (role.name === "Owner" || role.name === "Admin");
126+
const isFallbackAdmin = role === null && member.role === "ADMIN";
127+
128+
return hasEffectiveAdminRole || isFallbackAdmin ? [member.user.email] : [];
129+
});
130+
131+
return [...new Set(adminEmails)];
132+
}
133+
134+
async function findOrganizationMemberEmail(
135+
prisma: PrismaClientOrTransaction,
136+
userId: string,
137+
organizationId: string
138+
) {
139+
const user = await prisma.user.findFirst({
140+
where: {
141+
id: userId,
142+
orgMemberships: { some: { organizationId } },
143+
},
144+
select: { email: true },
145+
});
146+
147+
return user?.email;
148+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Body, Container, Head, Html, Link, Preview, Text } from "@react-email/components";
2+
import { z } from "zod";
3+
import { Footer } from "./components/Footer";
4+
import { Image } from "./components/Image";
5+
import { anchor, container, h1, main, paragraphLight } from "./components/styles";
6+
7+
export const NodeRuntimeDeprecationEmailSchema = z.object({
8+
email: z.literal("node-runtime-deprecation"),
9+
organization: z.string(),
10+
project: z.string(),
11+
environment: z.string(),
12+
version: z.string(),
13+
runtimeVersion: z.string(),
14+
deploymentLink: z.string().url(),
15+
projectsLink: z.string().url(),
16+
});
17+
18+
type NodeRuntimeDeprecationEmailProps = z.infer<typeof NodeRuntimeDeprecationEmailSchema>;
19+
20+
const previewDefaults: NodeRuntimeDeprecationEmailProps = {
21+
email: "node-runtime-deprecation",
22+
organization: "My Organization",
23+
project: "My Project",
24+
environment: "production",
25+
version: "20260910.1",
26+
runtimeVersion: "21.7.3",
27+
deploymentLink: "https://trigger.dev",
28+
projectsLink: "https://trigger.dev",
29+
};
30+
31+
export default function Email(props: NodeRuntimeDeprecationEmailProps) {
32+
const {
33+
organization,
34+
project,
35+
environment,
36+
version,
37+
runtimeVersion,
38+
deploymentLink,
39+
projectsLink,
40+
} = {
41+
...previewDefaults,
42+
...props,
43+
};
44+
45+
return (
46+
<Html>
47+
<Head />
48+
<Preview>{`${project} deployed using deprecated Node.js 21`}</Preview>
49+
<Body style={main}>
50+
<Container style={container}>
51+
<Text style={h1}>This deployment uses deprecated Node 21</Text>
52+
<Text style={paragraphLight}>
53+
This deployment was built using Node {runtimeVersion}. From 5 October, new deployments
54+
on Node 21 will start to fail.
55+
</Text>
56+
<Text style={paragraphLight}>
57+
Organization: <strong>{organization}</strong>
58+
<br />
59+
Environment: <strong>{environment}</strong>
60+
<br />
61+
Project: <strong>{project}</strong>
62+
<br />
63+
Version: <strong>{version}</strong>
64+
</Text>
65+
<Link href={deploymentLink} target="_blank" style={anchor}>
66+
View deployment
67+
</Link>
68+
69+
<Text style={sectionHeading}>Check all affected projects</Text>
70+
<Text style={paragraphLight}>
71+
Go to your{" "}
72+
<Link href={projectsLink} target="_blank" style={anchor}>
73+
Projects settings
74+
</Link>{" "}
75+
to see which projects need updating.
76+
</Text>
77+
<Text style={paragraphLight}>You can also use the latest version of the CLI:</Text>
78+
<Text style={code}>npx trigger.dev@latest projects list --needs-update</Text>
79+
80+
<Text style={sectionHeading}>How to upgrade</Text>
81+
<Text style={paragraphLight}>
82+
Edit <strong>trigger.config.ts</strong> in each project and set the{" "}
83+
<strong>runtime</strong> field:
84+
</Text>
85+
<Text style={code}>{'runtime: "node-24"'}</Text>
86+
<Text style={{ ...paragraphLight, marginBottom: "50px" }}>
87+
Full instructions are available in your{" "}
88+
<Link href={projectsLink} target="_blank" style={anchor}>
89+
Projects settings
90+
</Link>{" "}
91+
and in the{" "}
92+
<Link
93+
href="https://trigger.dev/docs/config/config-file#runtime"
94+
target="_blank"
95+
style={anchor}
96+
>
97+
runtime documentation
98+
</Link>
99+
.
100+
</Text>
101+
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
102+
<Footer />
103+
</Container>
104+
</Body>
105+
</Html>
106+
);
107+
}
108+
109+
const sectionHeading = {
110+
...paragraphLight,
111+
fontSize: "18px",
112+
fontWeight: "bold",
113+
marginTop: "32px",
114+
marginBottom: "8px",
115+
};
116+
117+
const code = {
118+
color: "#D7D9DD",
119+
backgroundColor: "#272A2E",
120+
borderRadius: "4px",
121+
fontFamily: "monospace",
122+
fontSize: "14px",
123+
lineHeight: "22px",
124+
padding: "12px 16px",
125+
};

‎internal-packages/emails/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"zod": "4.5.4"
2121
},
2222
"devDependencies": {
23+
"@react-email/ui": "6.5.0",
2324
"@types/nodemailer": "^8.0.0",
2425
"@types/react": "18.2.69",
2526
"@types/react-dom": "18.2.7",

‎internal-packages/emails/src/index.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import MfaDisabledEmail, { MfaDisabledEmailSchema } from "../emails/mfa-disabled
2424
import BulkActionCompletedEmail, {
2525
BulkActionCompletedEmailSchema,
2626
} from "../emails/bulk-action-complete";
27+
import NodeRuntimeDeprecationEmail, {
28+
NodeRuntimeDeprecationEmailSchema,
29+
} from "../emails/node-runtime-deprecation";
2730

2831
export { type MailTransportOptions };
2932

@@ -43,6 +46,7 @@ export const DeliverEmailSchema = z
4346
MfaEnabledEmailSchema,
4447
MfaDisabledEmailSchema,
4548
BulkActionCompletedEmailSchema,
49+
NodeRuntimeDeprecationEmailSchema,
4650
])
4751
.and(z.object({ to: z.string() }));
4852

@@ -170,6 +174,12 @@ export class EmailClient {
170174
component: <BulkActionCompletedEmail {...data} />,
171175
};
172176
}
177+
case "node-runtime-deprecation": {
178+
return {
179+
subject: `[${data.organization}] ${data.project} deployed using deprecated Node.js 21`,
180+
component: <NodeRuntimeDeprecationEmail {...data} />,
181+
};
182+
}
173183
}
174184
}
175185
}

0 commit comments

Comments
 (0)