- Added a 10-second sleep before the E2E testing begins to allow Vite/Uvicorn to start up, improving log visibility for troubleshooting. - Included commands to output the status of the Docker containers and the last 40 lines of logs for both frontend and backend services, aiding in debugging during the E2E testing process. - Adjusted the timeout for waiting on the frontend from 300 seconds to 180 seconds, streamlining the waiting logic.
513 lines
20 KiB
Groovy
513 lines
20 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
|
||
docker compose 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]]
|
||
])
|
||
}
|
||
}
|
||
|
||
// 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
|
||
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
|
||
'''
|
||
}
|
||
}
|
||
|
||
// 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
|
||
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
|
||
'
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stage('E2E (docker compose + Playwright)') {
|
||
options {
|
||
timeout(time: 90, unit: 'MINUTES')
|
||
}
|
||
steps {
|
||
sh '''
|
||
set -euxo pipefail
|
||
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}"
|
||
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" \
|
||
-f "$WORKSPACE/docker-compose.ci.yml" \
|
||
"$@"
|
||
}
|
||
|
||
# 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 \
|
||
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_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
|
||
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
|
||
}
|
||
|
||
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
|
||
|
||
# Dump temprano: 10s tras el up para ver arranque de Vite/Uvicorn antes de
|
||
# que el wait empiece a iterar; si algo falla, queda en logs sin esperar
|
||
# al timeout.
|
||
sleep 10
|
||
echo "== E2E: estado del stack tras 10s =="
|
||
e2e_compose ps -a 2>&1 || true
|
||
echo "--- docker logs anexo76-frontend --tail 40 ---"
|
||
docker logs --tail 40 anexo76-frontend 2>&1 || true
|
||
echo "--- docker logs anexo76-backend --tail 40 ---"
|
||
docker logs --tail 40 anexo76-backend 2>&1 || true
|
||
|
||
# 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 (el agente Jenkins NO ve esos
|
||
# puertos; por eso el wait corre dentro de este contenedor, no en el shell).
|
||
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 \
|
||
"$PLAYWRIGHT_TEST_IMAGE" \
|
||
sleep infinity
|
||
docker exec "$PW_CONTAINER" mkdir -p /workspace
|
||
docker cp "$WORKSPACE/." "$PW_CONTAINER:/workspace"
|
||
|
||
# Wait + tests E2E en el mismo container (que sí tiene red host).
|
||
# BACKEND_MAX_SEC=900 acomoda el start_period=600s del backend healthcheck
|
||
# + margen para Alembic en DB vacía.
|
||
if ! docker exec -w /workspace/frontend "$PW_CONTAINER" bash -lc '
|
||
set -euxo pipefail
|
||
test -f package.json
|
||
|
||
echo "== wait: backend http://127.0.0.1:8000/api/health =="
|
||
i=0
|
||
until curl -fsS --connect-timeout 3 --max-time 5 http://127.0.0.1:8000/api/health >/dev/null 2>&1; do
|
||
i=$((i + 5))
|
||
if [ "$i" -ge 900 ]; then
|
||
echo "ERROR: timeout esperando backend (900s)"
|
||
exit 1
|
||
fi
|
||
if [ $((i % 30)) -eq 0 ]; then
|
||
echo " ...esperando backend (${i}/900s)"
|
||
fi
|
||
sleep 5
|
||
done
|
||
echo "== backend listo tras ${i}s =="
|
||
|
||
echo "== wait: frontend http://127.0.0.1:5173/ =="
|
||
i=0
|
||
until curl -fsS --connect-timeout 3 --max-time 5 http://127.0.0.1:5173/ >/dev/null 2>&1; do
|
||
i=$((i + 5))
|
||
if [ "$i" -ge 180 ]; then
|
||
echo "ERROR: timeout esperando frontend (180s)"
|
||
exit 1
|
||
fi
|
||
if [ $((i % 30)) -eq 0 ]; then
|
||
echo " ...esperando frontend (${i}/180s)"
|
||
fi
|
||
sleep 5
|
||
done
|
||
echo "== frontend listo tras ${i}s =="
|
||
|
||
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
|
||
'''
|
||
}
|
||
}
|
||
|
||
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
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 {
|
||
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"
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 {
|
||
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
|
||
'''
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|