fix(deploy): vollständige pg-Closure + funktionierender RUN_SEED im Runner-Image

Zwei BLOCKING-Befunde für "Deployment (Docker + externes Traefik)":

1) pg-Laufzeit-Closure unvollständig: pg-types/lib/binaryParsers.js macht
   eager require('pg-int8'), postgres-interval/index.js require('xtend/mutable').
   Beide lagen NICHT im Runner-Image -> node docker/migrate.mjs crasht beim
   Container-Start mit ERR_MODULE_NOT_FOUND, Deploy kaputt. Fix: COPY für
   pg-int8 + xtend ergänzt. Neuer Test berechnet die reale Closure aus
   node_modules und schlägt fehl, sobald ein Paket nicht ins Image kopiert
   wird (schützt vor erneutem Brechen bei pg/drizzle-Updates).

2) RUN_SEED toter Pfad: entrypoint.sh rief docker/seed.mjs auf, das nie
   existierte -> RUN_SEED=true no-opte still zu leerem Katalog. Fix:
   scripts/build-seed-bundle.mjs bündelt src/db/seed (inkl. Schema + Daten,
   pg/drizzle-orm extern) per esbuild zu selbstständigem docker/seed.mjs;
   im builder erzeugt und ins Runner-Image kopiert. entrypoint.sh bricht
   jetzt laut ab, wenn RUN_SEED=true und das Bundle fehlt, statt still zu
   überspringen. docker/seed.mjs ist generiert -> gitignored.

Verifiziert offline: tsc --noEmit, vitest (deployment + seed-Daten, 36 grün),
Bundle baut + lädt sauber (externe Imports nur pg/drizzle-orm, exit 1 ohne
DATABASE_URL). docker build/run sind im Sandbox deferred (kein Docker/Postgres).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthias Hochmeister
2026-06-09 12:54:59 +02:00
parent d50ec765ab
commit 9927711192
5 changed files with 125 additions and 8 deletions

View File

@@ -30,6 +30,70 @@ describe("Deployment-Artefakte", () => {
expect(df).toMatch(/entrypoint\.sh/);
});
it("Dockerfile kopiert die VOLLSTÄNDIGE pg/drizzle-orm-Laufzeit-Closure ins Runner-Image", () => {
// Reale Laufzeit-Closure aus node_modules berechnen (nur dependencies,
// wie sie zur Laufzeit von docker/migrate.mjs + docker/seed.mjs benötigt
// wird). Jedes Paket MUSS eine COPY-Zeile im Runner-Stage haben, sonst
// crasht der Container-Start mit ERR_MODULE_NOT_FOUND (z. B. pg-int8,
// xtend). Schützt vor erneutem Brechen bei pg/drizzle-Updates.
const df = read("Dockerfile");
const nodeModules = resolve(root, "node_modules");
const seen = new Set<string>();
const resolvePkg = (name: string, fromDir: string): string | null => {
let cur = fromDir;
for (;;) {
const candidate = resolve(cur, "node_modules", name);
if (existsSync(resolve(candidate, "package.json"))) return candidate;
const parent = dirname(cur);
if (parent === cur) break;
cur = parent;
}
const top = resolve(nodeModules, name);
return existsSync(resolve(top, "package.json")) ? top : null;
};
const walk = (dir: string): void => {
const pkg = JSON.parse(
readFileSync(resolve(dir, "package.json"), "utf8"),
) as { dependencies?: Record<string, string> };
for (const dep of Object.keys(pkg.dependencies ?? {})) {
if (seen.has(dep)) continue;
seen.add(dep);
const resolved = resolvePkg(dep, dir);
// Nur skalierbar prüfbar, wenn auflösbar; sonst meldet npm ci es ohnehin.
if (resolved) walk(resolved);
}
};
const pgRoot = resolve(nodeModules, "pg");
const drizzleRoot = resolve(nodeModules, "drizzle-orm");
walk(pgRoot);
walk(drizzleRoot);
// pg + drizzle-orm selbst sind ebenfalls Teil der Closure.
const closure = new Set<string>([...seen, "pg", "drizzle-orm"]);
const missing = [...closure].filter(
(name) =>
!df.includes(`/app/node_modules/${name} ./node_modules/${name}`),
);
expect(missing, `nicht ins Runner-Image kopiert: ${missing.join(", ")}`).toEqual(
[],
);
});
it("Dockerfile bündelt den Seed (build-seed-bundle) und kopiert docker/seed.mjs ins Runner-Image", () => {
const df = read("Dockerfile");
// Im builder wird der Seed zu Plain-ESM gebündelt ...
expect(df).toMatch(/build-seed-bundle\.mjs/);
// ... und ins Runner-Image kopiert, damit RUN_SEED=true nicht still no-opt.
expect(df).toMatch(/docker\/seed\.mjs/);
// Der Bundler-Entrypoint existiert.
expect(existsSync(resolve(root, "scripts/build-seed-bundle.mjs"))).toBe(true);
// Das Seed-Quellmodul, das gebündelt wird, existiert.
expect(existsSync(resolve(root, "src/db/seed/index.ts"))).toBe(true);
});
it(".dockerignore schließt node_modules, .next und Secrets aus", () => {
const di = read(".dockerignore");
expect(di).toMatch(/node_modules/);