/** * Deployment Rollback System * * Provides comprehensive rollback capabilities to restore the system * to a previous state in case of deployment failures. */ import { execSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { migrationLogger } from "./migration-logger"; interface RollbackOptions { backupPath?: string; rollbackDatabase: boolean; rollbackCode: boolean; rollbackEnvironment: boolean; skipConfirmation: boolean; dryRun: boolean; } interface RollbackStep { name: string; description: string; critical: boolean; execute: () => Promise; verify?: () => Promise; } interface RollbackResult { success: boolean; completedSteps: string[]; failedStep?: string; totalDuration: number; error?: Error; } export class RollbackManager { private readonly defaultOptions: RollbackOptions = { rollbackDatabase: true, rollbackCode: true, rollbackEnvironment: true, skipConfirmation: false, dryRun: false, }; private options: RollbackOptions; private steps: RollbackStep[] = []; private completedSteps: string[] = []; constructor(options?: Partial) { this.options = { ...this.defaultOptions, ...options }; this.setupRollbackSteps(); } /** * Execute complete rollback process */ async rollback(): Promise { const startTime = Date.now(); try { migrationLogger.startPhase("ROLLBACK", "Starting deployment rollback"); // Confirmation check if (!this.options.skipConfirmation && !this.options.dryRun) { await this.confirmRollback(); } // Execute rollback steps for (const step of this.steps) { await this.executeRollbackStep(step); this.completedSteps.push(step.name); } const totalDuration = Date.now() - startTime; migrationLogger.completePhase("ROLLBACK"); migrationLogger.info("ROLLBACK", "Rollback completed successfully", { totalDuration, steps: this.completedSteps.length, }); return { success: true, completedSteps: this.completedSteps, totalDuration, }; } catch (error) { const totalDuration = Date.now() - startTime; migrationLogger.error("ROLLBACK", "Rollback failed", error as Error); return { success: false, completedSteps: this.completedSteps, totalDuration, error: error as Error, }; } } /** * Create rollback snapshot before deployment */ async createRollbackSnapshot(): Promise { migrationLogger.startStep( "ROLLBACK_SNAPSHOT", "Creating rollback snapshot" ); try { const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const snapshotDir = join(process.cwd(), "rollback-snapshots", timestamp); const fs = await import("node:fs/promises"); await fs.mkdir(snapshotDir, { recursive: true }); // Save environment snapshot await this.saveEnvironmentSnapshot(snapshotDir); // Save package.json and lock file snapshot await this.savePackageSnapshot(snapshotDir); // Save git commit information await this.saveGitSnapshot(snapshotDir); // Save deployment state await this.saveDeploymentState(snapshotDir); migrationLogger.completeStep("ROLLBACK_SNAPSHOT"); migrationLogger.info("ROLLBACK_SNAPSHOT", "Rollback snapshot created", { snapshotDir, }); return snapshotDir; } catch (error) { migrationLogger.failStep("ROLLBACK_SNAPSHOT", error as Error); throw error; } } private setupRollbackSteps(): void { this.steps = [ { name: "Pre-Rollback Validation", description: "Validate rollback prerequisites", critical: true, execute: async () => { await this.validateRollbackPrerequisites(); }, }, { name: "Stop Services", description: "Stop application services safely", critical: true, execute: async () => { await this.stopServices(); }, }, { name: "Database Rollback", description: "Restore database to previous state", critical: true, execute: async () => { if (this.options.rollbackDatabase) { await this.rollbackDatabase(); } else { migrationLogger.info("DB_ROLLBACK", "Database rollback skipped"); } }, verify: async () => { return await this.verifyDatabaseRollback(); }, }, { name: "Code Rollback", description: "Restore application code to previous version", critical: true, execute: async () => { if (this.options.rollbackCode) { await this.rollbackCode(); } else { migrationLogger.info("CODE_ROLLBACK", "Code rollback skipped"); } }, }, { name: "Environment Rollback", description: "Restore environment configuration", critical: false, execute: async () => { if (this.options.rollbackEnvironment) { await this.rollbackEnvironment(); } else { migrationLogger.info( "ENV_ROLLBACK", "Environment rollback skipped" ); } }, }, { name: "Dependencies Restoration", description: "Restore previous dependencies", critical: true, execute: async () => { await this.restoreDependencies(); }, }, { name: "Restart Services", description: "Restart services with previous configuration", critical: true, execute: async () => { await this.restartServices(); }, }, { name: "Verify Rollback", description: "Verify system is working correctly", critical: true, execute: async () => { await this.verifyRollback(); }, }, ]; } private async executeRollbackStep(step: RollbackStep): Promise { try { migrationLogger.startStep( step.name.replace(/\s+/g, "_").toUpperCase(), step.description ); if (this.options.dryRun) { migrationLogger.info("DRY_RUN", `Would execute rollback: ${step.name}`); await new Promise((resolve) => setTimeout(resolve, 100)); } else { await step.execute(); } // Run verification if provided if (step.verify && !this.options.dryRun) { const verified = await step.verify(); if (!verified) { throw new Error( `Verification failed for rollback step: ${step.name}` ); } } migrationLogger.completeStep( step.name.replace(/\s+/g, "_").toUpperCase() ); } catch (error) { migrationLogger.failStep( step.name.replace(/\s+/g, "_").toUpperCase(), error as Error ); if (step.critical) { throw error; } else { migrationLogger.warn( "ROLLBACK_STEP", `Non-critical rollback step failed: ${step.name}`, { error: (error as Error).message, } ); } } } private async confirmRollback(): Promise { console.log("\n⚠️ ROLLBACK CONFIRMATION REQUIRED ⚠️"); console.log("This will restore the system to a previous state."); console.log("The following actions will be performed:"); if (this.options.rollbackDatabase) { console.log(" - Restore database from backup"); } if (this.options.rollbackCode) { console.log(" - Restore application code to previous version"); } if (this.options.rollbackEnvironment) { console.log(" - Restore environment configuration"); } console.log("\nThis operation cannot be easily undone."); // In a real implementation, you would prompt for user input // For automation purposes, we'll check for a confirmation flag if (!process.env.ROLLBACK_CONFIRMED) { throw new Error( "Rollback not confirmed. Set ROLLBACK_CONFIRMED=true to proceed." ); } } private async validateRollbackPrerequisites(): Promise { migrationLogger.info( "ROLLBACK_VALIDATION", "Validating rollback prerequisites" ); // Check if backup exists if (this.options.rollbackDatabase && this.options.backupPath) { if (!existsSync(this.options.backupPath)) { throw new Error(`Backup file not found: ${this.options.backupPath}`); } } // Check if pg_restore is available for database rollback if (this.options.rollbackDatabase) { try { execSync("pg_restore --version", { stdio: "ignore" }); } catch (error) { throw new Error( "pg_restore not found - database rollback not possible" ); } } // Check git status for code rollback if (this.options.rollbackCode) { try { execSync("git status", { stdio: "ignore" }); } catch (error) { throw new Error("Git not available - code rollback not possible"); } } migrationLogger.info( "ROLLBACK_VALIDATION", "Prerequisites validated successfully" ); } private async stopServices(): Promise { migrationLogger.info("SERVICE_STOP", "Stopping application services"); // In a real deployment, this would stop the actual services // For this implementation, we'll simulate service stopping await new Promise((resolve) => setTimeout(resolve, 1000)); migrationLogger.info("SERVICE_STOP", "Services stopped successfully"); } private async rollbackDatabase(): Promise { if (!this.options.backupPath) { migrationLogger.warn( "DB_ROLLBACK", "No backup path specified, skipping database rollback" ); return; } migrationLogger.info( "DB_ROLLBACK", `Restoring database from backup: ${this.options.backupPath}` ); try { // Parse database URL const dbUrl = process.env.DATABASE_URL; if (!dbUrl) { throw new Error("DATABASE_URL not found"); } const parsed = new URL(dbUrl); // Drop existing connections migrationLogger.info( "DB_ROLLBACK", "Terminating existing database connections" ); // Restore from backup const restoreCommand = [ "pg_restore", "-h", parsed.hostname, "-p", parsed.port || "5432", "-U", parsed.username, "-d", parsed.pathname.slice(1), "--clean", "--if-exists", "--verbose", this.options.backupPath, ].join(" "); migrationLogger.debug("DB_ROLLBACK", `Executing: ${restoreCommand}`); execSync(restoreCommand, { env: { ...process.env, PGPASSWORD: parsed.password, }, stdio: "pipe", }); migrationLogger.info( "DB_ROLLBACK", "Database rollback completed successfully" ); } catch (error) { throw new Error(`Database rollback failed: ${(error as Error).message}`); } } private async verifyDatabaseRollback(): Promise { try { migrationLogger.info("DB_VERIFY", "Verifying database rollback"); // Test database connection const { PrismaClient } = await import("@prisma/client"); const prisma = new PrismaClient(); try { await prisma.$queryRaw`SELECT 1`; await prisma.$disconnect(); migrationLogger.info("DB_VERIFY", "Database verification successful"); return true; } catch (error) { await prisma.$disconnect(); migrationLogger.error( "DB_VERIFY", "Database verification failed", error as Error ); return false; } } catch (error) { migrationLogger.error( "DB_VERIFY", "Database verification error", error as Error ); return false; } } private async rollbackCode(): Promise { migrationLogger.info("CODE_ROLLBACK", "Rolling back application code"); try { // Get the previous commit (this is a simplified approach) const previousCommit = execSync("git rev-parse HEAD~1", { encoding: "utf8", }).trim(); migrationLogger.info( "CODE_ROLLBACK", `Rolling back to commit: ${previousCommit}` ); // Reset to previous commit execSync(`git reset --hard ${previousCommit}`, { stdio: "pipe" }); migrationLogger.info( "CODE_ROLLBACK", "Code rollback completed successfully" ); } catch (error) { throw new Error(`Code rollback failed: ${(error as Error).message}`); } } private async rollbackEnvironment(): Promise { migrationLogger.info( "ENV_ROLLBACK", "Rolling back environment configuration" ); try { // Look for environment backup const backupFiles = [ ".env.local.backup", ".env.backup", ".env.production.backup", ]; let restored = false; for (const backupFile of backupFiles) { const backupPath = join(process.cwd(), backupFile); const targetPath = backupPath.replace(".backup", ""); if (existsSync(backupPath)) { const backupContent = readFileSync(backupPath, "utf8"); writeFileSync(targetPath, backupContent); migrationLogger.info( "ENV_ROLLBACK", `Restored ${targetPath} from ${backupFile}` ); restored = true; } } if (!restored) { migrationLogger.warn( "ENV_ROLLBACK", "No environment backup found to restore" ); } else { migrationLogger.info( "ENV_ROLLBACK", "Environment rollback completed successfully" ); } } catch (error) { throw new Error( `Environment rollback failed: ${(error as Error).message}` ); } } private async restoreDependencies(): Promise { migrationLogger.info("DEPS_RESTORE", "Restoring dependencies"); try { // Check if package-lock.json backup exists const packageLockBackup = join(process.cwd(), "package-lock.json.backup"); const packageLock = join(process.cwd(), "package-lock.json"); if (existsSync(packageLockBackup)) { const backupContent = readFileSync(packageLockBackup, "utf8"); writeFileSync(packageLock, backupContent); migrationLogger.info( "DEPS_RESTORE", "Restored package-lock.json from backup" ); } // Reinstall dependencies execSync("npm ci", { stdio: "pipe" }); migrationLogger.info( "DEPS_RESTORE", "Dependencies restored successfully" ); } catch (error) { throw new Error( `Dependencies restoration failed: ${(error as Error).message}` ); } } private async restartServices(): Promise { migrationLogger.info( "SERVICE_RESTART", "Restarting services after rollback" ); // In a real deployment, this would restart the actual services await new Promise((resolve) => setTimeout(resolve, 2000)); migrationLogger.info("SERVICE_RESTART", "Services restarted successfully"); } private async verifyRollback(): Promise { migrationLogger.info("ROLLBACK_VERIFY", "Verifying rollback success"); try { // Test database connection const { PrismaClient } = await import("@prisma/client"); const prisma = new PrismaClient(); await prisma.$queryRaw`SELECT 1`; await prisma.$disconnect(); // Test basic application functionality // This would typically involve checking key endpoints or services migrationLogger.info( "ROLLBACK_VERIFY", "Rollback verification successful" ); } catch (error) { throw new Error( `Rollback verification failed: ${(error as Error).message}` ); } } private async saveEnvironmentSnapshot(snapshotDir: string): Promise { const fs = await import("node:fs/promises"); const envFiles = [".env.local", ".env.production", ".env"]; for (const envFile of envFiles) { const envPath = join(process.cwd(), envFile); if (existsSync(envPath)) { const content = await fs.readFile(envPath, "utf8"); await fs.writeFile(join(snapshotDir, envFile), content); } } } private async savePackageSnapshot(snapshotDir: string): Promise { const fs = await import("node:fs/promises"); const packageFiles = [ "package.json", "package-lock.json", "pnpm-lock.yaml", ]; for (const packageFile of packageFiles) { const packagePath = join(process.cwd(), packageFile); if (existsSync(packagePath)) { const content = await fs.readFile(packagePath, "utf8"); await fs.writeFile(join(snapshotDir, packageFile), content); } } } private async saveGitSnapshot(snapshotDir: string): Promise { try { const gitInfo = { commit: execSync("git rev-parse HEAD", { encoding: "utf8" }).trim(), branch: execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8", }).trim(), status: execSync("git status --porcelain", { encoding: "utf8" }).trim(), remotes: execSync("git remote -v", { encoding: "utf8" }).trim(), }; const fs = await import("node:fs/promises"); await fs.writeFile( join(snapshotDir, "git-info.json"), JSON.stringify(gitInfo, null, 2) ); } catch (error) { migrationLogger.warn("GIT_SNAPSHOT", "Failed to save git snapshot", { error: (error as Error).message, }); } } private async saveDeploymentState(snapshotDir: string): Promise { const deploymentState = { timestamp: new Date().toISOString(), nodeVersion: process.version, platform: process.platform, architecture: process.arch, environment: process.env.NODE_ENV, rollbackOptions: this.options, }; const fs = await import("node:fs/promises"); await fs.writeFile( join(snapshotDir, "deployment-state.json"), JSON.stringify(deploymentState, null, 2) ); } } // CLI interface if (import.meta.url === `file://${process.argv[1]}`) { const args = process.argv.slice(2); const options: Partial = {}; // Parse command line arguments args.forEach((arg, index) => { switch (arg) { case "--dry-run": options.dryRun = true; break; case "--skip-confirmation": options.skipConfirmation = true; break; case "--no-database": options.rollbackDatabase = false; break; case "--no-code": options.rollbackCode = false; break; case "--no-environment": options.rollbackEnvironment = false; break; case "--backup": options.backupPath = args[index + 1]; break; } }); const command = args[0]; if (command === "snapshot") { const rollbackManager = new RollbackManager(); rollbackManager .createRollbackSnapshot() .then((snapshotDir) => { console.log("\n=== ROLLBACK SNAPSHOT CREATED ==="); console.log(`Snapshot Directory: ${snapshotDir}`); console.log("\nThe snapshot contains:"); console.log(" - Environment configuration"); console.log(" - Package dependencies"); console.log(" - Git information"); console.log(" - Deployment state"); console.log("\nUse this snapshot for rollback if needed."); process.exit(0); }) .catch((error) => { console.error("Snapshot creation failed:", error); process.exit(1); }); } else { const rollbackManager = new RollbackManager(options); rollbackManager .rollback() .then((result) => { console.log("\n=== ROLLBACK RESULTS ==="); console.log(`Success: ${result.success ? "✅" : "❌"}`); console.log(`Total Duration: ${result.totalDuration}ms`); console.log(`Completed Steps: ${result.completedSteps.length}`); if (result.failedStep) { console.log(`Failed Step: ${result.failedStep}`); } if (result.error) { console.error(`Error: ${result.error.message}`); } console.log("\nCompleted Steps:"); result.completedSteps.forEach((step) => console.log(` ✅ ${step}`)); if (result.success) { console.log("\n🎉 ROLLBACK SUCCESSFUL!"); console.log("\nNext Steps:"); console.log("1. Verify system functionality"); console.log("2. Monitor logs for any issues"); console.log("3. Investigate root cause of deployment failure"); } else { console.log("\n💥 ROLLBACK FAILED!"); console.log("\nNext Steps:"); console.log("1. Check logs for error details"); console.log("2. Manual intervention may be required"); console.log("3. Contact system administrators"); } process.exit(result.success ? 0 : 1); }) .catch((error) => { console.error("Rollback failed:", error); process.exit(1); }); } }