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' } 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" DB_PORT="55432" 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 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 break fi sleep 2 done 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 ' ''' } } 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 { sh ''' set -euo pipefail docker build \ --build-arg APP_VERSION="${APP_VERSION}" \ -t "${REGISTRY}/${IMAGE_NAMESPACE}/backend:latest" \ -f ./backend/Dockerfile \ ./backend docker push "${REGISTRY}/${IMAGE_NAMESPACE}/backend:latest" ''' } } stage('Build + push frontend') { steps { sh ''' set -euo pipefail 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:latest" \ -f ./frontend/Dockerfile.prod \ ./frontend 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 remote = [ name: 'dev-server', host: env.DEV_SERVER_HOST, user: env.DEV_SERVER_USER ?: env.DEV_SERVER_SSH_USER, identityFile: env.DEV_SERVER_KEY, passphrase: env.DEV_SERVER_KEY_PASSPHRASE, allowAnyHosts: true ] 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 docker image prune -f ''' } } } } } }