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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-25 08:57:10 +01:00
parent dd5c0d502f
commit 1052469397
12 changed files with 343 additions and 115 deletions

View file

@ -18,6 +18,7 @@ COPY packages/credit-operations ./packages/credit-operations
COPY packages/mana-core-nestjs-integration ./packages/mana-core-nestjs-integration COPY packages/mana-core-nestjs-integration ./packages/mana-core-nestjs-integration
COPY packages/manadeck-database ./packages/manadeck-database COPY packages/manadeck-database ./packages/manadeck-database
COPY packages/shared-errors ./packages/shared-errors 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-auth ./packages/shared-nestjs-auth
COPY packages/shared-tsconfig ./packages/shared-tsconfig COPY packages/shared-tsconfig ./packages/shared-tsconfig
COPY packages/shared-error-tracking ./packages/shared-error-tracking 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 WORKDIR /app/packages/shared-errors
RUN pnpm build RUN pnpm build
WORKDIR /app/packages/shared-llm
RUN pnpm build
WORKDIR /app/packages/shared-nestjs-auth WORKDIR /app/packages/shared-nestjs-auth
RUN pnpm build RUN pnpm build

View file

@ -15,6 +15,7 @@ COPY patches ./patches
# Copy shared packages # Copy shared packages
COPY packages/shared-tsconfig ./packages/shared-tsconfig 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-auth ./packages/shared-nestjs-auth
COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup
COPY packages/shared-nestjs-health ./packages/shared-nestjs-health COPY packages/shared-nestjs-health ./packages/shared-nestjs-health

View file

@ -16,6 +16,7 @@ COPY patches ./patches
# Copy shared packages (all required dependencies) # Copy shared packages (all required dependencies)
COPY packages/shared-drizzle-config ./packages/shared-drizzle-config COPY packages/shared-drizzle-config ./packages/shared-drizzle-config
COPY packages/shared-errors ./packages/shared-errors 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-auth ./packages/shared-nestjs-auth
COPY packages/shared-nestjs-health ./packages/shared-nestjs-health COPY packages/shared-nestjs-health ./packages/shared-nestjs-health
COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics 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 WORKDIR /app/packages/shared-errors
RUN pnpm build RUN pnpm build
WORKDIR /app/packages/shared-llm
RUN pnpm build
WORKDIR /app/packages/shared-nestjs-auth WORKDIR /app/packages/shared-nestjs-auth
RUN pnpm build RUN pnpm build

View file

@ -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-health ./packages/shared-nestjs-health
COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics COPY packages/shared-nestjs-metrics ./packages/shared-nestjs-metrics
COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup 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-storage ./packages/shared-storage
COPY packages/shared-tsconfig ./packages/shared-tsconfig 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-nestjs-setup && pnpm build \
&& cd /app/packages/shared-error-tracking && pnpm build \ && cd /app/packages/shared-error-tracking && pnpm build \
&& cd /app/packages/shared-storage && 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/shared-drizzle-config && pnpm build 2>/dev/null || true \
&& cd /app/packages/credit-operations && pnpm build \ && cd /app/packages/credit-operations && pnpm build \
&& cd /app/packages/mana-core-nestjs-integration && pnpm build \ && cd /app/packages/mana-core-nestjs-integration && pnpm build \

View file

