/** * Simple Blue/Green Deployment Script * This script handles deploying to the inactive instance and switching traffic * * Usage: npx tsx scripts/deploy.ts */ import { execSync } from "child_process"; import { readFileSync, copyFileSync } from "fs"; import * as readline from "readline"; // ANSI color codes const colors = { red: "\x1b[0;31m", green: "\x1b[0;32m", blue: "\x1b[0;34m", cyan: "\x1b[0;36m", yellow: "\x1b[1;33m", bold: "\x1b[1m", reset: "\x1b[0m", } as const; // Configuration const APACHE_CONFIG = "/etc/apache2/sites-enabled/api.webamp.org-le-ssl.conf"; const BLUE_PORT = 3001; const GREEN_PORT = 3002; type Color = "blue" | "green"; interface DeploymentState { currentColor: Color; currentPort: number; newColor: Color; newPort: number; } function log(message: string, color?: keyof typeof colors): void { if (color) { console.log(`${colors[color]}${message}${colors.reset}`); } else { console.log(message); } } function logBlank(): void { console.log(); } function exec(command: string, description: string): void { try { execSync(command, { stdio: "inherit", shell: "/bin/bash" }); } catch (error) { log(`✗ Failed to ${description}`, "red"); throw error; } } function execSilent(command: string): string { return execSync(command, { encoding: "utf8" }); } function detectCurrentDeployment(): DeploymentState { log("→ Detecting current active deployment...", "cyan"); const apacheConfig = readFileSync(APACHE_CONFIG, "utf8"); const isBlueActive = apacheConfig.includes(`localhost:${BLUE_PORT}`); if (isBlueActive) { log( ` Current active: ${colors.blue}blue${colors.reset} (port ${BLUE_PORT})` ); log( ` Deploying to: ${colors.green}green${colors.reset} (port ${GREEN_PORT})` ); logBlank(); return { currentColor: "blue", currentPort: BLUE_PORT, newColor: "green", newPort: GREEN_PORT, }; } else { log( ` Current active: ${colors.green}green${colors.reset} (port ${GREEN_PORT})` ); log( ` Deploying to: ${colors.blue}blue${colors.reset} (port ${BLUE_PORT})` ); logBlank(); return { currentColor: "green", currentPort: GREEN_PORT, newColor: "blue", newPort: BLUE_PORT, }; } } async function promptForConfirmation( newPort: number, newColor: Color ): Promise { const colorCode = newColor === "blue" ? colors.blue : colors.green; log("========================================", "cyan"); log(" MANUAL VALIDATION REQUIRED", "yellow"); log("========================================", "cyan"); logBlank(); log(` Test the new ${colorCode}${newColor}${colors.reset} deployment at:`); log(` ${colors.bold}https://${newColor}.api.webamp.org${colors.reset}`); logBlank(); log(` You can test it with:`); log( ` ${colors.cyan}curl -I https://${newColor}.api.webamp.org${colors.reset}` ); logBlank(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(" Does everything look good? (yes/no): ", (answer) => { rl.close(); logBlank(); resolve(answer.toLowerCase() === "yes"); }); }); } function switchApacheConfig(state: DeploymentState): void { const colorCode = state.newColor === "blue" ? colors.blue : colors.green; log( `→ Switching production to ${colorCode}${state.newColor}${colors.reset}...`, "cyan" ); log(" Updating Apache configuration...", "cyan"); // Backup current config const backupPath = `${APACHE_CONFIG}.backup`; copyFileSync(APACHE_CONFIG, backupPath); // Update the port in Apache config exec( `sudo sed -i 's/localhost:${state.currentPort}/localhost:${state.newPort}/g' "${APACHE_CONFIG}"`, "update Apache configuration" ); // Reload Apache exec("sudo systemctl reload apache2", "reload Apache"); log("✓ Apache configuration updated and reloaded", "cyan"); logBlank(); // Verify the change const updatedConfig = readFileSync(APACHE_CONFIG, "utf8"); if (!updatedConfig.includes(`localhost:${state.newPort}`)) { throw new Error("Configuration update verification failed"); } } function restoreBackup(): void { log(" Restoring backup...", "yellow"); const backupPath = `${APACHE_CONFIG}.backup`; exec(`sudo cp "${backupPath}" "${APACHE_CONFIG}"`, "restore backup"); exec("sudo systemctl reload apache2", "reload Apache"); log("✓ Backup restored", "yellow"); } async function main(): Promise { try { log("========================================", "cyan"); log(" Blue/Green Deployment Script", "cyan"); log("========================================", "cyan"); logBlank(); // Step 1: Detect current deployment const state = detectCurrentDeployment(); // Step 2: Pull from GitHub log("→ Pulling latest code from GitHub...", "cyan"); exec("git pull --rebase origin master", "pull from GitHub"); log("✓ Code updated", "cyan"); logBlank(); // Step 3: Ensure correct Node version log("→ Ensuring correct Node version...", "cyan"); exec( "source ~/.nvm/nvm.sh && nvm install", "ensure correct Node version from .nvmrc" ); log("✓ Node version verified", "cyan"); logBlank(); // Step 4: Install dependencies log("→ Installing dependencies...", "cyan"); exec( "source ~/.nvm/nvm.sh && nvm exec yarn install --frozen-lockfile", "install dependencies" ); log("✓ Dependencies installed", "cyan"); logBlank(); // Step 5: Build the site log("→ Building the site...", "cyan"); exec("source ~/.nvm/nvm.sh && nvm exec yarn build", "build the site"); log("✓ Build complete", "cyan"); logBlank(); // Step 5: Deploy to inactive instance const newColorCode = state.newColor === "blue" ? colors.blue : colors.green; log( `→ Restarting ${newColorCode}${state.newColor}${colors.reset} instance...`, "cyan" ); exec( `pm2 restart skin-database-${state.newColor}`, `restart ${state.newColor} instance` ); log( `✓ ${newColorCode}${state.newColor}${colors.reset} instance restarted`, "cyan" ); logBlank(); // Wait for the service to start log("→ Waiting for service to be ready...", "cyan"); await new Promise((resolve) => { setTimeout(resolve, 5000); }); // Check if the service is running const pm2List = execSilent("pm2 list"); const isRunning = pm2List.includes(`skin-database-${state.newColor}`) && pm2List.includes("online"); if (isRunning) { log( `✓ ${newColorCode}${state.newColor}${colors.reset} instance is running`, "cyan" ); } else { log( `✗ ${newColorCode}${state.newColor}${colors.reset} instance failed to start!`, "red" ); log(` Check PM2 logs: pm2 logs skin-database-${state.newColor}`, "red"); process.exit(1); } logBlank(); // Step 6: Manual validation prompt const confirmed = await promptForConfirmation( state.newPort, state.newColor ); if (!confirmed) { log("✗ Deployment cancelled!", "red"); log( ` The ${newColorCode}${state.newColor}${colors.reset} instance is running but not active in production.` ); log( ` You can rollback by restarting: pm2 restart skin-database-${state.newColor}` ); process.exit(1); } // Step 7: Switch Apache configuration switchApacheConfig(state); // Success message const currentColorCode = state.currentColor === "blue" ? colors.blue : colors.green; log("========================================", "cyan"); log(" DEPLOYMENT SUCCESSFUL!", "cyan"); log("========================================", "cyan"); logBlank(); log( ` Active deployment: ${newColorCode}${state.newColor}${colors.reset} (port ${state.newPort})` ); log( ` Previous deployment: ${currentColorCode}${state.currentColor}${colors.reset} (port ${state.currentPort}) - still running as backup` ); logBlank(); log("Note: If you need to rollback:", "yellow"); log(` 1. Edit: ${APACHE_CONFIG}`); log(` 2. Change port back to ${state.currentPort}`); log(` 3. Run: sudo systemctl reload apache2`); } catch (error) { if ( error instanceof Error && error.message === "Configuration update verification failed" ) { log("✗ Configuration update failed!", "red"); restoreBackup(); } process.exit(1); } } // Run the deployment main();