fix(fleet): resolve getSenchoVersion crash in Docker containers (#396)

* fix(fleet): resolve getSenchoVersion crash in Docker containers

Replace the runtime __dirname walk (which fails in Docker because the
root package.json is absent from the final image) with a build-time
version injection. A prebuild script reads the root package.json and
generates src/generated/version.ts with the version baked in as a
constant. The compiled output carries the correct version regardless
of the container's filesystem layout.

Retains a filesystem walk fallback for dev environments where the
prebuild hook may not have run.

* fix(fleet): skip postinstall version generation when script is absent

The Docker prod-deps stage copies only package.json (no source files)
before running npm ci --omit=dev. The postinstall hook now checks for
the script file before executing, so it no-ops in stages where
scripts/generate-version.js has not been copied.
This commit is contained in:
Anso
2026-04-06 00:44:36 -04:00
committed by GitHub
parent 89da454db4
commit 670a429168
5 changed files with 47 additions and 10 deletions
+1
View File
@@ -56,6 +56,7 @@ backend/public/
# Auto-generated files
backend/cookies.txt
backend/src/generated/version.ts
# Local-only projects (separate repos)
demo-video/
+1 -1
View File
@@ -330,7 +330,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* **licensing:** license card now shows "Duration: Lifetime" for lifetime licenses instead of an empty renewal date
* **fleet:** remote node capability detection now works reliably; `/api/meta` and `/api/health` are exempt from the global rate limiter so they are never blocked by proxied traffic, and a backend-side cache with stale-while-revalidate prevents transient failures from disabling capability-gated features (Auto-Update, Schedules, Audit, Console) on remote nodes
* **fleet:** failed capability fetches now retry after 30 seconds instead of being cached for the full 5-minute TTL
* **fleet:** `getSenchoVersion()` now resolves `package.json` via `__dirname` walk instead of a hardcoded relative path; the previous `require('../../../package.json')` resolved correctly in dev but crashed with a 500 in Docker (compiled path `dist/services/` has a different depth than source `src/services/`), causing `/api/meta` to always fail on containerized instances and all capability-gated features to appear unsupported on remote nodes
* **fleet:** `getSenchoVersion()` no longer crashes in Docker containers; version is now baked in at build time via a prebuild script that reads the root `package.json` and generates a TypeScript constant, with a filesystem walk fallback for dev environments where the prebuild hook may not have run
### Changed
+3
View File
@@ -4,8 +4,11 @@
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "test -f scripts/generate-version.js && node scripts/generate-version.js || true",
"prebuild": "node scripts/generate-version.js",
"build": "tsc",
"start": "node dist/index.js",
"predev": "node scripts/generate-version.js",
"dev": "nodemon --watch src --ext ts,json --exec ts-node src/index.ts",
"test": "vitest run",
"lint": "eslint src"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env node
// Reads the version from the root package.json and writes it into
// src/generated/version.ts so it's available as a compiled constant.
// Invoked automatically by prebuild/predev/postinstall npm hooks.
const fs = require('fs');
const path = require('path');
const rootPkg = path.resolve(__dirname, '..', '..', 'package.json');
const outFile = path.resolve(__dirname, '..', 'src', 'generated', 'version.ts');
let version = '0.0.0-dev';
try {
const pkg = JSON.parse(fs.readFileSync(rootPkg, 'utf8'));
if (pkg.version) version = pkg.version;
} catch {
console.warn('[generate-version] Could not read root package.json, using fallback version');
}
const content = `// Auto-generated by prebuild script. Do not edit manually.
// Regenerated on every \`npm run build\` from the root package.json.
export const SENCHO_VERSION: string = ${JSON.stringify(version)};
`;
fs.mkdirSync(path.dirname(outFile), { recursive: true });
fs.writeFileSync(outFile, content, 'utf8');
console.log(`[generate-version] Wrote ${version} to src/generated/version.ts`);
+15 -9
View File
@@ -1,6 +1,7 @@
import axios from 'axios';
import path from 'path';
import fs from 'fs';
import { SENCHO_VERSION } from '../generated/version';
/**
* Static registry of capabilities supported by THIS Sencho instance.
@@ -34,24 +35,29 @@ export const CAPABILITIES = [
export type Capability = (typeof CAPABILITIES)[number];
export function getSenchoVersion(): string {
// Walk up from __dirname to find the root package.json (name === 'sencho').
// In dev this is 3 levels up (src/services/ -> root), in Docker it's 2
// (dist/services/ -> /app/). Skips intermediate package.json files like
// backend/package.json which has a different name and version.
// Resolved once per process at import time, then cached.
function resolveVersion(): string {
if (SENCHO_VERSION !== '0.0.0-dev') return SENCHO_VERSION;
// Fallback for manual ts-node runs without the predev hook.
let dir = __dirname;
for (let i = 0; i < 5; i++) {
const candidate = path.join(dir, 'package.json');
if (fs.existsSync(candidate)) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pkg = require(candidate);
try {
const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
if (pkg.name === 'sencho') return pkg.version;
}
} catch { /* not found, keep walking */ }
dir = path.dirname(dir);
}
return 'unknown';
}
const cachedVersion = resolveVersion();
export function getSenchoVersion(): string {
return cachedVersion;
}
export interface RemoteMeta {
version: string | null;
capabilities: string[];