import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import assert from "node:assert/strict"; import { afterEach, describe, it } from "vitest"; import { checkDockerfileDepsManifestContract, checkDockerignoreContract, checkDockerfileApiContract, checkDockerfileWebContract, checkDockerfileWorkerContract, checkDockerComposeWorkerTopology, checkRequiredFiles, checkStartScript, formatVerificationResult, verifyDockerBaseline, } from "./verify-docker-baseline.mjs"; const VALID_DOCKERFILE = `FROM node:20-bookworm-slim AS base FROM base AS deps WORKDIR /myapp ADD package.json pnpm-lock.yaml pnpm-workspace.yaml ./ ADD prisma prisma COPY apps/api/package.json apps/api/package.json COPY apps/web/package.json apps/web/package.json COPY apps/worker/package.json apps/worker/package.json RUN --mount=type=cache,id=toolctrl-pnpm-store,target=/pnpm/store \\ pnpm fetch --frozen-lockfile RUN --mount=type=cache,id=toolctrl-pnpm-store,target=/pnpm/store \\ pnpm install --frozen-lockfile --prod=false --offline FROM base AS production-deps WORKDIR /myapp COPY --from=deps /myapp /myapp RUN pnpm prune --prod FROM base AS build RUN pnpm --filter @toolctrl/db prisma:generate RUN pnpm --filter @toolctrl/db build FROM build AS web-build ARG VITE_SENTRY_DSN_WEB="" ENV VITE_SENTRY_DSN_WEB="\${VITE_SENTRY_DSN_WEB}" RUN pnpm --filter @toolctrl/web build FROM build AS api-build RUN pnpm --filter @toolctrl/api build FROM build AS worker-build RUN pnpm --filter @toolctrl/worker build FROM web-build AS web-runtime-artifacts RUN find /myapp/build /myapp/apps/web/build -type f -name '*.map' -delete FROM api-build AS api-runtime-artifacts RUN find /myapp/apps/api/dist -type f -name '*.map' -delete FROM worker-build AS worker-runtime-artifacts RUN find /myapp/apps/worker/dist -type f -name '*.map' -delete FROM base AS worker ARG APP_REVISION=unknown ENV APP_REVISION="\${APP_REVISION}" LABEL org.opencontainers.image.revision="\${APP_REVISION}" COPY --from=build /myapp/package.json /myapp/package.json COPY --from=build /myapp/pnpm-lock.yaml /myapp/pnpm-lock.yaml COPY --from=build /myapp/pnpm-workspace.yaml /myapp/pnpm-workspace.yaml COPY --from=build /myapp/apps/worker/package.json /myapp/apps/worker/package.json COPY --from=worker-runtime-artifacts /myapp/apps/worker/dist /myapp/apps/worker/dist COPY --from=build /myapp/packages/config /myapp/packages/config COPY --from=build /myapp/packages/db /myapp/packages/db COPY --from=build /myapp/packages/db/generated/prisma/client /myapp/packages/db/generated/prisma/client COPY --from=build /myapp/packages/mail /myapp/packages/mail COPY --from=build /myapp/packages/queue /myapp/packages/queue COPY --from=build /myapp/prisma /myapp/prisma COPY --from=build /myapp/prisma.config.ts /myapp/prisma.config.ts HEALTHCHECK CMD node apps/worker/dist/health.js CMD [ "node", "apps/worker/dist/index.js" ] FROM base AS api ARG APP_REVISION=unknown ENV APP_REVISION="\${APP_REVISION}" LABEL org.opencontainers.image.revision="\${APP_REVISION}" EXPOSE 8080 COPY --from=api-runtime-artifacts /myapp/apps/api/dist /myapp/apps/api/dist COPY --from=build /myapp/apps/api/package.json /myapp/apps/api/package.json COPY --from=build /myapp/packages/db /myapp/packages/db COPY --from=build /myapp/packages/db/generated/prisma/client /myapp/packages/db/generated/prisma/client COPY --from=build /myapp/prisma /myapp/prisma COPY --from=build /myapp/prisma.config.ts /myapp/prisma.config.ts COPY --from=build /myapp/package.json /myapp/package.json COPY --from=build /myapp/pnpm-lock.yaml /myapp/pnpm-lock.yaml COPY --from=build /myapp/pnpm-workspace.yaml /myapp/pnpm-workspace.yaml HEALTHCHECK CMD curl -fsS "http://127.0.0.1:\${API_PORT:-\${PORT}}/healthz" CMD [ "node", "apps/api/dist/server.js" ] FROM base AS web ARG APP_REVISION=unknown ENV APP_REVISION="\${APP_REVISION}" LABEL org.opencontainers.image.revision="\${APP_REVISION}" EXPOSE 8080 COPY --from=web-runtime-artifacts /myapp/build /myapp/build COPY --from=web-runtime-artifacts /myapp/build/server-runtime /myapp/build/server-runtime COPY --from=build /myapp/prisma.config.ts /myapp/prisma.config.ts COPY --from=build /myapp/packages/db /myapp/packages/db COPY --from=build /myapp/packages/storage /myapp/packages/storage COPY --from=deps /myapp/packages/storage/node_modules /myapp/packages/storage/node_modules COPY --from=build /myapp/packages/url /myapp/packages/url HEALTHCHECK CMD curl -fsS "http://127.0.0.1:\${PORT}/healthz" ENTRYPOINT [ "./start.sh" ] `; const VALID_DOCKER_COMPOSE = `services: redis: image: redis:7-alpine clamav: image: clamav/clamav:stable worker-general: &worker-base environment: - WORKER_QUEUE_GROUP=general - WORKER_CONCURRENCY_GENERAL=\${WORKER_CONCURRENCY_GENERAL:-10} - WORKER_HEALTH_PORT=9101 - WORKER_ADMIN_PORT=9201 - APP_URL=\${APP_URL:-} - OUTBOX_DISPATCHER_ENABLED=\${OUTBOX_DISPATCHER_ENABLED:-false} - OUTBOX_DISPATCHER_REQUIRED=\${OUTBOX_DISPATCHER_REQUIRED:-false} - WORKFLOW_TRIGGER_EXECUTOR_MODE=\${WORKFLOW_TRIGGER_EXECUTOR_MODE:-http} - WORKFLOW_TRIGGER_EXECUTOR_URL=\${WORKFLOW_TRIGGER_EXECUTOR_URL:-} - API_ACCESS_TOKEN=\${API_ACCESS_TOKEN:-} - WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS=\${WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS:-30000} - WORKFLOW_TRIGGER_OUTBOX_ENABLED=\${WORKFLOW_TRIGGER_OUTBOX_ENABLED:-true} - WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY=\${WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY:-5} - WORKFLOW_TRIGGER_OUTBOX_REQUIRED=\${WORKFLOW_TRIGGER_OUTBOX_REQUIRED:-false} - SCAN_QUEUE_ENABLED=\${SCAN_QUEUE_ENABLED:-false} - CLAMAV_HOST=\${CLAMAV_HOST:-clamav} - CLAMAV_PORT=\${CLAMAV_PORT:-3310} depends_on: redis: condition: service_healthy clamav: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9101/healthz || exit 1"] worker-mail: <<: *worker-base environment: - WORKER_QUEUE_GROUP=mail - WORKER_CONCURRENCY_MAIL=\${WORKER_CONCURRENCY_MAIL:-5} - WORKER_HEALTH_PORT=9102 - WORKER_ADMIN_PORT=9202 depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9102/healthz || exit 1"] worker-import: <<: *worker-base environment: - WORKER_QUEUE_GROUP=import - WORKER_CONCURRENCY_IMPORT=\${WORKER_CONCURRENCY_IMPORT:-1} - WORKER_HEALTH_PORT=9103 - WORKER_ADMIN_PORT=9203 depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9103/healthz || exit 1"] worker-webhook: <<: *worker-base environment: - WORKER_QUEUE_GROUP=webhook - WORKER_CONCURRENCY_WEBHOOK=\${WORKER_CONCURRENCY_WEBHOOK:-20} - WORKER_HEALTH_PORT=9104 - WORKER_ADMIN_PORT=9204 depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9104/healthz || exit 1"] worker-plugin: <<: *worker-base environment: - WORKER_QUEUE_GROUP=plugin - WORKER_CONCURRENCY_PLUGIN=\${WORKER_CONCURRENCY_PLUGIN:-5} - WORKER_HEALTH_PORT=9105 - WORKER_ADMIN_PORT=9205 depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9105/healthz || exit 1"] volumes: redis-data: clamav-db: `; const VALID_DOCKER_COMPOSE_STAGING = `# docker compose -f docker-compose.yml -f docker-compose.staging.yml up postgres pgbouncer api web worker-general worker-mail worker-import worker-webhook worker-plugin services: worker-general: environment: - DATABASE_URL=postgres://boilerplate:boilerplate@pgbouncer:6432/boilerplate_validation?pgbouncer=true - DIRECT_URL=postgres://boilerplate:boilerplate@postgres:5432/boilerplate_validation - OUTBOX_DISPATCHER_ENABLED=true - OUTBOX_DISPATCHER_REQUIRED=true - WORKFLOW_TRIGGER_OUTBOX_ENABLED=true - WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true depends_on: pgbouncer: condition: service_healthy worker-mail: environment: - DATABASE_URL=postgres://boilerplate:boilerplate@pgbouncer:6432/boilerplate_validation?pgbouncer=true - DIRECT_URL=postgres://boilerplate:boilerplate@postgres:5432/boilerplate_validation depends_on: pgbouncer: condition: service_healthy worker-import: environment: - DATABASE_URL=postgres://boilerplate:boilerplate@pgbouncer:6432/boilerplate_validation?pgbouncer=true - DIRECT_URL=postgres://boilerplate:boilerplate@postgres:5432/boilerplate_validation depends_on: pgbouncer: condition: service_healthy worker-webhook: environment: - DATABASE_URL=postgres://boilerplate:boilerplate@pgbouncer:6432/boilerplate_validation?pgbouncer=true - DIRECT_URL=postgres://boilerplate:boilerplate@postgres:5432/boilerplate_validation depends_on: pgbouncer: condition: service_healthy worker-plugin: environment: - DATABASE_URL=postgres://boilerplate:boilerplate@pgbouncer:6432/boilerplate_validation?pgbouncer=true - DIRECT_URL=postgres://boilerplate:boilerplate@postgres:5432/boilerplate_validation depends_on: pgbouncer: condition: service_healthy volumes: postgres-data: `; const fixtures = []; function writeFile(rootDir, relativePath, content) { const absolutePath = path.join(rootDir, relativePath); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); fs.writeFileSync(absolutePath, content); } function writeJson(rootDir, relativePath, value) { writeFile(rootDir, relativePath, `${JSON.stringify(value, null, 2)}\n`); } function createFixture(overrides = {}) { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "docker-baseline-")); fixtures.push(rootDir); writeFile(rootDir, "Dockerfile", overrides.dockerfile ?? VALID_DOCKERFILE); writeFile(rootDir, "docker-compose.yml", overrides.dockerCompose ?? VALID_DOCKER_COMPOSE); writeFile(rootDir, "docker-compose.staging.yml", overrides.dockerComposeStaging ?? VALID_DOCKER_COMPOSE_STAGING); writeFile( rootDir, ".dockerignore", overrides.dockerignore ?? [ "node_modules", "/docs", ".cache", "**/.cache", ".eslintcache", "**/.eslintcache", "coverage", "**/coverage", ".nyc_output", "**/.nyc_output", "reports", "**/reports", "public/build", "/build", "/apps/*/build", "apps/*/dist", "packages/*/dist", "playwright-report", "**/playwright-report", "test-results", "**/test-results", "blob-report", "**/blob-report", "", ].join("\n"), ); writeFile(rootDir, "start.sh", overrides.startScript ?? "#!/bin/sh\nexec node build/server-runtime/server.js\n"); writeFile(rootDir, "pnpm-workspace.yaml", overrides.workspace ?? "packages:\n - apps/*\n"); writeJson( rootDir, "package.json", overrides.rootPackage ?? { private: true, scripts: { build: "vite build && node scripts/build-web-server.mjs", lint: "eslint .", test: "pnpm --filter @toolctrl/web test", typecheck: "tsc -b", start: "pnpm --filter @toolctrl/web start", }, }, ); writeJson( rootDir, "apps/web/package.json", overrides.webPackage ?? { name: "@toolctrl/web", scripts: { start: "node ../../build/server-runtime/server.js", }, }, ); writeJson( rootDir, "apps/api/package.json", overrides.apiPackage ?? { name: "@toolctrl/api", scripts: { build: "tsc -p tsconfig.json", start: "node dist/server.js", }, }, ); writeJson( rootDir, "apps/worker/package.json", overrides.workerPackage ?? { name: "@toolctrl/worker", scripts: { start: "node dist/index.js", health: "node dist/health.js", }, }, ); if (overrides.remove) { for (const relativePath of overrides.remove) { fs.rmSync(path.join(rootDir, relativePath), { recursive: true, force: true }); } } return rootDir; } afterEach(() => { while (fixtures.length > 0) { fs.rmSync(fixtures.pop(), { recursive: true, force: true }); } }); describe("verify-docker-baseline", () => { it("passes for a repository with required Docker files and runtime scripts", () => { const rootDir = createFixture(); const result = verifyDockerBaseline(rootDir); assert.deepEqual(result.failures, []); assert.ok(result.passes.some((pass) => pass === "Dockerfile: exists")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps stage copies all workspace package manifests before install")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps target fetches lockfile dependencies before offline install")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps target uses frozen offline dev dependency install")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: deps target uses BuildKit pnpm store cache")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: production-deps reuses deps filesystem for prune")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: defines named web target from base")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: copies build/server-runtime into web image")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: build target leaves web, api, and worker app builds to split stages")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: build target keeps VITE build args out of api/worker cache paths")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web-build target runs only @toolctrl/web build")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: api-build target runs only @toolctrl/api build")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker-build target runs only @toolctrl/worker build")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: target-specific runtime artifacts remove web, api, and worker sourcemaps before runtime COPY")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target exposes port 8080")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target avoids recursive runtime chown")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies prisma.config.ts")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies packages/db production seed artifact")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies packages/storage")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: web target copies packages/url")); assert.ok(result.passes.some((pass) => pass === "start.sh: execs the production start process for container signal handling")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: defines named worker target from base")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target copies apps/worker/dist")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target copies prisma.config.ts")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target runtime does not use tsx")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target avoids recursive runtime chown")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: defines named api target from base")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target copies packages/db/generated/prisma/client")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target copies prisma.config.ts")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target exposes port 8080")); assert.ok(result.passes.some((pass) => pass === "Dockerfile: api target avoids recursive runtime chown")); assert.ok(result.passes.some((pass) => pass === "package.json: script \"typecheck\" is defined")); assert.ok(result.passes.some((pass) => pass === "package.json: production start script delegates to @toolctrl/web")); assert.ok(result.passes.some((pass) => pass === "apps/web/package.json: owns production web server start command")); assert.ok(result.passes.some((pass) => pass === "package.json: production start script does not use tsx")); assert.ok(result.passes.some((pass) => pass === "apps/api/package.json: production start script uses node")); assert.ok(result.passes.some((pass) => pass === "apps/worker/package.json: script \"start\" targets dist/index.js")); assert.ok(result.passes.some((pass) => pass === "apps/worker/package.json: script \"health\" targets dist/health.js")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through OUTBOX_DISPATCHER_ENABLED")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through OUTBOX_DISPATCHER_REQUIRED")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general passes through WORKFLOW_TRIGGER_OUTBOX_REQUIRED")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general uses distinct WORKER_ADMIN_PORT=9201")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-mail uses distinct WORKER_ADMIN_PORT=9202")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-plugin uses distinct WORKER_ADMIN_PORT=9205")); assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces OUTBOX_DISPATCHER_ENABLED=true")); assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces OUTBOX_DISPATCHER_REQUIRED=true")); assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces WORKFLOW_TRIGGER_OUTBOX_ENABLED=true")); assert.ok(result.passes.some((pass) => pass === "docker-compose.staging.yml: worker-general forces WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-general owns ClamAV dependency for file scanning")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-mail depends on redis only and does not wait for ClamAV")); assert.ok(result.passes.some((pass) => pass === "docker-compose.yml: worker-plugin depends on redis only and does not wait for ClamAV")); }); it("fails with a path-specific message when Dockerfile is missing", () => { const rootDir = createFixture({ remove: ["Dockerfile"] }); const result = checkRequiredFiles(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: missing required Docker baseline file/); }); it("fails when the deps target omits a workspace package manifest", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace("COPY apps/api/package.json apps/api/package.json\n", ""), }); const result = checkDockerfileDepsManifestContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: deps target must copy apps\/api\/package\.json before pnpm install/); }); it("fails when the compose worker topology omits the plugin worker group", () => { const rootDir = createFixture({ dockerCompose: VALID_DOCKER_COMPOSE.replace(/\n worker-plugin:[\s\S]*?(?=\nvolumes:)/, ""), dockerComposeStaging: VALID_DOCKER_COMPOSE_STAGING.replace(/\n worker-plugin:[\s\S]*?(?=\nvolumes:)/, ""), }); const result = checkDockerComposeWorkerTopology(rootDir); assert.match(result.failures.join("\n"), /docker-compose\.yml: missing worker-plugin service for WORKER_QUEUE_GROUP=plugin/); assert.match(result.failures.join("\n"), /docker-compose\.staging\.yml: missing worker-plugin staging overlay/); }); it("fails when non-scanning workers inherit the ClamAV dependency", () => { const rootDir = createFixture({ dockerCompose: VALID_DOCKER_COMPOSE.replace( / worker-mail:\n <<: \*worker-base\n environment:[\s\S]*? healthcheck:/, ` worker-mail: <<: *worker-base environment: - WORKER_QUEUE_GROUP=mail - WORKER_CONCURRENCY_MAIL=\${WORKER_CONCURRENCY_MAIL:-5} - WORKER_HEALTH_PORT=9102 - WORKER_ADMIN_PORT=9202 healthcheck:`, ), }); const result = checkDockerComposeWorkerTopology(rootDir); assert.match(result.failures.join("\n"), /docker-compose\.yml: worker-mail must override worker-base depends_on so it does not inherit clamav/); }); it("fails when a worker group omits its distinct admin port", () => { const rootDir = createFixture({ dockerCompose: VALID_DOCKER_COMPOSE.replace(" - WORKER_ADMIN_PORT=9202\n", ""), }); const result = checkDockerComposeWorkerTopology(rootDir); assert.match(result.failures.join("\n"), /docker-compose\.yml: worker-mail must set WORKER_ADMIN_PORT=9202 distinct from WORKER_HEALTH_PORT=9102/); }); it("fails when dependency targets copy full workspace source before install or prune", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE .replace("COPY apps/api/package.json apps/api/package.json\n", "ADD apps apps\n") .replace("COPY --from=deps /myapp /myapp\n", "ADD packages packages\n"), }); const result = checkDockerfileDepsManifestContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: deps target must copy workspace package manifests, not full apps or packages source, before pnpm install/); assert.match(result.failures.join("\n"), /Dockerfile: production-deps target must copy --from=deps \/myapp \/myapp before pnpm prune --prod/); assert.match(result.failures.join("\n"), /Dockerfile: production-deps target must not copy full apps or packages source before pnpm prune/); }); it("fails when the deps target installs without a BuildKit pnpm store cache", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( "RUN --mount=type=cache,id=toolctrl-pnpm-store,target=/pnpm/store \\\n pnpm install --frozen-lockfile --prod=false --offline", "RUN pnpm install --frozen-lockfile --prod=false" ), }); const result = checkDockerfileDepsManifestContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: deps target must use BuildKit cache mount for \/pnpm\/store during pnpm install/); }); it("fails when the deps target does not fetch before offline install", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( "RUN --mount=type=cache,id=toolctrl-pnpm-store,target=/pnpm/store \\\n pnpm fetch --frozen-lockfile\n", "" ), }); const result = checkDockerfileDepsManifestContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: deps target must run pnpm fetch --frozen-lockfile with BuildKit cache before install/); }); it("fails when .dockerignore broadly excludes source build directories", () => { const rootDir = createFixture({ dockerignore: [ "node_modules", "/docs", ".cache", "**/.cache", ".eslintcache", "**/.eslintcache", "coverage", "**/coverage", ".nyc_output", "**/.nyc_output", "reports", "**/reports", "public/build", "/build", "**/build", "apps/*/dist", "packages/*/dist", "playwright-report", "**/playwright-report", "test-results", "**/test-results", "blob-report", "**/blob-report", "", ].join("\n"), }); const result = checkDockerignoreContract(rootDir); assert.match(result.failures.join("\n"), /\.dockerignore: must not use broad \*\*\/build/); }); it("fails when .dockerignore omits generated build ignores", () => { const rootDir = createFixture({ dockerignore: [ "node_modules", "apps/*/dist", "packages/*/dist", "", ].join("\n"), }); const result = checkDockerignoreContract(rootDir); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore generated public\/build/); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore generated \/build/); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore generated \/apps\/\*\/build/); }); it("fails when .dockerignore omits Docker context artifact ignores", () => { const rootDir = createFixture({ dockerignore: [ "node_modules", "public/build", "/build", "/apps/*/build", "apps/*/dist", "packages/*/dist", "", ].join("\n"), }); const result = checkDockerignoreContract(rootDir); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \/docs/); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/coverage/); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/reports/); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/blob-report/); assert.match(result.failures.join("\n"), /\.dockerignore: must ignore Docker context artifact \*\*\/\.nyc_output/); }); it("fails when .dockerignore broadly excludes cache or report source directories", () => { const rootDir = createFixture({ dockerignore: [ "node_modules", "/docs", ".cache", "**/.cache", "public/build", "/build", "/apps/*/build", "apps/*/dist", "packages/*/dist", "**/cache", "**/report", "coverage", "**/coverage", ".nyc_output", "**/.nyc_output", "reports", "**/reports", "playwright-report", "**/playwright-report", "test-results", "**/test-results", "blob-report", "**/blob-report", "", ].join("\n"), }); const result = checkDockerignoreContract(rootDir); assert.match(result.failures.join("\n"), /\.dockerignore: must not use broad \*\*\/cache/); assert.match(result.failures.join("\n"), /\.dockerignore: must not use broad \*\*\/report/); }); it("fails when start.sh does not exec the production start process", () => { const rootDir = createFixture({ startScript: "#!/bin/sh\npnpm start\n" }); const result = checkStartScript(rootDir); assert.match(result.failures.join("\n"), /start\.sh: must use "exec node build\/server-runtime\/server\.js" so container signals reach the web process without pnpm indirection/); }); it("fails when the root production start script uses tsx", () => { const rootDir = createFixture({ rootPackage: { private: true, scripts: { build: "vite build && node scripts/build-web-server.mjs", lint: "eslint .", test: "pnpm --filter @toolctrl/web test", typecheck: "tsc -b", start: "tsx server/server.ts", }, }, }); const result = verifyDockerBaseline(rootDir); assert.match(result.failures.join("\n"), /package\.json: script "start" must delegate to @toolctrl\/web/); assert.match(result.failures.join("\n"), /package\.json: script "start" must not use tsx for web production runtime/); }); it("fails when Dockerfile does not define the named web target", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace("FROM base AS web", "FROM base"), }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: must define web target with "FROM base AS web"/); }); it("fails when Dockerfile does not define the named api target", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace("FROM base AS api", "FROM base"), }); const result = checkDockerfileApiContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: must define api target with "FROM base AS api"/); }); it("fails when Dockerfile does not define the named worker target", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace("FROM base AS worker", "FROM base"), }); const result = checkDockerfileWorkerContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: must define worker target with "FROM base AS worker"/); }); it("fails when runtime images use recursive chown", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE .replace("HEALTHCHECK CMD curl", "RUN chown -R node:node /myapp\nHEALTHCHECK CMD curl") .replace("HEALTHCHECK CMD node", "RUN chown -R node:node /myapp\nHEALTHCHECK CMD node") .replace("ENTRYPOINT [ \"./start.sh\" ]", "RUN chown -R node:node /myapp\nENTRYPOINT [ \"./start.sh\" ]"), }); assert.match(checkDockerfileWebContract(rootDir).failures.join("\n"), /web target must not use recursive chown/); assert.match(checkDockerfileApiContract(rootDir).failures.join("\n"), /api target must not use recursive chown/); assert.match(checkDockerfileWorkerContract(rootDir).failures.join("\n"), /worker target must not use recursive chown/); }); it("fails when Dockerfile misses the worker dist copy", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( "COPY --from=worker-runtime-artifacts /myapp/apps/worker/dist /myapp/apps/worker/dist\n", "", ), }); const result = checkDockerfileWorkerContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: worker target must copy \/myapp\/apps\/worker\/dist/); }); it("fails when Dockerfile worker target uses tsx at runtime", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( 'CMD [ "node", "apps/worker/dist/index.js" ]', 'CMD [ "tsx", "apps/worker/src/index.ts" ]', ), }); const result = checkDockerfileWorkerContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: worker target must invoke node apps\/worker\/dist\/index\.js directly/); assert.match(result.failures.join("\n"), /Dockerfile: worker target must not use tsx at runtime/); }); it("fails when Dockerfile misses the api generated Prisma client copy", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replaceAll( "COPY --from=build /myapp/packages/db/generated/prisma/client /myapp/packages/db/generated/prisma/client\n", "", ), }); const result = checkDockerfileApiContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: api target must copy \/myapp\/packages\/db\/generated\/prisma\/client/); }); it("fails when Dockerfile api target uses tsx at runtime", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( 'CMD [ "node", "apps/api/dist/server.js" ]', 'CMD [ "tsx", "apps/api/src/server.ts" ]', ), }); const result = checkDockerfileApiContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: api target must invoke node apps\/api\/dist\/server\.js directly/); assert.match(result.failures.join("\n"), /Dockerfile: api target must not use tsx at runtime/); }); it("fails when Dockerfile misses the server-runtime copy", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( "COPY --from=web-runtime-artifacts /myapp/build/server-runtime /myapp/build/server-runtime\n", "", ), }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/build\/server-runtime from web-runtime-artifacts stage/); }); it("fails when Dockerfile does not remove sourcemaps before runtime artifact copies", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( "RUN find /myapp/build /myapp/apps/web/build -type f -name '*.map' -delete\n", "", ), }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: web\/api\/worker runtime artifact targets must delete their own \*\.map files before runtime COPY/); }); it("fails when the shared build stage runs app builds instead of split build targets", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace("RUN pnpm --filter @toolctrl/db build\n", "RUN pnpm --filter @toolctrl/db build\nRUN pnpm build\n"), }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: build target must not run root\/web\/api\/worker app builds/); }); it("fails when the shared build stage owns Vite build args", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace("RUN pnpm --filter @toolctrl/db prisma:generate\n", 'ARG VITE_SENTRY_DSN_WEB=""\nRUN pnpm --filter @toolctrl/db prisma:generate\n'), }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: build target must not declare VITE_\* ARG\/ENV values/); }); it("fails when Dockerfile misses required web workspace package copies", () => { const rootDir = createFixture({ dockerfile: VALID_DOCKERFILE.replace( "COPY --from=build /myapp/packages/storage /myapp/packages/storage\nCOPY --from=deps /myapp/packages/storage/node_modules /myapp/packages/storage/node_modules\nCOPY --from=build /myapp/packages/url /myapp/packages/url\n", "", ), }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/packages\/storage/); assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/packages\/storage\/node_modules/); assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/packages\/url/); }); it("checks web runtime copy and entrypoint only inside the web target", () => { const rootDir = createFixture({ dockerfile: `FROM node:20-bookworm-slim AS base FROM base AS build COPY --from=build /myapp/build/server-runtime /myapp/build/server-runtime ENTRYPOINT [ "./start.sh" ] FROM base AS web COPY --from=build /myapp/build /myapp/build `, }); const result = checkDockerfileWebContract(rootDir); assert.match(result.failures.join("\n"), /Dockerfile: web target must copy \/myapp\/build\/server-runtime from web-runtime-artifacts stage/); assert.match(result.failures.join("\n"), /Dockerfile: web target must keep ENTRYPOINT/); }); it("fails with the script name and manifest path when root typecheck is missing", () => { const rootDir = createFixture({ rootPackage: { private: true, scripts: { build: "vite build", lint: "eslint .", test: "pnpm --filter @toolctrl/web test", }, }, }); const result = verifyDockerBaseline(rootDir); assert.match(result.failures.join("\n"), /package\.json: missing required script "typecheck"/); }); it("fails with a path-specific message when a workspace package manifest is missing", () => { const rootDir = createFixture({ remove: ["apps/api/package.json"] }); const result = verifyDockerBaseline(rootDir); assert.match(result.failures.join("\n"), /apps\/api\/package\.json: missing required Docker baseline file/); }); it("fails with a path-specific message when the web package manifest is missing", () => { const rootDir = createFixture({ remove: ["apps/web/package.json"] }); const result = verifyDockerBaseline(rootDir); assert.match(result.failures.join("\n"), /apps\/web\/package\.json: missing required Docker baseline file/); }); it("fails when worker production scripts use tsx", () => { const rootDir = createFixture({ workerPackage: { name: "@toolctrl/worker", scripts: { start: "tsx src/index.ts", health: "tsx src/health.ts", }, }, }); const result = verifyDockerBaseline(rootDir); assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "start" must contain node for worker production runtime/); assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "start" must target dist\/index\.js/); assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "start" must not use tsx for worker production runtime/); assert.match(result.failures.join("\n"), /apps\/worker\/package\.json: script "health" must target dist\/health\.js/); }); it("formats concise PASS, OBSERVE, and FAIL lines", () => { const output = formatVerificationResult({ passes: ["Dockerfile: exists"], observations: ["apps/worker/package.json: dev script uses tsx"], failures: ["package.json: missing required script \"typecheck\""], }); assert.match(output, /^FAIL Docker baseline verification/m); assert.match(output, /^PASS Dockerfile: exists/m); assert.match(output, /^OBSERVE apps\/worker\/package\.json: dev script uses tsx/m); assert.match(output, /^FAIL package\.json: missing required script "typecheck"/m); }); });