mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(docker): take the version from the image, not from the container's env
Portainer's recreate and a copied run config carry the previous image's APP_VERSION into a container built from the new one, so the server kept announcing the old release and clients never handed over to the new bundle. The Dockerfile now writes the version into server/VERSION, which the server reads before APP_VERSION, and prerelease bundles take their version from the same build argument.
This commit is contained in:
@@ -4,6 +4,7 @@ node_modules/
|
||||
# Build output
|
||||
client/dist/
|
||||
server/dist/
|
||||
server/VERSION
|
||||
shared/dist/
|
||||
server/public/*
|
||||
!server/public/.gitkeep
|
||||
|
||||
+10
-1
@@ -24,6 +24,9 @@ COPY client/scripts/patch-maplibre.mjs ./client/scripts/
|
||||
RUN npm ci --workspace=client
|
||||
COPY --from=shared-builder /app/shared/dist ./shared/dist
|
||||
COPY client/ ./client/
|
||||
# A prerelease build does not bump client/package.json, so the bundle takes its
|
||||
# version from the same build argument the server's VERSION file comes from.
|
||||
ARG APP_VERSION=dev
|
||||
RUN npm run build --workspace=client
|
||||
|
||||
# ── Stage 3: server ──────────────────────────────────────────────────────────
|
||||
@@ -120,8 +123,14 @@ RUN mkdir -p /app/data/logs /app/uploads/files /app/uploads/covers /app/uploads/
|
||||
ENV NODE_ENV=production
|
||||
ENV NODE_USE_ENV_PROXY=1
|
||||
ENV PORT=3000
|
||||
# The version goes into a file, not into ENV. A container keeps its environment
|
||||
# when it is recreated on a newer image (Portainer's recreate, a copied run
|
||||
# config), so an APP_VERSION env var would keep announcing the old release and
|
||||
# the clients would never pick up the new bundle. The server reads this file
|
||||
# first (server/src/app-config/image-version.ts).
|
||||
ARG APP_VERSION=dev
|
||||
ENV APP_VERSION=${APP_VERSION}
|
||||
RUN if [ "$APP_VERSION" != "dev" ]; then printf '%s
|
||||
' "$APP_VERSION" > /app/server/VERSION; fi
|
||||
|
||||
# OCI metadata: Renovate, Watchtower and the registries read the source label to
|
||||
# link an image update to its release notes (#1498). The release workflows add
|
||||
|
||||
@@ -5,11 +5,12 @@ import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { rtlTextAlias, plyrSpriteAlias } from './rtlTextAlias.js';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
// The version this bundle is built as, baked in at build time. The release image
|
||||
// bumps every package.json before it builds, so this matches the server's
|
||||
// APP_VERSION there; a source checkout matches the server's own package.json.
|
||||
// The version this bundle is built as, baked in at build time. The Docker build
|
||||
// passes APP_VERSION, which the prerelease images need because they do not bump
|
||||
// package.json; a source checkout matches the server's own package.json.
|
||||
// The server hands the release notice only to a bundle built for its version.
|
||||
const UI_VERSION = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
||||
const BUILD_VERSION = process.env.APP_VERSION && process.env.APP_VERSION !== 'dev' ? process.env.APP_VERSION : null;
|
||||
const UI_VERSION = BUILD_VERSION ?? JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
||||
|
||||
// `npm run build:analyze` writes dist/stats.html — a treemap of what actually ended
|
||||
// up in each chunk. The plain build only reports chunk sizes, which tells you a chunk
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
resolveSessionTtlMs,
|
||||
stripTrailingSlashes,
|
||||
} from './parsers';
|
||||
import { imageVersion } from './image-version';
|
||||
|
||||
export type RawEnv = Record<string, string | undefined>;
|
||||
|
||||
@@ -43,8 +44,13 @@ export function deriveApp(raw: RawEnv) {
|
||||
isTest: nodeEnv === 'test',
|
||||
port: numberOr(raw.PORT, 3001),
|
||||
host: raw.HOST,
|
||||
/** Raw APP_VERSION — fallbacks differ per site ('0.0.0' vs package.json vs semver-validated); each keeps its own. */
|
||||
appVersion: raw.APP_VERSION,
|
||||
/**
|
||||
* The image's own VERSION file, else raw APP_VERSION. Fallbacks differ per
|
||||
* site ('0.0.0' vs package.json vs semver-validated); each keeps its own.
|
||||
* The file wins because a container keeps its env across image updates
|
||||
* (image-version.ts).
|
||||
*/
|
||||
appVersion: imageVersion() ?? raw.APP_VERSION,
|
||||
/** Raw APP_URL — trailing-slash stripping differs per site (feeds strips one, notifications strips all). */
|
||||
appUrl: raw.APP_URL,
|
||||
tz: raw.TZ,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
/**
|
||||
* The version the Docker image was built as, from the `VERSION` file the
|
||||
* Dockerfile writes next to `server/package.json`.
|
||||
*
|
||||
* It wins over the APP_VERSION env var because an env var belongs to the
|
||||
* container, not to the image. Portainer's "recreate" and `docker run` with a
|
||||
* copied config carry the previous image's APP_VERSION into a container built
|
||||
* from the new one, so the server kept announcing the old release and no client
|
||||
* ever handed over to the new bundle. A file inside the image cannot outlive it.
|
||||
*
|
||||
* Read once: the file is part of the image and does not change while the
|
||||
* process runs. `null` outside an image (source checkout, tests), where the env
|
||||
* var and package.json keep deciding as before.
|
||||
*/
|
||||
let cached: string | null | undefined;
|
||||
|
||||
export function imageVersion(file = resolve(__dirname, '../../VERSION')): string | null {
|
||||
if (cached !== undefined) return cached;
|
||||
try {
|
||||
cached = readFileSync(file, 'utf8').trim() || null;
|
||||
} catch {
|
||||
cached = null;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** Test seam: forget the cached read. */
|
||||
export function resetImageVersion(): void {
|
||||
cached = undefined;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { deriveApp } from '../../../src/app-config/derive';
|
||||
import { imageVersion, resetImageVersion } from '../../../src/app-config/image-version';
|
||||
|
||||
describe('imageVersion', () => {
|
||||
const dirs: string[] = [];
|
||||
const versionFile = (content: string): string => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trek-image-version-'));
|
||||
dirs.push(dir);
|
||||
const file = path.join(dir, 'VERSION');
|
||||
fs.writeFileSync(file, content);
|
||||
return file;
|
||||
};
|
||||
|
||||
beforeEach(() => resetImageVersion());
|
||||
|
||||
afterEach(() => {
|
||||
resetImageVersion();
|
||||
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('IMGVER-001: reads the version the image was built as, trimmed', () => {
|
||||
expect(imageVersion(versionFile('4.4.0-pre.3\n'))).toBe('4.4.0-pre.3');
|
||||
});
|
||||
|
||||
it('IMGVER-002: is null outside an image and for an empty file', () => {
|
||||
expect(imageVersion(path.join(os.tmpdir(), 'trek-no-such-dir', 'VERSION'))).toBeNull();
|
||||
resetImageVersion();
|
||||
expect(imageVersion(versionFile(' \n'))).toBeNull();
|
||||
});
|
||||
|
||||
it('IMGVER-003: reads once, because the image does not change under a running process', () => {
|
||||
const file = versionFile('4.3.3');
|
||||
expect(imageVersion(file)).toBe('4.3.3');
|
||||
fs.writeFileSync(file, '9.9.9');
|
||||
expect(imageVersion(file)).toBe('4.3.3');
|
||||
});
|
||||
|
||||
it("IMGVER-004: the image's version wins over an APP_VERSION a recreated container carried over", () => {
|
||||
imageVersion(versionFile('4.4.0'));
|
||||
expect(deriveApp({ APP_VERSION: '4.3.3' }).appVersion).toBe('4.4.0');
|
||||
});
|
||||
|
||||
it('IMGVER-005: without an image file, APP_VERSION still decides', () => {
|
||||
imageVersion(path.join(os.tmpdir(), 'trek-no-such-dir', 'VERSION'));
|
||||
expect(deriveApp({ APP_VERSION: '4.3.3' }).appVersion).toBe('4.3.3');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user