mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat(infra): add Dockerfile dependency validator + fix 16 missing COPYs
New script validates that all workspace deps in package.json have matching COPY statements in Dockerfiles. Fixed missing shared-pwa, shared-vite-config, patches/, and project-specific package COPYs across 7 web app Dockerfiles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
10df359fdb
commit
a2605e8816
12 changed files with 286 additions and 7 deletions
|
|
@ -43,6 +43,10 @@ COPY packages/shared-vite-config ./packages/shared-vite-config
|
|||
COPY packages/shared-api-client ./packages/shared-api-client
|
||||
COPY packages/shared-stores ./packages/shared-stores
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy chat packages
|
||||
COPY apps/chat/packages ./apps/chat/packages
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ COPY packages/shared-vite-config ./packages/shared-vite-config
|
|||
COPY packages/shared-api-client ./packages/shared-api-client
|
||||
COPY packages/shared-stores ./packages/shared-stores
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy clock packages and web
|
||||
COPY apps/clock/packages ./apps/clock/packages
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ COPY packages/shared-utils ./packages/shared-utils
|
|||
COPY packages/shared-error-tracking ./packages/shared-error-tracking
|
||||
COPY packages/shared-vite-config ./packages/shared-vite-config
|
||||
COPY packages/shared-tsconfig ./packages/shared-tsconfig
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy nutriphi packages and web
|
||||
COPY apps/nutriphi/packages ./apps/nutriphi/packages
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ COPY packages/shared-vite-config ./packages/shared-vite-config
|
|||
COPY packages/shared-api-client ./packages/shared-api-client
|
||||
COPY packages/shared-stores ./packages/shared-stores
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy photos shared package
|
||||
COPY apps/photos/packages/shared ./apps/photos/packages/shared
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ COPY packages/shared-branding ./packages/shared-branding
|
|||
COPY packages/shared-i18n ./packages/shared-i18n
|
||||
COPY packages/shared-icons ./packages/shared-icons
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy llm-playground service
|
||||
COPY apps/playground/apps/web ./apps/playground/apps/web
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,14 @@ COPY packages/shared-ui ./packages/shared-ui
|
|||
COPY packages/shared-error-tracking ./packages/shared-error-tracking
|
||||
COPY packages/shared-utils ./packages/shared-utils
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
COPY packages/shared-vite-config ./packages/shared-vite-config
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy presi shared package
|
||||
COPY apps/presi/packages/shared ./apps/presi/packages/shared
|
||||
|
||||
# Copy presi web
|
||||
COPY apps/presi/apps/web ./apps/presi/apps/web
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ COPY packages/shared-error-tracking ./packages/shared-error-tracking
|
|||
COPY packages/shared-vite-config ./packages/shared-vite-config
|
||||
COPY packages/shared-api-client ./packages/shared-api-client
|
||||
COPY packages/shared-app-onboarding ./packages/shared-app-onboarding
|
||||
COPY packages/shared-ui ./packages/shared-ui
|
||||
COPY packages/shared-pwa ./packages/shared-pwa
|
||||
|
||||
# Copy patches (referenced by pnpm-lock.yaml)
|
||||
COPY patches/ ./patches/
|
||||
|
||||
# Copy skilltree web
|
||||
COPY apps/skilltree/apps/web ./apps/skilltree/apps/web
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './create-task.dto';
|
||||
export * from './update-task.dto';
|
||||
export * from './query-tasks.dto';
|
||||
export * from './reorder-tasks.dto';
|
||||
|
|
|
|||
11
apps/todo/apps/backend/src/task/dto/reorder-tasks.dto.ts
Normal file
11
apps/todo/apps/backend/src/task/dto/reorder-tasks.dto.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { IsArray, IsString, IsOptional } from 'class-validator';
|
||||
|
||||
export class ReorderTasksDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
taskIds: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
projectId?: string | null;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nes
|
|||
import { UseCredits } from '@manacore/nestjs-integration';
|
||||
import { CreditOperationType } from '@manacore/credit-operations';
|
||||
import { TaskService } from './task.service';
|
||||
import { CreateTaskDto, UpdateTaskDto, QueryTasksDto } from './dto';
|
||||
import { CreateTaskDto, UpdateTaskDto, QueryTasksDto, ReorderTasksDto } from './dto';
|
||||
|
||||
@Controller('tasks')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
@ -121,12 +121,8 @@ export class TaskController {
|
|||
}
|
||||
|
||||
@Put('reorder')
|
||||
async reorder(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Body('taskIds') taskIds: string[],
|
||||
@Body('projectId') projectId?: string | null
|
||||
) {
|
||||
const tasks = await this.taskService.reorder(user.userId, taskIds, projectId);
|
||||
async reorder(@CurrentUser() user: CurrentUserData, @Body() dto: ReorderTasksDto) {
|
||||
const tasks = await this.taskService.reorder(user.userId, dto.taskIds, dto.projectId);
|
||||
return { tasks };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"clean": "turbo run clean",
|
||||
"format": "prettier --config .prettierrc.json --write \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
|
||||
"format:check": "prettier --config .prettierrc.json --check \"**/*.{ts,tsx,js,jsx,json,md,svelte,astro}\"",
|
||||
"validate:dockerfiles": "node scripts/validate-dockerfiles.mjs",
|
||||
"setup:env": "node scripts/generate-env.mjs",
|
||||
"setup:db": "./scripts/setup-databases.sh",
|
||||
"setup:db:chat": "./scripts/setup-databases.sh chat",
|
||||
|
|
|
|||
238
scripts/validate-dockerfiles.mjs
Executable file
238
scripts/validate-dockerfiles.mjs
Executable file
|
|
@ -0,0 +1,238 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Validate that all web app Dockerfiles include COPY statements
|
||||
* for every workspace dependency listed in package.json.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
||||
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/*
|
||||
function buildPackageMap() {
|
||||
const map = new Map();
|
||||
|
||||
// packages/*
|
||||
const packagesDir = join(ROOT, 'packages');
|
||||
if (existsSync(packagesDir)) {
|
||||
for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const pkgJsonPath = join(packagesDir, entry.name, 'package.json');
|
||||
if (!existsSync(pkgJsonPath)) continue;
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||||
map.set(pkg.name, `packages/${entry.name}`);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// apps/*/packages/*
|
||||
const appsDir = join(ROOT, 'apps');
|
||||
if (existsSync(appsDir)) {
|
||||
for (const appEntry of readdirSync(appsDir, { withFileTypes: true })) {
|
||||
if (!appEntry.isDirectory()) continue;
|
||||
const appPkgsDir = join(appsDir, appEntry.name, 'packages');
|
||||
if (!existsSync(appPkgsDir)) continue;
|
||||
for (const pkgEntry of readdirSync(appPkgsDir, { withFileTypes: true })) {
|
||||
if (!pkgEntry.isDirectory()) continue;
|
||||
const pkgJsonPath = join(appPkgsDir, pkgEntry.name, 'package.json');
|
||||
if (!existsSync(pkgJsonPath)) continue;
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||||
map.set(pkg.name, `apps/${appEntry.name}/packages/${pkgEntry.name}`);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// Extract workspace deps from package.json
|
||||
function getWorkspaceDeps(pkgJsonPath) {
|
||||
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||||
const deps = [];
|
||||
for (const section of ['dependencies', 'devDependencies']) {
|
||||
if (!pkg[section]) continue;
|
||||
for (const [name, version] of Object.entries(pkg[section])) {
|
||||
if (typeof version === 'string' && version.startsWith('workspace:')) {
|
||||
deps.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
// Extract COPY paths from Dockerfile (only from builder stage, before RUN install)
|
||||
function getDockerfileCopyPaths(dockerfilePath) {
|
||||
const content = readFileSync(dockerfilePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
const copyPaths = new Set();
|
||||
let hasPatchesCopy = false;
|
||||
|
||||
for (const line of lines) {
|
||||
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+)/);
|
||||
if (copyMatch) {
|
||||
copyPaths.add(copyMatch[1]);
|
||||
}
|
||||
// Check for patches copy
|
||||
if (trimmed.match(/^COPY\s+patches[\s/]/)) {
|
||||
hasPatchesCopy = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { copyPaths, hasPatchesCopy };
|
||||
}
|
||||
|
||||
// Extract @scope/package imports from a source file
|
||||
function extractImports(filePath) {
|
||||
if (!existsSync(filePath)) return [];
|
||||
const content = readFileSync(filePath, 'utf8');
|
||||
const imports = new Set();
|
||||
// Match import ... from '@scope/package' and import '@scope/package'
|
||||
// Also match dynamic imports and require
|
||||
const patterns = [
|
||||
/from\s+['"](@[^'"/]+\/[^'"/]+)/g,
|
||||
/import\s*\(\s*['"](@[^'"/]+\/[^'"/]+)/g,
|
||||
/import\s+['"](@[^'"/]+\/[^'"/]+)/g,
|
||||
/require\s*\(\s*['"](@[^'"/]+\/[^'"/]+)/g,
|
||||
];
|
||||
for (const pattern of patterns) {
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
imports.add(match[1]);
|
||||
}
|
||||
}
|
||||
return [...imports];
|
||||
}
|
||||
|
||||
function main() {
|
||||
const packageMap = buildPackageMap();
|
||||
const appsDir = join(ROOT, 'apps');
|
||||
let hasErrors = false;
|
||||
const results = [];
|
||||
|
||||
// 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)) {
|
||||
results.push({
|
||||
path: `apps/${appName}/apps/web/Dockerfile`,
|
||||
errors: ['package.json not found in same directory'],
|
||||
warnings: [],
|
||||
depCount: 0,
|
||||
});
|
||||
hasErrors = true;
|
||||
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)
|
||||
);
|
||||
if (!found) {
|
||||
errors.push(`MISSING: ${dep} → add: COPY ${dirPath} ./${dirPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check patches
|
||||
if (!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
|
||||
const hooksPath = join(appsDir, appName, 'apps', 'web', 'src', 'hooks.server.ts');
|
||||
const vitePath = join(appsDir, appName, 'apps', 'web', '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) {
|
||||
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(
|
||||
`\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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
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' : ''}`
|
||||
);
|
||||
|
||||
if (hasErrors) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Loading…
Add table
Add a link
Reference in a new issue