diff --git a/Jenkinsfile b/Jenkinsfile index 53faac86..e85ded5c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,5 @@ // Pipeline CI/CD — Aduanasoft Anexo76 -// Ejecuta: tests → security scan → docker build/push → E2E → deploy dev → smoke test +// Ejecuta: tests+security (paralelo) → docker build/push (paralelo) → E2E → deploy dev → smoke test // // Credenciales requeridas en Jenkins: // - harbor-credentials : username/password para dev.aduanasoft.com @@ -37,8 +37,13 @@ pipeline { stages { - // ── Tests (backend y frontend unit en paralelo) ─────────────────────── - stage('Tests') { + // ── Tests + Security (4 trabajos en paralelo) ───────────────────────── + // Backend tests, frontend Vitest, pip-audit y npm audit son independientes: + // todos en un solo bloque parallel — la stage tarda el máximo de los 4, + // no la suma. pip y pnpm cachean en volumes persistentes del agente + // (jenkins-a76-pip-cache, jenkins-a76-pnpm-store) para reusar deps + // entre builds (~30-60s menos cada uno cuando lockfiles no cambian). + stage('Tests & Security') { parallel { stage('Test — Backend') { @@ -92,7 +97,11 @@ pipeline { " docker rm -f "$C" >/dev/null 2>&1 || true - docker run -d --name "$C" --network "$NET" "$PYTHON_IMAGE" sleep infinity + # Volume persistente para el cache de pip (~/.cache/pip): + # primer build lo puebla, siguientes reusan wheels si requirements.txt no cambió. + docker run -d --name "$C" --network "$NET" \ + -v jenkins-a76-pip-cache:/root/.cache/pip \ + "$PYTHON_IMAGE" sleep infinity docker cp "$WORKSPACE/backend/." "$C:/app" docker exec \ @@ -144,7 +153,12 @@ pipeline { trap cleanup EXIT docker rm -f "$C" >/dev/null 2>&1 || true - docker run -d --name "$C" "$PLAYWRIGHT_IMAGE" sleep infinity + # Volume persistente para el store de pnpm (content-addressable): + # primer build lo puebla, siguientes reusan paquetes si pnpm-lock.yaml no cambió. + # Path estándar de pnpm@9 en Linux como root: /root/.local/share/pnpm/store + docker run -d --name "$C" \ + -v jenkins-a76-pnpm-store:/root/.local/share/pnpm/store \ + "$PLAYWRIGHT_IMAGE" sleep infinity docker exec "$C" mkdir -p /workspace docker cp "$WORKSPACE/." "$C:/workspace" @@ -165,12 +179,6 @@ pipeline { } } - } - } - - // ── Escaneo de vulnerabilidades ─────────────────────────────────────── - stage('Security Scan') { - parallel { stage('pip-audit') { steps { sh ''' @@ -180,7 +188,9 @@ pipeline { trap cleanup EXIT docker rm -f "$C" >/dev/null 2>&1 || true - docker run -d --name "$C" "$PYTHON_IMAGE" sleep infinity + docker run -d --name "$C" \ + -v jenkins-a76-pip-cache:/root/.cache/pip \ + "$PYTHON_IMAGE" sleep infinity docker cp "$WORKSPACE/backend/requirements.txt" "$C:/requirements.txt" docker exec "$C" bash -lc ' @@ -212,6 +222,7 @@ pipeline { ''' } } + } } @@ -243,55 +254,86 @@ pipeline { } } - // ── Docker Build + Push ─────────────────────────────────────────────── + // ── Docker Build + Push (backend y frontend en paralelo) ────────────── + // Antes era secuencial: ~6-10 min totales. Ahora cada imagen builda+pushea + // en su propio substage en paralelo → tarda el máximo de los dos. + // BuildKit inline cache + --cache-from reusan las layers de :latest cuando + // pip-lock / pnpm-lock no cambiaron (~1-3 min menos por imagen). stage('Docker Build') { when { anyOf { branch 'main'; branch 'development' } } - steps { - script { - withCredentials([ - usernamePassword( - credentialsId: 'harbor-credentials', - usernameVariable: 'HARBOR_USER', - passwordVariable: 'HARBOR_PASS' - ) - ]) { - sh "echo \"\${HARBOR_PASS}\" | docker login ${HARBOR_REGISTRY} -u \"\${HARBOR_USER}\" --password-stdin" - - sh """ - docker build --progress=plain \ - --build-arg APP_VERSION=${env.APP_VERSION} \ - -t ${IMAGE_BACKEND}:${env.APP_VERSION} \ - -t ${IMAGE_BACKEND}:latest \ - -f backend/Dockerfile \ - backend/ - """ + parallel { + stage('Build & Push — Backend') { + steps { withCredentials([ + usernamePassword( + credentialsId: 'harbor-credentials', + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ) + ]) { + sh """ + set -euo pipefail + export DOCKER_BUILDKIT=1 + echo "\${HARBOR_PASS}" | docker login ${HARBOR_REGISTRY} -u "\${HARBOR_USER}" --password-stdin + + # Pull de :latest para alimentar --cache-from (silencioso si no existe) + docker pull ${IMAGE_BACKEND}:latest >/dev/null 2>&1 || true + + docker build --progress=plain \\ + --cache-from ${IMAGE_BACKEND}:latest \\ + --build-arg BUILDKIT_INLINE_CACHE=1 \\ + --build-arg APP_VERSION=${env.APP_VERSION} \\ + -t ${IMAGE_BACKEND}:${env.APP_VERSION} \\ + -t ${IMAGE_BACKEND}:latest \\ + -f backend/Dockerfile \\ + backend/ + + docker push ${IMAGE_BACKEND}:${env.APP_VERSION} + docker push ${IMAGE_BACKEND}:latest + """ + } + } + } + + stage('Build & Push — Frontend') { + steps { + withCredentials([ + usernamePassword( + credentialsId: 'harbor-credentials', + usernameVariable: 'HARBOR_USER', + passwordVariable: 'HARBOR_PASS' + ), string(credentialsId: 'a76-public-url-dev', variable: 'A76_URL') ]) { sh """ - docker build --progress=plain \ - --build-arg VITE_API_URL=\${A76_URL}/api/ \ - --build-arg VITE_KEYCLOAK_URL=${KC_URL} \ - --build-arg VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \ - --build-arg INTERNAL_API_URL=http://backend:3467/api/ \ - -t ${IMAGE_FRONTEND}:${env.APP_VERSION} \ - -t ${IMAGE_FRONTEND}:latest \ - -f frontend/Dockerfile.prod \ + set -euo pipefail + export DOCKER_BUILDKIT=1 + echo "\${HARBOR_PASS}" | docker login ${HARBOR_REGISTRY} -u "\${HARBOR_USER}" --password-stdin + + docker pull ${IMAGE_FRONTEND}:latest >/dev/null 2>&1 || true + + docker build --progress=plain \\ + --cache-from ${IMAGE_FRONTEND}:latest \\ + --build-arg BUILDKIT_INLINE_CACHE=1 \\ + --build-arg VITE_API_URL=\${A76_URL}/api/ \\ + --build-arg VITE_KEYCLOAK_URL=${KC_URL} \\ + --build-arg VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \\ + --build-arg INTERNAL_API_URL=http://backend:3467/api/ \\ + -t ${IMAGE_FRONTEND}:${env.APP_VERSION} \\ + -t ${IMAGE_FRONTEND}:latest \\ + -f frontend/Dockerfile.prod \\ frontend/ + + docker push ${IMAGE_FRONTEND}:${env.APP_VERSION} + docker push ${IMAGE_FRONTEND}:latest """ } - - sh """ - docker push ${IMAGE_BACKEND}:${env.APP_VERSION} - docker push ${IMAGE_BACKEND}:latest - docker push ${IMAGE_FRONTEND}:${env.APP_VERSION} - docker push ${IMAGE_FRONTEND}:latest - """ } } + } }