Refactor Jenkinsfile to streamline CI image build and testing process

- Updated the Jenkinsfile to build backend and frontend images using Docker Compose, enhancing the efficiency of the CI pipeline.
- Removed the obsolete e2e_alembic_upgrade.py script, as its functionality is no longer required.
- Improved comments for clarity regarding the build and testing stages, ensuring better understanding of the CI process.
This commit is contained in:
2026-04-24 16:03:47 -05:00
parent e204ba4b2b
commit fe51c508d1
3 changed files with 219 additions and 223 deletions

372
Jenkinsfile vendored
View File

@@ -29,6 +29,7 @@ pipeline {
exit 1
fi
docker --version
docker compose version
'''
}
}
@@ -47,133 +48,144 @@ pipeline {
}
}
stage('Test backend') {
// Build único: ambas imágenes se copian del workspace al daemon vía BuildKit
// y se reutilizan en los stages de test y E2E. El override ci quita los bind
// mounts (./backend:/app, ./frontend:/app) que el daemon de Jenkins no puede
// resolver al no ver $WORKSPACE del agente.
stage('Build CI images') {
steps {
sh '''
set -euxo pipefail
echo "== Test backend stage started =="
echo "Workspace: $WORKSPACE"
docker --version
DB_CONTAINER="anexo76-test-db-${BUILD_NUMBER}"
DB_NAME="anexo76_test"
DB_USER="anexo76"
DB_PASS="anexo76"
export TEST_DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_CONTAINER}:5432/${DB_NAME}"
PY_CONTAINER="anexo76-test-py-${BUILD_NUMBER}"
TEST_NETWORK="anexo76-test-net-${BUILD_NUMBER}"
cleanup() {
docker rm -f "$PY_CONTAINER" >/dev/null 2>&1 || true
docker rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
docker network rm "$TEST_NETWORK" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker network rm "$TEST_NETWORK" >/dev/null 2>&1 || true
docker network create "$TEST_NETWORK"
docker rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$DB_CONTAINER" \
--network "$TEST_NETWORK" \
-e POSTGRES_DB="$DB_NAME" \
-e POSTGRES_USER="$DB_USER" \
-e POSTGRES_PASSWORD="$DB_PASS" \
postgres:16-alpine
# Espera a que Postgres acepte conexiones (hasta ~60s)
READY=0
for i in $(seq 1 30); do
if docker exec "$DB_CONTAINER" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then
READY=1
break
fi
sleep 2
done
if [ "$READY" != "1" ]; then
echo "ERROR: Postgres no quedó listo a tiempo (30 intentos × 2s)."
exit 1
fi
docker exec "$DB_CONTAINER" pg_isready -U "$DB_USER" -d "$DB_NAME"
docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -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;
"
docker rm -f "$PY_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$PY_CONTAINER" --network "$TEST_NETWORK" python:3.12-slim sleep infinity
docker exec "$PY_CONTAINER" mkdir -p /workspace
docker cp "$WORKSPACE/." "$PY_CONTAINER:/workspace"
docker exec \
-e TEST_DATABASE_URL="$TEST_DATABASE_URL" \
"$PY_CONTAINER" \
sh -lc '
set -euxo pipefail
python --version
python -m pip install --upgrade pip
if [ -f /workspace/backend/requirements.txt ]; then
pip install -r /workspace/backend/requirements.txt
elif [ -f /workspace/backend/requirements/base.txt ]; then
pip install -r /workspace/backend/requirements/base.txt
else
echo "ERROR: No requirements file found in /workspace/backend"
ls -la /workspace || true
ls -la /workspace/backend || true
ls -la /workspace/backend/requirements || true
exit 1
fi
cd /workspace/backend
alembic upgrade head
pytest -q tests -v -ra -s
'
export DOCKER_BUILDKIT=1
docker compose \
-f "$WORKSPACE/docker-compose.yml" \
-f "$WORKSPACE/docker-compose.ci.yml" \
build backend frontend
docker image inspect anexo76-backend:latest >/dev/null
docker image inspect anexo76-frontend:latest >/dev/null
'''
}
}
// Código = WORKSPACE (docker cp), no la imagen Harbor. Imagen base: mcr.microsoft.com/playwright (Chromium p/ Vitest y E2E).
stage('Test frontend (Vitest + Svelte browser)') {
steps {
sh '''
set -euxo pipefail
echo "== Test frontend: vitest (Node + @vitest/browser) con $PLAYWRIGHT_TEST_IMAGE =="
if [ ! -f "$WORKSPACE/frontend/package.json" ]; then
echo "ERROR: $WORKSPACE/frontend/package.json no existe"
ls -la "$WORKSPACE" 2>&1 | head -40
exit 1
fi
FE_CONTAINER="anexo76-test-fe-${BUILD_NUMBER}"
cleanup() {
docker rm -f "$FE_CONTAINER" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker rm -f "$FE_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$FE_CONTAINER" "$PLAYWRIGHT_TEST_IMAGE" sleep infinity
docker exec "$FE_CONTAINER" mkdir -p /workspace
docker cp "$WORKSPACE/." "$FE_CONTAINER:/workspace"
docker exec \
-e CI=true \
-e NODE_ENV=test \
-e "JENKINS_URL=${JENKINS_URL}" \
-e JENKINS_VITEST_FULL=1 \
-w /workspace/frontend \
"$FE_CONTAINER" \
bash -lc '
// Feedback rápido en paralelo: si unit rompe, no gastamos el stack E2E.
stage('Unit tests') {
parallel {
// Reutiliza anexo76-backend:latest (ya incluye pytest + alembic + código)
stage('Test backend (pytest)') {
steps {
sh '''
set -euxo pipefail
test -f package.json
node --version
corepack enable
pnpm --version
pnpm install --frozen-lockfile
pnpm run i18n:compile
pnpm run test:unit -- --run
'
'''
echo "== Test backend: pytest en anexo76-backend:latest =="
DB_CONTAINER="anexo76-test-db-${BUILD_NUMBER}"
DB_NAME="anexo76_test"
DB_USER="anexo76"
DB_PASS="anexo76"
PY_CONTAINER="anexo76-test-py-${BUILD_NUMBER}"
TEST_NETWORK="anexo76-test-net-${BUILD_NUMBER}"
TEST_DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_CONTAINER}:5432/${DB_NAME}"
cleanup() {
docker rm -f "$PY_CONTAINER" >/dev/null 2>&1 || true
docker rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
docker network rm "$TEST_NETWORK" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker network rm "$TEST_NETWORK" >/dev/null 2>&1 || true
docker network create "$TEST_NETWORK"
docker rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$DB_CONTAINER" \
--network "$TEST_NETWORK" \
-e POSTGRES_DB="$DB_NAME" \
-e POSTGRES_USER="$DB_USER" \
-e POSTGRES_PASSWORD="$DB_PASS" \
postgres:16-alpine
READY=0
for i in $(seq 1 30); do
if docker exec "$DB_CONTAINER" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then
READY=1
break
fi
sleep 2
done
if [ "$READY" != "1" ]; then
echo "ERROR: Postgres no quedó listo a tiempo (30 intentos × 2s)."
exit 1
fi
docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -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;
"
# --entrypoint sleep: evita que docker-entrypoint.sh bloquee esperando Keycloak en 127.0.0.1
docker rm -f "$PY_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$PY_CONTAINER" \
--network "$TEST_NETWORK" \
--entrypoint sleep \
anexo76-backend:latest infinity
docker exec \
-e TEST_DATABASE_URL="$TEST_DATABASE_URL" \
-w /app \
"$PY_CONTAINER" \
sh -lc '
set -euxo pipefail
python --version
alembic upgrade head
pytest -q tests -v -ra -s
'
'''
}
}
// Vitest + @vitest/browser necesitan Chromium; imagen Playwright ya lo trae.
stage('Test frontend (vitest)') {
steps {
sh '''
set -euxo pipefail
echo "== Test frontend: vitest con $PLAYWRIGHT_TEST_IMAGE =="
if [ ! -f "$WORKSPACE/frontend/package.json" ]; then
echo "ERROR: $WORKSPACE/frontend/package.json no existe"
ls -la "$WORKSPACE" 2>&1 | head -40
exit 1
fi
FE_CONTAINER="anexo76-test-fe-${BUILD_NUMBER}"
cleanup() {
docker rm -f "$FE_CONTAINER" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker rm -f "$FE_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$FE_CONTAINER" "$PLAYWRIGHT_TEST_IMAGE" sleep infinity
docker exec "$FE_CONTAINER" mkdir -p /workspace
docker cp "$WORKSPACE/." "$FE_CONTAINER:/workspace"
docker exec \
-e CI=true \
-e NODE_ENV=test \
-e "JENKINS_URL=${JENKINS_URL}" \
-e JENKINS_VITEST_FULL=1 \
-w /workspace/frontend \
"$FE_CONTAINER" \
bash -lc '
set -euxo pipefail
test -f package.json
node --version
corepack enable
pnpm --version
pnpm install --frozen-lockfile
pnpm run i18n:compile
pnpm run test:unit -- --run
'
'''
}
}
}
}
@@ -184,9 +196,9 @@ pipeline {
steps {
sh '''
set -euxo pipefail
echo "== E2E: levanta docker-compose.yml (host) y pnpm test:e2e =="
if [ ! -f "$WORKSPACE/frontend/package.json" ] || [ ! -f "$WORKSPACE/docker-compose.yml" ]; then
echo "ERROR: faltan archivos de repo (frontend o compose)"
echo "== E2E: compose up con ci override + pnpm test:e2e =="
if [ ! -f "$WORKSPACE/frontend/package.json" ] || [ ! -f "$WORKSPACE/docker-compose.yml" ] || [ ! -f "$WORKSPACE/docker-compose.ci.yml" ]; then
echo "ERROR: faltan archivos de repo (frontend, compose o ci.override)"
exit 1
fi
export E2E_COMPOSE_PROJECT="anexo76-e2e-${BUILD_NUMBER}"
@@ -194,6 +206,7 @@ pipeline {
cd "$WORKSPACE"
: "${BUILD_NUMBER:=0}"
E2E_ENV_FILE="${WORKSPACE}/.env.e2e.generated"
# Puertos (dash/sh en Jenkins, sin depender de $RANDOM): rango ~2000060k
T=$(date +%s 2>/dev/null || echo 0)
r1=$((T % 20000))
@@ -210,10 +223,14 @@ pipeline {
echo "E2E: Keycloak (host) en puertos de .env e2e:" && cat "$E2E_ENV_FILE"
e2e_compose() {
docker compose --env-file "$E2E_ENV_FILE" -f "$WORKSPACE/docker-compose.yml" "$@"
docker compose \
--env-file "$E2E_ENV_FILE" \
-f "$WORKSPACE/docker-compose.yml" \
-f "$WORKSPACE/docker-compose.ci.yml" \
"$@"
}
# Nombres fijos en docker-compose: otro run / otro COMPOSE_PROJECT deja contenedors → "name already in use"
# Nombres fijos en docker-compose: otro run / otro COMPOSE_PROJECT deja contenedores → "name already in use"
e2e_force_rm_stale_containers() {
echo "E2E: eliminando contenedors anteriores (mismos container_name) si siguen en el nodo"
for c in \
@@ -236,9 +253,7 @@ pipeline {
e2e_compose down --remove-orphans 2>/dev/null || true
e2e_force_rm_stale_containers
}
e2e_force_rm_stale_containers
trap compose_down EXIT
compose_down
e2e_compose_fail_logs() {
echo "== E2E: fallo al levantar stack; diagnóstico =="
echo "--- .env.e2e.generated ---"
@@ -246,72 +261,54 @@ pipeline {
export COMPOSE_PROJECT_NAME="${E2E_COMPOSE_PROJECT}"
echo "--- docker compose ps -a ---"
e2e_compose ps -a 2>&1 || true
echo "--- logs: anexo76-postgres-a76 (Postgres app) ---"
docker logs --tail 250 anexo76-postgres-a76 2>&1 || true
echo "--- logs: anexo76-postgres-keycloak (Postgres de Keycloak) ---"
docker logs --tail 400 anexo76-postgres-keycloak 2>&1 || true
echo "--- logs: anexo76-keycloak (Keycloak) ---"
docker logs --tail 250 anexo76-keycloak 2>&1 || true
echo "--- logs: anexo76-backend (FastAPI / migraciones) ---"
docker logs --tail 400 anexo76-backend 2>&1 || true
echo "--- logs: worker (celery) ---"
docker logs --tail 200 worker 2>&1 || true
echo "--- logs: celery_beat ---"
docker logs --tail 200 celery_beat 2>&1 || true
for svc in anexo76-postgres-a76 anexo76-postgres-keycloak anexo76-keycloak anexo76-backend anexo76-frontend worker celery_beat anexo76-minio valkey; do
echo "--- logs: $svc ---"
docker logs --tail 300 "$svc" 2>&1 || true
done
}
# Postgresql (app) + Alembic antes del resto: evita 10+ min "Waiting" mientras el lifespan
# migra y frontend/celery esperan service_healthy. El start de backend hará "upgrade" idempotente.
echo "== E2E: Postgres (app) + migraciones Antes del resto =="
if ! e2e_compose build backend; then
e2e_compose_fail_logs
exit 1
fi
if ! e2e_compose up -d postgres-a76; then
e2e_compose_fail_logs
exit 1
fi
_pg=0
while [ "$_pg" -lt 90 ]; do
if e2e_compose exec -T postgres-a76 pg_isready -U postgres -d anexo76_core 2>/dev/null; then
echo "E2E: Postgres (anexo76_core) acepta conexiones"
break
fi
_pg=$((_pg + 1))
sleep 2
done
if ! e2e_compose exec -T postgres-a76 pg_isready -U postgres -d anexo76_core 2>/dev/null; then
echo "ERROR: timeout esperando a postgres-a76"
e2e_compose_fail_logs
exit 1
fi
# e2e_alembic_upgrade.py: carga /app/alembic.ini en la API; la CLI a veces queda con config vacío bajo compose (No 'script_location')
if ! e2e_compose run --rm --no-deps --entrypoint /usr/local/bin/python3 backend /app/e2e_alembic_upgrade.py; then
echo "ERROR: falló alembic upgrade head (job previo al stack completo)"
e2e_compose_fail_logs
exit 1
fi
echo "== E2E: stack completo =="
if ! e2e_compose up -d --build; then
e2e_force_rm_stale_containers
trap compose_down EXIT
compose_down
# El lifespan del backend ejecuta Alembic al arrancar. El healthcheck del
# backend tiene start_period=600s precisamente para permitirlo, y los
# servicios dependientes (frontend, celery) esperan service_healthy.
echo "== E2E: levantar stack completo =="
if ! e2e_compose up -d; then
e2e_compose_fail_logs
exit 1
fi
bash "$WORKSPACE/scripts/wait-for-jenkins-stack.sh"
# --network host: en Linux el contenedor de tests ve localhost:5173/8000 publicados por compose
docker run --rm --network host \
# Playwright con docker cp (no -v): el workspace del agente Jenkins puede
# no ser visible al docker daemon. `--network host` conecta al host del
# daemon donde compose publica 5173/8000.
PW_CONTAINER="anexo76-e2e-pw-${BUILD_NUMBER}"
docker rm -f "$PW_CONTAINER" >/dev/null 2>&1 || true
docker run -d --name "$PW_CONTAINER" --network host \
-e CI=true \
-e "JENKINS_URL=${JENKINS_URL}" \
-e PLAYWRIGHT_TEST_BASE_URL=http://127.0.0.1:5173 \
-v "$WORKSPACE:/workspace" \
-w /workspace/frontend \
"$PLAYWRIGHT_TEST_IMAGE" \
bash -lc '
set -euxo pipefail
test -f package.json
corepack enable
pnpm install --frozen-lockfile
pnpm run i18n:compile
pnpm run test:e2e
'
sleep infinity
docker exec "$PW_CONTAINER" mkdir -p /workspace
docker cp "$WORKSPACE/." "$PW_CONTAINER:/workspace"
if ! docker exec -w /workspace/frontend "$PW_CONTAINER" bash -lc '
set -euxo pipefail
test -f package.json
corepack enable
pnpm install --frozen-lockfile
pnpm run i18n:compile
pnpm run test:e2e
'; then
docker rm -f "$PW_CONTAINER" >/dev/null 2>&1 || true
e2e_compose_fail_logs
exit 1
fi
docker rm -f "$PW_CONTAINER" >/dev/null 2>&1 || true
'''
}
}
@@ -353,6 +350,9 @@ pipeline {
}
}
// Build independiente con APP_VERSION (el tag release se materializa como build-arg
// y como tag de imagen). No reutilizamos anexo76-backend:latest: aquí queda el tag
// remoto ${REGISTRY}/${IMAGE_NAMESPACE}/backend:${APP_VERSION}.
stage('Build + push backend') {
steps {
script {
@@ -375,6 +375,8 @@ pipeline {
}
}
// Frontend Dockerfile.prod ≠ Dockerfile (dev). No reutilizamos anexo76-frontend:latest
// del stage CI; el build de producción necesita VITE_* fijos al dominio dev.
stage('Build + push frontend') {
steps {
script {
@@ -412,7 +414,7 @@ pipeline {
git remote set-url origin "https://${GIT_USERNAME}:${GIT_PASSWORD}@git.aduanasoft.com/ADUANASOFT/anexo76.git"
git push origin "v${APP_VERSION}"
'''
}
}
}
}