Skip to content

Commit 71598e9

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
feat(webapp): node-21 deprecation banner
Mono-RevId: dab37c34d7f8fb423e9e41b5cf05baa9a7751d42
1 parent fbcd6ce commit 71598e9

11 files changed

Lines changed: 192 additions & 91 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Warn when Production projects still use Node.js 21 and link directly to update instructions

apps/webapp/app/components/billing/OrgBanner.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ import {
1010
useOrganization,
1111
useBillingLimit,
1212
useCanManageBillingLimits,
13+
useHasProjectRuntimeUpdate,
1314
} from "~/hooks/useOrganizations";
1415
import { useOptionalProject, useProject } from "~/hooks/useProject";
1516
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
1617
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
17-
import { v3BillingLimitsPath, v3BillingPath, v3QueuesPath } from "~/utils/pathBuilder";
18+
import {
19+
organizationProjectsPath,
20+
v3BillingLimitsPath,
21+
v3BillingPath,
22+
v3QueuesPath,
23+
} from "~/utils/pathBuilder";
1824
import { ENVIRONMENT_PAUSE_SOURCE_BILLING_LIMIT } from "~/utils/environmentPauseSource";
1925

2026
function getUpgradeResetDate(): Date {
@@ -30,6 +36,7 @@ export function OrgBanner() {
3036
const project = useOptionalProject();
3137
const environment = useOptionalEnvironment();
3238
const billingLimit = useBillingLimit();
39+
const hasProjectRuntimeUpdate = useHasProjectRuntimeUpdate();
3340
const currentPlan = useCurrentPlan();
3441
const showSelfServe = useShowSelfServe();
3542
const location = useLocation();
@@ -48,6 +55,7 @@ export function OrgBanner() {
4855
const isArchived = !!(organization && project && environment && environment.archivedAt);
4956

5057
const bannerKind = selectOrgBanner({
58+
hasProjectRuntimeUpdate,
5159
billingLimit,
5260
hasExceededFreeTier: currentPlan?.v3Usage.hasExceededFreeTier === true,
5361
showEnvironmentWarning: isPaused || isArchived,
@@ -58,6 +66,8 @@ export function OrgBanner() {
5866
const hideBillingLimitBanner = location.pathname.endsWith("/settings/billing-limits");
5967

6068
switch (bannerKind) {
69+
case OrgBannerKind.RuntimeUpdate:
70+
return <RuntimeUpdateBanner />;
6171
case OrgBannerKind.LimitRejected:
6272
return hideBillingLimitBanner ? null : <LimitRejectedBanner />;
6373
case OrgBannerKind.LimitGrace:
@@ -77,6 +87,24 @@ export function OrgBanner() {
7787
}
7888
}
7989

90+
function RuntimeUpdateBanner() {
91+
const organization = useOrganization();
92+
93+
return (
94+
<AnimatedOrgBannerBar
95+
show
96+
variant="warning"
97+
action={
98+
<LinkButton variant="tertiary/small" to={organizationProjectsPath(organization)}>
99+
See projects and upgrade
100+
</LinkButton>
101+
}
102+
>
103+
At least one of your projects uses Node 21: deployments will fail from 5 October.
104+
</AnimatedOrgBannerBar>
105+
);
106+
}
107+
80108
function LimitRejectedBanner() {
81109
const organization = useOrganization();
82110
const showSelfServe = useShowSelfServe();

apps/webapp/app/components/billing/selectOrgBanner.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { BillingLimitResult } from "~/services/billingLimit.schemas";
22

33
export enum OrgBannerKind {
4+
RuntimeUpdate = "runtime-update",
45
LimitRejected = "limit-rejected",
56
LimitGrace = "limit-grace",
67
NoLimitConfigured = "no-limit",
@@ -10,13 +11,24 @@ export enum OrgBannerKind {
1011
}
1112

1213
export function selectOrgBanner(input: {
14+
hasProjectRuntimeUpdate?: boolean;
1315
billingLimit?: BillingLimitResult;
1416
hasExceededFreeTier?: boolean;
1517
showEnvironmentWarning?: boolean;
1618
/** Self-serve billing UI — hide configure-limit prompt for managed customers. */
1719
showSelfServe?: boolean;
1820
}): OrgBannerKind {
19-
const { billingLimit, hasExceededFreeTier, showEnvironmentWarning, showSelfServe = true } = input;
21+
const {
22+
hasProjectRuntimeUpdate,
23+
billingLimit,
24+
hasExceededFreeTier,
25+
showEnvironmentWarning,
26+
showSelfServe = true,
27+
} = input;
28+
29+
if (hasProjectRuntimeUpdate) {
30+
return OrgBannerKind.RuntimeUpdate;
31+
}
2032

2133
if (billingLimit?.isConfigured) {
2234
const status = billingLimit.limitState.status;

apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { UserGroupIcon } from "~/assets/icons/UserGroupIcon";
1212
import { VercelLogo } from "~/components/integrations/VercelLogo";
1313
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
1414
import { useFeatures } from "~/hooks/useFeatures";
15-
import { type MatchedOrganization } from "~/hooks/useOrganizations";
15+
import { type MatchedOrganization, useHasProjectRuntimeUpdate } from "~/hooks/useOrganizations";
1616
import { cn } from "~/utils/cn";
1717
import {
1818
organizationPath,
@@ -51,18 +51,17 @@ export function OrganizationSettingsSideMenu({
5151
buildInfo,
5252
isUsingPlugin,
5353
isSsoUsingPlugin,
54-
hasProjectRuntimeUpdate,
5554
}: {
5655
organization: MatchedOrganization;
5756
buildInfo: BuildInfo;
5857
isUsingPlugin: boolean;
5958
isSsoUsingPlugin: boolean;
60-
hasProjectRuntimeUpdate: boolean;
6159
}) {
6260
const { isManagedCloud } = useFeatures();
6361
const featureFlags = useFeatureFlags();
6462
const currentPlan = useCurrentPlan();
6563
const showSelfServe = useShowSelfServe();
64+
const hasProjectRuntimeUpdate = useHasProjectRuntimeUpdate();
6665
const isAdmin = useHasAdminAccess();
6766
const showBuildInfo = isAdmin || !isManagedCloud;
6867

apps/webapp/app/hooks/useOrganizations.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,11 @@ export function useCanManageBillingLimits(matches?: UIMatch[]) {
9494
});
9595
return data?.canManageBillingLimits === true;
9696
}
97+
98+
export function useHasProjectRuntimeUpdate(matches?: UIMatch[]) {
99+
const data = useTypedMatchesData<typeof orgLoader>({
100+
id: "routes/_app.orgs.$organizationSlug",
101+
matches,
102+
});
103+
return data?.hasProjectRuntimeUpdate === true;
104+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings/route.tsx

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Outlet, useRouteLoaderData } from "@remix-run/react";
2-
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
32
import { VERSION as coreVersion } from "@trigger.dev/core";
43
import { type ReactNode } from "react";
54
import { typedjson, useTypedLoaderData } from "remix-typedjson";
@@ -10,51 +9,15 @@ import {
109
OrganizationSettingsSideMenu,
1110
} from "~/components/navigation/OrganizationSettingsSideMenu";
1211
import { useOrganization } from "~/hooks/useOrganizations";
13-
import { resolveOrgIdFromSlugForUser } from "~/models/organization.server";
14-
import { organizationHasProjectRuntimeUpdate } from "~/services/projectRuntimeUpdates.server";
1512
import { rbac } from "~/services/rbac.server";
16-
import { requireUserId } from "~/services/session.server";
1713
import { ssoController } from "~/services/sso.server";
1814

1915
const SETTINGS_ROUTE_ID = "routes/_app.orgs.$organizationSlug.settings";
2016

21-
// The side-menu dot links to the Projects settings page, which requires `read` on
22-
// `deployments`, so gate the dot on the same ability the page checks.
23-
async function canReadDeployments({
24-
request,
25-
userId,
26-
organizationSlug,
27-
}: {
28-
request: Request;
29-
userId: string;
30-
organizationSlug: string;
31-
}) {
32-
// Membership-scoped so the dot is never computed against an org the user is not in.
33-
const organizationId = await resolveOrgIdFromSlugForUser(organizationSlug, userId);
34-
if (!organizationId) {
35-
return false;
36-
}
37-
38-
const auth = await rbac.authenticateAuthorizeSession(
39-
request,
40-
{ userId, organizationId },
41-
{ action: "read", resource: { type: "deployments" } }
42-
);
43-
return auth.ok;
44-
}
45-
46-
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
47-
const userId = await requireUserId(request);
48-
const organizationSlug = params.organizationSlug;
49-
50-
const [isUsingPlugin, isSsoUsingPlugin, hasProjectRuntimeUpdate] = await Promise.all([
17+
export const loader = async () => {
18+
const [isUsingPlugin, isSsoUsingPlugin] = await Promise.all([
5119
rbac.isUsingPlugin(),
5220
ssoController.isUsingPlugin(),
53-
organizationSlug
54-
? canReadDeployments({ request, userId, organizationSlug }).then((canRead) =>
55-
canRead ? organizationHasProjectRuntimeUpdate({ organizationSlug, userId }) : false
56-
)
57-
: Promise.resolve(false),
5821
]);
5922
return typedjson({
6023
buildInfo: {
@@ -66,21 +29,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
6629
} satisfies BuildInfo,
6730
isUsingPlugin,
6831
isSsoUsingPlugin,
69-
hasProjectRuntimeUpdate,
7032
});
7133
};
7234

7335
function SettingsChrome({
7436
buildInfo,
7537
isUsingPlugin,
7638
isSsoUsingPlugin,
77-
hasProjectRuntimeUpdate,
7839
children,
7940
}: {
8041
buildInfo: BuildInfo;
8142
isUsingPlugin: boolean;
8243
isSsoUsingPlugin: boolean;
83-
hasProjectRuntimeUpdate: boolean;
8444
children: ReactNode;
8545
}) {
8646
const organization = useOrganization();
@@ -93,7 +53,6 @@ function SettingsChrome({
9353
buildInfo={buildInfo}
9454
isUsingPlugin={isUsingPlugin}
9555
isSsoUsingPlugin={isSsoUsingPlugin}
96-
hasProjectRuntimeUpdate={hasProjectRuntimeUpdate}
9756
/>
9857
<MainBody>{children}</MainBody>
9958
</div>
@@ -102,15 +61,13 @@ function SettingsChrome({
10261
}
10362

10463
export default function Page() {
105-
const { buildInfo, isUsingPlugin, isSsoUsingPlugin, hasProjectRuntimeUpdate } =
106-
useTypedLoaderData<typeof loader>();
64+
const { buildInfo, isUsingPlugin, isSsoUsingPlugin } = useTypedLoaderData<typeof loader>();
10765

10866
return (
10967
<SettingsChrome
11068
buildInfo={buildInfo}
11169
isUsingPlugin={isUsingPlugin}
11270
isSsoUsingPlugin={isSsoUsingPlugin}
113-
hasProjectRuntimeUpdate={hasProjectRuntimeUpdate}
11471
>
11572
<Outlet />
11673
</SettingsChrome>
@@ -127,7 +84,6 @@ export function ErrorBoundary() {
12784
buildInfo: BuildInfo;
12885
isUsingPlugin: boolean;
12986
isSsoUsingPlugin: boolean;
130-
hasProjectRuntimeUpdate: boolean;
13187
}
13288
| undefined;
13389

@@ -140,7 +96,6 @@ export function ErrorBoundary() {
14096
buildInfo={data.buildInfo}
14197
isUsingPlugin={data.isUsingPlugin}
14298
isSsoUsingPlugin={data.isSsoUsingPlugin}
143-
hasProjectRuntimeUpdate={data.hasProjectRuntimeUpdate}
14499
>
145100
<RouteErrorDisplay />
146101
</SettingsChrome>

apps/webapp/app/routes/_app.orgs.$organizationSlug/route.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { useTypedMatchesData } from "~/hooks/useTypedMatchData";
99
import { OrganizationsPresenter } from "~/presenters/OrganizationsPresenter.server";
1010
import { RegionsPresenter, type Region } from "~/presenters/v3/RegionsPresenter.server";
1111
import { getImpersonationId } from "~/services/impersonation.server";
12+
import { logger } from "~/services/logger.server";
1213
import { getCachedUsage, getBillingLimit, getCurrentPlan } from "~/services/platform.v3.server";
1314
import { rbac } from "~/services/rbac.server";
1415
import { ssoController } from "~/services/sso.server";
16+
import { organizationHasProjectRuntimeUpdate } from "~/services/projectRuntimeUpdates.server";
1517
import { canManageBillingLimits } from "~/services/routeBuilders/permissions.server";
1618
import { requireUser } from "~/services/session.server";
1719
import { telemetry } from "~/services/telemetry.server";
@@ -128,6 +130,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
128130
regions,
129131
isUsingRbacPlugin,
130132
isUsingSsoPlugin,
133+
organizationHasRuntimeUpdate,
131134
] = await Promise.all([
132135
rbac
133136
.authenticateSession(request, {
@@ -157,10 +160,21 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
157160
// items. Both calls are cheap and cached.
158161
rbac.isUsingPlugin().catch(() => false),
159162
ssoController.isUsingPlugin().catch(() => false),
163+
organizationHasProjectRuntimeUpdate({ organizationId: organization.id }).catch((error) => {
164+
logger.error("Failed to check project runtime updates", {
165+
organizationId: organization.id,
166+
error,
167+
});
168+
return false;
169+
}),
160170
]);
161171
const userCanManageBillingLimits = sessionAuth.ok
162172
? canManageBillingLimits(sessionAuth.ability)
163173
: false;
174+
const hasProjectRuntimeUpdate =
175+
sessionAuth.ok &&
176+
sessionAuth.ability.can("read", { type: "deployments" }) &&
177+
organizationHasRuntimeUpdate;
164178

165179
let hasExceededFreeTier = false;
166180
let usagePercentage = 0;
@@ -218,6 +232,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
218232
},
219233
widgetLimitPerDashboard,
220234
canManageBillingLimits: userCanManageBillingLimits,
235+
hasProjectRuntimeUpdate,
221236
isUsingRbacPlugin,
222237
isUsingSsoPlugin,
223238
});

apps/webapp/app/services/deleteProject.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { PrismaClient } from "@trigger.dev/database";
22
import { prisma } from "~/db.server";
3+
import { invalidateOrganizationProjectRuntimeUpdateCache } from "~/services/projectRuntimeUpdates.server";
34
import { engine } from "~/v3/runEngine.server";
45
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56

@@ -58,6 +59,8 @@ export class DeleteProjectService {
5859
},
5960
});
6061

62+
await invalidateOrganizationProjectRuntimeUpdateCache(project.organization.id);
63+
6164
/** project.deletedAt, which the engine gates enqueue and dequeue on, changed; drop every cached env of this project. */
6265
for (const environment of project.environments) {
6366
controlPlaneResolver.invalidateEnvironment(environment.id);

0 commit comments

Comments
 (0)