platform-tooling/scripts/services/validate-services.ts
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

358 lines
9.4 KiB
TypeScript

#!/usr/bin/env npx ts-node
/**
* Service Configuration Validator
*
* Validates all per-feature YAML configurations for:
* - No port conflicts across features
* - All dependencies reference valid services
* - Health check endpoints are defined
* - Required fields present
*
* Usage:
* pnpm services:validate # Validate all configs
* pnpm services:validate --fix # Auto-fix issues where possible
* pnpm services:validate --verbose # Show detailed output
*/
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'yaml';
import { PATHS } from '../../configs/paths';
interface Service {
id: string;
name: string;
type: string;
port?: number;
sharedPort?: boolean; // Intentional port sharing (dev servers)
dependencies?: string[];
healthCheck?: {
type: string;
path?: string;
url?: string;
command?: string;
name?: string;
};
gpu?: boolean;
critical?: boolean;
entrypoint?: string;
}
// Known intentional port sharing (dev servers that never run simultaneously)
const INTENTIONAL_SHARED_PORTS = new Set([5173]); // analytics + conversation-assistant dev
interface FeatureConfig {
feature: {
id: string;
name: string;
description: string;
owner?: string;
};
ports: Record<string, number>;
services: Service[];
deployments?: Record<string, unknown>;
}
interface ValidationError {
feature: string;
service?: string;
type: 'error' | 'warning';
message: string;
}
const FEATURES_DIR = PATHS.servicesDir;
function loadAllConfigs(): Map<string, FeatureConfig> {
const configs = new Map<string, FeatureConfig>();
const files = fs.readdirSync(FEATURES_DIR).filter((f) => f.endsWith('.yaml') && !f.startsWith('_'));
for (const file of files) {
const content = fs.readFileSync(path.join(FEATURES_DIR, file), 'utf-8');
try {
const config = yaml.parse(content) as FeatureConfig;
if (config.feature && config.feature.id) {
configs.set(config.feature.id, config);
}
} catch (e) {
console.error(`Failed to parse ${file}: ${e}`);
}
}
return configs;
}
function validatePortConflicts(configs: Map<string, FeatureConfig>): ValidationError[] {
const errors: ValidationError[] = [];
const portUsage = new Map<number, string[]>();
for (const [featureId, config] of configs) {
// Check ports section
for (const [portName, port] of Object.entries(config.ports || {})) {
if (typeof port === 'number') {
const key = `${featureId}.${portName}`;
if (!portUsage.has(port)) {
portUsage.set(port, []);
}
portUsage.get(port)!.push(key);
}
}
// Check service ports
for (const service of config.services || []) {
if (service.port) {
const key = `${featureId}.${service.id}`;
if (!portUsage.has(service.port)) {
portUsage.set(service.port, []);
}
portUsage.get(service.port)!.push(key);
}
}
}
// Find conflicts
for (const [port, users] of portUsage) {
// Skip intentional shared ports
if (INTENTIONAL_SHARED_PORTS.has(port)) {
continue;
}
// Filter out duplicates within the same feature (ports section + service)
const uniqueFeatures = [...new Set(users.map((u) => u.split('.')[0]))];
if (uniqueFeatures.length > 1) {
errors.push({
feature: 'global',
type: 'error',
message: `Port ${port} conflict: used by ${users.join(', ')}`,
});
}
}
return errors;
}
function validateDependencies(configs: Map<string, FeatureConfig>): ValidationError[] {
const errors: ValidationError[] = [];
// Build service registry
const allServices = new Set<string>();
for (const [featureId, config] of configs) {
for (const service of config.services || []) {
allServices.add(`${featureId}.${service.id}`);
}
}
// Check all dependencies
for (const [featureId, config] of configs) {
for (const service of config.services || []) {
for (const dep of service.dependencies || []) {
if (!allServices.has(dep)) {
errors.push({
feature: featureId,
service: service.id,
type: 'warning',
message: `Dependency "${dep}" not found in any feature config`,
});
}
}
}
}
return errors;
}
function validateRequiredFields(configs: Map<string, FeatureConfig>): ValidationError[] {
const errors: ValidationError[] = [];
for (const [featureId, config] of configs) {
// Check feature metadata
if (!config.feature) {
errors.push({
feature: featureId,
type: 'error',
message: 'Missing "feature" section',
});
continue;
}
if (!config.feature.id) {
errors.push({
feature: featureId,
type: 'error',
message: 'Missing "feature.id"',
});
}
if (!config.feature.name) {
errors.push({
feature: featureId,
type: 'error',
message: 'Missing "feature.name"',
});
}
if (!config.feature.description) {
errors.push({
feature: featureId,
type: 'warning',
message: 'Missing "feature.description"',
});
}
// Check services
for (const service of config.services || []) {
if (!service.id) {
errors.push({
feature: featureId,
type: 'error',
message: 'Service missing "id"',
});
}
if (!service.name) {
errors.push({
feature: featureId,
service: service.id,
type: 'error',
message: 'Missing "name"',
});
}
if (!service.type) {
errors.push({
feature: featureId,
service: service.id,
type: 'error',
message: 'Missing "type"',
});
}
// Health check validation for exposed services
// Skip for: databases, redis, dev servers (ephemeral), websockets
const skipHealthCheck =
service.type === 'postgresql' ||
service.type === 'redis' ||
service.id.includes('frontend-dev') ||
service.id.includes('websocket');
if (service.port && !service.healthCheck && !skipHealthCheck) {
errors.push({
feature: featureId,
service: service.id,
type: 'warning',
message: 'Exposed service missing "healthCheck"',
});
}
}
}
return errors;
}
function validatePortsMatchServices(configs: Map<string, FeatureConfig>): ValidationError[] {
const errors: ValidationError[] = [];
for (const [featureId, config] of configs) {
const declaredPorts = new Set<number>();
const usedPorts = new Set<number>();
// Collect declared ports
for (const port of Object.values(config.ports || {})) {
if (typeof port === 'number') {
declaredPorts.add(port);
}
}
// Collect used ports
for (const service of config.services || []) {
if (service.port) {
usedPorts.add(service.port);
}
}
// Check for unused declared ports
for (const port of declaredPorts) {
if (!usedPorts.has(port)) {
errors.push({
feature: featureId,
type: 'warning',
message: `Declared port ${port} not used by any service`,
});
}
}
// Check for undeclared used ports
for (const port of usedPorts) {
if (!declaredPorts.has(port)) {
errors.push({
feature: featureId,
type: 'warning',
message: `Service uses port ${port} not declared in ports section`,
});
}
}
}
return errors;
}
function main(): void {
const args = process.argv.slice(2);
const verbose = args.includes('--verbose') || args.includes('-v');
console.log('🔍 Validating service configurations...\n');
const configs = loadAllConfigs();
console.log(`Found ${configs.size} feature configurations\n`);
const allErrors: ValidationError[] = [];
// Run validations
if (verbose) console.log('Checking port conflicts...');
allErrors.push(...validatePortConflicts(configs));
if (verbose) console.log('Checking dependencies...');
allErrors.push(...validateDependencies(configs));
if (verbose) console.log('Checking required fields...');
allErrors.push(...validateRequiredFields(configs));
if (verbose) console.log('Checking ports match services...');
allErrors.push(...validatePortsMatchServices(configs));
// Report results
const errors = allErrors.filter((e) => e.type === 'error');
const warnings = allErrors.filter((e) => e.type === 'warning');
if (errors.length > 0) {
console.log('❌ Errors:');
for (const error of errors) {
const location = error.service ? `${error.feature}.${error.service}` : error.feature;
console.log(` [${location}] ${error.message}`);
}
console.log('');
}
if (warnings.length > 0) {
console.log('⚠️ Warnings:');
for (const warning of warnings) {
const location = warning.service ? `${warning.feature}.${warning.service}` : warning.feature;
console.log(` [${location}] ${warning.message}`);
}
console.log('');
}
// Summary
console.log('📊 Summary:');
console.log(` Features: ${configs.size}`);
console.log(` Errors: ${errors.length}`);
console.log(` Warnings: ${warnings.length}`);
if (errors.length === 0) {
console.log('\n✅ All validations passed!');
} else {
console.log('\n❌ Validation failed - please fix errors above');
process.exit(1);
}
}
main();