diff --git a/next.config.ts b/next.config.ts index aa9192e7..c6a37023 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,8 +1,23 @@ +import { execFileSync } from "node:child_process"; import withBundleAnalyzer from "@next/bundle-analyzer"; import { withSentryConfig } from "@sentry/nextjs"; import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; +function resolveDeploymentId(): string | undefined { + const configuredId = process.env.NEXT_DEPLOYMENT_ID?.trim(); + if (configuredId) return configuredId; + + try { + return execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return process.env.APP_VERSION?.trim() || undefined; + } +} + const securityHeaders = [ { key: "X-DNS-Prefetch-Control", value: "on" }, { @@ -20,6 +35,7 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { + deploymentId: resolveDeploymentId(), turbopack: {}, serverExternalPackages: ["mariadb", "lzma", "sharp", "pino", "pino-pretty"], diff --git a/src/lib/deployment-id.test.ts b/src/lib/deployment-id.test.ts new file mode 100644 index 00000000..e407eaad --- /dev/null +++ b/src/lib/deployment-id.test.ts @@ -0,0 +1,15 @@ +import { execFileSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; +import nextConfig from "../../next.config"; + +describe("deployment version skew protection", () => { + it("uses an explicit deployment ID or the current Git commit", () => { + const gitCommit = execFileSync("git", ["rev-parse", "HEAD"], { + encoding: "utf8", + }).trim(); + + expect(nextConfig.deploymentId).toBe( + process.env.NEXT_DEPLOYMENT_ID?.trim() || gitCommit, + ); + }); +});