Install deps above WORKDIR so no root-owned node_modules lands in the checkout
CI / full-gate (push) Successful in 2m45s

This commit is contained in:
2026-08-05 18:18:49 +02:00
parent 2852722873
commit bcf4d7fb1f
7 changed files with 53 additions and 13 deletions
+17 -1
View File
@@ -6,7 +6,7 @@
// by running the stack; this catches edits.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { readFileSync, readdirSync } from "node:fs";
const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
const compose = read("compose.yml");
@@ -89,6 +89,22 @@ test("a one-shot bootstrap seeds the stack before web starts", () => {
"web waits for bootstrap to finish");
});
test("deps live above WORKDIR, so no mount creates a root-owned dir in the checkout", () => {
// The daemon creates a missing mount destination as root whatever user the container runs as, so
// a volume at /app/node_modules leaves a root-owned node_modules/ in the developer's own checkout
// (dev bind-mounts `.:/app`). Installing above /app lets Node resolve upward instead — nothing to
// shadow, so nothing to mount over.
const beforeWorkdir = read("Dockerfile").split("WORKDIR /app")[0]!;
assert.match(beforeWorkdir, /npm ci/, "npm ci runs before WORKDIR /app");
assert.match(beforeWorkdir, /mv\s+node_modules\s+\/node_modules/, "and its tree lands at /node_modules");
const composeFiles = readdirSync(new URL("../e2e-tests", import.meta.url))
.filter((f) => f.startsWith("compose."))
.map((f) => `e2e-tests/${f}`);
for (const f of ["compose.yml", "compose.override.yml", ...composeFiles])
assert.ok(!read(f).includes("/app/node_modules"), `${f} mounts nothing at /app/node_modules`);
});
test("the visual E2E does not drag in the Ory stack", () => {
// web's Ory deps are reset for E2E (the dashboard is mock data — no Ory needed).
assert.match(visual, /depends_on:\s*!reset\b/, "E2E resets web's depends_on");
+2 -1
View File
@@ -7,7 +7,8 @@ import ejs from "ejs";
import { ICON_NAMES, buildIconSprite } from "./icons.ts";
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const lucideDir = join(rootDir, "node_modules", "lucide-static", "icons");
// Resolved by specifier, not by path: the install lives above the app dir, not in it (Dockerfile).
const lucideDir = join(dirname(fileURLToPath(import.meta.resolve("lucide-static/package.json"))), "icons");
const partial = join(rootDir, "views", "partials", "icons.ejs");
const symbolInner = (sprite: string, id: string): string =>