From 4cf857fd2df5779544bc20dc1628edbab6ed2587 Mon Sep 17 00:00:00 2001 From: Dasemu Date: Thu, 8 Oct 2026 03:12:58 +0200 Subject: [PATCH] fix: pin NIXPACKS_NODE_VERSION from the repo's .nvmrc/engines.node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nixpacks silently falls back to its own default Node version when a repo doesn't match what it expects, instead of failing the build — confirmed in practice as a green build that crashed at runtime on a missing global. detectNodeVersion() reads the repo's own .nvmrc or package.json engines.node and passes it through as NIXPACKS_NODE_VERSION so the build actually uses what the repo declares. Co-Authored-By: Claude Sonnet 5 --- LESSONS.md | 2 +- apps/server/src/build/nixpacks.ts | 46 ++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/LESSONS.md b/LESSONS.md index 530edea..4b4fe16 100644 --- a/LESSONS.md +++ b/LESSONS.md @@ -6,7 +6,7 @@ Recurring mistakes and gotchas found while building and operating this project, Nixpacks does not fail when a repo's `engines`/version requirement isn't met — it silently falls back to its own default (e.g. picked Node 18 for a repo requiring Node >=20 in `Social-MCP`). The build goes green, then the container crashes at runtime (`crypto is not defined`, missing globals, etc.). -**Avoid it:** don't trust a green build as proof the runtime is correct. Check the deployed container's actual runtime version against the repo's declared requirement, or pin it explicitly (`NIXPACKS_NODE_VERSION`, `.nvmrc`, etc.) — not yet automated in V1, tracked as a known gap. +**Avoid it:** don't trust a green build as proof the runtime is correct. The manager now detects a Node version hint from the repo itself (`.nvmrc`, else `package.json` `engines.node`) and passes it to Nixpacks as `NIXPACKS_NODE_VERSION` (`apps/server/src/build/nixpacks.ts`, `detectNodeVersion`), so the build uses the version the repo actually declares instead of Nixpacks' own default. Repos with no version hint at all still get Nixpacks' default, unchanged. ## Nixpacks needs a dependency manifest, and fails cleanly without one diff --git a/apps/server/src/build/nixpacks.ts b/apps/server/src/build/nixpacks.ts index b436db9..fb9636e 100644 --- a/apps/server/src/build/nixpacks.ts +++ b/apps/server/src/build/nixpacks.ts @@ -12,6 +12,41 @@ export interface BuildResult { logPath: string; } +function parseMajorVersion(raw: string): string | undefined { + const match = raw.match(/(\d+)/); + return match ? match[1] : undefined; +} + +/** + * Nixpacks doesn't fail when a repo's declared runtime requirement isn't met — it silently + * falls back to its own default version (seen in practice: Node 18 picked for a repo requiring + * >=20, build green, container crashed at runtime on a missing global). Pinning + * NIXPACKS_NODE_VERSION from the repo's own .nvmrc/package.json closes that gap for Node repos. + */ +export function detectNodeVersion(repoDir: string): string | undefined { + const nvmrcPath = path.join(repoDir, ".nvmrc"); + if (fs.existsSync(nvmrcPath)) { + const version = parseMajorVersion(fs.readFileSync(nvmrcPath, "utf8")); + if (version) return version; + } + + const pkgPath = path.join(repoDir, "package.json"); + if (fs.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { engines?: { node?: string } }; + const engineNode = pkg.engines?.node; + if (typeof engineNode === "string") { + const version = parseMajorVersion(engineNode); + if (version) return version; + } + } catch { + // malformed package.json — nothing to pin, let nixpacks use its default + } + } + + return undefined; +} + export function runNixpacksBuild(slug: string, repoDir: string, imageTag: string): Promise { return new Promise((resolve) => { const buildId = Date.now().toString(); @@ -19,7 +54,16 @@ export function runNixpacksBuild(slug: string, repoDir: string, imageTag: string fs.mkdirSync(path.dirname(logPath), { recursive: true }); const logStream = fs.createWriteStream(logPath, { flags: "a" }); - const child = spawn("nixpacks", ["build", repoDir, "--name", imageTag]); + const env = { ...process.env }; + const nodeVersion = detectNodeVersion(repoDir); + if (nodeVersion) { + env.NIXPACKS_NODE_VERSION = nodeVersion; + logStream.write( + `[remote-mcp-manager] pinning NIXPACKS_NODE_VERSION=${nodeVersion} (detected from .nvmrc/package.json engines.node)\n`, + ); + } + + const child = spawn("nixpacks", ["build", repoDir, "--name", imageTag], { env }); child.stdout.pipe(logStream, { end: false }); child.stderr.pipe(logStream, { end: false });