// ============================================================================= // ERP-SUITE - Jenkins Multi-Vertical Pipeline // ============================================================================= // Gestiona el despliegue de erp-core y todas las verticales // Servidor: 72.60.226.4 // ============================================================================= pipeline { agent any parameters { choice( name: 'VERTICAL', choices: ['erp-core', 'construccion', 'vidrio-templado', 'mecanicas-diesel', 'retail', 'clinicas', 'pos-micro', 'ALL'], description: 'Vertical a desplegar (ALL despliega todas las activas)' ) choice( name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Ambiente de despliegue' ) booleanParam( name: 'RUN_MIGRATIONS', defaultValue: false, description: 'Ejecutar migraciones de BD' ) booleanParam( name: 'SKIP_TESTS', defaultValue: false, description: 'Saltar tests (solo para hotfixes)' ) } environment { PROJECT_NAME = 'erp-suite' DOCKER_REGISTRY = '72.60.226.4:5000' DEPLOY_SERVER = '72.60.226.4' DEPLOY_USER = 'deploy' DEPLOY_PATH = '/opt/apps/erp-suite' VERSION = "${env.BUILD_NUMBER}" GIT_COMMIT_SHORT = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() // Verticales activas (con código desarrollado) ACTIVE_VERTICALS = 'erp-core,construccion,mecanicas-diesel' } options { timeout(time: 60, unit: 'MINUTES') disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '15')) timestamps() } stages { // ===================================================================== // PREPARACIÓN // ===================================================================== stage('Checkout') { steps { checkout scm script { env.GIT_BRANCH = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() currentBuild.displayName = "#${BUILD_NUMBER} - ${params.VERTICAL} - ${GIT_COMMIT_SHORT}" } } } stage('Determine Verticals') { steps { script { if (params.VERTICAL == 'ALL') { env.VERTICALS_TO_BUILD = env.ACTIVE_VERTICALS } else { env.VERTICALS_TO_BUILD = params.VERTICAL } echo "Verticales a construir: ${env.VERTICALS_TO_BUILD}" } } } // ===================================================================== // BUILD & TEST POR VERTICAL // ===================================================================== stage('Build Verticals') { steps { script { def verticals = env.VERTICALS_TO_BUILD.split(',') def parallelStages = [:] verticals.each { vertical -> parallelStages["Build ${vertical}"] = { buildVertical(vertical) } } parallel parallelStages } } } stage('Run Tests') { when { expression { return !params.SKIP_TESTS } } steps { script { def verticals = env.VERTICALS_TO_BUILD.split(',') def parallelTests = [:] verticals.each { vertical -> parallelTests["Test ${vertical}"] = { testVertical(vertical) } } parallel parallelTests } } } // ===================================================================== // DOCKER BUILD & PUSH // ===================================================================== stage('Docker Build & Push') { when { anyOf { branch 'main' branch 'develop' } } steps { script { def verticals = env.VERTICALS_TO_BUILD.split(',') verticals.each { vertical -> def config = getVerticalConfig(vertical) // Build Backend if (fileExists("${config.path}/backend/Dockerfile")) { sh """ docker build -t ${DOCKER_REGISTRY}/erp-${vertical}-backend:${VERSION} \ -t ${DOCKER_REGISTRY}/erp-${vertical}-backend:latest \ ${config.path}/backend/ docker push ${DOCKER_REGISTRY}/erp-${vertical}-backend:${VERSION} docker push ${DOCKER_REGISTRY}/erp-${vertical}-backend:latest """ } // Build Frontend def frontendPath = config.frontendPath ?: "${config.path}/frontend" if (fileExists("${frontendPath}/Dockerfile")) { sh """ docker build -t ${DOCKER_REGISTRY}/erp-${vertical}-frontend:${VERSION} \ -t ${DOCKER_REGISTRY}/erp-${vertical}-frontend:latest \ ${frontendPath}/ docker push ${DOCKER_REGISTRY}/erp-${vertical}-frontend:${VERSION} docker push ${DOCKER_REGISTRY}/erp-${vertical}-frontend:latest """ } echo "✅ Docker images pushed for ${vertical}" } } } } // ===================================================================== // DATABASE MIGRATIONS // ===================================================================== stage('Run Migrations') { when { expression { return params.RUN_MIGRATIONS } } steps { script { // Siempre ejecutar migraciones de erp-core primero if (env.VERTICALS_TO_BUILD.contains('erp-core') || params.VERTICAL == 'ALL') { echo "Ejecutando migraciones de erp-core..." runMigrations('erp-core') } // Luego migraciones de verticales def verticals = env.VERTICALS_TO_BUILD.split(',') verticals.each { vertical -> if (vertical != 'erp-core') { echo "Ejecutando migraciones de ${vertical}..." runMigrations(vertical) } } } } } // ===================================================================== // DEPLOY // ===================================================================== stage('Deploy to Staging') { when { allOf { branch 'develop' expression { return params.ENVIRONMENT == 'staging' } } } steps { script { deployVerticals('staging') } } } stage('Deploy to Production') { when { allOf { branch 'main' expression { return params.ENVIRONMENT == 'production' } } } steps { input message: '¿Confirmar despliegue a PRODUCCIÓN?', ok: 'Desplegar' script { deployVerticals('production') } } } // ===================================================================== // HEALTH CHECKS // ===================================================================== stage('Health Checks') { steps { script { def verticals = env.VERTICALS_TO_BUILD.split(',') verticals.each { vertical -> def config = getVerticalConfig(vertical) def healthUrl = "http://${DEPLOY_SERVER}:${config.backendPort}/health" retry(5) { sleep(time: 10, unit: 'SECONDS') def response = sh(script: "curl -sf ${healthUrl}", returnStatus: true) if (response != 0) { error "Health check failed for ${vertical}" } } echo "✅ ${vertical} is healthy" } } } } } // ========================================================================= // POST ACTIONS // ========================================================================= post { success { script { def message = """ ✅ *ERP-Suite Deploy Exitoso* • *Verticales:* ${env.VERTICALS_TO_BUILD} • *Ambiente:* ${params.ENVIRONMENT} • *Build:* #${BUILD_NUMBER} • *Commit:* ${GIT_COMMIT_SHORT} """.stripIndent() echo message // slackSend(color: 'good', message: message) } } failure { script { def message = """ ❌ *ERP-Suite Deploy Fallido* • *Verticales:* ${env.VERTICALS_TO_BUILD} • *Build:* #${BUILD_NUMBER} • *Console:* ${BUILD_URL}console """.stripIndent() echo message // slackSend(color: 'danger', message: message) } } always { cleanWs() } } } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= def getVerticalConfig(String vertical) { def configs = [ 'erp-core': [ path: 'apps/erp-core', frontendPath: 'apps/erp-core/frontend', frontendPort: 3010, backendPort: 3011, dbSchema: 'auth,core,inventory', active: true ], 'construccion': [ path: 'apps/verticales/construccion', frontendPath: 'apps/verticales/construccion/frontend/web', frontendPort: 3020, backendPort: 3021, dbSchema: 'construccion', active: true ], 'vidrio-templado': [ path: 'apps/verticales/vidrio-templado', frontendPort: 3030, backendPort: 3031, dbSchema: 'vidrio', active: false ], 'mecanicas-diesel': [ path: 'apps/verticales/mecanicas-diesel', frontendPort: 3040, backendPort: 3041, dbSchema: 'service_management,parts_management,vehicle_management', active: true ], 'retail': [ path: 'apps/verticales/retail', frontendPort: 3050, backendPort: 3051, dbSchema: 'retail', active: false ], 'clinicas': [ path: 'apps/verticales/clinicas', frontendPort: 3060, backendPort: 3061, dbSchema: 'clinicas', active: false ], 'pos-micro': [ path: 'apps/products/pos-micro', frontendPort: 3070, backendPort: 3071, dbSchema: 'pos', active: false ] ] return configs[vertical] ?: error("Vertical ${vertical} no configurada") } def buildVertical(String vertical) { def config = getVerticalConfig(vertical) stage("Install ${vertical}") { if (fileExists("${config.path}/backend/package.json")) { dir("${config.path}/backend") { sh 'npm ci --prefer-offline' } } def frontendPath = config.frontendPath ?: "${config.path}/frontend" if (fileExists("${frontendPath}/package.json")) { dir(frontendPath) { sh 'npm ci --prefer-offline' } } } stage("Build ${vertical}") { if (fileExists("${config.path}/backend/package.json")) { dir("${config.path}/backend") { sh 'npm run build' } } def frontendPath = config.frontendPath ?: "${config.path}/frontend" if (fileExists("${frontendPath}/package.json")) { dir(frontendPath) { sh 'npm run build' } } } } def testVertical(String vertical) { def config = getVerticalConfig(vertical) if (fileExists("${config.path}/backend/package.json")) { dir("${config.path}/backend") { sh 'npm run test || true' sh 'npm run lint || true' } } } def runMigrations(String vertical) { def config = getVerticalConfig(vertical) sshagent(['deploy-ssh-key']) { sh """ ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_SERVER} ' cd ${DEPLOY_PATH}/${vertical} docker-compose exec -T backend npm run migration:run || true ' """ } } def deployVerticals(String environment) { def verticals = env.VERTICALS_TO_BUILD.split(',') sshagent(['deploy-ssh-key']) { // Desplegar erp-core primero si está en la lista if (verticals.contains('erp-core')) { deployVertical('erp-core', environment) } // Luego el resto de verticales verticals.each { vertical -> if (vertical != 'erp-core') { deployVertical(vertical, environment) } } } } def deployVertical(String vertical, String environment) { def config = getVerticalConfig(vertical) echo "Desplegando ${vertical} a ${environment}..." sh """ ssh -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_SERVER} ' cd ${DEPLOY_PATH}/${vertical} # Pull nuevas imágenes docker-compose -f docker-compose.prod.yml pull # Detener contenedores actuales docker-compose -f docker-compose.prod.yml down --remove-orphans # Iniciar nuevos contenedores docker-compose -f docker-compose.prod.yml up -d # Cleanup docker system prune -f echo "✅ ${vertical} desplegado" ' """ }