UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

823 lines 36.5 kB
import { EventEmitter } from "events"; import { writeFile, readFile, mkdir, readdir } from "fs/promises"; import { join } from "path"; import { spawn } from "child_process"; import { Logger } from "../core/logger.js"; import { ConfigManager } from "../core/config.js"; export class DeploymentManager extends EventEmitter { deployments = new Map(); environments = new Map(); strategies = new Map(); pipelines = new Map(); activeProcesses = new Map(); deploymentsPath; logger; config; constructor(deploymentsPath = "./deployments", logger, config) { super(); this.deploymentsPath = deploymentsPath; this.logger = logger || new Logger({ level: "info", format: "text", destination: "console" }); this.config = config || ConfigManager.getInstance(); } async initialize() { try { await mkdir(this.deploymentsPath, { recursive: true }); await mkdir(join(this.deploymentsPath, "environments"), { recursive: true }); await mkdir(join(this.deploymentsPath, "strategies"), { recursive: true }); await mkdir(join(this.deploymentsPath, "pipelines"), { recursive: true }); await this.loadConfigurations(); await this.initializeDefaultStrategies(); this.logger.info("Deployment Manager initialized successfully"); } catch (error) { this.logger.error("Failed to initialize Deployment Manager", { error }); throw error; } } async createEnvironment(environmentData) { const environment = { id: environmentData.id || `env-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: environmentData.name || "Unnamed Environment", type: environmentData.type || "development", status: "inactive", configuration: { region: "us-east-1", provider: "aws", endpoints: [], secrets: {}, environment_variables: {}, resources: { cpu: "1", memory: "1Gi", storage: "10Gi", replicas: 1, }, ...environmentData.configuration, }, healthCheck: { url: "/health", method: "GET", expectedStatus: 200, timeout: 30000, interval: 60000, retries: 3, ...environmentData.healthCheck, }, monitoring: { enabled: true, alerts: [], metrics: ["cpu", "memory", "requests", "errors"], logs: { level: "info", retention: "30d", aggregation: true, }, ...environmentData.monitoring, }, security: { tls: true, authentication: true, authorization: ["admin", "deploy"], compliance: [], scanning: { vulnerabilities: true, secrets: true, licenses: true, }, ...environmentData.security, }, createdAt: new Date(), updatedAt: new Date(), }; this.environments.set(environment.id, environment); await this.saveEnvironment(environment); this.emit("environment:created", environment); this.logger.info(`Environment created: ${environment.name} (${environment.id})`); return environment; } async createDeployment(deploymentData) { const environment = this.environments.get(deploymentData.environmentId); if (!environment) { throw new Error(`Environment not found: ${deploymentData.environmentId}`); } const strategy = this.strategies.get(deploymentData.strategyId); if (!strategy) { throw new Error(`Strategy not found: ${deploymentData.strategyId}`); } const deployment = { id: `deploy-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: deploymentData.name, version: deploymentData.version, projectId: deploymentData.projectId, environmentId: deploymentData.environmentId, strategyId: deploymentData.strategyId, status: "pending", initiatedBy: deploymentData.initiatedBy, source: deploymentData.source, artifacts: { files: [], ...deploymentData.artifacts, }, metrics: { startTime: new Date(), deploymentSize: 0, successRate: 0, errorRate: 0, performanceMetrics: {}, }, stages: strategy.stages.map(stage => ({ ...stage, status: "pending", logs: [], })), approvals: [], notifications: [], auditLog: [], createdAt: new Date(), updatedAt: new Date(), }; this.addAuditEntry(deployment, deploymentData.initiatedBy, "deployment_created", "deployment", { deploymentId: deployment.id, environment: environment.name, strategy: strategy.name, }); this.deployments.set(deployment.id, deployment); await this.saveDeployment(deployment); this.emit("deployment:created", deployment); this.logger.info(`Deployment created: ${deployment.name} (${deployment.id})`); return deployment; } async executeDeployment(deploymentId) { const deployment = this.deployments.get(deploymentId); if (!deployment) { throw new Error(`Deployment not found: ${deploymentId}`); } if (deployment.status !== "pending") { throw new Error(`Deployment ${deploymentId} is not in pending status`); } deployment.status = "running"; deployment.metrics.startTime = new Date(); deployment.updatedAt = new Date(); this.addAuditEntry(deployment, "system", "deployment_started", "deployment", { deploymentId, }); await this.saveDeployment(deployment); this.emit("deployment:started", deployment); try { for (const stage of deployment.stages) { await this.executeStage(deployment, stage); if (stage.status === "failed") { await this.handleDeploymentFailure(deployment, stage); return; } } await this.completeDeployment(deployment); } catch (error) { await this.handleDeploymentError(deployment, error); } } async executeStage(deployment, stage) { stage.status = "running"; stage.startTime = new Date(); this.addLog(stage, "info", `Starting stage: ${stage.name}`, "system"); try { // Check conditions if (!this.evaluateStageConditions(deployment, stage)) { stage.status = "skipped"; this.addLog(stage, "info", "Stage skipped due to conditions", "system"); return; } // Handle approvals if (stage.type === "deploy" && await this.requiresApproval(deployment, stage)) { await this.requestApproval(deployment, stage); // Wait for approval while (await this.isPendingApproval(deployment, stage)) { await new Promise(resolve => setTimeout(resolve, 10000)); // Check every 10 seconds } if (!(await this.isApproved(deployment, stage))) { stage.status = "failed"; this.addLog(stage, "error", "Stage rejected by approver", "system"); return; } } // Execute commands for (const command of stage.commands) { await this.executeCommand(deployment, stage, command); } stage.status = "success"; stage.endTime = new Date(); stage.duration = stage.endTime.getTime() - stage.startTime.getTime(); this.addLog(stage, "info", `Stage completed successfully in ${stage.duration}ms`, "system"); } catch (error) { stage.status = "failed"; stage.endTime = new Date(); this.addLog(stage, "error", `Stage failed: ${error.message}`, "system"); // Retry logic if (stage.retryPolicy.maxRetries > 0) { await this.retryStage(deployment, stage); } } await this.saveDeployment(deployment); this.emit("stage:completed", { deployment, stage }); } async executeCommand(deployment, stage, command) { return new Promise((resolve, reject) => { const environment = this.environments.get(deployment.environmentId); const processEnv = { ...process.env, ...environment?.configuration.environment_variables, ...command.environment, DEPLOYMENT_ID: deployment.id, DEPLOYMENT_VERSION: deployment.version, ENVIRONMENT_ID: deployment.environmentId, }; this.addLog(stage, "info", `Executing: ${command.command} ${command.args.join(" ")}`, "command"); const childProcess = spawn(command.command, command.args, { cwd: command.workingDirectory || process.cwd(), env: processEnv, stdio: ["pipe", "pipe", "pipe"], }); this.activeProcesses.set(`${deployment.id}-${stage.id}-${command.id}`, childProcess); let stdout = ""; let stderr = ""; childProcess.stdout?.on("data", (data) => { const output = data.toString(); stdout += output; this.addLog(stage, "info", output.trim(), "stdout"); }); childProcess.stderr?.on("data", (data) => { const output = data.toString(); stderr += output; this.addLog(stage, "error", output.trim(), "stderr"); }); const timeout = setTimeout(() => { childProcess.kill("SIGTERM"); reject(new Error(`Command timed out after ${command.timeout}ms`)); }, command.timeout); childProcess.on("close", (code) => { clearTimeout(timeout); this.activeProcesses.delete(`${deployment.id}-${stage.id}-${command.id}`); // Check success criteria const success = this.evaluateCommandSuccess(command, code, stdout, stderr); if (success) { this.addLog(stage, "info", `Command completed successfully (exit code: ${code})`, "command"); resolve(); } else { this.addLog(stage, "error", `Command failed (exit code: ${code})`, "command"); reject(new Error(`Command failed with exit code ${code}`)); } }); childProcess.on("error", (error) => { clearTimeout(timeout); this.activeProcesses.delete(`${deployment.id}-${stage.id}-${command.id}`); this.addLog(stage, "error", `Command error: ${error.message}`, "command"); reject(error); }); }); } async rollbackDeployment(deploymentId, reason, userId = "system") { const deployment = this.deployments.get(deploymentId); if (!deployment) { throw new Error(`Deployment not found: ${deploymentId}`); } // Find previous successful deployment const previousDeployment = await this.getPreviousSuccessfulDeployment(deployment.projectId, deployment.environmentId, deploymentId); if (!previousDeployment) { throw new Error("No previous successful deployment found for rollback"); } const rollbackStartTime = new Date(); deployment.rollback = { triggered: true, reason, timestamp: rollbackStartTime, previousDeploymentId: previousDeployment.id, rollbackDuration: 0, }; deployment.status = "rolled-back"; deployment.updatedAt = new Date(); this.addAuditEntry(deployment, userId, "rollback_initiated", "deployment", { deploymentId, previousDeploymentId: previousDeployment.id, reason, }); try { // Execute rollback strategy await this.executeRollbackStrategy(deployment, previousDeployment); deployment.rollback.rollbackDuration = Date.now() - rollbackStartTime.getTime(); this.addAuditEntry(deployment, userId, "rollback_completed", "deployment", { deploymentId, rollbackDuration: deployment.rollback.rollbackDuration, }); this.emit("deployment:rolled-back", deployment); this.logger.info(`Deployment rolled back: ${deploymentId}`); } catch (error) { this.addAuditEntry(deployment, userId, "rollback_failed", "deployment", { deploymentId, error: error.message, }); this.logger.error(`Rollback failed for deployment ${deploymentId}`, { error }); throw error; } await this.saveDeployment(deployment); } async getDeploymentMetrics(filters) { let deployments = Array.from(this.deployments.values()); // Apply filters if (filters) { if (filters.projectId) { deployments = deployments.filter(d => d.projectId === filters.projectId); } if (filters.environmentId) { deployments = deployments.filter(d => d.environmentId === filters.environmentId); } if (filters.strategyId) { deployments = deployments.filter(d => d.strategyId === filters.strategyId); } if (filters.timeRange) { deployments = deployments.filter(d => d.createdAt >= filters.timeRange.start && d.createdAt <= filters.timeRange.end); } } const totalDeployments = deployments.length; const successfulDeployments = deployments.filter(d => d.status === "success").length; const failedDeployments = deployments.filter(d => d.status === "failed").length; const rolledBackDeployments = deployments.filter(d => d.status === "rolled-back").length; const completedDeployments = deployments.filter(d => d.metrics.endTime && d.metrics.startTime); const averageDeploymentTime = completedDeployments.length > 0 ? completedDeployments.reduce((sum, d) => sum + (d.metrics.endTime.getTime() - d.metrics.startTime.getTime()), 0) / completedDeployments.length : 0; // Calculate environment metrics const environmentMetrics = {}; for (const env of this.environments.values()) { const envDeployments = deployments.filter(d => d.environmentId === env.id); const envSuccessful = envDeployments.filter(d => d.status === "success").length; environmentMetrics[env.id] = { deployments: envDeployments.length, successRate: envDeployments.length > 0 ? (envSuccessful / envDeployments.length) * 100 : 0, averageTime: envDeployments.length > 0 ? envDeployments.reduce((sum, d) => sum + (d.metrics.duration || 0), 0) / envDeployments.length : 0, }; } // Calculate strategy metrics const strategyMetrics = {}; for (const strategy of this.strategies.values()) { const strategyDeployments = deployments.filter(d => d.strategyId === strategy.id); const strategySuccessful = strategyDeployments.filter(d => d.status === "success").length; const strategyRolledBack = strategyDeployments.filter(d => d.status === "rolled-back").length; strategyMetrics[strategy.id] = { deployments: strategyDeployments.length, successRate: strategyDeployments.length > 0 ? (strategySuccessful / strategyDeployments.length) * 100 : 0, rollbackRate: strategyDeployments.length > 0 ? (strategyRolledBack / strategyDeployments.length) * 100 : 0, }; } return { totalDeployments, successfulDeployments, failedDeployments, rolledBackDeployments, averageDeploymentTime, deploymentFrequency: this.calculateDeploymentFrequency(deployments), meanTimeToRecovery: this.calculateMTTR(deployments), changeFailureRate: (failedDeployments + rolledBackDeployments) / Math.max(totalDeployments, 1) * 100, leadTime: this.calculateLeadTime(deployments), environmentMetrics, strategyMetrics, }; } // Private helper methods async loadConfigurations() { // Load environments, strategies, and pipelines from disk try { const envFiles = await readdir(join(this.deploymentsPath, "environments")); for (const file of envFiles.filter(f => f.endsWith(".json"))) { const content = await readFile(join(this.deploymentsPath, "environments", file), "utf-8"); const env = JSON.parse(content); this.environments.set(env.id, env); } const strategyFiles = await readdir(join(this.deploymentsPath, "strategies")); for (const file of strategyFiles.filter(f => f.endsWith(".json"))) { const content = await readFile(join(this.deploymentsPath, "strategies", file), "utf-8"); const strategy = JSON.parse(content); this.strategies.set(strategy.id, strategy); } const pipelineFiles = await readdir(join(this.deploymentsPath, "pipelines")); for (const file of pipelineFiles.filter(f => f.endsWith(".json"))) { const content = await readFile(join(this.deploymentsPath, "pipelines", file), "utf-8"); const pipeline = JSON.parse(content); this.pipelines.set(pipeline.id, pipeline); } this.logger.info(`Loaded ${this.environments.size} environments, ${this.strategies.size} strategies, ${this.pipelines.size} pipelines`); } catch (error) { this.logger.warn("Failed to load some configurations", { error }); } } async initializeDefaultStrategies() { const defaultStrategies = [ { name: "Blue-Green Deployment", type: "blue-green", configuration: { monitoringDuration: 300000, // 5 minutes automatedRollback: true, rollbackThreshold: 5, }, stages: [ { id: "build", name: "Build", order: 1, type: "build", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 600000, retryPolicy: { maxRetries: 2, backoffMultiplier: 2, initialDelay: 1000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "deploy-green", name: "Deploy to Green", order: 2, type: "deploy", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 900000, retryPolicy: { maxRetries: 1, backoffMultiplier: 2, initialDelay: 5000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "verify", name: "Verify Green", order: 3, type: "verify", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 300000, retryPolicy: { maxRetries: 3, backoffMultiplier: 1.5, initialDelay: 2000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "switch-traffic", name: "Switch Traffic", order: 4, type: "promote", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 60000, retryPolicy: { maxRetries: 1, backoffMultiplier: 1, initialDelay: 1000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, ], rollbackStrategy: { automatic: true, conditions: [ { metric: "error_rate", threshold: 5, operator: ">", duration: 60000, description: "Error rate exceeds 5%", }, ], timeout: 300000, }, }, { name: "Canary Deployment", type: "canary", configuration: { trafficSplitPercentage: 10, monitoringDuration: 600000, // 10 minutes automatedRollback: true, rollbackThreshold: 2, }, stages: [ { id: "build", name: "Build", order: 1, type: "build", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 600000, retryPolicy: { maxRetries: 2, backoffMultiplier: 2, initialDelay: 1000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "deploy-canary", name: "Deploy Canary (10%)", order: 2, type: "deploy", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 900000, retryPolicy: { maxRetries: 1, backoffMultiplier: 2, initialDelay: 5000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "monitor-canary", name: "Monitor Canary", order: 3, type: "verify", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 600000, retryPolicy: { maxRetries: 1, backoffMultiplier: 1, initialDelay: 10000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "promote-full", name: "Promote to 100%", order: 4, type: "promote", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 300000, retryPolicy: { maxRetries: 1, backoffMultiplier: 1, initialDelay: 1000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, ], rollbackStrategy: { automatic: true, conditions: [ { metric: "error_rate", threshold: 2, operator: ">", duration: 120000, description: "Canary error rate exceeds 2%", }, { metric: "response_time", threshold: 500, operator: ">", duration: 180000, description: "Response time exceeds 500ms", }, ], timeout: 180000, }, }, { name: "Rolling Deployment", type: "rolling", configuration: { maxUnavailable: 1, maxSurge: 1, monitoringDuration: 120000, automatedRollback: false, }, stages: [ { id: "build", name: "Build", order: 1, type: "build", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 600000, retryPolicy: { maxRetries: 2, backoffMultiplier: 2, initialDelay: 1000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "rolling-update", name: "Rolling Update", order: 2, type: "deploy", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 1200000, retryPolicy: { maxRetries: 1, backoffMultiplier: 2, initialDelay: 5000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, { id: "health-check", name: "Health Check", order: 3, type: "verify", status: "pending", commands: [], conditions: { runIf: [], skipIf: [] }, timeout: 300000, retryPolicy: { maxRetries: 3, backoffMultiplier: 1.5, initialDelay: 5000 }, artifacts: { inputs: [], outputs: [] }, logs: [], }, ], rollbackStrategy: { automatic: false, conditions: [], timeout: 600000, }, }, ]; for (const strategyData of defaultStrategies) { if (!Array.from(this.strategies.values()).some(s => s.name === strategyData.name)) { const strategy = { id: `strategy-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, notifications: { channels: [], events: ["deployment:started", "deployment:completed", "deployment:failed"], }, ...strategyData, }; this.strategies.set(strategy.id, strategy); await this.saveStrategy(strategy); } } } async saveEnvironment(environment) { const filePath = join(this.deploymentsPath, "environments", `${environment.id}.json`); await writeFile(filePath, JSON.stringify(environment, null, 2)); } async saveStrategy(strategy) { const filePath = join(this.deploymentsPath, "strategies", `${strategy.id}.json`); await writeFile(filePath, JSON.stringify(strategy, null, 2)); } async saveDeployment(deployment) { const filePath = join(this.deploymentsPath, `${deployment.id}.json`); await writeFile(filePath, JSON.stringify(deployment, null, 2)); } addAuditEntry(deployment, userId, action, target, details) { const entry = { id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date(), userId, action, target, details, }; deployment.auditLog.push(entry); } addLog(stage, level, message, source, metadata) { const log = { timestamp: new Date(), level, message, source, metadata, }; stage.logs.push(log); } evaluateStageConditions(deployment, stage) { // Implement condition evaluation logic return true; // Simplified for now } async requiresApproval(deployment, stage) { const strategy = this.strategies.get(deployment.strategyId); return strategy?.configuration.approvalRequired || false; } async requestApproval(deployment, stage) { // Implement approval request logic this.emit("approval:requested", { deployment, stage }); } async isPendingApproval(deployment, stage) { // Check if there are pending approvals for this stage return false; // Simplified for now } async isApproved(deployment, stage) { // Check if stage is approved return true; // Simplified for now } evaluateCommandSuccess(command, exitCode, stdout, stderr) { if (command.successCriteria.exitCode !== undefined && exitCode !== command.successCriteria.exitCode) { return false; } if (command.successCriteria.outputContains) { for (const pattern of command.successCriteria.outputContains) { if (!stdout.includes(pattern)) { return false; } } } if (command.successCriteria.outputNotContains) { for (const pattern of command.successCriteria.outputNotContains) { if (stdout.includes(pattern) || stderr.includes(pattern)) { return false; } } } return true; } async retryStage(deployment, stage) { // Implement retry logic this.logger.info(`Retrying stage: ${stage.name}`); } async handleDeploymentFailure(deployment, failedStage) { deployment.status = "failed"; deployment.metrics.endTime = new Date(); deployment.updatedAt = new Date(); this.addAuditEntry(deployment, "system", "deployment_failed", "deployment", { deploymentId: deployment.id, failedStage: failedStage.name, reason: "Stage execution failed", }); await this.saveDeployment(deployment); this.emit("deployment:failed", { deployment, failedStage }); // Check if automatic rollback is enabled const strategy = this.strategies.get(deployment.strategyId); if (strategy?.rollbackStrategy.automatic) { await this.rollbackDeployment(deployment.id, "Automatic rollback due to deployment failure"); } } async handleDeploymentError(deployment, error) { deployment.status = "failed"; deployment.metrics.endTime = new Date(); deployment.updatedAt = new Date(); this.addAuditEntry(deployment, "system", "deployment_error", "deployment", { deploymentId: deployment.id, error: error.message, }); await this.saveDeployment(deployment); this.emit("deployment:error", { deployment, error }); this.logger.error(`Deployment error: ${deployment.id}`, { error }); } async completeDeployment(deployment) { deployment.status = "success"; deployment.metrics.endTime = new Date(); deployment.metrics.duration = deployment.metrics.endTime.getTime() - deployment.metrics.startTime.getTime(); deployment.updatedAt = new Date(); this.addAuditEntry(deployment, "system", "deployment_completed", "deployment", { deploymentId: deployment.id, duration: deployment.metrics.duration, }); await this.saveDeployment(deployment); this.emit("deployment:completed", deployment); this.logger.info(`Deployment completed: ${deployment.id} in ${deployment.metrics.duration}ms`); } async getPreviousSuccessfulDeployment(projectId, environmentId, currentDeploymentId) { const deployments = Array.from(this.deployments.values()) .filter(d => d.projectId === projectId && d.environmentId === environmentId && d.status === "success" && d.id !== currentDeploymentId) .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); return deployments[0] ?? null; } async executeRollbackStrategy(deployment, previousDeployment) { // Implement rollback execution logic this.logger.info(`Executing rollback from ${deployment.id} to ${previousDeployment.id}`); // This would typically involve: // 1. Switching traffic back to previous version // 2. Updating load balancer configuration // 3. Rolling back container deployments // 4. Reverting database migrations if needed // 5. Updating DNS records this.emit("rollback:executed", { deployment, previousDeployment }); } calculateDeploymentFrequency(deployments) { if (deployments.length === 0) return 0; const sortedDeployments = deployments.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); const firstDeployment = sortedDeployments[0]; const lastDeployment = sortedDeployments[sortedDeployments.length - 1]; const timeSpan = lastDeployment.createdAt.getTime() - firstDeployment.createdAt.getTime(); const days = timeSpan / (1000 * 60 * 60 * 24); return deployments.length / Math.max(days, 1); } calculateMTTR(deployments) { const failedDeployments = deployments.filter(d => d.status === "failed" || d.status === "rolled-back"); if (failedDeployments.length === 0) return 0; const recoveryTimes = failedDeployments .map(d => d.rollback?.rollbackDuration || 0) .filter(time => time > 0); if (recoveryTimes.length === 0) return 0; return recoveryTimes.reduce((sum, time) => sum + time, 0) / recoveryTimes.length; } calculateLeadTime(deployments) { // This would typically calculate from commit to production // For now, return average deployment time const completedDeployments = deployments.filter(d => d.metrics.duration); if (completedDeployments.length === 0) return 0; return completedDeployments.reduce((sum, d) => sum + (d.metrics.duration || 0), 0) / completedDeployments.length; } } //# sourceMappingURL=deployment-manager.js.map