#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; const root = process.cwd(); const failures = []; const warnings = []; const read = (relativePath) => fs.readFileSync(path.join(root, relativePath), "utf8"); const exists = (relativePath) => fs.existsSync(path.join(root, relativePath)); const requiredBuiltDependencies = ["@prisma/client", "@prisma/engines", "@swc/core", "esbuild", "prisma"]; const parseWorkspaceAllowBuilds = (workspaceYaml) => { const allowed = new Set(); const allowBuildsIndex = workspaceYaml.indexOf("allowBuilds:"); if (allowBuildsIndex === -1) return allowed; const afterAllowBuilds = workspaceYaml.slice(allowBuildsIndex).split("\n").slice(1); for (const line of afterAllowBuilds) { if (/^[^\s#]/.test(line)) break; const match = line.match(/^\s*[\'"]?([^\'":]+)[\'"]?\s*:\s*(true|false)\s*(?:#.*)?$/); if (match && match[2] === "true") allowed.add(match[1].trim()); } return allowed; }; if (!exists("package.json")) { failures.push("Missing package.json."); } else { const rootPackage = JSON.parse(read("package.json")); if (rootPackage.packageManager !== "pnpm@11.5.2") { failures.push("package.json must pin packageManager to pnpm@11.5.2 for reproducible Docker/Corepack installs."); } if (rootPackage.pnpm) { failures.push("package.json must not contain a legacy pnpm field; pnpm 11 ignores it and emits noisy warnings during Docker builds. Keep build approvals in pnpm-workspace.yaml allowBuilds."); } } if (!exists("pnpm-workspace.yaml")) { failures.push("Missing pnpm-workspace.yaml; pnpm 11 Docker builds need workspace-level allowBuilds."); } else { const workspaceYaml = read("pnpm-workspace.yaml"); const workspaceAllowedBuilds = parseWorkspaceAllowBuilds(workspaceYaml); if (!workspaceYaml.includes("fetchWarnTimeout: 120000")) { warnings.push("pnpm-workspace.yaml should set fetchWarnTimeout: 120000 for quiet Docker registry fetch logs."); } for (const dependency of requiredBuiltDependencies) { if (!workspaceAllowedBuilds.has(dependency)) { failures.push(`pnpm-workspace.yaml allowBuilds must set ${dependency}: true for non-interactive pnpm 11 Docker installs.`); } } } for (const dockerfilePath of ["backend/Dockerfile", "Dockerfile.frontend"]) { if (!exists(dockerfilePath)) { failures.push(`Missing ${dockerfilePath}.`); continue; } const dockerfile = read(dockerfilePath); if (!dockerfile.includes("corepack enable")) warnings.push(`${dockerfilePath} does not explicitly enable Corepack.`); if (!dockerfile.includes("ENV CI=true")) warnings.push(`${dockerfilePath} should set CI=true to suppress interactive package-manager notices during Docker builds.`); if (!dockerfile.includes("COREPACK_ENABLE_DOWNLOAD_PROMPT=0")) warnings.push(`${dockerfilePath} should suppress Corepack download prompts for clean non-interactive Docker logs.`); if (!dockerfile.includes("npm_config_fetch_warn_timeout=60000")) warnings.push(`${dockerfilePath} should raise npm_config_fetch_warn_timeout to avoid noisy slow-registry warnings in Docker logs.`); if (!dockerfile.includes("pnpm install --frozen-lockfile")) failures.push(`${dockerfilePath} does not run pnpm install with a frozen lockfile.`); if (!dockerfile.includes('"[W" "ARN] Request took"')) failures.push(`${dockerfilePath} must filter pnpm slow-registry timing warnings from successful Docker install logs.`); } if (exists(".dockerignore")) { const dockerignore = read(".dockerignore"); if (!dockerignore.includes("backend/prisma/migrations/20260606200000_init_project_workspace")) { failures.push(".dockerignore must exclude the obsolete duplicate init migration so update overlays cannot copy it into Docker images."); } } else { failures.push("Missing .dockerignore."); } if (exists("backend/scripts/docker-entrypoint.sh")) { const entrypoint = read("backend/scripts/docker-entrypoint.sh"); if (!entrypoint.includes("20260606200000_init_project_workspace")) { failures.push("backend/scripts/docker-entrypoint.sh must guard against the obsolete duplicate init migration at runtime."); } if (!entrypoint.includes("prisma migrate resolve --rolled-back 20260606200000_init_project_workspace")) { failures.push("backend/scripts/docker-entrypoint.sh must recover failed obsolete duplicate init migration records with prisma migrate resolve --rolled-back."); } } if (exists("backend/prisma/migrations/20260606200000_init_project_workspace")) { failures.push("backend/prisma/migrations/20260606200000_init_project_workspace must be deleted from source; it duplicates the init migration and breaks fresh database startup."); } if (exists("backend/Dockerfile")) { const backendDockerfile = read("backend/Dockerfile"); if (!backendDockerfile.includes("pnpm run prisma:generate")) failures.push("backend/Dockerfile must generate Prisma client after install and copy."); if (!backendDockerfile.includes("scripts/docker-entrypoint.sh")) failures.push("backend/Dockerfile must use the database-aware entrypoint."); if (!backendDockerfile.includes("apk add --no-cache") || !backendDockerfile.includes("openssl")) failures.push("backend/Dockerfile must install openssl in the Alpine image so Prisma schema engines can run during migrate deploy."); if (!backendDockerfile.includes("libc6-compat")) warnings.push("backend/Dockerfile should install libc6-compat for native engine compatibility on Alpine."); if (!backendDockerfile.includes("PRISMA_SKIP_POSTINSTALL_GENERATE=true")) warnings.push("backend/Dockerfile should skip Prisma postinstall generate and run the explicit prisma:generate step after the schema is copied."); } if (exists("backend/prisma/schema.prisma")) { const prismaSchema = read("backend/prisma/schema.prisma"); if (!prismaSchema.includes("linux-musl-openssl-3.0.x")) failures.push("Prisma generator must include linux-musl-openssl-3.0.x binaryTarget for the Node Alpine Docker runtime."); } if (exists("backend/src/services/blueprintEngine.ts")) { const backendBlueprintEngine = read("backend/src/services/blueprintEngine.ts"); if (!backendBlueprintEngine.includes("const archetypes: Archetype[]")) { failures.push("backend/src/services/blueprintEngine.ts must type archetypes as Archetype[] so cluster includes checks accept every BlueprintClusterIdDto during backend builds."); } if (backendBlueprintEngine.includes("type Archetype = (typeof archetypes)[number]")) { failures.push("backend/src/services/blueprintEngine.ts must not infer Archetype from the const archetype array; that narrows clusters per object and breaks Docker backend tsc."); } } const result = { status: failures.length === 0 ? "PASS" : "FAIL", migrationRoots: ["backend/prisma/migrations"], requiredBuiltDependencies, workspaceAllowBuilds: exists("pnpm-workspace.yaml") ? Array.from(parseWorkspaceAllowBuilds(read("pnpm-workspace.yaml"))).sort() : [], opensslRuntime: exists("backend/Dockerfile") ? read("backend/Dockerfile").includes("openssl") : false, prismaMuslTarget: exists("backend/prisma/schema.prisma") ? read("backend/prisma/schema.prisma").includes("linux-musl-openssl-3.0.x") : false, obsoleteDuplicateInitMigrationExcluded: exists(".dockerignore") ? read(".dockerignore").includes("backend/prisma/migrations/20260606200000_init_project_workspace") : false, obsoleteDuplicateInitMigrationAbsent: !exists("backend/prisma/migrations/20260606200000_init_project_workspace"), failures, warnings, }; if (process.argv.includes("--json")) { console.log(JSON.stringify(result, null, 2)); } else { console.log(`ProjectBrain Docker build readiness: ${result.status}`); console.log(`Migration roots checked: ${result.migrationRoots.join(", ")}`); console.log(`Approved build dependencies required: ${requiredBuiltDependencies.join(", ")}`); console.log(`Workspace allowBuilds: ${exists("pnpm-workspace.yaml") ? Array.from(parseWorkspaceAllowBuilds(read("pnpm-workspace.yaml"))).sort().join(", ") : "missing"}`); console.log(`Backend OpenSSL runtime: ${exists("backend/Dockerfile") && read("backend/Dockerfile").includes("openssl") ? "yes" : "no"}`); console.log(`Prisma Alpine binary target: ${exists("backend/prisma/schema.prisma") && read("backend/prisma/schema.prisma").includes("linux-musl-openssl-3.0.x") ? "yes" : "no"}`); console.log(`Obsolete duplicate init migration excluded: ${exists(".dockerignore") && read(".dockerignore").includes("backend/prisma/migrations/20260606200000_init_project_workspace") ? "yes" : "no"}`); if (warnings.length > 0) { console.log("Warnings:"); for (const warning of warnings) console.log(`- ${warning}`); } if (failures.length > 0) { console.log("Failures:"); for (const failure of failures) console.log(`- ${failure}`); } } if (failures.length > 0) process.exit(1);