diff --git a/Jenkinsfile b/Jenkinsfile index 1221c6c8..7814fbcc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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" ''' } } diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 00000000..f53da1be --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,157 @@ +# Stack efímero para pruebas E2E (Playwright) en CI. +# Levantado por Jenkinsfile (stage "E2E (Playwright)") ANTES del deploy a dev, +# para validar la imagen recién buildeada contra una DB limpia y servicios aislados. +# +# Diferencias clave vs docker-compose.prod.yml: +# - Sin volúmenes persistidos (todo se descarta en `down -v`). +# - Sin `container_name` ni `restart` (efímero, COMPOSE_PROJECT_NAME aísla los nombres). +# - Sin `celery_worker` / `celery_beat` (no se ejercitan en los specs actuales). +# - Imágenes vía env vars: la del frontend se rebuildea localmente con +# VITE_API_URL=http://localhost:8000/api/ (la prod tiene la URL de dev bakeada). +# - Puertos fijos: frontend 5173 / backend 8000 (URIs ya registradas en Workspace). +# +# Variables requeridas en el entorno al invocar docker compose: +# E2E_BACKEND_IMAGE — imagen del backend recién pusheada a Harbor +# E2E_FRONTEND_IMAGE — imagen temporal del frontend con VITE_API_URL=localhost + +services: + postgres-a76: + image: postgres:18-alpine + environment: + POSTGRES_DB: anexo76_core + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_INITDB_ARGS: "--encoding=UTF8" + networks: + - e2e-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d anexo76_core || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 20s + tmpfs: + - /var/lib/postgresql/data + + minio: + image: minio/minio:RELEASE.2025-09-07T16-13-09Z + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + networks: + - e2e-net + healthcheck: + test: ["CMD-SHELL", "curl -f http://127.0.0.1:9000/minio/health/live || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + tmpfs: + - /data + + valkey: + image: valkey/valkey:7.2 + networks: + - e2e-net + + backend: + image: ${E2E_BACKEND_IMAGE} + environment: + - DEBUG=False + - ENVIRONMENT=e2e + - PYTHONUNBUFFERED=1 + - PYTHONDONTWRITEBYTECODE=1 + - CORE_DB_HOST=postgres-a76 + - CORE_DB_PORT=5432 + - CORE_DB_NAME=anexo76_core + - CORE_DB_USER=postgres + - CORE_DB_PASSWORD=postgres + # Keycloak — apunta a Workspace real; el cliente anexo76-frontend ya + # tiene http://localhost:5173/auth/callback como redirect URI permitida. + - KEYCLOAK_SERVER_URL=https://workspace.aduanasoft.com/kcauth + - KEYCLOAK_REALM=master + - KEYCLOAK_CLIENT_ID=anexo76-frontend + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-} + - CORS_ORIGINS=http://localhost:5173 + - VALKEY_URL=redis://valkey:6379/0 + - HUB_URL=https://workspace.aduanasoft.com + - APP_PUBLIC_URL=http://localhost:5173 + - CSV_IMPORT_STORAGE=minio + - S3_ENDPOINT_URL=http://minio:9000 + - S3_ACCESS_KEY=minioadmin + - S3_SECRET_KEY=minioadmin + - S3_BUCKET=anexo76 + - S3_REGION=us-east-1 + - S3_USE_SSL=false + - S3_FILE_STORAGE=true + ports: + - "8000:8000" + depends_on: + postgres-a76: + condition: service_healthy + minio: + condition: service_healthy + networks: + - e2e-net + # Replica el comando de prod (gunicorn) para que el test ejerza el mismo runtime + # que se desplegará. El CMD del Dockerfile usa uvicorn --reload (modo dev). + command: + - gunicorn + - main:app + - -k + - uvicorn.workers.UvicornWorker + - -w + - "1" + - -b + - 0.0.0.0:8000 + - --log-level + - info + - --forwarded-allow-ips + - "*" + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8000/api/health || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 60s + + frontend: + image: ${E2E_FRONTEND_IMAGE} + environment: + - NODE_ENV=production + # VITE_API_URL ya está bakeada en E2E_FRONTEND_IMAGE; este valor solo sirve + # para fallback server-side en frontend/src/lib/server/api.ts. + - VITE_API_URL=http://localhost:8000/api/ + - INTERNAL_API_URL=http://backend:8000/api/ + - INTERNAL_HUB_URL=https://workspace.aduanasoft.com + - HUB_URL=https://workspace.aduanasoft.com + - VITE_HUB_URL=https://workspace.aduanasoft.com + - VITE_KEYCLOAK_URL=https://workspace.aduanasoft.com/kcauth + - VITE_KEYCLOAK_REALM=master + - VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend + - KEYCLOAK_URL=https://workspace.aduanasoft.com/kcauth + - KEYCLOAK_REALM=master + - KEYCLOAK_CLIENT_ID=anexo76-frontend + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-} + # ORIGIN controla url.origin y el flag secure de cookies — debe coincidir con la URL + # registrada en Workspace para que el callback de Keycloak resuelva correctamente. + - ORIGIN=http://localhost:5173 + - SITE_URL=http://localhost:5173 + ports: + - "5173:5173" + depends_on: + backend: + condition: service_healthy + networks: + - e2e-net + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5173/ || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + +networks: + e2e-net: + driver: bridge