diff --git a/app/controllers/healthcontroller.js b/app/controllers/healthcontroller.js index e1c2903..1068351 100644 --- a/app/controllers/healthcontroller.js +++ b/app/controllers/healthcontroller.js @@ -2,8 +2,25 @@ // Copyright 2026 Aaron K. Clark "use strict"; +const path = require('node:path'); const db = require('../config/db.config.js'); +// Read the version from package.json once at module load. `npm_package_version` +// is only set when the process is started via an npm script — production +// deployments that launch `node server.js` directly (including the Dockerfile +// in this repo, CMD ["node", "server.js"]) get `undefined` there and the +// /healthz response degrades to `version: "unknown"`. Reading the manifest +// gives operators a real version string regardless of how the process started. +let PACKAGE_VERSION = 'unknown'; +try { + PACKAGE_VERSION = require(path.resolve(__dirname, '../../package.json')).version + || PACKAGE_VERSION; +} catch (_err) { + // package.json missing or malformed — keep the 'unknown' fallback rather + // than crashing module load. /healthz is operationally important and + // must come up even on a broken install. +} + /** * GET /healthz * @@ -83,7 +100,7 @@ exports.healthz = async (req, res) => { status: dbOk ? 'ok' : 'degraded', db: dbOk ? 'ok' : 'down', uptime_s: Math.round(process.uptime()), - version: process.env.npm_package_version || 'unknown', + version: PACKAGE_VERSION, elapsed_ms: Math.round(elapsedMs * 100) / 100, migration, }; diff --git a/tests/api/healthz.test.js b/tests/api/healthz.test.js index dd9d60f..82eef21 100644 --- a/tests/api/healthz.test.js +++ b/tests/api/healthz.test.js @@ -78,4 +78,18 @@ describe('GET /healthz', () => { const m = res.body.migration; expect(m === null || typeof m === 'string').toBe(true); }); + + test('`version` is read from package.json, not npm_package_version', async () => { + // Production Dockerfile launches `node server.js` directly — no + // npm wrapper, so process.env.npm_package_version is undefined. + // The controller must read package.json directly so /healthz + // reports a real version regardless of how the process started. + const pkg = require('../../package.json'); + const res = await request(app).get('/healthz'); + expect(res.body.version).toBe(pkg.version); + // Defensive: the version must NEVER fall back to 'unknown' when + // package.json is readable — that fallback path is only for the + // exotic case of a broken install. + expect(res.body.version).not.toBe('unknown'); + }); });