@ -1,8 +1,13 @@
#!/usr/bin/env node #!/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 // resolves workspace dependencies to their directory paths, and updates the
// COPY block between marker comments. // 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 // Package map: package name -> directory path relative to repo root
// Reuses the same logic from validate-dockerfiles.mjs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function buildPackageMap() { function buildPackageMap() {
const map = new Map(); 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; return map;
} }
@ -100,9 +123,10 @@ function generateCopyBlock(workspaceDeps, packageMap) {
depPaths.sort(); 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 rootPackages = depPaths.filter((p) => p.startsWith('packages/'));
const appPackages = depPaths.filter((p) => p.startsWith('apps/')); const appPackages = depPaths.filter((p) => p.startsWith('apps/'));
const servicePackages = depPaths.filter((p) => p.startsWith('services/'));
for (const p of rootPackages) { for (const p of rootPackages) {
copyLines.push(`COPY ${p} ./${p}`); copyLines.push(`COPY ${p} ./${p}`);
@ -110,6 +134,9 @@ function generateCopyBlock(workspaceDeps, packageMap) {
for (const p of appPackages) { for (const p of appPackages) {
copyLines.push(`COPY ${p} ./${p}`); copyLines.push(`COPY ${p} ./${p}`);
} }
for (const p of servicePackages) {
copyLines.push(`COPY ${p} ./${p}`);
}
if (unknownDeps.length > 0) { if (unknownDeps.length > 0) {
copyLines.push(''); copyLines.push('');
@ -308,7 +335,7 @@ function insertMarkersAndBlock(lines, copyLines, generatedPaths, appName) {
} }
// COPY for apps/{appName}/packages (app-specific workspace packages) // 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++; i++;
continue; continue;
} }
@ -373,34 +400,16 @@ function cleanBlankLines(lines) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Main // Process a single Dockerfile entry (shared logic for all types)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function main() { function processEntry(dockerfilePath, pkgJsonPath, relPath, appName, packageMap, stats) {
const packageMap = buildPackageMap();
const appsDir = join(ROOT, 'apps');
let changed = 0;
let unchanged = 0;
let errors = 0;
const appDirs = readdirSync(appsDir, { withFileTypes: true })
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort();
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)) { if (!existsSync(pkgJsonPath)) {
console.error(` ERROR: ${appName} - package.json not found`); console.error(` ERROR: ${relPath} - package.json not found`);
errors++; stats.errors++;
continue; return;
} }
const relPath = `apps/${appName}/apps/web/Dockerfile`;
const original = readFileSync(dockerfilePath, 'utf8'); const original = readFileSync(dockerfilePath, 'utf8');
const workspaceDeps = getWorkspaceDeps(pkgJsonPath); const workspaceDeps = getWorkspaceDeps(pkgJsonPath);
const copyLines = generateCopyBlock(workspaceDeps, packageMap); const copyLines = generateCopyBlock(workspaceDeps, packageMap);
const updated = processDockerfile(original, appName, copyLines); const updated = processDockerfile(original, appName, copyLines);
@ -408,28 +417,94 @@ function main() {
if (updated !== original) { if (updated !== original) {
if (isCheck) { if (isCheck) {
console.log(` NEEDS UPDATE: ${relPath}`); console.log(` NEEDS UPDATE: ${relPath}`);
changed++; stats.changed++;
} else { } else {
writeFileSync(dockerfilePath, updated, 'utf8'); writeFileSync(dockerfilePath, updated, 'utf8');
console.log(` UPDATED: ${relPath} (${workspaceDeps.length} deps)`); console.log(` UPDATED: ${relPath} (${workspaceDeps.length} deps)`);
changed++; stats.changed++;
} }
} else { } else {
console.log(` OK: ${relPath} (${workspaceDeps.length} deps)`); console.log(` OK: ${relPath} (${workspaceDeps.length} deps)`);
unchanged++; stats.unchanged++;
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
function main() {
const packageMap = buildPackageMap();
const appsDir = join(ROOT, 'apps');
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');
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 pkgJsonPath = join(appsDir, appName, 'apps', 'backend', 'package.json');
const relPath = `apps/${appName}/apps/backend/Dockerfile`;
processEntry(dockerfilePath, pkgJsonPath, relPath, appName, packageMap, stats);
}
// --- 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();
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;
}
const pkgJsonPath = join(servicesDir, svcName, 'package.json');
const relPath = `services/${svcName}/Dockerfile`;
processEntry(dockerfilePath, pkgJsonPath, relPath, svcName, packageMap, stats);
} }
} }
console.log(''); console.log('');
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.'); console.log('\nRun `pnpm generate:dockerfiles` to fix.');
process.exit(1); process.exit(1);
} }
if (errors > 0) { if (stats.errors > 0) {
process.exit(1); process.exit(1);
} }
} }

View file

