149 lines
8.6 KiB
JavaScript
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);
|