#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const REQUIRED_FILES = [ "Dockerfile", "docker-compose.yml", "docker-compose.staging.yml", ".dockerignore", "start.sh", "package.json", "pnpm-workspace.yaml", "apps/web/package.json", "apps/api/package.json", "apps/worker/package.json", ]; const ROOT_REQUIRED_SCRIPTS = ["build", "lint", "test", "typecheck"]; const API_REQUIRED_SCRIPTS = ["build"]; const REQUIRED_WORKER_GROUPS = [ { service: "worker-general", group: "general", healthPort: "9101", adminPort: "9201", concurrencyEnv: "WORKER_CONCURRENCY_GENERAL" }, { service: "worker-mail", group: "mail", healthPort: "9102", adminPort: "9202", concurrencyEnv: "WORKER_CONCURRENCY_MAIL" }, { service: "worker-import", group: "import", healthPort: "9103", adminPort: "9203", concurrencyEnv: "WORKER_CONCURRENCY_IMPORT" }, { service: "worker-webhook", group: "webhook", healthPort: "9104", adminPort: "9204", concurrencyEnv: "WORKER_CONCURRENCY_WEBHOOK" }, { service: "worker-plugin", group: "plugin", healthPort: "9105", adminPort: "9205", concurrencyEnv: "WORKER_CONCURRENCY_PLUGIN" }, ]; const REQUIRED_WORKER_GENERAL_WORKFLOW_ENV = [ "APP_URL", "OUTBOX_DISPATCHER_ENABLED", "OUTBOX_DISPATCHER_REQUIRED", "WORKFLOW_TRIGGER_EXECUTOR_MODE", "WORKFLOW_TRIGGER_EXECUTOR_URL", "API_ACCESS_TOKEN", "WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS", "WORKFLOW_TRIGGER_OUTBOX_ENABLED", "WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY", "WORKFLOW_TRIGGER_OUTBOX_REQUIRED", ]; const REQUIRED_STAGING_WORKER_GENERAL_ENV = [ ["OUTBOX_DISPATCHER_ENABLED", "true"], ["OUTBOX_DISPATCHER_REQUIRED", "true"], ["WORKFLOW_TRIGGER_OUTBOX_ENABLED", "true"], ["WORKFLOW_TRIGGER_OUTBOX_REQUIRED", "true"], ]; const SCAN_WORKER_SERVICE = "worker-general"; const NON_SCAN_WORKER_SERVICES = REQUIRED_WORKER_GROUPS .map((worker) => worker.service) .filter((service) => service !== SCAN_WORKER_SERVICE); const WEB_TARGET_PATTERN = /^FROM\s+base\s+AS\s+web\s*$/im; const API_TARGET_PATTERN = /^FROM\s+base\s+AS\s+api\s*$/im; const WORKER_TARGET_PATTERN = /^FROM\s+base\s+AS\s+worker\s*$/im; const WEB_BUILD_TARGET_PATTERN = /^FROM\s+build\s+AS\s+web-build\s*$/im; const API_BUILD_TARGET_PATTERN = /^FROM\s+build\s+AS\s+api-build\s*$/im; const WORKER_BUILD_TARGET_PATTERN = /^FROM\s+build\s+AS\s+worker-build\s*$/im; const BUILD_STAGE_FORBIDDEN_APP_BUILD_PATTERN = /\bpnpm\s+(?:run\s+)?build\b|\bpnpm\s+--filter\s+@toolctrl\/web\s+build\b|\bpnpm\s+--filter\s+@toolctrl\/api\s+build\b|\bpnpm\s+--filter\s+@toolctrl\/worker\s+build\b/; const SHARED_BUILD_FORBIDDEN_VITE_PATTERN = /\b(?:ARG|ENV)\s+VITE_[A-Z0-9_]+/; const WEB_BUILD_COMMAND_PATTERN = /^RUN\s+pnpm\s+--filter\s+@toolctrl\/web\s+build\s*$/im; const API_BUILD_COMMAND_PATTERN = /^RUN\s+pnpm\s+--filter\s+@toolctrl\/api\s+build\s*$/im; const WORKER_BUILD_COMMAND_PATTERN = /^RUN\s+pnpm\s+--filter\s+@toolctrl\/worker\s+build\s*$/im; const WEB_RUNTIME_ARTIFACTS_TARGET_PATTERN = /^FROM\s+web-build\s+AS\s+web-runtime-artifacts\s*$/im; const API_RUNTIME_ARTIFACTS_TARGET_PATTERN = /^FROM\s+api-build\s+AS\s+api-runtime-artifacts\s*$/im; const WORKER_RUNTIME_ARTIFACTS_TARGET_PATTERN = /^FROM\s+worker-build\s+AS\s+worker-runtime-artifacts\s*$/im; const WEB_RUNTIME_ARTIFACTS_SOURCEMAP_DELETE_PATTERN = /^RUN\s+find\s+\/myapp\/build\s+\/myapp\/apps\/web\/build\s+-type\s+f\s+-name\s+['"]\*\.map['"]\s+-delete\s*$/im; const API_RUNTIME_ARTIFACTS_SOURCEMAP_DELETE_PATTERN = /^RUN\s+find\s+\/myapp\/apps\/api\/dist\s+-type\s+f\s+-name\s+['"]\*\.map['"]\s+-delete\s*$/im; const WORKER_RUNTIME_ARTIFACTS_SOURCEMAP_DELETE_PATTERN = /^RUN\s+find\s+\/myapp\/apps\/worker\/dist\s+-type\s+f\s+-name\s+['"]\*\.map['"]\s+-delete\s*$/im; const SERVER_RUNTIME_COPY_PATTERN = /^COPY\s+--from=web-runtime-artifacts\s+\/myapp\/build\/server-runtime\s+\/myapp\/build\/server-runtime\s*$/im; const START_SH_ENTRYPOINT_PATTERN = /^ENTRYPOINT\s+\[\s*["']\.\/start\.sh["']\s*\]\s*$/im; const APP_REVISION_ARG_PATTERN = /\bARG\s+APP_REVISION(?:=unknown)?\b/; const APP_REVISION_ENV_PATTERN = /\bENV\s+APP_REVISION=["']?\$\{APP_REVISION\}["']?/; const APP_REVISION_LABEL_PATTERN = /\bLABEL\s+org\.opencontainers\.image\.revision=["']?\$\{APP_REVISION\}["']?/; const WEB_HEALTHCHECK_PATTERN = /\bHEALTHCHECK[\s\S]*?\bcurl\b[\s\S]*?127\.0\.0\.1:\$\{PORT\}\/healthz/; const API_HEALTHCHECK_PATTERN = /\bHEALTHCHECK[\s\S]*?\bcurl\b[\s\S]*?127\.0\.0\.1:\$\{API_PORT:-\$\{PORT\}\}\/healthz/; const RECURSIVE_CHOWN_PATTERN = /\bchown\s+-R\b/; const EXPOSE_8080_PATTERN = /^EXPOSE\s+8080\s*$/im; const API_RUNTIME_PATTERN = /\bnode\b[\s\S]*?(?:\/myapp\/)?apps\/api\/dist\/server\.js\b/; const WORKER_RUNTIME_PATTERN = /\bnode\b[\s\S]*?(?:\/myapp\/)?apps\/worker\/dist\/index\.js\b/; const WORKER_HEALTH_PATTERN = /\bnode\b[\s\S]*?(?:\/myapp\/)?apps\/worker\/dist\/health\.js\b/; const FORBIDDEN_DOCKERIGNORE_PATTERNS = [ ["build", "**/build"], ["dist", "**/dist"], ["cache", "**/cache"], ["report", "**/report"], ]; const REQUIRED_DOCKERIGNORE_PATTERNS = [ "public/build", "/build", "/apps/*/build", "apps/*/dist", "packages/*/dist", ]; const REQUIRED_DOCKERIGNORE_ARTIFACT_PATTERNS = [ "/docs", ".cache", "**/.cache", ".eslintcache", "**/.eslintcache", "coverage", "**/coverage", ".nyc_output", "**/.nyc_output", "reports", "**/reports", "playwright-report", "**/playwright-report", "test-results", "**/test-results", "blob-report", "**/blob-report", ]; const API_REQUIRED_COPY_PATTERNS = [ ["apps/api/dist", /^COPY\s+--from=api-runtime-artifacts\s+\/myapp\/apps\/api\/dist\s+\/myapp\/apps\/api\/dist\s*$/im], ["apps/api/package.json", /^COPY\s+--from=build\s+\/myapp\/apps\/api\/package\.json\s+\/myapp\/apps\/api\/package\.json\s*$/im], ["packages/db", /^COPY\s+--from=build\s+\/myapp\/packages\/db\s+\/myapp\/packages\/db\s*$/im], ["packages/db/generated/prisma/client", /^COPY\s+--from=build\s+\/myapp\/packages\/db\/generated\/prisma\/client\s+\/myapp\/packages\/db\/generated\/prisma\/client\s*$/im], ["prisma", /^COPY\s+--from=build\s+\/myapp\/prisma\s+\/myapp\/prisma\s*$/im], ["prisma.config.ts", /^COPY\s+--from=build\s+\/myapp\/prisma\.config\.ts\s+\/myapp\/prisma\.config\.ts\s*$/im], ["package.json", /^COPY\s+--from=build\s+\/myapp\/package\.json\s+\/myapp\/package\.json\s*$/im], ["pnpm-lock.yaml", /^COPY\s+--from=build\s+\/myapp\/pnpm-lock\.yaml\s+\/myapp\/pnpm-lock\.yaml\s*$/im], ["pnpm-workspace.yaml", /^COPY\s+--from=build\s+\/myapp\/pnpm-workspace\.yaml\s+\/myapp\/pnpm-workspace\.yaml\s*$/im], ]; const WORKER_REQUIRED_COPY_PATTERNS = [ ["apps/worker/dist", /^COPY\s+--from=worker-runtime-artifacts\s+\/myapp\/apps\/worker\/dist\s+\/myapp\/apps\/worker\/dist\s*$/im], ["apps/worker/package.json", /^COPY\s+--from=build\s+\/myapp\/apps\/worker\/package\.json\s+\/myapp\/apps\/worker\/package\.json\s*$/im], ["packages/config", /^COPY\s+--from=build\s+\/myapp\/packages\/config\s+\/myapp\/packages\/config\s*$/im], ["packages/db", /^COPY\s+--from=build\s+\/myapp\/packages\/db\s+\/myapp\/packages\/db\s*$/im], ["packages/db/generated/prisma/client", /^COPY\s+--from=build\s+\/myapp\/packages\/db\/generated\/prisma\/client\s+\/myapp\/packages\/db\/generated\/prisma\/client\s*$/im], ["packages/mail", /^COPY\s+--from=build\s+\/myapp\/packages\/mail\s+\/myapp\/packages\/mail\s*$/im], ["packages/queue", /^COPY\s+--from=build\s+\/myapp\/packages\/queue\s+\/myapp\/packages\/queue\s*$/im], ["prisma", /^COPY\s+--from=build\s+\/myapp\/prisma\s+\/myapp\/prisma\s*$/im], ["prisma.config.ts", /^COPY\s+--from=build\s+\/myapp\/prisma\.config\.ts\s+\/myapp\/prisma\.config\.ts\s*$/im], ["package.json", /^COPY\s+--from=build\s+\/myapp\/package\.json\s+\/myapp\/package\.json\s*$/im], ["pnpm-lock.yaml", /^COPY\s+--from=build\s+\/myapp\/pnpm-lock\.yaml\s+\/myapp\/pnpm-lock\.yaml\s*$/im], ["pnpm-workspace.yaml", /^COPY\s+--from=build\s+\/myapp\/pnpm-workspace\.yaml\s+\/myapp\/pnpm-workspace\.yaml\s*$/im], ]; const WEB_REQUIRED_COPY_PATTERNS = [ ["prisma.config.ts", /^COPY\s+--from=build\s+\/myapp\/prisma\.config\.ts\s+\/myapp\/prisma\.config\.ts\s*$/im], ["packages/db production seed artifact", /^COPY\s+--from=build\s+\/myapp\/packages\/db\s+\/myapp\/packages\/db\s*$/im], ["packages/storage", /^COPY\s+--from=build\s+\/myapp\/packages\/storage\s+\/myapp\/packages\/storage\s*$/im], ["packages/storage/node_modules", /^COPY\s+--from=deps\s+\/myapp\/packages\/storage\/node_modules\s+\/myapp\/packages\/storage\/node_modules\s*$/im], ["packages/url", /^COPY\s+--from=build\s+\/myapp\/packages\/url\s+\/myapp\/packages\/url\s*$/im], ]; const START_SCRIPT_EXEC_PATTERN = /^exec\s+node\s+build\/server-runtime\/server\.js\s*$/m; const FULL_WORKSPACE_SOURCE_COPY_PATTERN = /^(?:ADD|COPY)\s+(apps|packages)\s+\1\s*$/im; const FROZEN_FETCH_PATTERN = /^RUN\s+--mount=type=cache,[^\n]*target=\/pnpm\/store[^\n]*\s+\\?\s*\n\s*pnpm\s+fetch\s+--frozen-lockfile\s*$/im; const FROZEN_DEV_INSTALL_PATTERN = /^RUN\s+(?:--mount=type=cache,[^\n]*\s+)?(?:\\\s*\n\s*)?pnpm\s+install\s+--frozen-lockfile\s+--prod=false(?:\s+--offline)?\s*$/im; const PNPM_BUILDKIT_CACHE_PATTERN = /^RUN\s+--mount=type=cache,[^\n]*target=\/pnpm\/store[^\n]*\s+\\?\s*\n\s*pnpm\s+install\b/im; const PRODUCTION_PRUNE_REUSE_DEPS_PATTERN = /^COPY\s+--from=deps\s+\/myapp\s+\/myapp\s*$/im; function manifestPath(relativePath) { return relativePath.split(path.sep).join("/"); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function readJsonFile(rootDir, relativePath) { const absolutePath = path.join(rootDir, relativePath); return JSON.parse(fs.readFileSync(absolutePath, "utf8")); } function hasScript(manifest, scriptName) { return typeof manifest.scripts?.[scriptName] === "string" && manifest.scripts[scriptName].trim().length > 0; } function getComposeServiceBlock(content, serviceName) { const escaped = escapeRegExp(serviceName); return content.match(new RegExp(`^ ${escaped}:[^\\n]*\\n([\\s\\S]*?)(?=^ [A-Za-z0-9_-]+:[^\\n]*\\n|^volumes:\\n|^networks:\\n|(?![\\s\\S]))`, "m"))?.[1] ?? ""; } function listWorkspacePackageManifests(rootDir) { const manifests = []; for (const workspaceRoot of ["apps", "packages"]) { const absoluteWorkspaceRoot = path.join(rootDir, workspaceRoot); if (!fs.existsSync(absoluteWorkspaceRoot)) { continue; } for (const entry of fs.readdirSync(absoluteWorkspaceRoot, { withFileTypes: true })) { if (!entry.isDirectory()) { continue; } const relativePath = manifestPath(path.join(workspaceRoot, entry.name, "package.json")); if (fs.existsSync(path.join(rootDir, relativePath))) { manifests.push(relativePath); } } } return manifests.sort(); } export function checkRequiredFiles(rootDir) { const passes = []; const failures = []; for (const relativePath of REQUIRED_FILES) { if (fs.existsSync(path.join(rootDir, relativePath))) { passes.push(`${relativePath}: exists`); } else { failures.push(`${relativePath}: missing required Docker baseline file`); } } return { passes, failures, observations: [] }; } export function checkDockerignoreContract(rootDir) { const relativePath = ".dockerignore"; const passes = []; const failures = []; const content = fs.readFileSync(path.join(rootDir, relativePath), "utf8"); const patterns = content .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith("#")); for (const [sourceName, pattern] of FORBIDDEN_DOCKERIGNORE_PATTERNS) { if (patterns.includes(pattern)) { failures.push(`${relativePath}: must not use broad ${pattern}; it can exclude source directories named ${sourceName}`); } else { passes.push(`${relativePath}: does not exclude all ${sourceName} directories recursively`); } } for (const pattern of REQUIRED_DOCKERIGNORE_PATTERNS) { if (patterns.includes(pattern)) { passes.push(`${relativePath}: ignores generated ${pattern}`); } else { failures.push(`${relativePath}: must ignore generated ${pattern} without broad recursive source exclusions`); } } for (const pattern of REQUIRED_DOCKERIGNORE_ARTIFACT_PATTERNS) { if (patterns.includes(pattern)) { passes.push(`${relativePath}: ignores Docker context artifact ${pattern}`); } else { failures.push(`${relativePath}: must ignore Docker context artifact ${pattern} without broad recursive source exclusions`); } } return { passes, failures, observations: [] }; } export function checkStartScript(rootDir) { const relativePath = "start.sh"; const passes = []; const failures = []; const content = fs.readFileSync(path.join(rootDir, relativePath), "utf8"); if (/^#!\/bin\/sh\s*$/m.test(content)) { passes.push(`${relativePath}: uses POSIX shell shebang`); } else { failures.push(`${relativePath}: must use #!/bin/sh`); } if (START_SCRIPT_EXEC_PATTERN.test(content)) { passes.push(`${relativePath}: execs the production start process for container signal handling`); } else { failures.push(`${relativePath}: must use "exec node build/server-runtime/server.js" so container signals reach the web process without pnpm indirection`); } return { passes, failures, observations: [] }; } export function checkDockerComposeWorkerTopology(rootDir) { const passes = []; const failures = []; const composePath = "docker-compose.yml"; const stagingPath = "docker-compose.staging.yml"; const compose = fs.readFileSync(path.join(rootDir, composePath), "utf8"); const staging = fs.readFileSync(path.join(rootDir, stagingPath), "utf8"); for (const { service, group, healthPort, adminPort, concurrencyEnv } of REQUIRED_WORKER_GROUPS) { const block = getComposeServiceBlock(compose, service); if (!block) { failures.push(`${composePath}: missing ${service} service for WORKER_QUEUE_GROUP=${group}`); } else if (block.includes(`WORKER_QUEUE_GROUP=${group}`)) { passes.push(`${composePath}: ${service} sets WORKER_QUEUE_GROUP=${group}`); } else { failures.push(`${composePath}: ${service} must set WORKER_QUEUE_GROUP=${group}`); } if (!block) { // Already reported above. } else if (block.includes(concurrencyEnv)) { passes.push(`${composePath}: ${service} exposes ${concurrencyEnv}`); } else { failures.push(`${composePath}: ${service} must expose ${concurrencyEnv}`); } if (!block) { // Already reported above. } else if (block.includes(`WORKER_HEALTH_PORT=${healthPort}`) && block.includes(`127.0.0.1:${healthPort}/healthz`)) { passes.push(`${composePath}: ${service} healthcheck targets ${healthPort}/healthz`); } else { failures.push(`${composePath}: ${service} must use WORKER_HEALTH_PORT=${healthPort} and a matching healthcheck`); } if (!block) { // Already reported above. } else if (block.includes(`WORKER_ADMIN_PORT=${adminPort}`) && adminPort !== healthPort) { passes.push(`${composePath}: ${service} uses distinct WORKER_ADMIN_PORT=${adminPort}`); } else { failures.push(`${composePath}: ${service} must set WORKER_ADMIN_PORT=${adminPort} distinct from WORKER_HEALTH_PORT=${healthPort}`); } const stagingBlock = getComposeServiceBlock(staging, service); if (!stagingBlock) { failures.push(`${stagingPath}: missing ${service} staging overlay`); continue; } if (stagingBlock.includes("pgbouncer:6432") && stagingBlock.includes("DIRECT_URL=") && stagingBlock.includes("condition: service_healthy")) { passes.push(`${stagingPath}: ${service} routes staging database traffic through PgBouncer with DIRECT_URL fallback`); } else { failures.push(`${stagingPath}: ${service} must set staging DATABASE_URL, DIRECT_URL, and pgbouncer dependency`); } } const generalBlock = getComposeServiceBlock(compose, "worker-general"); const stagingGeneralBlock = getComposeServiceBlock(staging, "worker-general"); for (const envName of REQUIRED_WORKER_GENERAL_WORKFLOW_ENV) { if (generalBlock.includes(`${envName}=`)) { passes.push(`${composePath}: worker-general passes through ${envName}`); } else { failures.push(`${composePath}: worker-general must pass through ${envName} so outbox.workflow is explicitly configured`); } } for (const [envName, expectedValue] of REQUIRED_STAGING_WORKER_GENERAL_ENV) { if (stagingGeneralBlock.includes(`${envName}=${expectedValue}`)) { passes.push(`${stagingPath}: worker-general forces ${envName}=${expectedValue}`); } else { failures.push(`${stagingPath}: worker-general must force ${envName}=${expectedValue} so staging/prod outbox.workflow cannot silently idle`); } } if (generalBlock.includes("CLAMAV_HOST=") && generalBlock.includes("CLAMAV_PORT=") && generalBlock.includes("clamav:")) { passes.push(`${composePath}: worker-general owns ClamAV dependency for file scanning`); } else { failures.push(`${composePath}: worker-general must keep CLAMAV_* env and depend on clamav for file scanning`); } for (const service of NON_SCAN_WORKER_SERVICES) { const block = getComposeServiceBlock(compose, service); if (!block) { continue; } if (!block.includes("depends_on:")) { failures.push(`${composePath}: ${service} must override worker-base depends_on so it does not inherit clamav`); continue; } if (block.includes("clamav:") || block.includes("CLAMAV_HOST=") || block.includes("CLAMAV_PORT=")) { failures.push(`${composePath}: ${service} must not depend on clamav or receive CLAMAV_* scan env`); } else if (block.includes("redis:") && block.includes("condition: service_healthy")) { passes.push(`${composePath}: ${service} depends on redis only and does not wait for ClamAV`); } else { failures.push(`${composePath}: ${service} must depend on redis with condition: service_healthy after overriding worker-base depends_on`); } } const missingCommandServices = REQUIRED_WORKER_GROUPS.map((worker) => worker.service).filter((service) => !staging.includes(service)); if (missingCommandServices.length === 0) { passes.push(`${stagingPath}: reference compose command includes all worker group services`); } else { failures.push(`${stagingPath}: reference compose command must include ${missingCommandServices.join(", ")}`); } return { passes, failures, observations: [] }; } export function checkRootManifest(rootDir) { const relativePath = "package.json"; const passes = []; const failures = []; const manifest = readJsonFile(rootDir, relativePath); for (const scriptName of ROOT_REQUIRED_SCRIPTS) { if (hasScript(manifest, scriptName)) { passes.push(`${relativePath}: script "${scriptName}" is defined`); } else { failures.push(`${relativePath}: missing required script "${scriptName}"`); } } if (!hasScript(manifest, "start")) { failures.push(`${relativePath}: missing required script "start"`); } else { const startCommand = manifest.scripts.start; if (startCommand === "pnpm --filter @toolctrl/web start") { passes.push(`${relativePath}: production start script delegates to @toolctrl/web`); } else { failures.push(`${relativePath}: script "start" must delegate to @toolctrl/web`); } if (/\btsx\b/.test(startCommand)) { failures.push(`${relativePath}: script "start" must not use tsx for web production runtime`); } else { passes.push(`${relativePath}: production start script does not use tsx`); } } return { passes, failures, observations: [] }; } export function checkWebManifest(rootDir) { const relativePath = "apps/web/package.json"; const passes = []; const failures = []; const manifest = readJsonFile(rootDir, relativePath); if (!hasScript(manifest, "start")) { failures.push(`${relativePath}: missing required script "start"`); } else { const startCommand = manifest.scripts.start; if (startCommand === "node ../../build/server-runtime/server.js") { passes.push(`${relativePath}: owns production web server start command`); } else { failures.push(`${relativePath}: script "start" must run node ../../build/server-runtime/server.js`); } if (/\btsx\b/.test(startCommand)) { failures.push(`${relativePath}: script "start" must not use tsx for web production runtime`); } else { passes.push(`${relativePath}: production start script does not use tsx`); } } return { passes, failures, observations: [] }; } export function checkApiManifest(rootDir) { const relativePath = "apps/api/package.json"; const passes = []; const failures = []; const manifest = readJsonFile(rootDir, relativePath); for (const scriptName of API_REQUIRED_SCRIPTS) { if (hasScript(manifest, scriptName)) { passes.push(`${relativePath}: script "${scriptName}" is defined`); } else { failures.push(`${relativePath}: missing required script "${scriptName}"`); } } if (!hasScript(manifest, "start")) { failures.push(`${relativePath}: missing required script "start"`); } else if (/\bnode\b/.test(manifest.scripts.start)) { passes.push(`${relativePath}: production start script uses node`); } else { failures.push(`${relativePath}: script "start" must contain node for production runtime`); } return { passes, failures, observations: [] }; } export function checkWorkerManifest(rootDir) { const relativePath = "apps/worker/package.json"; const passes = []; const failures = []; const manifest = readJsonFile(rootDir, relativePath); const requiredScripts = [ ["start", /\bdist\/index\.js\b/], ["health", /\bdist\/health\.js\b/], ]; for (const [scriptName, distPattern] of requiredScripts) { if (!hasScript(manifest, scriptName)) { failures.push(`${relativePath}: missing required script "${scriptName}"`); continue; } const command = manifest.scripts[scriptName]; if (/\bnode\b/.test(command)) { passes.push(`${relativePath}: script "${scriptName}" uses node`); } else { failures.push(`${relativePath}: script "${scriptName}" must contain node for worker production runtime`); } if (distPattern.test(command)) { passes.push(`${relativePath}: script "${scriptName}" targets ${scriptName === "start" ? "dist/index.js" : "dist/health.js"}`); } else { failures.push(`${relativePath}: script "${scriptName}" must target ${scriptName === "start" ? "dist/index.js" : "dist/health.js"}`); } if (/\btsx\b/.test(command)) { failures.push(`${relativePath}: script "${scriptName}" must not use tsx for worker production runtime`); } else { passes.push(`${relativePath}: script "${scriptName}" does not use tsx`); } } return { passes, failures, observations: [] }; } export function checkDockerfileWebContract(rootDir) { const relativePath = "Dockerfile"; const dockerfile = fs.readFileSync(path.join(rootDir, relativePath), "utf8"); const passes = []; const failures = []; const buildBody = getDockerfileTargetBody(dockerfile, "build"); const webBuildBody = getDockerfileTargetBody(dockerfile, "web-build"); const apiBuildBody = getDockerfileTargetBody(dockerfile, "api-build"); const workerBuildBody = getDockerfileTargetBody(dockerfile, "worker-build"); const webBody = getDockerfileTargetBody(dockerfile, "web"); const webRuntimeArtifactsBody = getDockerfileTargetBody(dockerfile, "web-runtime-artifacts"); const apiRuntimeArtifactsBody = getDockerfileTargetBody(dockerfile, "api-runtime-artifacts"); const workerRuntimeArtifactsBody = getDockerfileTargetBody(dockerfile, "worker-runtime-artifacts"); if (WEB_TARGET_PATTERN.test(dockerfile)) { passes.push(`${relativePath}: defines named web target from base`); } else { failures.push(`${relativePath}: must define web target with "FROM base AS web"`); } if (webBody && SERVER_RUNTIME_COPY_PATTERN.test(webBody)) { passes.push(`${relativePath}: copies build/server-runtime into web image`); } else { failures.push(`${relativePath}: web target must copy /myapp/build/server-runtime from web-runtime-artifacts stage`); } if (buildBody && !BUILD_STAGE_FORBIDDEN_APP_BUILD_PATTERN.test(buildBody)) { passes.push(`${relativePath}: build target leaves web, api, and worker app builds to split stages`); } else { failures.push(`${relativePath}: build target must not run root/web/api/worker app builds; use web-build, api-build, and worker-build stages`); } if (buildBody && !SHARED_BUILD_FORBIDDEN_VITE_PATTERN.test(buildBody)) { passes.push(`${relativePath}: build target keeps VITE build args out of api/worker cache paths`); } else { failures.push(`${relativePath}: build target must not declare VITE_* ARG/ENV values; keep them in web-build only`); } if (WEB_BUILD_TARGET_PATTERN.test(dockerfile) && webBuildBody && WEB_BUILD_COMMAND_PATTERN.test(webBuildBody)) { passes.push(`${relativePath}: web-build target runs only @toolctrl/web build`); } else { failures.push(`${relativePath}: must define web-build target from build and run pnpm --filter @toolctrl/web build`); } if (API_BUILD_TARGET_PATTERN.test(dockerfile) && apiBuildBody && API_BUILD_COMMAND_PATTERN.test(apiBuildBody)) { passes.push(`${relativePath}: api-build target runs only @toolctrl/api build`); } else { failures.push(`${relativePath}: must define api-build target from build and run pnpm --filter @toolctrl/api build`); } if (WORKER_BUILD_TARGET_PATTERN.test(dockerfile) && workerBuildBody && WORKER_BUILD_COMMAND_PATTERN.test(workerBuildBody)) { passes.push(`${relativePath}: worker-build target runs only @toolctrl/worker build`); } else { failures.push(`${relativePath}: must define worker-build target from build and run pnpm --filter @toolctrl/worker build`); } if ( WEB_RUNTIME_ARTIFACTS_TARGET_PATTERN.test(dockerfile) && API_RUNTIME_ARTIFACTS_TARGET_PATTERN.test(dockerfile) && WORKER_RUNTIME_ARTIFACTS_TARGET_PATTERN.test(dockerfile) && webRuntimeArtifactsBody && apiRuntimeArtifactsBody && workerRuntimeArtifactsBody && WEB_RUNTIME_ARTIFACTS_SOURCEMAP_DELETE_PATTERN.test(webRuntimeArtifactsBody) && API_RUNTIME_ARTIFACTS_SOURCEMAP_DELETE_PATTERN.test(apiRuntimeArtifactsBody) && WORKER_RUNTIME_ARTIFACTS_SOURCEMAP_DELETE_PATTERN.test(workerRuntimeArtifactsBody) ) { passes.push(`${relativePath}: target-specific runtime artifacts remove web, api, and worker sourcemaps before runtime COPY`); } else { failures.push(`${relativePath}: web/api/worker runtime artifact targets must delete their own *.map files before runtime COPY`); } if (webBody && (START_SH_ENTRYPOINT_PATTERN.test(webBody) || /\bpnpm\s+start\b|\bnpm\s+start\b|\bnode\s+build\/server-runtime\/server\.js\b/.test(webBody))) { passes.push(`${relativePath}: web runtime invokes production start path`); } else { failures.push(`${relativePath}: web target must keep ENTRYPOINT [ "./start.sh" ] or invoke the root production start path`); } if (webBody && RECURSIVE_CHOWN_PATTERN.test(webBody)) { failures.push(`${relativePath}: web target must not use recursive chown in the runtime image`); } else if (webBody) { passes.push(`${relativePath}: web target avoids recursive runtime chown`); } if (webBody && WEB_HEALTHCHECK_PATTERN.test(webBody)) { passes.push(`${relativePath}: web target contains HEALTHCHECK using /healthz`); } else { failures.push(`${relativePath}: web target HEALTHCHECK must probe http://127.0.0.1:${"${PORT}"}/healthz`); } if (webBody && EXPOSE_8080_PATTERN.test(webBody)) { passes.push(`${relativePath}: web target exposes port 8080`); } else { failures.push(`${relativePath}: web target must declare EXPOSE 8080 for Coolify port discovery`); } if (webBody && APP_REVISION_ARG_PATTERN.test(webBody) && APP_REVISION_ENV_PATTERN.test(webBody) && APP_REVISION_LABEL_PATTERN.test(webBody)) { passes.push(`${relativePath}: web target persists APP_REVISION in env and OCI label`); } else { failures.push(`${relativePath}: web target must persist APP_REVISION in ENV and org.opencontainers.image.revision label`); } for (const [copyName, copyPattern] of WEB_REQUIRED_COPY_PATTERNS) { if (webBody && copyPattern.test(webBody)) { passes.push(`${relativePath}: web target copies ${copyName}`); } else { failures.push(`${relativePath}: web target must copy /myapp/${copyName}`); } } return { passes, failures, observations: [] }; } function getDockerfileTargetBody(dockerfile, targetName) { const targetPattern = new RegExp(`^FROM\\s+\\S+(?:\\s+AS\\s+${targetName})\\s*$`, "im"); const targetMatch = targetPattern.exec(dockerfile); if (!targetMatch) { return null; } const bodyStart = targetMatch.index + targetMatch[0].length; const nextTargetMatch = /^FROM\s+/gim.exec(dockerfile.slice(bodyStart)); const bodyEnd = nextTargetMatch ? bodyStart + nextTargetMatch.index : dockerfile.length; return dockerfile.slice(bodyStart, bodyEnd); } export function checkDockerfileDepsManifestContract(rootDir) { const relativePath = "Dockerfile"; const dockerfile = fs.readFileSync(path.join(rootDir, relativePath), "utf8"); const passes = []; const failures = []; const depsBody = getDockerfileTargetBody(dockerfile, "deps"); const productionDepsBody = getDockerfileTargetBody(dockerfile, "production-deps"); const workspaceManifests = listWorkspacePackageManifests(rootDir); if (!depsBody) { failures.push(`${relativePath}: must define deps target with "FROM base AS deps"`); } else { if (FULL_WORKSPACE_SOURCE_COPY_PATTERN.test(depsBody)) { failures.push(`${relativePath}: deps target must copy workspace package manifests, not full apps or packages source, before pnpm install`); } else { passes.push(`${relativePath}: deps target does not copy full workspace source before pnpm install`); } for (const workspaceManifest of workspaceManifests) { const copyPattern = new RegExp(`^COPY\\s+${escapeRegExp(workspaceManifest)}\\s+${escapeRegExp(workspaceManifest)}\\s*$`, "m"); if (copyPattern.test(depsBody)) { passes.push(`${relativePath}: deps target copies ${workspaceManifest}`); } else { failures.push(`${relativePath}: deps target must copy ${workspaceManifest} before pnpm install`); } } if (FROZEN_FETCH_PATTERN.test(depsBody)) { passes.push(`${relativePath}: deps target fetches lockfile dependencies before offline install`); } else { failures.push(`${relativePath}: deps target must run pnpm fetch --frozen-lockfile with BuildKit cache before install`); } if (FROZEN_DEV_INSTALL_PATTERN.test(depsBody) && /\bpnpm\s+install\s+--frozen-lockfile\s+--prod=false\s+--offline\b/.test(depsBody)) { passes.push(`${relativePath}: deps target uses frozen offline dev dependency install`); } else { failures.push(`${relativePath}: deps target must run pnpm install --frozen-lockfile --prod=false --offline`); } if (PNPM_BUILDKIT_CACHE_PATTERN.test(depsBody)) { passes.push(`${relativePath}: deps target uses BuildKit pnpm store cache`); } else { failures.push(`${relativePath}: deps target must use BuildKit cache mount for /pnpm/store during pnpm install`); } if (workspaceManifests.length > 0 && workspaceManifests.every((workspaceManifest) => { const copyPattern = new RegExp(`^COPY\\s+${escapeRegExp(workspaceManifest)}\\s+${escapeRegExp(workspaceManifest)}\\s*$`, "m"); return copyPattern.test(depsBody); })) { passes.push(`${relativePath}: deps stage copies all workspace package manifests before install`); } } if (!productionDepsBody) { failures.push(`${relativePath}: must define production-deps target with "FROM base AS production-deps"`); } else { if (PRODUCTION_PRUNE_REUSE_DEPS_PATTERN.test(productionDepsBody)) { passes.push(`${relativePath}: production-deps reuses deps filesystem for prune`); } else { failures.push(`${relativePath}: production-deps target must copy --from=deps /myapp /myapp before pnpm prune --prod`); } if (FULL_WORKSPACE_SOURCE_COPY_PATTERN.test(productionDepsBody)) { failures.push(`${relativePath}: production-deps target must not copy full apps or packages source before pnpm prune`); } else { passes.push(`${relativePath}: production-deps target does not recopy full workspace source before prune`); } } return { passes, failures, observations: [] }; } export function checkDockerfileWorkerContract(rootDir) { const relativePath = "Dockerfile"; const dockerfile = fs.readFileSync(path.join(rootDir, relativePath), "utf8"); const passes = []; const failures = []; const workerBody = getDockerfileTargetBody(dockerfile, "worker"); if (WORKER_TARGET_PATTERN.test(dockerfile)) { passes.push(`${relativePath}: defines named worker target from base`); } else { failures.push(`${relativePath}: must define worker target with "FROM base AS worker"`); } if (workerBody && WORKER_RUNTIME_PATTERN.test(workerBody)) { passes.push(`${relativePath}: worker runtime invokes @toolctrl/worker production start path`); } else { failures.push(`${relativePath}: worker target must invoke node apps/worker/dist/index.js directly`); } if (workerBody && WORKER_HEALTH_PATTERN.test(workerBody)) { passes.push(`${relativePath}: worker target invokes worker health path directly`); } else { failures.push(`${relativePath}: worker target must invoke node apps/worker/dist/health.js directly`); } if (workerBody && APP_REVISION_ARG_PATTERN.test(workerBody) && APP_REVISION_ENV_PATTERN.test(workerBody) && APP_REVISION_LABEL_PATTERN.test(workerBody)) { passes.push(`${relativePath}: worker target persists APP_REVISION in env and OCI label`); } else { failures.push(`${relativePath}: worker target must persist APP_REVISION in ENV and org.opencontainers.image.revision label`); } if (workerBody && /\btsx\b/.test(workerBody)) { failures.push(`${relativePath}: worker target must not use tsx at runtime`); } else if (workerBody) { passes.push(`${relativePath}: worker target runtime does not use tsx`); } if (workerBody && RECURSIVE_CHOWN_PATTERN.test(workerBody)) { failures.push(`${relativePath}: worker target must not use recursive chown in the runtime image`); } else if (workerBody) { passes.push(`${relativePath}: worker target avoids recursive runtime chown`); } for (const [copyName, copyPattern] of WORKER_REQUIRED_COPY_PATTERNS) { if (workerBody && copyPattern.test(workerBody)) { passes.push(`${relativePath}: worker target copies ${copyName}`); } else { failures.push(`${relativePath}: worker target must copy /myapp/${copyName}`); } } return { passes, failures, observations: [] }; } export function checkDockerfileApiContract(rootDir) { const relativePath = "Dockerfile"; const dockerfile = fs.readFileSync(path.join(rootDir, relativePath), "utf8"); const passes = []; const failures = []; const apiBody = getDockerfileTargetBody(dockerfile, "api"); if (API_TARGET_PATTERN.test(dockerfile)) { passes.push(`${relativePath}: defines named api target from base`); } else { failures.push(`${relativePath}: must define api target with "FROM base AS api"`); } if (apiBody && API_RUNTIME_PATTERN.test(apiBody)) { passes.push(`${relativePath}: api runtime invokes @toolctrl/api production start path`); } else { failures.push(`${relativePath}: api target must invoke node apps/api/dist/server.js directly`); } if (apiBody && /\btsx\b/.test(apiBody)) { failures.push(`${relativePath}: api target must not use tsx at runtime`); } else if (apiBody) { passes.push(`${relativePath}: api target runtime does not use tsx`); } if (apiBody && RECURSIVE_CHOWN_PATTERN.test(apiBody)) { failures.push(`${relativePath}: api target must not use recursive chown in the runtime image`); } else if (apiBody) { passes.push(`${relativePath}: api target avoids recursive runtime chown`); } if (apiBody && API_HEALTHCHECK_PATTERN.test(apiBody)) { passes.push(`${relativePath}: api target contains HEALTHCHECK using /healthz`); } else { failures.push(`${relativePath}: api target HEALTHCHECK must probe http://127.0.0.1:\${API_PORT:-\${PORT}}/healthz`); } if (apiBody && EXPOSE_8080_PATTERN.test(apiBody)) { passes.push(`${relativePath}: api target exposes port 8080`); } else { failures.push(`${relativePath}: api target must declare EXPOSE 8080 for Coolify port discovery`); } if (apiBody && APP_REVISION_ARG_PATTERN.test(apiBody) && APP_REVISION_ENV_PATTERN.test(apiBody) && APP_REVISION_LABEL_PATTERN.test(apiBody)) { passes.push(`${relativePath}: api target persists APP_REVISION in env and OCI label`); } else { failures.push(`${relativePath}: api target must persist APP_REVISION in ENV and org.opencontainers.image.revision label`); } for (const [copyName, copyPattern] of API_REQUIRED_COPY_PATTERNS) { if (apiBody && copyPattern.test(apiBody)) { passes.push(`${relativePath}: api target copies ${copyName}`); } else { failures.push(`${relativePath}: api target must copy /myapp/${copyName}`); } } return { passes, failures, observations: [] }; } export function verifyDockerBaseline(rootDir = process.cwd()) { const requiredFilesResult = checkRequiredFiles(rootDir); const results = [requiredFilesResult]; if (fs.existsSync(path.join(rootDir, "Dockerfile"))) { results.push(checkDockerfileDepsManifestContract(rootDir)); results.push(checkDockerfileWebContract(rootDir)); results.push(checkDockerfileWorkerContract(rootDir)); results.push(checkDockerfileApiContract(rootDir)); } if (fs.existsSync(path.join(rootDir, ".dockerignore"))) { results.push(checkDockerignoreContract(rootDir)); } if (fs.existsSync(path.join(rootDir, "start.sh"))) { results.push(checkStartScript(rootDir)); } if (fs.existsSync(path.join(rootDir, "docker-compose.yml")) && fs.existsSync(path.join(rootDir, "docker-compose.staging.yml"))) { results.push(checkDockerComposeWorkerTopology(rootDir)); } const manifestChecks = [ ["package.json", checkRootManifest], ["apps/web/package.json", checkWebManifest], ["apps/api/package.json", checkApiManifest], ["apps/worker/package.json", checkWorkerManifest], ]; for (const [relativePath, check] of manifestChecks) { if (fs.existsSync(path.join(rootDir, relativePath))) { try { results.push(check(rootDir)); } catch (error) { results.push({ passes: [], failures: [`${manifestPath(relativePath)}: failed to read manifest JSON: ${error.message}`], observations: [], }); } } } return results.reduce( (result, check) => ({ passes: [...result.passes, ...check.passes], failures: [...result.failures, ...check.failures], observations: [...result.observations, ...check.observations], }), { passes: [], failures: [], observations: [] }, ); } export function formatVerificationResult(result) { const status = result.failures.length > 0 ? "FAIL" : "PASS"; return [ `${status} Docker baseline verification`, ...result.passes.map((pass) => `PASS ${pass}`), ...result.observations.map((observation) => `OBSERVE ${observation}`), ...result.failures.map((failure) => `FAIL ${failure}`), `Summary: ${result.passes.length} passed, ${result.observations.length} observed, ${result.failures.length} failed`, "", ].join("\n"); } const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); if (isCli) { const result = verifyDockerBaseline(process.cwd()); const output = formatVerificationResult(result); if (result.failures.length > 0) { process.stderr.write(output); process.exitCode = 1; } else { process.stdout.write(output); } }