@ -1,8 +1,13 @@
#!/usr/bin/env node #!/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. * 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'; import { readFileSync, readdirSync, existsSync } from 'node:fs';
@ -11,7 +16,7 @@ import { join, resolve } from 'node:path';
const ROOT = resolve(import.meta.dirname, '..'); const ROOT = resolve(import.meta.dirname, '..');
// Build a map of package name -> directory path (relative to repo root) // 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() { function buildPackageMap() {
const map = new Map(); 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; return map;
} }
@ -66,7 +90,25 @@ function getWorkspaceDeps(pkgJsonPath) {
return deps; 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) { function getDockerfileCopyPaths(dockerfilePath) {
const content = readFileSync(dockerfilePath, 'utf8'); const content = readFileSync(dockerfilePath, 'utf8');
const lines = content.split('\n'); const lines = content.split('\n');
@ -77,7 +119,8 @@ function getDockerfileCopyPaths(dockerfilePath) {
const trimmed = line.trim(); const trimmed = line.trim();
// Match COPY statements like: COPY packages/shared-utils ./packages/shared-utils // Match COPY statements like: COPY packages/shared-utils ./packages/shared-utils
// or COPY apps/zitare/packages/content ./apps/zitare/packages/content // 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) { if (copyMatch) {
copyPaths.add(copyMatch[1]); copyPaths.add(copyMatch[1]);
} }
@ -90,6 +133,22 @@ function getDockerfileCopyPaths(dockerfilePath) {
return { copyPaths, hasPatchesCopy }; 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 // Extract @scope/package imports from a source file
function extractImports(filePath) { function extractImports(filePath) {
if (!existsSync(filePath)) return []; if (!existsSync(filePath)) return [];
@ -112,32 +171,23 @@ function extractImports(filePath) {
return [...imports]; return [...imports];
} }
function main() { // Validate a single Dockerfile and return result
const packageMap = buildPackageMap(); function validateDockerfile(dockerfilePath, pkgJsonPath, relPath, packageMap, opts = {}) {
const appsDir = join(ROOT, 'apps'); const {
let hasErrors = false; isNestjsBase = false,
const results = []; 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();
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)) { if (!existsSync(pkgJsonPath)) {
results.push({ return {
path: `apps/${appName}/apps/web/Dockerfile`, path: relPath,
errors: ['package.json not found in same directory'], errors: ['package.json not found in same directory'],
warnings: [], warnings: [],
depCount: 0, depCount: 0,
}); };
hasErrors = true;
continue;
} }
const workspaceDeps = getWorkspaceDeps(pkgJsonPath); const workspaceDeps = getWorkspaceDeps(pkgJsonPath);
@ -152,8 +202,16 @@ function main() {
warnings.push(`UNKNOWN PACKAGE: ${dep} (not found in workspace)`); warnings.push(`UNKNOWN PACKAGE: ${dep} (not found in workspace)`);
continue; continue;
} }
// 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 (isCoveredByBase) continue;
}
// Check if any COPY path matches or is a parent directory of this package // 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( const found = [...copyPaths].some(
(cp) => cp === dirPath || dirPath.startsWith(cp + '/') || cp.startsWith(dirPath) (cp) => cp === dirPath || dirPath.startsWith(cp + '/') || cp.startsWith(dirPath)
); );
@ -162,38 +220,36 @@ function main() {
} }
} }
// Check patches // Check patches (only for web apps that need them)
if (!hasPatchesCopy) { if (checkPatches && !isNestjsBase && !hasPatchesCopy) {
errors.push('MISSING: patches/ directory → add: COPY patches/ ./patches/'); errors.push('MISSING: patches/ directory → add: COPY patches/ ./patches/');
} }
// Check for imports in hooks.server.ts and vite.config.ts not in package.json // Check for imports in hooks.server.ts and vite.config.ts not in package.json (web apps only)
const hooksPath = join(appsDir, appName, 'apps', 'web', 'src', 'hooks.server.ts'); if (checkImports && appDir) {
const vitePath = join(appsDir, appName, 'apps', 'web', 'vite.config.ts'); const hooksPath = join(appDir, 'src', 'hooks.server.ts');
const vitePath = join(appDir, 'vite.config.ts');
for (const filePath of [hooksPath, vitePath]) { for (const filePath of [hooksPath, vitePath]) {
const imports = extractImports(filePath); const imports = extractImports(filePath);
const fileName = filePath.includes('hooks.server') ? 'hooks.server.ts' : 'vite.config.ts'; const fileName = filePath.includes('hooks.server') ? 'hooks.server.ts' : 'vite.config.ts';
for (const imp of imports) { for (const imp of imports) {
// Only check workspace packages (ones we know about)
if (packageMap.has(imp) && !workspaceDeps.includes(imp)) { if (packageMap.has(imp) && !workspaceDeps.includes(imp)) {
warnings.push(`NOT IN package.json: ${imp} (imported in ${fileName})`); warnings.push(`NOT IN package.json: ${imp} (imported in ${fileName})`);
} }
} }
} }
}
if (errors.length > 0) hasErrors = true; return {
path: relPath,
results.push({
path: `apps/${appName}/apps/web/Dockerfile`,
errors, errors,
warnings, warnings,
depCount: workspaceDeps.length, depCount: workspaceDeps.length,
}); };
} }
// Print results function printResult(result) {
for (const result of results) {
const totalIssues = result.errors.length + result.warnings.length; const totalIssues = result.errors.length + result.warnings.length;
if (totalIssues === 0) { if (totalIssues === 0) {
console.log(`\u2713 ${result.path} - all ${result.depCount} deps covered`); console.log(`\u2713 ${result.path} - all ${result.depCount} deps covered`);
@ -216,18 +272,96 @@ function main() {
console.log(` \u26A0 ${warn}`); console.log(` \u26A0 ${warn}`);
} }
} }
if (result.errors.length === 0 && result.warnings.length > 0) {
// Already printed the warning header above
} }
}
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 // 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 errorCount = results.reduce((sum, r) => sum + r.errors.length, 0);
const warnCount = results.reduce((sum, r) => sum + r.warnings.length, 0); const warnCount = results.reduce((sum, r) => sum + r.warnings.length, 0);
console.log(''); console.log('');
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) { if (hasErrors) {

View file

@ -11,6 +11,7 @@ FROM base AS deps
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY services/mana-api-gateway/package.json ./services/mana-api-gateway/ 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-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... RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --filter @manacore/api-gateway...
# Build the application # 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/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 --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-nestjs-auth ./packages/shared-nestjs-auth
COPY packages/shared-drizzle-config ./packages/shared-drizzle-config
COPY services/mana-api-gateway ./services/mana-api-gateway COPY services/mana-api-gateway ./services/mana-api-gateway
WORKDIR /app/services/mana-api-gateway WORKDIR /app/services/mana-api-gateway
RUN pnpm build RUN pnpm build

View file

@ -12,11 +12,13 @@ FROM base AS builder
# Copy workspace files # Copy workspace files
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY services/mana-notify/package.json ./services/mana-notify/ COPY services/mana-notify/package.json ./services/mana-notify/
COPY packages/shared-drizzle-config/package.json ./packages/shared-drizzle-config/
# Install dependencies # Install dependencies
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --filter @manacore/mana-notify RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --filter @manacore/mana-notify
# Copy source code # Copy source code
COPY packages/shared-drizzle-config ./packages/shared-drizzle-config
COPY services/mana-notify ./services/mana-notify COPY services/mana-notify ./services/mana-notify
# Build # Build

View file

@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
# Copy shared packages that this bot depends on # Copy shared packages that this bot depends on
COPY packages/bot-services ./packages/bot-services COPY packages/bot-services ./packages/bot-services
COPY packages/matrix-bot-common ./packages/matrix-bot-common COPY packages/matrix-bot-common ./packages/matrix-bot-common
COPY services/mana-media/packages/client ./services/mana-media/packages/client
# Copy this bot # Copy this bot
COPY services/matrix-nutriphi-bot ./services/matrix-nutriphi-bot COPY services/matrix-nutriphi-bot ./services/matrix-nutriphi-bot

View file

@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
# Copy shared packages that this bot depends on # Copy shared packages that this bot depends on
COPY packages/bot-services ./packages/bot-services COPY packages/bot-services ./packages/bot-services
COPY packages/matrix-bot-common ./packages/matrix-bot-common COPY packages/matrix-bot-common ./packages/matrix-bot-common
COPY packages/shared-llm ./packages/shared-llm
# Copy this bot # Copy this bot
COPY services/matrix-ollama-bot ./services/matrix-ollama-bot COPY services/matrix-ollama-bot ./services/matrix-ollama-bot

View file

@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
# Copy shared packages that this bot depends on # Copy shared packages that this bot depends on
COPY packages/bot-services ./packages/bot-services COPY packages/bot-services ./packages/bot-services
COPY packages/matrix-bot-common ./packages/matrix-bot-common COPY packages/matrix-bot-common ./packages/matrix-bot-common
COPY packages/shared-llm ./packages/shared-llm
# Copy this bot # Copy this bot
COPY services/matrix-project-doc-bot ./services/matrix-project-doc-bot COPY services/matrix-project-doc-bot ./services/matrix-project-doc-bot

View file

@ -13,6 +13,7 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
# Copy shared packages that this bot depends on # Copy shared packages that this bot depends on
COPY packages/bot-services ./packages/bot-services COPY packages/bot-services ./packages/bot-services
COPY packages/matrix-bot-common ./packages/matrix-bot-common COPY packages/matrix-bot-common ./packages/matrix-bot-common
COPY apps/zitare/packages/content ./apps/zitare/packages/content
# Copy this bot # Copy this bot
COPY services/matrix-zitare-bot ./services/matrix-zitare-bot COPY services/matrix-zitare-bot ./services/matrix-zitare-bot