From 1052469397787f17e6a96a410dcef6147b118d12 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 25 Mar 2026 08:57:10 +0100 Subject: [PATCH] feat(infra): extend Dockerfile validator to backends and services Validator now checks 52 Dockerfiles (web + backend + service). Fixed 10 missing COPYs across backends, services, and nestjs-base. Generator also supports backend/service Dockerfiles with markers. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/manadeck/apps/backend/Dockerfile | 4 + apps/nutriphi/apps/backend/Dockerfile | 1 + apps/planta/apps/backend/Dockerfile | 4 + docker/Dockerfile.nestjs-base | 2 + scripts/generate-dockerfiles.mjs | 135 +++++++-- scripts/validate-dockerfiles.mjs | 304 +++++++++++++++------ services/mana-api-gateway/Dockerfile | 2 + services/mana-notify/Dockerfile | 2 + services/matrix-nutriphi-bot/Dockerfile | 1 + services/matrix-ollama-bot/Dockerfile | 1 + services/matrix-project-doc-bot/Dockerfile | 1 + services/matrix-zitare-bot/Dockerfile | 1 + 12 files changed, 343 insertions(+), 115 deletions(-) diff --git a/apps/manadeck/apps/backend/Dockerfile b/apps/manadeck/apps/backend/Dockerfile index 38b6daeba..7ce189a4a 100644 --- a/apps/manadeck/apps/backend/Dockerfile +++ b/apps/manadeck/apps/backend/Dockerfile @@ -18,6 +18,7 @@ COPY packages/credit-operations ./packages/credit-operations COPY packages/mana-core-nestjs-integration ./packages/mana-core-nestjs-integration COPY packages/manadeck-database ./packages/manadeck-database COPY packages/shared-errors ./packages/shared-errors +COPY packages/shared-llm ./packages/shared-llm COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth COPY packages/shared-tsconfig ./packages/shared-tsconfig COPY packages/shared-error-tracking ./packages/shared-error-tracking @@ -33,6 +34,9 @@ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install WORKDIR /app/packages/shared-errors RUN pnpm build +WORKDIR /app/packages/shared-llm +RUN pnpm build + WORKDIR /app/packages/shared-nestjs-auth RUN pnpm build diff --git a/apps/nutriphi/apps/backend/Dockerfile b/apps/nutriphi/apps/backend/Dockerfile index 21b1273d1..29ebb784a 100644 --- a/apps/nutriphi/apps/backend/Dockerfile +++ b/apps/nutriphi/apps/backend/Dockerfile @@ -15,6 +15,7 @@ COPY patches ./patches # Copy shared packages COPY packages/shared-tsconfig ./packages/shared-tsconfig +COPY packages/shared-llm ./packages/shared-llm COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup COPY packages/shared-nestjs-health ./packages/shared-nestjs-health diff --git a/apps/planta/apps/backend/Dockerfile b/apps/planta/apps/backend/Dockerfile index 92d55b2a1..49298a90c 100644 --- a/apps/planta/apps/backend/Dockerfile +++ b/apps/planta/apps/backend/Dockerfile @@ -16,6 +16,7 @@ COPY patches ./patches # Copy shared packages (all required dependencies) COPY packages/shared-drizzle-config ./packages/shared-drizzle-config COPY packages/shared-errors ./packages/shared-errors +COPY packages/shared-llm ./packages/shared-llm COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth COPY packages/shared-nestjs-health ./packages/shared-nestjs-health COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics @@ -36,6 +37,9 @@ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install WORKDIR /app/packages/shared-errors RUN pnpm build +WORKDIR /app/packages/shared-llm +RUN pnpm build + WORKDIR /app/packages/shared-nestjs-auth RUN pnpm build diff --git a/docker/Dockerfile.nestjs-base b/docker/Dockerfile.nestjs-base index 96631e80c..8d690e624 100644 --- a/docker/Dockerfile.nestjs-base +++ b/docker/Dockerfile.nestjs-base @@ -33,6 +33,7 @@ COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth COPY packages/shared-nestjs-health ./packages/shared-nestjs-health COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup +COPY packages/shared-llm ./packages/shared-llm COPY packages/shared-storage ./packages/shared-storage COPY packages/shared-tsconfig ./packages/shared-tsconfig @@ -48,6 +49,7 @@ RUN cd packages/shared-errors && pnpm build \ && cd /app/packages/shared-nestjs-setup && pnpm build \ && cd /app/packages/shared-error-tracking && pnpm build \ && cd /app/packages/shared-storage && pnpm build \ + && cd /app/packages/shared-llm && pnpm build \ && cd /app/packages/shared-drizzle-config && pnpm build 2>/dev/null || true \ && cd /app/packages/credit-operations && pnpm build \ && cd /app/packages/mana-core-nestjs-integration && pnpm build \ diff --git a/scripts/generate-dockerfiles.mjs b/scripts/generate-dockerfiles.mjs index 6b40b7b20..91be8ef19 100755 --- a/scripts/generate-dockerfiles.mjs +++ b/scripts/generate-dockerfiles.mjs @@ -1,8 +1,13 @@ #!/usr/bin/env node -// Generate COPY statements in web app Dockerfiles from package.json workspace dependencies. +// Generate COPY statements in Dockerfiles from package.json workspace dependencies. // -// For each apps/{name}/apps/web/Dockerfile, reads the corresponding package.json, +// Processes: +// - apps/{name}/apps/web/Dockerfile (web apps) +// - apps/{name}/apps/backend/Dockerfile (app backends, only if markers exist) +// - services/{name}/Dockerfile (services, only if markers exist) +// +// For each Dockerfile, reads the corresponding package.json, // resolves workspace dependencies to their directory paths, and updates the // COPY block between marker comments. // @@ -21,7 +26,6 @@ const isCheck = process.argv.includes('--check'); // --------------------------------------------------------------------------- // Package map: package name -> directory path relative to repo root -// Reuses the same logic from validate-dockerfiles.mjs // --------------------------------------------------------------------------- function buildPackageMap() { const map = new Map(); @@ -57,6 +61,25 @@ function buildPackageMap() { } } + // services/*/packages/* + const servicesDir = join(ROOT, 'services'); + if (existsSync(servicesDir)) { + for (const svcEntry of readdirSync(servicesDir, { withFileTypes: true })) { + if (!svcEntry.isDirectory()) continue; + const svcPkgsDir = join(servicesDir, svcEntry.name, 'packages'); + if (!existsSync(svcPkgsDir)) continue; + for (const pkgEntry of readdirSync(svcPkgsDir, { withFileTypes: true })) { + if (!pkgEntry.isDirectory()) continue; + const pkgJsonPath = join(svcPkgsDir, pkgEntry.name, 'package.json'); + if (!existsSync(pkgJsonPath)) continue; + try { + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + map.set(pkg.name, `services/${svcEntry.name}/packages/${pkgEntry.name}`); + } catch {} + } + } + } + return map; } @@ -100,9 +123,10 @@ function generateCopyBlock(workspaceDeps, packageMap) { depPaths.sort(); - // Root packages first, then app-specific packages + // Root packages first, then app-specific packages, then service packages const rootPackages = depPaths.filter((p) => p.startsWith('packages/')); const appPackages = depPaths.filter((p) => p.startsWith('apps/')); + const servicePackages = depPaths.filter((p) => p.startsWith('services/')); for (const p of rootPackages) { copyLines.push(`COPY ${p} ./${p}`); @@ -110,6 +134,9 @@ function generateCopyBlock(workspaceDeps, packageMap) { for (const p of appPackages) { copyLines.push(`COPY ${p} ./${p}`); } + for (const p of servicePackages) { + copyLines.push(`COPY ${p} ./${p}`); + } if (unknownDeps.length > 0) { copyLines.push(''); @@ -308,7 +335,7 @@ function insertMarkersAndBlock(lines, copyLines, generatedPaths, appName) { } // COPY for apps/{appName}/packages (app-specific workspace packages) - if (trimmed.match(new RegExp(`^COPY\\s+apps/${appName}/packages`))) { + if (appName && trimmed.match(new RegExp(`^COPY\\s+apps/${appName}/packages`))) { i++; continue; } @@ -372,64 +399,112 @@ function cleanBlankLines(lines) { return result; } +// --------------------------------------------------------------------------- +// Process a single Dockerfile entry (shared logic for all types) +// --------------------------------------------------------------------------- +function processEntry(dockerfilePath, pkgJsonPath, relPath, appName, packageMap, stats) { + if (!existsSync(pkgJsonPath)) { + console.error(` ERROR: ${relPath} - package.json not found`); + stats.errors++; + return; + } + + const original = readFileSync(dockerfilePath, 'utf8'); + const workspaceDeps = getWorkspaceDeps(pkgJsonPath); + const copyLines = generateCopyBlock(workspaceDeps, packageMap); + const updated = processDockerfile(original, appName, copyLines); + + if (updated !== original) { + if (isCheck) { + console.log(` NEEDS UPDATE: ${relPath}`); + stats.changed++; + } else { + writeFileSync(dockerfilePath, updated, 'utf8'); + console.log(` UPDATED: ${relPath} (${workspaceDeps.length} deps)`); + stats.changed++; + } + } else { + console.log(` OK: ${relPath} (${workspaceDeps.length} deps)`); + stats.unchanged++; + } +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- function main() { const packageMap = buildPackageMap(); const appsDir = join(ROOT, 'apps'); - let changed = 0; - let unchanged = 0; - let errors = 0; + const servicesDir = join(ROOT, 'services'); + const stats = { changed: 0, unchanged: 0, errors: 0 }; const appDirs = readdirSync(appsDir, { withFileTypes: true }) .filter((e) => e.isDirectory()) .map((e) => e.name) .sort(); + // --- Web app Dockerfiles (always process, insert markers if missing) --- + console.log('=== Web App Dockerfiles ==='); for (const appName of appDirs) { const dockerfilePath = join(appsDir, appName, 'apps', 'web', 'Dockerfile'); if (!existsSync(dockerfilePath)) continue; const pkgJsonPath = join(appsDir, appName, 'apps', 'web', 'package.json'); - if (!existsSync(pkgJsonPath)) { - console.error(` ERROR: ${appName} - package.json not found`); - errors++; + const relPath = `apps/${appName}/apps/web/Dockerfile`; + processEntry(dockerfilePath, pkgJsonPath, relPath, appName, packageMap, stats); + } + + // --- Backend app Dockerfiles (only if markers already exist) --- + console.log('\n=== Backend App Dockerfiles ==='); + for (const appName of appDirs) { + const dockerfilePath = join(appsDir, appName, 'apps', 'backend', 'Dockerfile'); + if (!existsSync(dockerfilePath)) continue; + + const content = readFileSync(dockerfilePath, 'utf8'); + if (!content.includes(START_MARKER)) { + console.log(` SKIP: apps/${appName}/apps/backend/Dockerfile (no markers)`); continue; } - const relPath = `apps/${appName}/apps/web/Dockerfile`; - const original = readFileSync(dockerfilePath, 'utf8'); + const pkgJsonPath = join(appsDir, appName, 'apps', 'backend', 'package.json'); + const relPath = `apps/${appName}/apps/backend/Dockerfile`; + processEntry(dockerfilePath, pkgJsonPath, relPath, appName, packageMap, stats); + } - const workspaceDeps = getWorkspaceDeps(pkgJsonPath); - const copyLines = generateCopyBlock(workspaceDeps, packageMap); - const updated = processDockerfile(original, appName, copyLines); + // --- Service Dockerfiles (only if markers already exist) --- + console.log('\n=== Service Dockerfiles ==='); + if (existsSync(servicesDir)) { + const svcDirs = readdirSync(servicesDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); - if (updated !== original) { - if (isCheck) { - console.log(` NEEDS UPDATE: ${relPath}`); - changed++; - } else { - writeFileSync(dockerfilePath, updated, 'utf8'); - console.log(` UPDATED: ${relPath} (${workspaceDeps.length} deps)`); - changed++; + for (const svcName of svcDirs) { + const dockerfilePath = join(servicesDir, svcName, 'Dockerfile'); + if (!existsSync(dockerfilePath)) continue; + + const content = readFileSync(dockerfilePath, 'utf8'); + if (!content.includes(START_MARKER)) { + console.log(` SKIP: services/${svcName}/Dockerfile (no markers)`); + continue; } - } else { - console.log(` OK: ${relPath} (${workspaceDeps.length} deps)`); - unchanged++; + + const pkgJsonPath = join(servicesDir, svcName, 'package.json'); + const relPath = `services/${svcName}/Dockerfile`; + processEntry(dockerfilePath, pkgJsonPath, relPath, svcName, packageMap, stats); } } console.log(''); console.log( - `Processed ${changed + unchanged + errors} Dockerfiles: ${changed} ${isCheck ? 'need updates' : 'updated'}, ${unchanged} unchanged, ${errors} errors` + `Processed ${stats.changed + stats.unchanged + stats.errors} Dockerfiles: ${stats.changed} ${isCheck ? 'need updates' : 'updated'}, ${stats.unchanged} unchanged, ${stats.errors} errors` ); - if (isCheck && changed > 0) { + if (isCheck && stats.changed > 0) { console.log('\nRun `pnpm generate:dockerfiles` to fix.'); process.exit(1); } - if (errors > 0) { + if (stats.errors > 0) { process.exit(1); } } diff --git a/scripts/validate-dockerfiles.mjs b/scripts/validate-dockerfiles.mjs index 76760fb4a..120166123 100755 --- a/scripts/validate-dockerfiles.mjs +++ b/scripts/validate-dockerfiles.mjs @@ -1,8 +1,13 @@ #!/usr/bin/env node /** - * Validate that all web app Dockerfiles include COPY statements + * Validate that Dockerfiles include COPY statements * for every workspace dependency listed in package.json. + * + * Checks: + * - apps/star/apps/web/Dockerfile -- web apps + * - apps/star/apps/backend/Dockerfile -- app backends + * - services/star/Dockerfile -- standalone services */ import { readFileSync, readdirSync, existsSync } from 'node:fs'; @@ -11,7 +16,7 @@ import { join, resolve } from 'node:path'; const ROOT = resolve(import.meta.dirname, '..'); // Build a map of package name -> directory path (relative to repo root) -// for both packages/* and apps/*/packages/* +// for packages/*, apps/*/packages/*, and services/*/packages/* function buildPackageMap() { const map = new Map(); @@ -48,6 +53,25 @@ function buildPackageMap() { } } + // services/*/packages/* + const servicesDir = join(ROOT, 'services'); + if (existsSync(servicesDir)) { + for (const svcEntry of readdirSync(servicesDir, { withFileTypes: true })) { + if (!svcEntry.isDirectory()) continue; + const svcPkgsDir = join(servicesDir, svcEntry.name, 'packages'); + if (!existsSync(svcPkgsDir)) continue; + for (const pkgEntry of readdirSync(svcPkgsDir, { withFileTypes: true })) { + if (!pkgEntry.isDirectory()) continue; + const pkgJsonPath = join(svcPkgsDir, pkgEntry.name, 'package.json'); + if (!existsSync(pkgJsonPath)) continue; + try { + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + map.set(pkg.name, `services/${svcEntry.name}/packages/${pkgEntry.name}`); + } catch {} + } + } + } + return map; } @@ -66,7 +90,25 @@ function getWorkspaceDeps(pkgJsonPath) { return deps; } -// Extract COPY paths from Dockerfile (only from builder stage, before RUN install) +// Check if a Dockerfile uses nestjs-base:local as its base image +function usesNestjsBase(dockerfilePath) { + const content = readFileSync(dockerfilePath, 'utf8'); + return content.includes('FROM nestjs-base:local'); +} + +// Check if a Dockerfile is a non-monorepo build (standalone, no workspace COPY needed) +function isStandaloneBuild(dockerfilePath) { + const content = readFileSync(dockerfilePath, 'utf8'); + // If Dockerfile uses npm (not pnpm) or doesn't copy pnpm-workspace.yaml, it's standalone + // Also skip Python-based services + return ( + (!content.includes('pnpm-workspace.yaml') && !content.includes('pnpm-lock.yaml')) || + content.includes('FROM python:') || + content.includes('pip install') + ); +} + +// Extract COPY paths from Dockerfile function getDockerfileCopyPaths(dockerfilePath) { const content = readFileSync(dockerfilePath, 'utf8'); const lines = content.split('\n'); @@ -77,7 +119,8 @@ function getDockerfileCopyPaths(dockerfilePath) { const trimmed = line.trim(); // Match COPY statements like: COPY packages/shared-utils ./packages/shared-utils // or COPY apps/zitare/packages/content ./apps/zitare/packages/content - const copyMatch = trimmed.match(/^COPY\s+((?:packages|apps)\/\S+)/); + // or COPY services/mana-core-auth ./services/mana-core-auth + const copyMatch = trimmed.match(/^COPY\s+((?:packages|apps|services)\/\S+)/); if (copyMatch) { copyPaths.add(copyMatch[1]); } @@ -90,6 +133,22 @@ function getDockerfileCopyPaths(dockerfilePath) { return { copyPaths, hasPatchesCopy }; } +// Get packages pre-built in nestjs-base image +function getNestjsBasePackages() { + const baseDockerfile = join(ROOT, 'docker', 'Dockerfile.nestjs-base'); + if (!existsSync(baseDockerfile)) return new Set(); + const content = readFileSync(baseDockerfile, 'utf8'); + const paths = new Set(); + for (const line of content.split('\n')) { + const trimmed = line.trim(); + const copyMatch = trimmed.match(/^COPY\s+(packages\/\S+)/); + if (copyMatch) { + paths.add(copyMatch[1]); + } + } + return paths; +} + // Extract @scope/package imports from a source file function extractImports(filePath) { if (!existsSync(filePath)) return []; @@ -112,122 +171,197 @@ function extractImports(filePath) { return [...imports]; } -function main() { - const packageMap = buildPackageMap(); - const appsDir = join(ROOT, 'apps'); - let hasErrors = false; - const results = []; +// Validate a single Dockerfile and return result +function validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, opts = {}) { + const { + isNestjsBase = false, + nestjsBasePackagePaths = new Set(), + checkImports = false, + appDir = null, + checkPatches = false, + } = opts; - // Find all web app Dockerfiles - const appDirs = readdirSync(appsDir, { withFileTypes: true }) - .filter((e) => e.isDirectory()) - .map((e) => e.name) - .sort(); + if (!existsSync(pkgJsonPath)) { + return { + path: relPath, + errors: ['package.json not found in same directory'], + warnings: [], + depCount: 0, + }; + } - for (const appName of appDirs) { - const dockerfilePath = join(appsDir, appName, 'apps', 'web', 'Dockerfile'); - if (!existsSync(dockerfilePath)) continue; + const workspaceDeps = getWorkspaceDeps(pkgJsonPath); + const { copyPaths, hasPatchesCopy } = getDockerfileCopyPaths(dockerfilePath); + const errors = []; + const warnings = []; - const pkgJsonPath = join(appsDir, appName, 'apps', 'web', 'package.json'); - if (!existsSync(pkgJsonPath)) { - results.push({ - path: `apps/${appName}/apps/web/Dockerfile`, - errors: ['package.json not found in same directory'], - warnings: [], - depCount: 0, - }); - hasErrors = true; + // Check each workspace dep has a corresponding COPY + for (const dep of workspaceDeps) { + const dirPath = packageMap.get(dep); + if (!dirPath) { + warnings.push(`UNKNOWN PACKAGE: ${dep} (not found in workspace)`); continue; } - const workspaceDeps = getWorkspaceDeps(pkgJsonPath); - const { copyPaths, hasPatchesCopy } = getDockerfileCopyPaths(dockerfilePath); - const errors = []; - const warnings = []; - - // Check each workspace dep has a corresponding COPY - for (const dep of workspaceDeps) { - const dirPath = packageMap.get(dep); - if (!dirPath) { - warnings.push(`UNKNOWN PACKAGE: ${dep} (not found in workspace)`); - continue; - } - // Check if any COPY path matches or is a parent directory of this package - // e.g. "apps/calendar/packages" covers "apps/calendar/packages/shared" - const found = [...copyPaths].some( - (cp) => cp === dirPath || dirPath.startsWith(cp + '/') || cp.startsWith(dirPath) + // For nestjs-base backends, packages/* are pre-built in the base image + if (isNestjsBase && dirPath.startsWith('packages/')) { + const isCoveredByBase = [...nestjsBasePackagePaths].some( + (bp) => bp === dirPath || dirPath.startsWith(bp + '/') || bp.startsWith(dirPath) ); - if (!found) { - errors.push(`MISSING: ${dep} → add: COPY ${dirPath} ./${dirPath}`); - } + if (isCoveredByBase) continue; } - // Check patches - if (!hasPatchesCopy) { - errors.push('MISSING: patches/ directory → add: COPY patches/ ./patches/'); + // Check if any COPY path matches or is a parent directory of this package + const found = [...copyPaths].some( + (cp) => cp === dirPath || dirPath.startsWith(cp + '/') || cp.startsWith(dirPath) + ); + if (!found) { + errors.push(`MISSING: ${dep} → add: COPY ${dirPath} ./${dirPath}`); } + } - // Check for imports in hooks.server.ts and vite.config.ts not in package.json - const hooksPath = join(appsDir, appName, 'apps', 'web', 'src', 'hooks.server.ts'); - const vitePath = join(appsDir, appName, 'apps', 'web', 'vite.config.ts'); + // Check patches (only for web apps that need them) + if (checkPatches && !isNestjsBase && !hasPatchesCopy) { + errors.push('MISSING: patches/ directory → add: COPY patches/ ./patches/'); + } + + // Check for imports in hooks.server.ts and vite.config.ts not in package.json (web apps only) + if (checkImports && appDir) { + const hooksPath = join(appDir, 'src', 'hooks.server.ts'); + const vitePath = join(appDir, 'vite.config.ts'); for (const filePath of [hooksPath, vitePath]) { const imports = extractImports(filePath); const fileName = filePath.includes('hooks.server') ? 'hooks.server.ts' : 'vite.config.ts'; for (const imp of imports) { - // Only check workspace packages (ones we know about) if (packageMap.has(imp) && !workspaceDeps.includes(imp)) { warnings.push(`NOT IN package.json: ${imp} (imported in ${fileName})`); } } } - - if (errors.length > 0) hasErrors = true; - - results.push({ - path: `apps/${appName}/apps/web/Dockerfile`, - errors, - warnings, - depCount: workspaceDeps.length, - }); } - // Print results - for (const result of results) { - const totalIssues = result.errors.length + result.warnings.length; - if (totalIssues === 0) { - console.log(`\u2713 ${result.path} - all ${result.depCount} deps covered`); - } else { - if (result.errors.length > 0) { + return { + path: relPath, + errors, + warnings, + depCount: workspaceDeps.length, + }; +} + +function printResult(result) { + const totalIssues = result.errors.length + result.warnings.length; + if (totalIssues === 0) { + console.log(`\u2713 ${result.path} - all ${result.depCount} deps covered`); + } else { + if (result.errors.length > 0) { + console.log( + `\u2717 ${result.path} - ${result.errors.length} issue${result.errors.length !== 1 ? 's' : ''}:` + ); + for (const err of result.errors) { + console.log(` ${err}`); + } + } + if (result.warnings.length > 0) { + if (result.errors.length === 0) { console.log( - `\u2717 ${result.path} - ${result.errors.length} issue${result.errors.length !== 1 ? 's' : ''}:` + `\u26A0 ${result.path} - ${result.warnings.length} warning${result.warnings.length !== 1 ? 's' : ''}:` ); - for (const err of result.errors) { - console.log(` ${err}`); - } } - if (result.warnings.length > 0) { - if (result.errors.length === 0) { - console.log( - `\u26A0 ${result.path} - ${result.warnings.length} warning${result.warnings.length !== 1 ? 's' : ''}:` - ); - } - for (const warn of result.warnings) { - console.log(` \u26A0 ${warn}`); - } - } - if (result.errors.length === 0 && result.warnings.length > 0) { - // Already printed the warning header above + for (const warn of result.warnings) { + console.log(` \u26A0 ${warn}`); } } } +} + +function main() { + const packageMap = buildPackageMap(); + const appsDir = join(ROOT, 'apps'); + const servicesDir = join(ROOT, 'services'); + let hasErrors = false; + const results = []; + const nestjsBasePackagePaths = getNestjsBasePackages(); + + // Find all app directories + const appDirs = readdirSync(appsDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + + // --- Web app Dockerfiles --- + console.log('=== Web App Dockerfiles ==='); + for (const appName of appDirs) { + const dockerfilePath = join(appsDir, appName, 'apps', 'web', 'Dockerfile'); + if (!existsSync(dockerfilePath)) continue; + + const pkgJsonPath = join(appsDir, appName, 'apps', 'web', 'package.json'); + const relPath = `apps/${appName}/apps/web/Dockerfile`; + const appDir = join(appsDir, appName, 'apps', 'web'); + + const result = validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, { + checkImports: true, + checkPatches: true, + appDir, + }); + if (result.errors.length > 0) hasErrors = true; + results.push(result); + printResult(result); + } + + // --- Backend app Dockerfiles --- + console.log('\n=== Backend App Dockerfiles ==='); + for (const appName of appDirs) { + const dockerfilePath = join(appsDir, appName, 'apps', 'backend', 'Dockerfile'); + if (!existsSync(dockerfilePath)) continue; + + const pkgJsonPath = join(appsDir, appName, 'apps', 'backend', 'package.json'); + const relPath = `apps/${appName}/apps/backend/Dockerfile`; + const isNestjsBase = usesNestjsBase(dockerfilePath); + + const result = validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, { + isNestjsBase, + nestjsBasePackagePaths, + }); + if (result.errors.length > 0) hasErrors = true; + results.push(result); + printResult(result); + } + + // --- Service Dockerfiles --- + console.log('\n=== Service Dockerfiles ==='); + if (existsSync(servicesDir)) { + const svcDirs = readdirSync(servicesDir, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + + for (const svcName of svcDirs) { + const dockerfilePath = join(servicesDir, svcName, 'Dockerfile'); + if (!existsSync(dockerfilePath)) continue; + + // Skip standalone (non-monorepo) builds + if (isStandaloneBuild(dockerfilePath)) continue; + + const pkgJsonPath = join(servicesDir, svcName, 'package.json'); + const relPath = `services/${svcName}/Dockerfile`; + + const result = validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap); + if (result.errors.length > 0) hasErrors = true; + results.push(result); + printResult(result); + } + } // Summary + const webCount = results.filter((r) => r.path.includes('/web/')).length; + const backendCount = results.filter((r) => r.path.includes('/backend/')).length; + const serviceCount = results.filter((r) => r.path.startsWith('services/')).length; const errorCount = results.reduce((sum, r) => sum + r.errors.length, 0); const warnCount = results.reduce((sum, r) => sum + r.warnings.length, 0); console.log(''); console.log( - `Checked ${results.length} Dockerfiles: ${errorCount} error${errorCount !== 1 ? 's' : ''}, ${warnCount} warning${warnCount !== 1 ? 's' : ''}` + `Checked ${results.length} Dockerfiles (${webCount} web, ${backendCount} backend, ${serviceCount} service): ${errorCount} error${errorCount !== 1 ? 's' : ''}, ${warnCount} warning${warnCount !== 1 ? 's' : ''}` ); if (hasErrors) { diff --git a/services/mana-api-gateway/Dockerfile b/services/mana-api-gateway/Dockerfile index 3344b4c38..7c36f7a12 100644 --- a/services/mana-api-gateway/Dockerfile +++ b/services/mana-api-gateway/Dockerfile @@ -11,6 +11,7 @@ FROM base AS deps COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY services/mana-api-gateway/package.json ./services/mana-api-gateway/ COPY packages/shared-nestjs-auth/package.json ./packages/shared-nestjs-auth/ +COPY packages/shared-drizzle-config/package.json ./packages/shared-drizzle-config/ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --filter @manacore/api-gateway... # Build the application @@ -19,6 +20,7 @@ COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/services/mana-api-gateway/node_modules ./services/mana-api-gateway/node_modules COPY --from=deps /app/packages/shared-nestjs-auth/node_modules ./packages/shared-nestjs-auth/node_modules 2>/dev/null || true COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth +COPY packages/shared-drizzle-config ./packages/shared-drizzle-config COPY services/mana-api-gateway ./services/mana-api-gateway WORKDIR /app/services/mana-api-gateway RUN pnpm build diff --git a/services/mana-notify/Dockerfile b/services/mana-notify/Dockerfile index 8328d9b47..0e50b0827 100644 --- a/services/mana-notify/Dockerfile +++ b/services/mana-notify/Dockerfile @@ -12,11 +12,13 @@ FROM base AS builder # Copy workspace files COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY services/mana-notify/package.json ./services/mana-notify/ +COPY packages/shared-drizzle-config/package.json ./packages/shared-drizzle-config/ # Install dependencies RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --filter @manacore/mana-notify # Copy source code +COPY packages/shared-drizzle-config ./packages/shared-drizzle-config COPY services/mana-notify ./services/mana-notify # Build diff --git a/services/matrix-nutriphi-bot/Dockerfile b/services/matrix-nutriphi-bot/Dockerfile index 3af3fccd8..ff5adad6b 100644 --- a/services/matrix-nutriphi-bot/Dockerfile +++ b/services/matrix-nutriphi-bot/Dockerfile @@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ # Copy shared packages that this bot depends on COPY packages/bot-services ./packages/bot-services COPY packages/matrix-bot-common ./packages/matrix-bot-common +COPY services/mana-media/packages/client ./services/mana-media/packages/client # Copy this bot COPY services/matrix-nutriphi-bot ./services/matrix-nutriphi-bot diff --git a/services/matrix-ollama-bot/Dockerfile b/services/matrix-ollama-bot/Dockerfile index 47f2e54c1..dd71b85cf 100644 --- a/services/matrix-ollama-bot/Dockerfile +++ b/services/matrix-ollama-bot/Dockerfile @@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ # Copy shared packages that this bot depends on COPY packages/bot-services ./packages/bot-services COPY packages/matrix-bot-common ./packages/matrix-bot-common +COPY packages/shared-llm ./packages/shared-llm # Copy this bot COPY services/matrix-ollama-bot ./services/matrix-ollama-bot diff --git a/services/matrix-project-doc-bot/Dockerfile b/services/matrix-project-doc-bot/Dockerfile index 5f9a4156f..9f813fb9d 100644 --- a/services/matrix-project-doc-bot/Dockerfile +++ b/services/matrix-project-doc-bot/Dockerfile @@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ # Copy shared packages that this bot depends on COPY packages/bot-services ./packages/bot-services COPY packages/matrix-bot-common ./packages/matrix-bot-common +COPY packages/shared-llm ./packages/shared-llm # Copy this bot COPY services/matrix-project-doc-bot ./services/matrix-project-doc-bot diff --git a/services/matrix-zitare-bot/Dockerfile b/services/matrix-zitare-bot/Dockerfile index e6c317165..7f8ccfc84 100644 --- a/services/matrix-zitare-bot/Dockerfile +++ b/services/matrix-zitare-bot/Dockerfile @@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ # Copy shared packages that this bot depends on COPY packages/bot-services ./packages/bot-services COPY packages/matrix-bot-common ./packages/matrix-bot-common +COPY apps/zitare/packages/content ./apps/zitare/packages/content # Copy this bot COPY services/matrix-zitare-bot ./services/matrix-zitare-bot