fix: pin NIXPACKS_NODE_VERSION from the repo's .nvmrc/engines.node

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 03:12:58 +02:00
parent b4628ac255
commit 4cf857fd2d
2 changed files with 46 additions and 2 deletions

View File

@@ -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

View File

@@ -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<BuildResult> {
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 });