- Changed PostgreSQL data volume paths in docker-compose.yml to remove the trailing 'data' directory. - Improved the 'Test frontend' stage in Jenkinsfile by adding cleanup steps and ensuring the workspace is correctly copied to the Docker container for testing.
323 lines
12 KiB
Groovy
323 lines
12 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'
|
||
}
|
||
|
||
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
|
||
'
|
||
'''
|
||
}
|
||
}
|
||
|
||
// Test frontend: OBLIGATORIO usar el repo clonado (WORKSPACE), no la imagen del frontend en Harbor.
|
||
// Svelte se compila en el Dockerfile: la imagen de producción solo trae build/ + deps de runtime; no hay fuentes
|
||
// para vitest, ni paraglide desde inlang, ni pnpm devDependencies. Imagen usada aquí: solo node:22-bookworm.
|
||
// Mismo criterio que Test backend: docker cp (no -v) si Jenkins vive en Docker. Vitest: sin @vitest/browser en CI.
|
||
stage('Test frontend') {
|
||
steps {
|
||
sh '''
|
||
set -euxo pipefail
|
||
echo "== Test frontend (vitest) =="
|
||
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" node:22-bookworm 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}" \
|
||
-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('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
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|