feat(ci): validar imagen nueva en E2E antes del deploy a dev

El stage E2E corría contra A76_URL (versión viva en dev), así que detectaba
regresiones un build tarde: el código roto ya estaba desplegado cuando el
siguiente pipeline lo encontraba. Ahora E2E levanta un stack efímero con la
imagen del backend recién pusheada a Harbor y una imagen del frontend
buildeada localmente con VITE_API_URL=http://localhost:8000/api/ (la imagen
de prod tiene la URL de dev bakeada en el bundle). Si E2E falla, el deploy
no ocurre — el guardrail actúa sobre lo que se va a desplegar.

Cambios:
- docker-compose.e2e.yml (nuevo): postgres + minio + valkey + backend +
  frontend efímeros, sin volúmenes persistidos, image tags vía env vars.
- Jenkinsfile: stage E2E reescrito. Build temp frontend en paralelo con
  startup DB+alembic; preparación Playwright en paralelo con health wait
  de backend/frontend. --cache-from de la imagen prod para reusar layer
  de pnpm install. Cleanup garantizado vía trap EXIT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 13:41:31 -05:00
parent e7ece2a2a3
commit c60533eb3a
2 changed files with 308 additions and 28 deletions

179
Jenkinsfile vendored
View File

@@ -293,15 +293,17 @@ pipeline {
}
}
// ── E2E (Playwright) — contra la versión actualmente desplegada ─────────
// Corre ANTES del deploy con la imagen ya en Harbor pero aún no desplegada.
// Playwright apunta a A76_URL (versión viva anterior) — dominio de confianza
// para Workspace. Si E2E falla, el deploy no ocurre.
// ── E2E (Playwright) — contra la imagen recién buildeada ────────────────
// Levanta un stack efímero (docker-compose.e2e.yml) con la imagen del backend
// recién pusheada a Harbor y una imagen del frontend buildeada localmente con
// VITE_API_URL=http://localhost:8000/api/ (la imagen de prod tiene la URL de dev
// bakeada en el bundle, ver Dockerfile.prod:19-20). Si E2E falla, el deploy
// no ocurre — el guardrail actúa sobre el código que se va a desplegar,
// no sobre la versión viva.
stage('E2E (Playwright)') {
when { branch 'development' }
steps {
withCredentials([
string(credentialsId: 'a76-public-url-dev', variable: 'A76_URL'),
usernamePassword(
credentialsId: 'a76-e2e-credentials',
usernameVariable: 'E2E_USER',
@@ -310,43 +312,164 @@ pipeline {
]) {
sh '''
set -euo pipefail
C="a76-test-e2e-${BUILD_NUMBER}"
cleanup() { docker rm -f "$C" >/dev/null 2>&1 || true; }
export DOCKER_BUILDKIT=1
PROJECT="a76-e2e-${BUILD_NUMBER}"
FE_IMAGE_E2E="${IMAGE_FRONTEND}:e2e-${BUILD_NUMBER}"
BE_IMAGE_E2E="${IMAGE_BACKEND}:${APP_VERSION}"
PLAYWRIGHT_C="a76-test-e2e-${BUILD_NUMBER}"
BUILD_LOG="${WORKSPACE}/e2e-frontend-build.log"
PREP_LOG="${WORKSPACE}/e2e-playwright-prep.log"
export E2E_BACKEND_IMAGE="$BE_IMAGE_E2E"
export E2E_FRONTEND_IMAGE="$FE_IMAGE_E2E"
cleanup() {
echo "=== E2E cleanup ==="
docker rm -f "$PLAYWRIGHT_C" >/dev/null 2>&1 || true
docker compose -p "$PROJECT" -f docker-compose.e2e.yml down -v --remove-orphans >/dev/null 2>&1 || true
docker rmi -f "$FE_IMAGE_E2E" >/dev/null 2>&1 || true
rm -f "$BUILD_LOG" "$PREP_LOG" || true
}
trap cleanup EXIT
docker rm -f "$C" >/dev/null 2>&1 || true
docker run -d --name "$C" "$PLAYWRIGHT_IMAGE" sleep infinity
docker exec "$C" mkdir -p /workspace
docker cp "$WORKSPACE/." "$C:/workspace"
# ── PARALELO #1 ─────────────────────────────────────────────────
# Lanzar en background el build del frontend temporal Y el stack
# de dependencias. El build es lo más lento (~2-4 min); mientras
# corre, levantamos postgres+minio+valkey y aplicamos migraciones.
# ────────────────────────────────────────────────────────────────
# Build temporal del frontend con VITE_API_URL=localhost.
# --cache-from reusa la layer de pnpm install de la imagen de prod
# recién buildeada (las deps son idénticas, solo cambia VITE_API_URL).
# No se pushea a Harbor; solo existe en este agente durante el stage.
( docker build --progress=plain \
--cache-from "${IMAGE_FRONTEND}:latest" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--build-arg VITE_API_URL=http://localhost:8000/api/ \
--build-arg VITE_KEYCLOAK_URL=${KC_URL} \
--build-arg VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \
--build-arg INTERNAL_API_URL=http://backend:8000/api/ \
-t "$FE_IMAGE_E2E" \
-f frontend/Dockerfile.prod \
frontend/ ) > "$BUILD_LOG" 2>&1 &
BUILD_PID=$!
# En foreground: levantar dependencias y correr migraciones
docker compose -p "$PROJECT" -f docker-compose.e2e.yml up -d postgres-a76 minio valkey
# Esperar postgres healthy (timeout 60s)
READY=0
for i in $(seq 1 30); do
STATUS=$(docker inspect -f '{{.State.Health.Status}}' \
"$(docker compose -p "$PROJECT" -f docker-compose.e2e.yml ps -q postgres-a76)" 2>/dev/null || echo "starting")
if [ "$STATUS" = "healthy" ]; then READY=1; break; fi
sleep 2
done
if [ "$READY" != "1" ]; then
echo "ERROR: Postgres no quedó healthy en 60s"
docker compose -p "$PROJECT" -f docker-compose.e2e.yml logs postgres-a76 || true
exit 1
fi
# Crear schemas que el backend espera (mismo patrón que Test — Backend)
DB_C=$(docker compose -p "$PROJECT" -f docker-compose.e2e.yml ps -q postgres-a76)
docker exec "$DB_C" psql -U postgres -d anexo76_core -v ON_ERROR_STOP=1 -c "
CREATE SCHEMA IF NOT EXISTS core;
CREATE SCHEMA IF NOT EXISTS a24;
CREATE SCHEMA IF NOT EXISTS a76;
CREATE SCHEMA IF NOT EXISTS public;
"
# `run --rm --no-deps` ejecuta alembic con la imagen del backend SIN
# arrancar los servicios dependientes (ya están corriendo).
docker compose -p "$PROJECT" -f docker-compose.e2e.yml run --rm --no-deps \
--entrypoint "" backend alembic upgrade head
# Esperar a que termine el build del frontend (si no terminó ya)
echo "Esperando build temporal del frontend..."
if ! wait "$BUILD_PID"; then
echo "ERROR: build del frontend E2E falló — log:"
cat "$BUILD_LOG" || true
exit 1
fi
echo "✓ Build temporal del frontend completado"
# ── Arrancar backend y frontend ─────────────────────────────────
docker compose -p "$PROJECT" -f docker-compose.e2e.yml up -d backend frontend
# ── PARALELO #2 ─────────────────────────────────────────────────
# Mientras backend+frontend se vuelven healthy (~40-80s),
# preparar el contenedor Playwright en background.
# ────────────────────────────────────────────────────────────────
docker rm -f "$PLAYWRIGHT_C" >/dev/null 2>&1 || true
# --network host: para que "localhost:5173" dentro del contenedor
# de Playwright resuelva a los puertos publicados por el stack.
# Necesario porque ORIGIN=http://localhost:5173 y el callback de
# Keycloak redirige a esa URL exacta.
docker run -d --name "$PLAYWRIGHT_C" --network host "$PLAYWRIGHT_IMAGE" sleep infinity
docker exec "$PLAYWRIGHT_C" mkdir -p /workspace
docker cp "$WORKSPACE/." "$PLAYWRIGHT_C:/workspace"
( docker exec -w /workspace/frontend "$PLAYWRIGHT_C" bash -lc '
set -euxo pipefail
npm install -g pnpm@9 --quiet
pnpm install --frozen-lockfile
pnpm run i18n:compile
' ) > "$PREP_LOG" 2>&1 &
PREP_PID=$!
# En foreground: esperar backend y frontend healthy
for SVC in backend frontend; do
READY=0
for i in $(seq 1 60); do
CID=$(docker compose -p "$PROJECT" -f docker-compose.e2e.yml ps -q "$SVC" 2>/dev/null)
if [ -n "$CID" ]; then
STATUS=$(docker inspect -f '{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting")
if [ "$STATUS" = "healthy" ]; then READY=1; break; fi
fi
sleep 2
done
if [ "$READY" != "1" ]; then
echo "ERROR: $SVC no quedó healthy en 120s"
docker compose -p "$PROJECT" -f docker-compose.e2e.yml logs "$SVC" || true
exit 1
fi
done
# Esperar a que termine la preparación de Playwright
echo "Esperando preparación de Playwright..."
if ! wait "$PREP_PID"; then
echo "ERROR: preparación de Playwright falló — log:"
cat "$PREP_LOG" || true
exit 1
fi
echo "✓ Playwright listo"
# ── Correr tests ────────────────────────────────────────────────
E2E_EXIT=0
docker exec \
-e CI=true \
-e "JENKINS_URL=${JENKINS_URL}" \
-e "PLAYWRIGHT_TEST_BASE_URL=${A76_URL}" \
-e "PLAYWRIGHT_TEST_BASE_URL=http://localhost:5173" \
-e "E2E_TEST_USER=${E2E_USER}" \
-e "E2E_TEST_PASSWORD=${E2E_PASS}" \
-e "PLAYWRIGHT_JUNIT_OUTPUT_NAME=playwright-results.xml" \
-w /workspace/frontend \
"$C" bash -lc '
set -euxo pipefail
npm install -g pnpm@9 --quiet
pnpm install --frozen-lockfile
pnpm run i18n:compile
"$PLAYWRIGHT_C" \
pnpm exec playwright test \
--reporter=junit,html \
--trace=retain-on-failure || E2E_EXIT=$?
E2E_EXIT=0
PLAYWRIGHT_JUNIT_OUTPUT_NAME=playwright-results.xml \
pnpm exec playwright test \
--reporter=junit,html \
--trace=retain-on-failure || E2E_EXIT=$?
exit "$E2E_EXIT"
'
docker cp "$C:/workspace/frontend/playwright-results.xml" \
# Copiar artefactos antes de salir (pase o falle)
docker cp "$PLAYWRIGHT_C:/workspace/frontend/playwright-results.xml" \
"$WORKSPACE/frontend/playwright-results.xml" 2>/dev/null || true
docker cp "$C:/workspace/frontend/playwright-report" \
docker cp "$PLAYWRIGHT_C:/workspace/frontend/playwright-report" \
"$WORKSPACE/frontend/playwright-report" 2>/dev/null || true
# test-results contiene trace.zip + error-context.md + screenshots por test fallido
docker cp "$C:/workspace/frontend/test-results" \
docker cp "$PLAYWRIGHT_C:/workspace/frontend/test-results" \
"$WORKSPACE/frontend/test-results" 2>/dev/null || true
exit "$E2E_EXIT"
'''
}
}