Files
localesp/scripts/deploy.sh
T
edgar.friendly adb2b4dfe4
deploy-branch / deploy (push) Successful in 2m12s
deploy-branch / teardown (push) Skipped
ci: layout unificado /opt/localesp/<rama>; migrados entornos legacy
2026-07-24 09:56:13 +02:00

154 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# deploy.sh — Despliega la rama actual a <slug>.localesp.es
#
# Estrategia A (entorno por rama):
# /opt/localesp-<slug> + systemd unit localesp-<slug> + vhost nginx + cert Let's Encrypt.
# Reutiliza el puerto de la unit existente (adopta entornos legacy) o asigna uno libre (>=8082).
#
# PRECONDICIÓN MANUAL: el registro A <slug>.localesp.es debe existir (se gestiona a mano).
# Si no resuelve aún, la app queda servida por HTTP y se emite el cert cuando el DNS apunte aquí.
#
# Ejecutar como root (host runner). Idempotente.
set -euo pipefail
BRANCH="${1:-${GITHUB_REF_NAME:-}}"
[ -n "$BRANCH" ] || { echo "usage: $0 <branch>" >&2; exit 2; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DOMAIN="localesp.es"
PORT_BASE=8082 # 8080/8081 son entornos legacy
log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[deploy:ERROR]\033[0m %s\n' "$*" >&2; }
slugify() {
printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
}
SLUG="$(slugify "$BRANCH")"
[ -n "$SLUG" ] || { err "slug vacío para la rama '$BRANCH'"; exit 2; }
DEST="/opt/localesp/$SLUG"
UNIT="localesp-$SLUG"
HOST="$SLUG.$DOMAIN"
# --- asignación de puerto ---
allocate_port() {
local u="/etc/systemd/system/$UNIT.service"
if [ -f "$u" ]; then # adopta entorno existente
local p; p=$(grep -oE 'PORT=[0-9]+' "$u" | head -1 | cut -d= -f2 || true)
[ -n "$p" ] && { echo "$p"; return; }
fi
if [ -f "$DEST/.port" ]; then cat "$DEST/.port"; return; fi
local used; used=$( { grep -rhoE 'PORT=[0-9]+' /etc/systemd/system/localesp*.service 2>/dev/null | cut -d= -f2;
grep -rhoE 'localhost:[0-9]+' /etc/nginx/conf.d/*.conf 2>/dev/null | cut -d: -f2; } | sort -un)
local p=$PORT_BASE
while printf '%s\n' "$used" | grep -qx "$p"; do p=$((p+1)); done
echo "$p"
}
PORT="$(allocate_port)"
log "rama=$BRANCH slug=$SLUG host=$HOST dest=$DEST puerto=$PORT"
# --- 1. build artifacts ---
[ -d "$ROOT/dist" ] || { err "falta dist/ — ¿se ejecutó el build?"; exit 1; }
log "sincronizando artefactos -> $DEST"
mkdir -p "$DEST/dist" "$DEST/server"
rsync -a --delete "$ROOT/dist/" "$DEST/dist/"
rsync -a --delete "$ROOT/server/" "$DEST/server/"
cp -a "$ROOT/package.json" "$ROOT/package-lock.json" "$DEST/"
# localesp.db se PRESERVA (datos de usuarios). No se toca aquí.
# --- 2. dependencias de producción ---
# Reutiliza los node_modules ya compilados por el workflow (better-sqlite3 v12 exige
# C++20, GCC 12). Copiar + npm prune evita recompilar en cada despliegue.
# Fallback a npm ci --omit=dev si se ejecuta a mano sin build previo.
log "instalando dependencias (prod)"
if [ -d "$ROOT/node_modules" ]; then
mkdir -p "$DEST/node_modules"
rsync -a --delete "$ROOT/node_modules/" "$DEST/node_modules/"
( cd "$DEST" && npm prune --omit=dev --no-audit --no-fund )
else
( cd "$DEST" && npm ci --omit=dev --no-audit --no-fund )
fi
# --- 3. systemd unit ---
log "escribiendo unit $UNIT (PORT=$PORT)"
cat > "/etc/systemd/system/$UNIT.service" <<EOF
[Unit]
Description=LocaleSP env rama '$BRANCH' ($HOST)
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=$DEST
ExecStart=/usr/bin/node server/index.js
Restart=on-failure
RestartSec=5s
Environment=NODE_ENV=production
Environment=PORT=$PORT
[Install]
WantedBy=multi-user.target
EOF
echo "$PORT" > "$DEST/.port"
systemctl daemon-reload
systemctl enable "$UNIT" >/dev/null 2>&1 || true
systemctl restart "$UNIT"
log "$UNIT arrancada"
# --- 4. vhost nginx (busca por server_name; crea o reajusta proxy_pass) ---
log "configurando nginx para $HOST"
HOST_RE="$(printf '%s' "$HOST" | sed 's/\./\\./g')"
VHOST="$(grep -rlE "server_name[[:space:]]+$HOST_RE[[:space:]]*;" /etc/nginx/conf.d/*.conf 2>/dev/null | head -1 || true)"
if [ -z "$VHOST" ]; then
VHOST="/etc/nginx/conf.d/$SLUG.conf"
log "creando vhost HTTP $VHOST"
cat > "$VHOST" <<EOF
server {
listen 80;
listen [::]:80;
server_name $HOST;
location / {
proxy_pass http://localhost:$PORT;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
else
log "vhost existente: $VHOST — forzando proxy_pass -> :$PORT"
sed -i -E "s|proxy_pass http://localhost:[0-9]+;|proxy_pass http://localhost:$PORT;|g" "$VHOST"
fi
nginx -t 2>&1 | tail -2
systemctl reload nginx
log "nginx recargado"
# --- 5. TLS (sólo si no hay cert y el DNS ya apunta aquí) ---
if [ -d "/etc/letsencrypt/live/$HOST" ]; then
log "cert TLS ya presente para $HOST"
else
log "comprobando DNS de $HOST"
PUBIP="$(curl -s4 --max-time 5 ifconfig.me || true)"
RESOLVED="$(getent hosts "$HOST" | awk '{print $1}' | head -1 || true)"
if [ -n "$PUBIP" ] && [ "$RESOLVED" = "$PUBIP" ]; then
log "emitiendo cert con certbot --nginx"
if certbot --nginx -d "$HOST" -n --redirect --keep-until-expiring; then
log "cert emitido ✓"
else
err "certbot falló; $HOST sigue en HTTP. Revisa y vuelve a lanzar el workflow."
fi
else
err "DNS de $HOST -> '${RESOLVED:-<sin resolver>}', esperado $PUBIP."
err "Crea el registro A $HOST -> $PUBIP y, tras propagar, re-lanza el workflow"
err "(o ejecuta: certbot --nginx -d $HOST). La app ya vive en http://$HOST"
fi
fi
PROTO=https; [ ! -d "/etc/letsencrypt/live/$HOST" ] && PROTO=http
log "LISTO: $PROTO://$HOST (rama=$BRANCH unit=$UNIT puerto=$PORT)"