154 lines
5.5 KiB
Bash
Executable File
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)"
|