From a261a599bc876317c06317f34d5bb99e72c4e4b2 Mon Sep 17 00:00:00 2001 From: Alexander Balashov <22360+divineforest@users.noreply.github.com> Date: Wed, 13 May 2026 16:09:58 +0200 Subject: [PATCH] feat(frontend): enable version skew protection via deploymentId During rolling deploys, tabs holding the old build can hit the new server's RSC routes and get inconsistent assets or shape-mismatched data. Setting `deploymentId` lets Next.js stamp assets with `?dpl=` and surface `x-nextjs-deployment-id` on RSC responses; the client router detects the mismatch and hard-reloads onto fresh assets. Reuses the existing `VERSION` build arg, sanitized to the `[a-zA-Z0-9_-]` charset Next.js requires. Declares `VERSION` on the Turbo build task so strict-mode env filtering doesn't strip it before `next build` reads it. --- platform/frontend/next.config.ts | 10 ++++++++++ platform/frontend/src/next-config.test.ts | 10 ++++++++++ platform/turbo.json | 3 ++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/platform/frontend/next.config.ts b/platform/frontend/next.config.ts index cfc8cfd74f..4bcb892ef0 100644 --- a/platform/frontend/next.config.ts +++ b/platform/frontend/next.config.ts @@ -14,6 +14,16 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_APP_VERSION: platformPkg.version, }, output: "standalone", + // Version skew protection during rolling deployments. + // https://nextjs.org/docs/app/api-reference/config/next-config-js/deploymentId + // VERSION is set as a build arg by CI and baked into the + // client bundle here. On client navigation, a mismatch between + // the client's deployment id and the server's response header triggers a + // hard reload, fetching fresh assets that match the server build. + // Next.js restricts the id to [a-zA-Z0-9_-], so non-conforming characters + // (e.g. the dots in `v1.2.41`) are replaced with hyphens. + // https://nextjs.org/docs/messages/deploymentid-invalid-characters + deploymentId: process.env.VERSION?.replace(/[^a-zA-Z0-9_-]/g, "-"), transpilePackages: ["@shared"], // Disable dev indicators so they don't show up in docs automated screenshots devIndicators: false, diff --git a/platform/frontend/src/next-config.test.ts b/platform/frontend/src/next-config.test.ts index 0024ae147c..d3dfd40929 100644 --- a/platform/frontend/src/next-config.test.ts +++ b/platform/frontend/src/next-config.test.ts @@ -6,7 +6,17 @@ vi.mock("@sentry/nextjs", () => ({ describe("next config rewrites", () => { beforeEach(() => { + vi.resetModules(); delete process.env.ARCHESTRA_INTERNAL_API_BASE_URL; + delete process.env.VERSION; + }); + + it("uses sanitized VERSION as the deployment id", async () => { + process.env.VERSION = "v1.2.41+build.5"; + + const { default: nextConfig } = await import("../next.config"); + + expect(nextConfig.deploymentId).toBe("v1-2-41-build-5"); }); it("proxies well-known oauth discovery routes to the backend by default", async () => { diff --git a/platform/turbo.json b/platform/turbo.json index ffe620f383..be51db5f54 100644 --- a/platform/turbo.json +++ b/platform/turbo.json @@ -5,7 +5,8 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**", "dist/**"] + "outputs": [".next/**", "!.next/cache/**", "dist/**"], + "env": ["VERSION"] }, "dev": { "persistent": true,