Files
projectbrain-Public/scripts/check-docker-build-readiness.mjs
T

149 lines
8.6 KiB
JavaScript

#!/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);