#!/usr/bin/env node /** * Script de verificación pre-deploy * Fase 7.4 - Go Live y Soporte * * Este script verifica que todo esté listo antes de un despliegue a producción. * * Uso: * node scripts/pre-deploy-check.js * * Salida: * - Código 0 si todas las verificaciones pasan * - Código 1 si alguna verificación falla */ const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); // Colores para output const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; // Resultados const results = { passed: [], failed: [], warnings: [], }; /** * Imprime un mensaje con color */ function print(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } /** * Ejecuta un comando y retorna el resultado */ function runCommand(command, options = {}) { try { return execSync(command, { encoding: 'utf-8', stdio: options.silent ? 'pipe' : 'inherit', ...options }); } catch (error) { if (options.ignoreError) { return error.stdout || ''; } throw error; } } /** * Verifica variables de entorno requeridas */ function checkEnvironmentVariables() { print('\n🔍 Verificando variables de entorno...', 'cyan'); const required = [ 'DATABASE_URL', 'JWT_SECRET', 'NODE_ENV', ]; const recommended = [ 'SMTP_HOST', 'SMTP_USER', 'SMTP_PASS', 'MERCADOPAGO_ACCESS_TOKEN', 'FRONTEND_URL', 'API_URL', ]; let allRequiredPresent = true; // Verificar requeridas for (const env of required) { if (!process.env[env]) { print(` ❌ ${env}: NO DEFINIDA`, 'red'); results.failed.push(`Variable requerida faltante: ${env}`); allRequiredPresent = false; } else { print(` ✅ ${env}: Definida`, 'green'); } } // Verificar recomendadas for (const env of recommended) { if (!process.env[env]) { print(` ⚠️ ${env}: No definida (recomendada)`, 'yellow'); results.warnings.push(`Variable recomendada faltante: ${env}`); } else { print(` ✅ ${env}: Definida`, 'green'); } } if (allRequiredPresent) { results.passed.push('Variables de entorno requeridas'); } return allRequiredPresent; } /** * Verifica conexión a base de datos */ async function checkDatabaseConnection() { print('\n🔍 Verificando conexión a base de datos...', 'cyan'); try { const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); // Intentar conectar await prisma.$connect(); // Verificar que podemos hacer queries await prisma.$queryRaw`SELECT 1`; // Obtener información de la BD const dbInfo = await prisma.$queryRaw`SELECT sqlite_version() as version`; await prisma.$disconnect(); print(` ✅ Conexión a base de datos exitosa`, 'green'); print(` 📊 Versión: ${dbInfo[0]?.version || 'N/A'}`, 'blue'); results.passed.push('Conexión a base de datos'); return true; } catch (error) { print(` ❌ Error de conexión: ${error.message}`, 'red'); results.failed.push(`Conexión a base de datos fallida: ${error.message}`); return false; } } /** * Verifica migraciones pendientes */ async function checkPendingMigrations() { print('\n🔍 Verificando migraciones pendientes...', 'cyan'); try { // Generar cliente prisma primero runCommand('npx prisma generate', { silent: true }); // Verificar estado de migraciones const output = runCommand('npx prisma migrate status', { silent: true, ignoreError: true }); if (output.includes('Database schema is up to date') || output.includes('No pending migrations')) { print(` ✅ No hay migraciones pendientes`, 'green'); results.passed.push('Migraciones de base de datos'); return true; } else if (output.includes('pending migration')) { print(` ⚠️ Hay migraciones pendientes`, 'yellow'); print(` Ejecute: npx prisma migrate deploy`, 'yellow'); results.warnings.push('Hay migraciones pendientes de aplicar'); return true; // Es warning, no error } else { print(` ✅ Estado de migraciones verificado`, 'green'); results.passed.push('Migraciones de base de datos'); return true; } } catch (error) { print(` ⚠️ No se pudo verificar estado de migraciones`, 'yellow'); results.warnings.push(`Verificación de migraciones: ${error.message}`); return true; // No es crítico para el deploy } } /** * Verifica dependencias críticas */ function checkDependencies() { print('\n🔍 Verificando dependencias críticas...', 'cyan'); const criticalDeps = [ '@prisma/client', 'express', 'bcrypt', 'jsonwebtoken', 'cors', 'helmet', 'dotenv', ]; const packageJsonPath = path.join(process.cwd(), 'package.json'); if (!fs.existsSync(packageJsonPath)) { print(` ❌ package.json no encontrado`, 'red'); results.failed.push('package.json no encontrado'); return false; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; let allPresent = true; for (const dep of criticalDeps) { if (allDeps[dep]) { print(` ✅ ${dep}@${allDeps[dep]}`, 'green'); } else { print(` ❌ ${dep}: NO INSTALADO`, 'red'); results.failed.push(`Dependencia crítica faltante: ${dep}`); allPresent = false; } } if (allPresent) { results.passed.push('Dependencias críticas instaladas'); } return allPresent; } /** * Verifica espacio en disco */ function checkDiskSpace() { print('\n🔍 Verificando espacio en disco...', 'cyan'); try { // En Linux/Mac, usar df const platform = process.platform; if (platform === 'linux' || platform === 'darwin') { const output = runCommand('df -h .', { silent: true }); const lines = output.trim().split('\n'); const dataLine = lines[lines.length - 1]; const parts = dataLine.split(/\s+/); const usedPercent = parseInt(parts[4].replace('%', '')); if (usedPercent > 90) { print(` ❌ Uso de disco crítico: ${usedPercent}%`, 'red'); results.failed.push(`Uso de disco crítico: ${usedPercent}%`); return false; } else if (usedPercent > 80) { print(` ⚠️ Uso de disco alto: ${usedPercent}%`, 'yellow'); results.warnings.push(`Uso de disco alto: ${usedPercent}%`); } else { print(` ✅ Uso de disco: ${usedPercent}%`, 'green'); } results.passed.push('Espacio en disco'); return true; } else { print(` ⚠️ Verificación de disco no soportada en ${platform}`, 'yellow'); results.warnings.push(`Verificación de disco no soportada en ${platform}`); return true; } } catch (error) { print(` ⚠️ No se pudo verificar espacio en disco`, 'yellow'); results.warnings.push(`Verificación de disco: ${error.message}`); return true; } } /** * Verifica que el build funcione */ function checkBuild() { print('\n🔍 Verificando build de TypeScript...', 'cyan'); try { // Limpiar build anterior si existe if (fs.existsSync(path.join(process.cwd(), 'dist'))) { print(` 🧹 Limpiando build anterior...`, 'blue'); fs.rmSync(path.join(process.cwd(), 'dist'), { recursive: true }); } // Intentar compilar runCommand('npx tsc --noEmit', { silent: true }); print(` ✅ TypeScript compila sin errores`, 'green'); results.passed.push('Build de TypeScript'); return true; } catch (error) { print(` ❌ Errores de compilación de TypeScript`, 'red'); print(` ${error.message}`, 'red'); results.failed.push('Errores de compilación de TypeScript'); return false; } } /** * Verifica archivos de configuración */ function checkConfigurationFiles() { print('\n🔍 Verificando archivos de configuración...', 'cyan'); const requiredFiles = [ 'package.json', 'tsconfig.json', 'prisma/schema.prisma', ]; const optionalFiles = [ '.env.example', 'Dockerfile', 'docker-compose.yml', ]; let allRequiredPresent = true; for (const file of requiredFiles) { const filePath = path.join(process.cwd(), file); if (fs.existsSync(filePath)) { print(` ✅ ${file}`, 'green'); } else { print(` ❌ ${file}: NO ENCONTRADO`, 'red'); results.failed.push(`Archivo requerido faltante: ${file}`); allRequiredPresent = false; } } for (const file of optionalFiles) { const filePath = path.join(process.cwd(), file); if (fs.existsSync(filePath)) { print(` ✅ ${file}`, 'green'); } else { print(` ⚠️ ${file}: No encontrado (opcional)`, 'yellow'); results.warnings.push(`Archivo opcional faltante: ${file}`); } } if (allRequiredPresent) { results.passed.push('Archivos de configuración requeridos'); } return allRequiredPresent; } /** * Verifica tests (si existen) */ function checkTests() { print('\n🔍 Verificando tests...', 'cyan'); // Verificar si hay tests const testDirs = ['tests', '__tests__', 'test', 'spec']; const hasTests = testDirs.some(dir => fs.existsSync(path.join(process.cwd(), dir)) ); if (!hasTests) { print(` ⚠️ No se encontraron directorios de tests`, 'yellow'); results.warnings.push('No hay tests configurados'); return true; } // Verificar si jest está configurado const packageJsonPath = path.join(process.cwd(), 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); if (!packageJson.scripts?.test) { print(` ⚠️ No hay script de test configurado`, 'yellow'); results.warnings.push('Script de test no configurado'); return true; } try { // Intentar ejecutar tests runCommand('npm test', { silent: true }); print(` ✅ Tests pasaron`, 'green'); results.passed.push('Tests pasando'); return true; } catch (error) { print(` ❌ Algunos tests fallaron`, 'red'); results.failed.push('Tests fallidos'); return false; } } /** * Verifica endpoints críticos (requiere servidor corriendo) */ async function checkCriticalEndpoints() { print('\n🔍 Verificando endpoints críticos...', 'cyan'); const baseUrl = process.env.API_URL || 'http://localhost:3000'; const endpoints = [ { path: '/api/v1/health', name: 'Health Check' }, ]; let allWorking = true; for (const endpoint of endpoints) { try { const response = await fetch(`${baseUrl}${endpoint.path}`); if (response.ok) { print(` ✅ ${endpoint.name} (${endpoint.path})`, 'green'); } else { print(` ❌ ${endpoint.name} (${endpoint.path}): HTTP ${response.status}`, 'red'); results.failed.push(`Endpoint no disponible: ${endpoint.path}`); allWorking = false; } } catch (error) { print(` ⚠️ ${endpoint.name} (${endpoint.path}): Servidor no disponible`, 'yellow'); results.warnings.push(`No se pudo verificar endpoint: ${endpoint.path}`); // No es crítico si el servidor no está corriendo durante el check } } if (allWorking) { results.passed.push('Endpoints críticos disponibles'); } return true; } /** * Verifica seguridad básica */ function checkSecurityConfig() { print('\n🔍 Verificando configuración de seguridad...', 'cyan'); const issues = []; // Verificar JWT_SECRET const jwtSecret = process.env.JWT_SECRET; if (jwtSecret) { if (jwtSecret.length < 32) { issues.push('JWT_SECRET es muy corto (mínimo 32 caracteres)'); } if (jwtSecret === 'your-secret-key' || jwtSecret === 'secret') { issues.push('JWT_SECRET usa valor por defecto inseguro'); } } // Verificar NODE_ENV if (process.env.NODE_ENV === 'development') { issues.push('NODE_ENV está en development'); } // Verificar CORS if (process.env.FRONTEND_URL === '*') { issues.push('CORS permite todos los orígenes (*)'); } if (issues.length === 0) { print(` ✅ Configuración de seguridad correcta`, 'green'); results.passed.push('Configuración de seguridad'); return true; } else { for (const issue of issues) { print(` ⚠️ ${issue}`, 'yellow'); } results.warnings.push('Problemas de seguridad encontrados'); return true; // Son warnings, no errores } } /** * Imprime resumen final */ function printSummary() { print('\n' + '='.repeat(60), 'cyan'); print('RESUMEN DE VERIFICACIÓN PRE-DEPLOY', 'cyan'); print('='.repeat(60), 'cyan'); print(`\n✅ Verificaciones exitosas: ${results.passed.length}`, 'green'); results.passed.forEach(item => print(` ✓ ${item}`, 'green')); if (results.warnings.length > 0) { print(`\n⚠️ Advertencias: ${results.warnings.length}`, 'yellow'); results.warnings.forEach(item => print(` • ${item}`, 'yellow')); } if (results.failed.length > 0) { print(`\n❌ Errores: ${results.failed.length}`, 'red'); results.failed.forEach(item => print(` ✗ ${item}`, 'red')); } print('\n' + '='.repeat(60), 'cyan'); if (results.failed.length === 0) { print('✅ TODAS LAS VERIFICACIONES CRÍTICAS PASARON', 'green'); print('El sistema está listo para deploy.', 'green'); return 0; } else { print('❌ HAY ERRORES CRÍTICOS QUE DEBEN CORREGIRSE', 'red'); print('Por favor corrija los errores antes de deployar.', 'red'); return 1; } } /** * Función principal */ async function main() { print('\n🚀 INICIANDO VERIFICACIÓN PRE-DEPLOY', 'cyan'); print(`📅 ${new Date().toISOString()}`, 'blue'); print(`📁 Directorio: ${process.cwd()}`, 'blue'); const checks = [ checkEnvironmentVariables(), checkDependencies(), checkConfigurationFiles(), checkBuild(), checkSecurityConfig(), checkDiskSpace(), ]; // Checks asíncronos await checkDatabaseConnection(); await checkPendingMigrations(); await checkCriticalEndpoints(); // Tests (opcional) try { checkTests(); } catch (e) { // Ignorar errores de tests } // Imprimir resumen y salir con código apropiado const exitCode = printSummary(); process.exit(exitCode); } // Ejecutar main().catch(error => { print(`\n💥 Error fatal: ${error.message}`, 'red'); process.exit(1); });