- Updated the Alembic upgrade command in the Jenkinsfile to specify the entrypoint correctly, ensuring the command executes in the intended context. - Added a comment to clarify the change regarding the entrypoint behavior in the Docker environment, enhancing maintainability and understanding of the script.
467 lines
18 KiB
Groovy
467 lines
18 KiB
Groovy
pipeline {
|
||
agent any
|
||
options {
|
||
timestamps()
|
||
disableConcurrentBuilds()
|
||
}
|
||
|
||
parameters {
|
||
choice(name: 'ENVIRONMENT', choices: ['development', 'main'], description: 'Rama a ejecutar')
|
||
string(name: 'VERSION_MAYOR', defaultValue: '1', description: 'Componente mayor de versión')
|
||
}
|
||
|
||
environment {
|
||
REGISTRY = 'dev.aduanasoft.com'
|
||
IMAGE_NAMESPACE = 'anexo76'
|
||
// Debe coincidir con @playwright/test del frontend (ver frontend/pnpm-lock)
|
||
PLAYWRIGHT_TEST_IMAGE = 'mcr.microsoft.com/playwright:v1.56.1-noble'
|
||
}
|
||
|
||
stages {
|
||
stage('Preflight tools') {
|
||
steps {
|
||
sh '''
|
||
set -euo pipefail
|
||
echo "Node: $(hostname)"
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
echo "ERROR: este agente no tiene docker instalado."
|
||
echo "Usa un nodo Jenkins con label 'docker' y acceso al daemon."
|
||
exit 1
|
||
fi
|
||
docker --version
|
||
'''
|
||
}
|
||
}
|
||
|
||
stage('Checkout') {
|
||
steps {
|
||
checkout([
|
||
$class: 'GitSCM',
|
||
branches: [[name: "*/${params.ENVIRONMENT}"]],
|
||
userRemoteConfigs: [[
|
||
url: 'https://git.aduanasoft.com/ADUANASOFT/anexo76.git',
|
||
credentialsId: 'gitea_acazares'
|
||
]],
|
||
extensions: [[$class: 'CloneOption', depth: 0, shallow: false]]
|
||
])
|
||
}
|
||
}
|
||
|
||
stage('Test backend') {
|
||
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
|
||
'
|
||
'''
|
||
}
|
||
}
|
||
|
||
// 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 '
|
||
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
|
||
'
|
||
'''
|
||
}
|
||
}
|
||
|
||
stage('E2E (docker compose + Playwright)') {
|
||
options {
|
||
timeout(time: 90, unit: 'MINUTES')
|
||
}
|
||
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)"
|
||
exit 1
|
||
fi
|
||
export E2E_COMPOSE_PROJECT="anexo76-e2e-${BUILD_NUMBER}"
|
||
export COMPOSE_PROJECT_NAME="${E2E_COMPOSE_PROJECT}"
|
||
cd "$WORKSPACE"
|
||
: "${BUILD_NUMBER:=0}"
|
||
E2E_ENV_FILE="${WORKSPACE}/.env.e2e.generated"
|
||
# Puertos (dash/sh en Jenkins, sin depender de $RANDOM): rango ~20000–60k
|
||
T=$(date +%s 2>/dev/null || echo 0)
|
||
r1=$((T % 20000))
|
||
r2=$((T % 15000))
|
||
KC_HTTP=$(( 20000 + r1 + BUILD_NUMBER % 2000 ))
|
||
KC_MGMT=$(( 40000 + r2 + (BUILD_NUMBER * 7) % 2000 ))
|
||
if [ "$KC_HTTP" -gt 64000 ] || [ "$KC_HTTP" -lt 20000 ]; then KC_HTTP=$(( 22000 + BUILD_NUMBER % 8000 )); fi
|
||
if [ "$KC_MGMT" -gt 65000 ] || [ "$KC_MGMT" -lt 30000 ]; then KC_MGMT=$(( 50000 + BUILD_NUMBER % 8000 )); fi
|
||
if [ "$KC_HTTP" -eq "$KC_MGMT" ]; then KC_MGMT=$((KC_MGMT + 1)); fi
|
||
{ echo "KEYCLOAK_HTTP_PORT=$KC_HTTP"; echo "KEYCLOAK_MANAGEMENT_PORT=$KC_MGMT"; echo "VITE_KEYCLOAK_URL=http://127.0.0.1:$KC_HTTP/kcauth"; } > "$E2E_ENV_FILE"
|
||
set -a
|
||
. "$E2E_ENV_FILE"
|
||
set +a
|
||
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" "$@"
|
||
}
|
||
|
||
# Nombres fijos en docker-compose: otro run / otro COMPOSE_PROJECT deja contenedors → "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 \
|
||
anexo76-postgres-a76 \
|
||
anexo76-postgres-keycloak \
|
||
anexo76-keycloak \
|
||
anexo76-backend \
|
||
anexo76-frontend \
|
||
valkey \
|
||
anexo76-minio \
|
||
worker \
|
||
celery_beat
|
||
do
|
||
docker rm -f "$c" 2>/dev/null || true
|
||
done
|
||
}
|
||
|
||
compose_down() {
|
||
export COMPOSE_PROJECT_NAME="${E2E_COMPOSE_PROJECT}"
|
||
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 ---"
|
||
cat "$E2E_ENV_FILE" 2>&1 || true
|
||
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
|
||
}
|
||
# 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
|
||
# --entrypoint "" se pierde en sh/Jenkins (Docker toma "backend" como entrypoint, rompe o ignora CWD/ini)
|
||
if ! e2e_compose run --rm --no-deps --entrypoint /bin/sh backend -c "set -eux; test -f /app/alembic.ini; cd /app; exec alembic -c /app/alembic.ini upgrade head"; 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_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 \
|
||
-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
|
||
'
|
||
'''
|
||
}
|
||
}
|
||
|
||
stage('Generate version') {
|
||
steps {
|
||
script {
|
||
def year = sh(returnStdout: true, script: 'date +%y').trim()
|
||
def month = sh(returnStdout: true, script: 'date +%m').trim()
|
||
|
||
if (params.ENVIRONMENT == 'development') {
|
||
def shortHash = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim()
|
||
env.APP_VERSION = "${year}.${month}.${params.VERSION_MAYOR}.${shortHash}"
|
||
} else {
|
||
def lastTag = sh(returnStdout: true, script: 'git describe --tags --abbrev=0 2>/dev/null || true').trim()
|
||
def commitCount = lastTag
|
||
? sh(returnStdout: true, script: "git rev-list ${lastTag}..HEAD --count").trim()
|
||
: sh(returnStdout: true, script: 'git rev-list --count HEAD').trim()
|
||
|
||
if (commitCount == '0') {
|
||
commitCount = '1'
|
||
}
|
||
env.APP_VERSION = "${year}.${month}.${params.VERSION_MAYOR}.${commitCount}"
|
||
}
|
||
|
||
echo "VERSION: ${env.APP_VERSION}"
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Docker login') {
|
||
steps {
|
||
withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')]) {
|
||
sh '''
|
||
set -euo pipefail
|
||
echo "$HARBOR_PASSWORD" | docker login "$REGISTRY" -u "$HARBOR_USERNAME" --password-stdin
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Build + push backend') {
|
||
steps {
|
||
script {
|
||
retry(3) {
|
||
sh '''
|
||
set -euo pipefail
|
||
export DOCKER_BUILDKIT=1
|
||
docker build \
|
||
--build-arg APP_VERSION="${APP_VERSION}" \
|
||
-t "${REGISTRY}/${IMAGE_NAMESPACE}/backend:${APP_VERSION}" \
|
||
-t "${REGISTRY}/${IMAGE_NAMESPACE}/backend:latest" \
|
||
-f ./backend/Dockerfile \
|
||
./backend
|
||
|
||
docker push "${REGISTRY}/${IMAGE_NAMESPACE}/backend:${APP_VERSION}"
|
||
docker push "${REGISTRY}/${IMAGE_NAMESPACE}/backend:latest"
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Build + push frontend') {
|
||
steps {
|
||
script {
|
||
retry(3) {
|
||
sh '''
|
||
set -euo pipefail
|
||
export DOCKER_BUILDKIT=1
|
||
docker build \
|
||
--build-arg VITE_API_URL=https://anexo76-dev.aduanasoft.com/api/ \
|
||
--build-arg VITE_KEYCLOAK_URL=https://anexo76-dev.aduanasoft.com/kcauth/ \
|
||
--build-arg INTERNAL_API_URL=http://backend:3467/api/ \
|
||
-t "${REGISTRY}/${IMAGE_NAMESPACE}/frontend:${APP_VERSION}" \
|
||
-t "${REGISTRY}/${IMAGE_NAMESPACE}/frontend:latest" \
|
||
-f ./frontend/Dockerfile.prod \
|
||
./frontend
|
||
|
||
docker push "${REGISTRY}/${IMAGE_NAMESPACE}/frontend:${APP_VERSION}"
|
||
docker push "${REGISTRY}/${IMAGE_NAMESPACE}/frontend:latest"
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Tag release (main)') {
|
||
when {
|
||
expression { params.ENVIRONMENT == 'main' }
|
||
}
|
||
steps {
|
||
withCredentials([usernamePassword(credentialsId: 'gitea_acazares', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) {
|
||
sh '''
|
||
set -euo pipefail
|
||
git -c user.name='Jenkins' -c user.email='jenkins@gitea.local' \
|
||
tag -a "v${APP_VERSION}" -m "Release version ${APP_VERSION}"
|
||
git remote set-url origin "https://${GIT_USERNAME}:${GIT_PASSWORD}@git.aduanasoft.com/ADUANASOFT/anexo76.git"
|
||
git push origin "v${APP_VERSION}"
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('Deploy development') {
|
||
when {
|
||
expression { params.ENVIRONMENT == 'development' }
|
||
}
|
||
steps {
|
||
withCredentials([
|
||
usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD'),
|
||
string(credentialsId: 'dev-server-user', variable: 'DEV_SERVER_USER'),
|
||
string(credentialsId: 'dev-server-host', variable: 'DEV_SERVER_HOST'),
|
||
sshUserPrivateKey(
|
||
credentialsId: 'dev-server-ssh',
|
||
keyFileVariable: 'DEV_SERVER_KEY',
|
||
usernameVariable: 'DEV_SERVER_SSH_USER',
|
||
passphraseVariable: 'DEV_SERVER_KEY_PASSPHRASE'
|
||
)
|
||
]) {
|
||
script {
|
||
def devHost = env.DEV_SERVER_HOST?.trim()
|
||
def knownHostsFile = "${env.WORKSPACE}/.jenkins-anexo76-known_hosts"
|
||
sh """
|
||
set -euo pipefail
|
||
ssh-keyscan -T 15 -H '${devHost}' > '${knownHostsFile}'
|
||
"""
|
||
def remote = [
|
||
name: 'dev-server',
|
||
host: devHost,
|
||
user: env.DEV_SERVER_USER ?: env.DEV_SERVER_SSH_USER,
|
||
identityFile: env.DEV_SERVER_KEY,
|
||
passphrase: env.DEV_SERVER_KEY_PASSPHRASE,
|
||
allowAnyHosts: false,
|
||
knownHosts: knownHostsFile
|
||
]
|
||
sshCommand remote: remote, command: '''
|
||
set -euo pipefail
|
||
cd ~/projects/anexo76
|
||
# Asume docker login ya configurado en el host destino para evitar exponer secretos por interpolación.
|
||
docker compose -f docker-compose.prod.yml pull
|
||
docker compose -f docker-compose.prod.yml up -d
|
||
# Solo imágenes colgantes (sin tag); no borra imágenes en uso por otros contenedores.
|
||
docker image prune -f
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|