Commit 0fb4762a authored by Rais Aryaguna's avatar Rais Aryaguna

add: zero downtime deployment script and utility functions

parent ed0527de
#!/usr/bin/env node
/* eslint-disable no-shadow */
/* eslint-disable no-loop-func */
/* eslint-disable no-await-in-loop */
/**
* Universal Zero Downtime Deployment Script
* Supports: development, staging, production
* All configurations loaded from environment variables
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import http from 'http';
import { config } from 'dotenv';
// Load environment-specific .env file
function loadEnvConfig(environment) {
const envFiles = [`.env.${environment}.local`, `.env.${environment}`, '.env.local', '.env'];
// eslint-disable-next-line no-restricted-syntax
for (const envFile of envFiles) {
const envPath = path.join(process.cwd(), envFile);
if (fs.existsSync(envPath)) {
logMessage(`📄 Loading config from: ${envFile}`);
config({ path: envPath });
break;
}
}
}
// Environment Configuration (loaded from .env files)
const getConfig = (environment) => ({
// App Configuration
APP_NAME: process.env.APP_NAME || `app-${environment}`,
APP_PORT: parseInt(process.env.APP_PORT || '9020', 10),
// PM2 Configuration
PM2_INSTANCES: parseInt(process.env.PM2_INSTANCES || '2', 10),
PM2_EXEC_MODE: process.env.PM2_EXEC_MODE || 'cluster',
PM2_MAX_MEMORY: process.env.PM2_MAX_MEMORY || '500M',
PM2_WAIT_READY: process.env.PM2_WAIT_READY === 'true',
PM2_LISTEN_TIMEOUT: parseInt(process.env.PM2_LISTEN_TIMEOUT || '10000', 10),
PM2_KILL_TIMEOUT: parseInt(process.env.PM2_KILL_TIMEOUT || '5000', 10),
// Build Configuration
BUILD_COMMAND: process.env.BUILD_COMMAND || 'yarn build',
INSTALL_COMMAND: process.env.INSTALL_COMMAND || 'yarn install --frozen-lockfile',
TMP_BUILD_DIR: process.env.TMP_BUILD_DIR || 'dist_building',
FINAL_DIST_DIR: process.env.FINAL_DIST_DIR || 'dist',
BACKUP_DIST_DIR: process.env.BACKUP_DIST_DIR || 'dist_backup',
// Health Check Configuration
HEALTH_CHECK_ENABLED: process.env.HEALTH_CHECK_ENABLED !== 'false',
HEALTH_CHECK_PATH: process.env.HEALTH_CHECK_PATH || '/',
HEALTH_CHECK_RETRIES: parseInt(process.env.HEALTH_CHECK_RETRIES || '5', 10),
HEALTH_CHECK_INTERVAL: parseInt(process.env.HEALTH_CHECK_INTERVAL || '2000', 10),
HEALTH_CHECK_TIMEOUT: parseInt(process.env.HEALTH_CHECK_TIMEOUT || '5000', 10),
// Deployment Configuration
AUTO_ROLLBACK: process.env.AUTO_ROLLBACK !== 'false',
CLEANUP_BACKUP: process.env.CLEANUP_BACKUP !== 'false',
PM2_SAVE: process.env.PM2_SAVE !== 'false',
// Logging Configuration
LOG_DIR: process.env.LOG_DIR || 'logs',
LOG_FILE_NAME: process.env.LOG_FILE_NAME || 'deployment.log',
ENABLE_CONSOLE_LOG: process.env.ENABLE_CONSOLE_LOG !== 'false',
// Environment
ENVIRONMENT: environment,
NODE_ENV: process.env.NODE_ENV || environment,
});
let CONFIG;
// Helper functions
function logMessage(message, config = CONFIG) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${config?.ENVIRONMENT || 'INIT'}] ${message}`;
if (!config || config.ENABLE_CONSOLE_LOG) {
console.log(logMessage);
}
if (config) {
const logDir = path.join(process.cwd(), config.LOG_DIR);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, config.LOG_FILE_NAME);
fs.appendFileSync(logFile, `${logMessage}\n`);
}
}
function executeCommand(command, throwOnError = true) {
logMessage(`⚙️ Executing: ${command}`);
try {
const output = execSync(command, {
encoding: 'utf8',
stdio: CONFIG.ENABLE_CONSOLE_LOG ? 'inherit' : 'pipe',
});
return output ? output.trim() : '';
} catch (error) {
logMessage(`❌ Error executing command: ${error.message}`);
if (throwOnError) throw error;
return null;
}
}
function checkAppStatus(appName) {
try {
const status = executeCommand(`pm2 describe ${appName}`, false);
return !!status;
} catch (error) {
return false;
}
}
// Health check function
async function checkHealth(retries = CONFIG.HEALTH_CHECK_RETRIES) {
if (!CONFIG.HEALTH_CHECK_ENABLED) {
logMessage('⚠️ Health check disabled, skipping...');
return true;
}
logMessage(`🏥 Starting health check (${retries} retries)...`);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < retries; i++) {
try {
await new Promise((resolve, reject) => {
const url = `http://localhost:${CONFIG.APP_PORT}${CONFIG.HEALTH_CHECK_PATH}`;
const req = http.get(url, (res) => {
if (res.statusCode === 200) {
logMessage(`✅ Health check passed (attempt ${i + 1}/${retries})`);
resolve(true);
} else {
reject(new Error(`Status code: ${res.statusCode}`));
}
});
req.on('error', reject);
req.setTimeout(CONFIG.HEALTH_CHECK_TIMEOUT, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
return true;
} catch (error) {
logMessage(`⚠️ Health check failed (attempt ${i + 1}/${retries}): ${error.message}`);
if (i < retries - 1) {
logMessage(`⏳ Waiting ${CONFIG.HEALTH_CHECK_INTERVAL}ms before retry...`);
await new Promise((resolve) => setTimeout(resolve, CONFIG.HEALTH_CHECK_INTERVAL));
}
}
}
return false;
}
// Atomic folder switch with backup
function atomicFolderSwitch() {
logMessage('🔄 Performing atomic folder switch...');
const tmpPath = path.join(process.cwd(), CONFIG.TMP_BUILD_DIR);
const finalPath = path.join(process.cwd(), CONFIG.FINAL_DIST_DIR);
const backupPath = path.join(process.cwd(), CONFIG.BACKUP_DIST_DIR);
// Create backup of current dist
if (fs.existsSync(finalPath)) {
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true, force: true });
}
fs.renameSync(finalPath, backupPath);
logMessage(`📦 Created backup: ${CONFIG.FINAL_DIST_DIR}${CONFIG.BACKUP_DIST_DIR}`);
}
// Switch to new build (atomic operation)
if (!fs.existsSync(tmpPath)) {
throw new Error(`Build directory ${CONFIG.TMP_BUILD_DIR} not found!`);
}
fs.renameSync(tmpPath, finalPath);
logMessage(`✅ Switched to new build: ${CONFIG.TMP_BUILD_DIR}${CONFIG.FINAL_DIST_DIR}`);
}
// Rollback function
function rollback() {
if (!CONFIG.AUTO_ROLLBACK) {
logMessage('⚠️ Auto rollback disabled, skipping...');
return;
}
logMessage('🔙 ROLLBACK: Restoring previous version...');
const finalPath = path.join(process.cwd(), CONFIG.FINAL_DIST_DIR);
const backupPath = path.join(process.cwd(), CONFIG.BACKUP_DIST_DIR);
if (fs.existsSync(backupPath)) {
if (fs.existsSync(finalPath)) {
fs.rmSync(finalPath, { recursive: true, force: true });
}
fs.renameSync(backupPath, finalPath);
logMessage('✅ Rollback completed. Reloading PM2...');
try {
executeCommand(`pm2 reload ${CONFIG.APP_NAME}`);
logMessage('✅ PM2 reloaded with previous version');
} catch (error) {
logMessage('❌ Failed to reload PM2 after rollback');
}
} else {
logMessage('❌ No backup found for rollback!');
}
}
// Generate PM2 ecosystem config
function generateEcosystemConfig() {
const configPath = path.join(process.cwd(), `ecosystem.${CONFIG.ENVIRONMENT}.config.cjs`);
const ecosystemConfig = `module.exports = {
apps: [
{
name: '${CONFIG.APP_NAME}',
script: 'npx',
args: 'serve ${CONFIG.FINAL_DIST_DIR} -s -l ${CONFIG.APP_PORT}',
// Instance Configuration
instances: ${CONFIG.PM2_INSTANCES},
exec_mode: '${CONFIG.PM2_EXEC_MODE}',
// Zero downtime reload settings
wait_ready: ${CONFIG.PM2_WAIT_READY},
listen_timeout: ${CONFIG.PM2_LISTEN_TIMEOUT},
kill_timeout: ${CONFIG.PM2_KILL_TIMEOUT},
// Auto restart settings
autorestart: true,
max_restarts: 10,
min_uptime: '10s',
// Resource limits
max_memory_restart: '${CONFIG.PM2_MAX_MEMORY}',
// Environment
env: {
NODE_ENV: '${CONFIG.NODE_ENV}',
PORT: ${CONFIG.APP_PORT},
ENVIRONMENT: '${CONFIG.ENVIRONMENT}',
},
// Logging
error_file: './${CONFIG.LOG_DIR}/pm2-error.log',
out_file: './${CONFIG.LOG_DIR}/pm2-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
},
],
};`;
fs.writeFileSync(configPath, ecosystemConfig);
logMessage(`📝 Generated PM2 config: ${configPath}`);
return configPath;
}
// Main deployment function
// eslint-disable-next-line consistent-return
async function deployApp(environment = 'production') {
const startTime = new Date();
// Load environment-specific configuration
loadEnvConfig(environment);
CONFIG = getConfig(environment);
logMessage(''.repeat(60));
logMessage(`🚀 STARTING ZERO DOWNTIME DEPLOYMENT`);
logMessage(`📍 Environment: ${CONFIG.ENVIRONMENT}`);
logMessage(`📦 App Name: ${CONFIG.APP_NAME}`);
logMessage(`🔌 Port: ${CONFIG.APP_PORT}`);
logMessage(`⚙️ Instances: ${CONFIG.PM2_INSTANCES} (${CONFIG.PM2_EXEC_MODE} mode)`);
logMessage(''.repeat(60));
try {
// Step 1: Build the application
logMessage('');
logMessage('📦 STEP 1: Building application...');
logMessage(''.repeat(60));
const tmpPath = path.join(process.cwd(), CONFIG.TMP_BUILD_DIR);
if (fs.existsSync(tmpPath)) {
logMessage(`🧹 Cleaning up temp folder: ${CONFIG.TMP_BUILD_DIR}`);
fs.rmSync(tmpPath, { recursive: true, force: true });
}
logMessage(`📥 Running: ${CONFIG.INSTALL_COMMAND}`);
executeCommand(CONFIG.INSTALL_COMMAND);
const buildCmd = `${CONFIG.BUILD_COMMAND} -- --mode ${CONFIG.ENVIRONMENT} --outDir ${CONFIG.TMP_BUILD_DIR}`;
logMessage(`🔨 Running: ${buildCmd}`);
executeCommand(buildCmd);
logMessage('✅ Build completed successfully');
// Step 2: Generate PM2 config
logMessage('');
logMessage('📝 STEP 2: Generating PM2 configuration...');
logMessage(''.repeat(60));
const configFile = generateEcosystemConfig();
// Step 3: Check if app is running
logMessage('');
logMessage('🔍 STEP 3: Checking application status...');
logMessage(''.repeat(60));
const isRunning = checkAppStatus(CONFIG.APP_NAME);
logMessage(`App status: ${isRunning ? '🟢 Running' : '🔴 Not running'}`);
if (isRunning) {
// Step 4: Zero-downtime deployment
logMessage('');
logMessage('🔄 STEP 4: Performing zero-downtime deployment...');
logMessage(''.repeat(60));
// 4a: Atomic folder switch
atomicFolderSwitch();
// 4b: Reload PM2
logMessage('🔄 Reloading PM2...');
executeCommand(`pm2 reload ${configFile} --env ${CONFIG.ENVIRONMENT}`);
// 4c: Wait for PM2 to stabilize
const waitTime = 3000;
logMessage(`⏳ Waiting ${waitTime}ms for PM2 to stabilize...`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
// 4d: Health check
logMessage('');
logMessage('🏥 STEP 5: Running health check...');
logMessage(''.repeat(60));
const isHealthy = await checkHealth();
if (!isHealthy) {
logMessage('❌ Health check failed! Initiating rollback...');
rollback();
throw new Error('Deployment failed: Health check unsuccessful');
}
logMessage('✅ Health check passed!');
// 4e: Cleanup backup after successful deployment
if (CONFIG.CLEANUP_BACKUP) {
const backupPath = path.join(process.cwd(), CONFIG.BACKUP_DIST_DIR);
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true, force: true });
logMessage('🧹 Cleaned up backup folder');
}
}
} else {
// Step 4: First time deployment
logMessage('');
logMessage('🚀 STEP 4: Starting application for the first time...');
logMessage(''.repeat(60));
atomicFolderSwitch();
executeCommand(`pm2 start ${configFile} --env ${CONFIG.ENVIRONMENT}`);
// Wait and health check
logMessage('⏳ Waiting for application to start...');
await new Promise((resolve) => setTimeout(resolve, 5000));
const isHealthy = await checkHealth();
if (!isHealthy) {
logMessage('❌ Health check failed on first start!');
executeCommand(`pm2 delete ${CONFIG.APP_NAME}`, false);
throw new Error('Deployment failed: Health check unsuccessful');
}
logMessage('✅ Application started successfully!');
}
// Save PM2 process list
if (CONFIG.PM2_SAVE) {
logMessage('💾 Saving PM2 process list...');
executeCommand('pm2 save');
}
const endTime = new Date();
const deploymentTime = ((endTime - startTime) / 1000).toFixed(2);
logMessage('');
logMessage(''.repeat(60));
logMessage(`✅ DEPLOYMENT COMPLETED SUCCESSFULLY!`);
logMessage(`⏱️ Total time: ${deploymentTime} seconds`);
logMessage(`🌍 Environment: ${CONFIG.ENVIRONMENT}`);
logMessage(`🔌 App running on: http://localhost:${CONFIG.APP_PORT}`);
logMessage(''.repeat(60));
logMessage('');
logMessage('📊 Quick commands:');
logMessage(` pm2 status ${CONFIG.APP_NAME}`);
logMessage(` pm2 logs ${CONFIG.APP_NAME}`);
logMessage(` pm2 monit`);
logMessage('');
return {
success: true,
message: `Deployment to ${CONFIG.ENVIRONMENT} completed successfully`,
deploymentTime: parseFloat(deploymentTime),
config: CONFIG,
};
} catch (error) {
logMessage('');
logMessage(''.repeat(60));
logMessage(`❌ DEPLOYMENT FAILED!`);
logMessage(`Error: ${error.message}`);
logMessage(''.repeat(60));
logMessage('Stack trace:');
logMessage(error.stack);
process.exit(1);
}
}
// Execute deployment if script is run directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const environment = process.argv[2] || 'production';
// Validate environment
const validEnvironments = ['development', 'staging', 'production'];
if (!validEnvironments.includes(environment)) {
console.error(`❌ Invalid environment: ${environment}`);
console.error(`✅ Valid environments: ${validEnvironments.join(', ')}`);
process.exit(1);
}
deployApp(environment);
}
// eslint-disable-next-line import/prefer-default-export
export { deployApp };
#!/usr/bin/env node
/* eslint-disable no-case-declarations */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-plusplus */
/* eslint-disable no-shadow */
/* eslint-disable no-restricted-syntax */
/**
* Utility Scripts for Deployment Management
* Provides standalone functions for rollback, health check, and other operations
*/
import { execSync } from 'child_process';
import fs from 'fs';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import http from 'http';
import { config } from 'dotenv';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Colors for console output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
};
// Load environment configuration
function loadEnvConfig(environment) {
const envFiles = [`.env.${environment}.local`, `.env.${environment}`, '.env.local', '.env'];
for (const envFile of envFiles) {
const envPath = path.join(process.cwd(), envFile);
if (fs.existsSync(envPath)) {
console.log(`${colors.blue}📄 Loading config from: ${envFile}${colors.reset}`);
config({ path: envPath });
return true;
}
}
return false;
}
// Get configuration from environment
const getConfig = (environment) => ({
APP_NAME: process.env.APP_NAME || `app-${environment}`,
APP_PORT: parseInt(process.env.APP_PORT || '9020', 10),
FINAL_DIST_DIR: process.env.FINAL_DIST_DIR || 'dist',
BACKUP_DIST_DIR: process.env.BACKUP_DIST_DIR || 'dist_backup',
HEALTH_CHECK_PATH: process.env.HEALTH_CHECK_PATH || '/',
HEALTH_CHECK_RETRIES: parseInt(process.env.HEALTH_CHECK_RETRIES || '5', 10),
HEALTH_CHECK_INTERVAL: parseInt(process.env.HEALTH_CHECK_INTERVAL || '2000', 10),
HEALTH_CHECK_TIMEOUT: parseInt(process.env.HEALTH_CHECK_TIMEOUT || '5000', 10),
LOG_DIR: process.env.LOG_DIR || 'logs',
ENVIRONMENT: environment,
});
// Execute shell command
function executeCommand(command, silent = false) {
if (!silent) {
console.log(`${colors.cyan}⚙️ Executing: ${command}${colors.reset}`);
}
try {
const output = execSync(command, { encoding: 'utf8', stdio: silent ? 'pipe' : 'inherit' });
return output ? output.trim() : '';
} catch (error) {
if (!silent) {
console.error(`${colors.red}❌ Error: ${error.message}${colors.reset}`);
}
return null;
}
}
// Health check function
async function performHealthCheck(config, verbose = true) {
if (verbose) {
console.log('');
console.log(''.repeat(60));
console.log(`${colors.blue}🏥 HEALTH CHECK${colors.reset}`);
console.log(''.repeat(60));
console.log(`Environment: ${config.ENVIRONMENT}`);
console.log(`URL: http://localhost:${config.APP_PORT}${config.HEALTH_CHECK_PATH}`);
console.log(`Retries: ${config.HEALTH_CHECK_RETRIES}`);
console.log(`Interval: ${config.HEALTH_CHECK_INTERVAL}ms`);
console.log(''.repeat(60));
}
for (let i = 0; i < config.HEALTH_CHECK_RETRIES; i++) {
try {
const startTime = Date.now();
await new Promise((resolve, reject) => {
const url = `http://localhost:${config.APP_PORT}${config.HEALTH_CHECK_PATH}`;
const req = http.get(url, (res) => {
const responseTime = Date.now() - startTime;
if (res.statusCode === 200) {
if (verbose) {
console.log(
`${colors.green}✅ Health check passed (attempt ${i + 1}/${
config.HEALTH_CHECK_RETRIES
})${colors.reset}`
);
console.log(` Status: ${res.statusCode} | Response time: ${responseTime}ms`);
}
resolve(true);
} else {
reject(new Error(`Status code: ${res.statusCode}`));
}
});
req.on('error', (error) => {
reject(error);
});
req.setTimeout(config.HEALTH_CHECK_TIMEOUT, () => {
req.destroy();
reject(new Error('Timeout'));
});
});
if (verbose) {
console.log(''.repeat(60));
console.log(`${colors.green}✅ HEALTH CHECK SUCCESSFUL${colors.reset}`);
console.log(''.repeat(60));
console.log('');
}
return true;
} catch (error) {
if (verbose) {
console.log(
`${colors.yellow}⚠️ Health check failed (attempt ${i + 1}/${
config.HEALTH_CHECK_RETRIES
})${colors.reset}`
);
console.log(` Error: ${error.message}`);
}
if (i < config.HEALTH_CHECK_RETRIES - 1) {
if (verbose) {
console.log(` Waiting ${config.HEALTH_CHECK_INTERVAL}ms before retry...`);
}
await new Promise((resolve) => setTimeout(resolve, config.HEALTH_CHECK_INTERVAL));
}
}
}
if (verbose) {
console.log(''.repeat(60));
console.log(`${colors.red}❌ HEALTH CHECK FAILED${colors.reset}`);
console.log(`All ${config.HEALTH_CHECK_RETRIES} attempts failed`);
console.log(''.repeat(60));
console.log('');
}
return false;
}
// Rollback function
async function performRollback(config, verbose = true) {
if (verbose) {
console.log('');
console.log(''.repeat(60));
console.log(`${colors.yellow}🔙 ROLLBACK${colors.reset}`);
console.log(''.repeat(60));
console.log(`Environment: ${config.ENVIRONMENT}`);
console.log(`App Name: ${config.APP_NAME}`);
console.log(''.repeat(60));
}
const finalPath = path.join(process.cwd(), config.FINAL_DIST_DIR);
const backupPath = path.join(process.cwd(), config.BACKUP_DIST_DIR);
// Check if backup exists
if (!fs.existsSync(backupPath)) {
console.error(`${colors.red}❌ No backup found at: ${backupPath}${colors.reset}`);
console.log('');
console.log('Cannot rollback without a backup.');
console.log(''.repeat(60));
return false;
}
try {
// Remove current dist
if (fs.existsSync(finalPath)) {
if (verbose) {
console.log(`🗑️ Removing current: ${config.FINAL_DIST_DIR}`);
}
fs.rmSync(finalPath, { recursive: true, force: true });
}
// Restore backup
if (verbose) {
console.log(`📦 Restoring backup: ${config.BACKUP_DIST_DIR}${config.FINAL_DIST_DIR}`);
}
fs.renameSync(backupPath, finalPath);
// Reload PM2
if (verbose) {
console.log(`🔄 Reloading PM2: ${config.APP_NAME}`);
}
const reloadResult = executeCommand(`pm2 reload ${config.APP_NAME}`, !verbose);
if (reloadResult === null) {
console.error(`${colors.red}❌ Failed to reload PM2${colors.reset}`);
return false;
}
// Wait for app to stabilize
if (verbose) {
console.log('⏳ Waiting for app to stabilize...');
}
await new Promise((resolve) => setTimeout(resolve, 3000));
// Verify rollback with health check
if (verbose) {
console.log('🏥 Verifying rollback...');
}
const isHealthy = await performHealthCheck(config, verbose);
if (isHealthy) {
console.log(''.repeat(60));
console.log(`${colors.green}✅ ROLLBACK SUCCESSFUL${colors.reset}`);
console.log('Application has been restored to previous version');
console.log(''.repeat(60));
console.log('');
return true;
}
console.log(''.repeat(60));
console.log(`${colors.red}❌ ROLLBACK COMPLETED BUT HEALTH CHECK FAILED${colors.reset}`);
console.log('Previous version restored but application is not responding');
console.log(''.repeat(60));
console.log('');
return false;
} catch (error) {
console.error(`${colors.red}❌ Rollback failed: ${error.message}${colors.reset}`);
console.log(''.repeat(60));
console.log('');
return false;
}
}
// Check PM2 app status
async function checkAppStatus(config, verbose = true) {
if (verbose) {
console.log('');
console.log(''.repeat(60));
console.log(`${colors.blue}📊 APPLICATION STATUS${colors.reset}`);
console.log(''.repeat(60));
console.log(`Environment: ${config.ENVIRONMENT}`);
console.log(`App Name: ${config.APP_NAME}`);
console.log(''.repeat(60));
}
try {
const output = executeCommand(`pm2 jlist`, true);
if (!output) {
console.log(`${colors.red}❌ Failed to get PM2 status${colors.reset}`);
return false;
}
const processes = JSON.parse(output);
const app = processes.find((p) => p.name === config.APP_NAME);
if (!app) {
console.log(`${colors.yellow}⚠️ App "${config.APP_NAME}" not found in PM2${colors.reset}`);
console.log('');
console.log('Available PM2 apps:');
processes.forEach((p) => {
console.log(` - ${p.name} (status: ${p.pm2_env.status})`);
});
console.log(''.repeat(60));
console.log('');
return false;
}
// Display app info
const { status } = app.pm2_env;
const uptime = app.pm2_env.pm_uptime;
const memory = (app.monit.memory / 1024 / 1024).toFixed(2);
const { cpu } = app.monit;
const restarts = app.pm2_env.restart_time;
const statusColor = status === 'online' ? colors.green : colors.red;
console.log(`Status: ${statusColor}${status.toUpperCase()}${colors.reset}`);
console.log(`PID: ${app.pid || 'N/A'}`);
console.log(`Uptime: ${uptime ? new Date(uptime).toLocaleString() : 'N/A'}`);
console.log(`Memory: ${memory} MB`);
console.log(`CPU: ${cpu}%`);
console.log(`Restarts: ${restarts}`);
console.log(`Instances: ${app.pm2_env.instances || 1}`);
console.log(`Mode: ${app.pm2_env.exec_mode || 'fork'}`);
if (verbose) {
console.log(''.repeat(60));
// Run health check
const isHealthy = await performHealthCheck(config, false);
if (isHealthy) {
console.log(`Health: ${colors.green}✅ HEALTHY${colors.reset}`);
} else {
console.log(`Health: ${colors.red}❌ UNHEALTHY${colors.reset}`);
}
}
console.log(''.repeat(60));
console.log('');
return status === 'online';
} catch (error) {
console.error(`${colors.red}❌ Error checking status: ${error.message}${colors.reset}`);
console.log(''.repeat(60));
console.log('');
return false;
}
}
// List all deployed apps
async function listApps(verbose = true) {
console.log('');
console.log(''.repeat(60));
console.log(`${colors.blue}📋 PM2 APPLICATIONS${colors.reset}`);
console.log(''.repeat(60));
try {
const output = executeCommand(`pm2 jlist`, true);
if (!output) {
console.log(`${colors.red}❌ Failed to get PM2 list${colors.reset}`);
return;
}
const processes = JSON.parse(output);
if (processes.length === 0) {
console.log(`${colors.yellow}⚠️ No PM2 applications running${colors.reset}`);
console.log(''.repeat(60));
console.log('');
return;
}
console.log('');
processes.forEach((app, index) => {
const { status } = app.pm2_env;
const statusColor = status === 'online' ? colors.green : colors.red;
const memory = (app.monit.memory / 1024 / 1024).toFixed(2);
console.log(`${index + 1}. ${colors.cyan}${app.name}${colors.reset}`);
console.log(` Status: ${statusColor}${status}${colors.reset}`);
console.log(` PID: ${app.pid || 'N/A'}`);
console.log(` Memory: ${memory} MB`);
console.log(` CPU: ${app.monit.cpu}%`);
console.log(` Restarts: ${app.pm2_env.restart_time}`);
console.log('');
});
console.log(''.repeat(60));
console.log('');
} catch (error) {
console.error(`${colors.red}❌ Error listing apps: ${error.message}${colors.reset}`);
}
}
// Cleanup old backups
function cleanupBackups(config, verbose = true) {
if (verbose) {
console.log('');
console.log(''.repeat(60));
console.log(`${colors.blue}🧹 CLEANUP BACKUPS${colors.reset}`);
console.log(''.repeat(60));
}
const backupPath = path.join(process.cwd(), config.BACKUP_DIST_DIR);
if (fs.existsSync(backupPath)) {
if (verbose) {
console.log(`Removing backup: ${config.BACKUP_DIST_DIR}`);
}
fs.rmSync(backupPath, { recursive: true, force: true });
console.log(`${colors.green}✅ Backup removed successfully${colors.reset}`);
} else {
console.log(`${colors.yellow}⚠️ No backup found${colors.reset}`);
}
if (verbose) {
console.log(''.repeat(60));
console.log('');
}
}
// Main CLI handler
async function main() {
const args = process.argv.slice(2);
const command = args[0];
const environment = args[1] || 'production';
// Validate environment
const validEnvironments = ['development', 'staging', 'production'];
if (!validEnvironments.includes(environment)) {
console.error(`${colors.red}❌ Invalid environment: ${environment}${colors.reset}`);
console.log(`Valid environments: ${validEnvironments.join(', ')}`);
process.exit(1);
}
// Load environment configuration
loadEnvConfig(environment);
const config = getConfig(environment);
// Execute command
switch (command) {
case 'health-check':
case 'health':
const healthResult = await performHealthCheck(config);
process.exit(healthResult ? 0 : 1);
break;
case 'rollback':
const rollbackResult = await performRollback(config);
process.exit(rollbackResult ? 0 : 1);
break;
case 'status':
const statusResult = await checkAppStatus(config);
process.exit(statusResult ? 0 : 1);
break;
case 'list':
await listApps();
break;
case 'cleanup':
cleanupBackups(config);
break;
default:
console.log('');
console.log(''.repeat(60));
console.log(`${colors.blue}🛠️ DEPLOYMENT UTILITIES${colors.reset}`);
console.log(''.repeat(60));
console.log('');
console.log('Usage: node utils.js <command> [environment]');
console.log('');
console.log('Commands:');
console.log(' health-check Check application health');
console.log(' rollback Rollback to previous version');
console.log(' status Show application status');
console.log(' list List all PM2 applications');
console.log(' cleanup Remove backup files');
console.log('');
console.log('Environments:');
console.log(' development');
console.log(' staging');
console.log(' production (default)');
console.log('');
console.log('Examples:');
console.log(` ${colors.cyan}node utils.js health-check production${colors.reset}`);
console.log(` ${colors.cyan}node utils.js rollback staging${colors.reset}`);
console.log(` ${colors.cyan}node utils.js status development${colors.reset}`);
console.log(` ${colors.cyan}node utils.js list${colors.reset}`);
console.log('');
console.log('Or use package.json scripts:');
console.log(` ${colors.cyan}yarn health:prod${colors.reset}`);
console.log(` ${colors.cyan}yarn rollback:prod${colors.reset}`);
console.log(` ${colors.cyan}yarn status:prod${colors.reset}`);
console.log(''.repeat(60));
console.log('');
process.exit(1);
}
}
// Run if called directly
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
}
export { performHealthCheck, performRollback, checkAppStatus, listApps, cleanupBackups };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment