mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:21:10 +02:00
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:
parent
dd5c0d502f
commit
1052469397
12 changed files with 343 additions and 115 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 \
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue