Monorepo · 3 servicios (api, web, worker) · GKE + Helm · Detección de cambios automática
// NutriTrack — Monorepo CI/CD Pipeline // Servicios: api (NestJS), web (React/Vite), worker (Node cron) // Infra: GKE + Helm + GCR · Kubernetes ephemeral agents @Library('nutritrack-pipeline-lib') _ pipeline { agent { kubernetes { yaml ''' apiVersion: v1 kind: Pod spec: serviceAccountName: jenkins-agent containers: - name: node image: node:20-alpine command: ["sleep", "99d"] resources: requests: { cpu: "500m", memory: "1Gi" } limits: { cpu: "2", memory: "2Gi" } - name: docker image: gcr.io/cloud-builders/docker:24 command: ["sleep", "99d"] securityContext: { privileged: true } - name: helm image: alpine/helm:3.14 command: ["sleep", "99d"] ''' defaultContainer 'node' } } options { timeout(time: 45, unit: 'MINUTES') disableConcurrentBuilds(abortPrevious: true) buildDiscarder(logRotator(numToKeepStr: '30')) timestamps() } environment { REGISTRY = 'gcr.io/nutritrack-prod' GIT_SHORT = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim() SERVICES = 'api web worker' HELM_TIMEOUT = '10m' } stages { // ─── DETECT CHANGES ────────────────────────────────────── stage('Detect Changes') { steps { script { env.CHANGED_API = sh(returnStdout: true, script: 'git diff --name-only HEAD~1 | grep -c "^services/api" || echo 0').trim() env.CHANGED_WEB = sh(returnStdout: true, script: 'git diff --name-only HEAD~1 | grep -c "^services/web" || echo 0').trim() env.CHANGED_WORKER = sh(returnStdout: true, script: 'git diff --name-only HEAD~1 | grep -c "^services/worker" || echo 0').trim() // shared-lib change triggers rebuild of all dependents def sharedChanged = sh(returnStdout: true, script: 'git diff --name-only HEAD~1 | grep -c "^libs/shared" || echo 0').trim() if (sharedChanged != '0') { env.CHANGED_API = env.CHANGED_WEB = env.CHANGED_WORKER = '1' echo "⚠ shared-lib modificado → rebuild completo" } } } } // ─── INSTALL ───────────────────────────────────────────── stage('Install') { steps { sh 'npm ci --workspaces' } } // ─── LINT & TEST & SECURITY (paralelo) ─────────────────── stage('Quality Gate') { parallel { stage('Lint') { steps { sh 'npm run lint --workspaces' } } stage('Unit Tests') { steps { sh 'npm test --workspaces -- --ci --coverage --reporters=junit' } post { always { junit '**/reports/junit.xml' publishHTML(target: [reportDir: 'coverage', reportFiles: 'index.html', reportName: 'Coverage Report']) } } } stage('Security Audit') { steps { sh 'npm audit --audit-level=high --workspaces' sh 'npx snyk test --severity-threshold=high || true' } } } } // ─── BUILD & PUSH DOCKER (solo servicios cambiados) ────── stage('Build Images') { parallel { stage('Build API') { when { expression { return env.CHANGED_API != '0' } } steps { container('docker') { buildDockerImage(service: 'api', tag: env.GIT_SHORT, registry: env.REGISTRY) } } } stage('Build Web') { when { expression { return env.CHANGED_WEB != '0' } } steps { container('docker') { buildDockerImage(service: 'web', tag: env.GIT_SHORT, registry: env.REGISTRY) } } } stage('Build Worker') { when { expression { return env.CHANGED_WORKER != '0' } } steps { container('docker') { buildDockerImage(service: 'worker', tag: env.GIT_SHORT, registry: env.REGISTRY) } } } } } // ─── DEPLOY STAGING (automático en main) ───────────────── stage('Deploy Staging') { when { anyOf { branch 'main' branch pattern: 'release/.*', comparator: 'REGEXP' } } steps { script { withCredentials([file(credentialsId: 'kubeconfig-staging', variable: 'KUBECONFIG')]) { container('helm') { ['api', 'web', 'worker'].each { svc -> if (env["CHANGED_${svc.toUpperCase()}"] != '0') { deployService(service: svc, env: 'staging', tag: env.GIT_SHORT, wait: true) } } } } } } } // ─── APROBACIÓN MANUAL ─────────────────────────────────── stage('Aprobar Producción') { when { branch 'main' } input { message '¿Desplegar en producción?' ok 'Desplegar' submitter 'platform-team,admin' parameters { booleanParam(defaultValue: true, name: 'RUN_SMOKE', description: 'Ejecutar smoke tests post-deploy') } } steps { echo 'Producción aprobada. Iniciando despliegue...' } } // ─── DEPLOY PRODUCCIÓN ─────────────────────────────────── stage('Deploy Producción') { when { branch 'main' } steps { script { withCredentials([file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG')]) { container('helm') { ['api', 'web', 'worker'].each { svc -> if (env["CHANGED_${svc.toUpperCase()}"] != '0') { deployService(service: svc, env: 'production', tag: env.GIT_SHORT, wait: true, rollbackOnFail: true) } } } } } } } } post { success { slackSend(channel: '#dev-ops', color: 'good', message: "✅ *NutriTrack* `${env.GIT_SHORT}` desplegado en producción · <${env.BUILD_URL}|Ver build>") } failure { slackSend(channel: '#dev-ops', color: 'danger', message: "🔴 *NutriTrack* build fallido en `${env.BRANCH_NAME}` · <${env.BUILD_URL}|Logs>") } always { cleanWs() } } }
// Shared Library — deployService // Uso: deployService(service: 'api', env: 'staging', // tag: '3a7f2c1', rollbackOnFail: true) def call(Map cfg) { def svc = cfg.service def targetEnv = cfg.env def tag = cfg.tag ?: env.GIT_SHORT def ns = targetEnv def releaseName = "nutritrack-${svc}" def chartPath = "./charts/${svc}" echo "→ Deploying ${svc}:${tag} to ${targetEnv}" try { sh """ helm upgrade --install ${releaseName} ${chartPath} \\ --namespace ${ns} \\ --create-namespace \\ --set image.repository=gcr.io/nutritrack-prod/${svc} \\ --set image.tag=${tag} \\ --set env=${targetEnv} \\ --wait \\ --timeout ${env.HELM_TIMEOUT ?: '10m'} \\ --atomic """ echo "✓ ${svc} desplegado correctamente en ${targetEnv}" } catch (Exception e) { echo "✗ Deploy falló: ${e.message}" if (cfg.rollbackOnFail) { echo "↩ Ejecutando rollback automático..." sh "helm rollback ${releaseName} 0 -n ${ns} --wait" } throw e } }
// Shared Library — buildDockerImage // Uso: buildDockerImage(service: 'api', tag: '3a7f2c1', // registry: 'gcr.io/nutritrack-prod') def call(Map cfg) { def svc = cfg.service def tag = cfg.tag def registry = cfg.registry def image = "${registry}/${svc}" def fullTag = "${image}:${tag}" def context = "./services/${svc}" sh "docker build -t ${fullTag} ${context}" // Workload Identity para GCR — sin credenciales explícitas sh "gcloud auth configure-docker gcr.io --quiet" sh "docker push ${fullTag}" if (env.BRANCH_NAME == 'main') { sh "docker tag ${fullTag} ${image}:latest" sh "docker push ${image}:latest" } echo "✓ Imagen publicada: ${fullTag}" return fullTag }
| Servicio | Estado | Acción |
|---|---|---|
| services/api | ● modificado | Build + Deploy |
| libs/shared | ● modificado | Fuerza rebuild total |
| services/web | — sin cambios | Skip build (↑ shared) |
| services/worker | — sin cambios | Skip build (↑ shared) |
3a7f2c1 desplegado en producciónapi web workermain · Build: #247production (GKE)