Compare commits
5
Commits
e08d959cf9
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc326e8ac | ||
|
|
6323fe8f0e | ||
|
|
adb2b4dfe4 | ||
|
|
98b19aa3a2 | ||
|
|
27166eb3f4 |
@@ -0,0 +1,54 @@
|
||||
name: deploy-branch
|
||||
|
||||
# CI/CD: cada push a cualquier rama se despliega en <rama>.localesp.es
|
||||
# Estrategia A — un entorno por rama. El registro DNS A es manual (precondición).
|
||||
# Detalles: docs/07-cicd.md
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
delete:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Rama a desplegar (vacío = la actual)"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event_name != 'delete'
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build (npm ci + vite build)
|
||||
shell: bash
|
||||
run: |
|
||||
# better-sqlite3 (v12) requiere C++20; el GCC 8 del sistema (AlmaLinux 8) no lo soporta.
|
||||
# gcc-toolset-12 (GCC 12) ya está instalado en /opt/rh.
|
||||
source /opt/rh/gcc-toolset-12/enable 2>/dev/null || echo "WARN: gcc-toolset-12 no encontrado"
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Deploy branch
|
||||
env:
|
||||
BRANCH: ${{ github.event.inputs.branch || github.ref_name }}
|
||||
run: bash scripts/deploy.sh "$BRANCH"
|
||||
|
||||
# Al borrar una rama, se limpia su entorno
|
||||
teardown:
|
||||
if: github.event_name == 'delete'
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout (default branch, trae scripts/)
|
||||
uses: actions/checkout@v4
|
||||
- name: Teardown branch env
|
||||
env:
|
||||
# github.event.ref en un 'delete' es refs/heads/<rama>
|
||||
BRANCH: ${{ github.event.ref }}
|
||||
run: bash scripts/teardown.sh "${BRANCH#refs/heads/}"
|
||||
@@ -6,3 +6,7 @@ dist
|
||||
.idea
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# artefactos de despliegue manual / datos locales
|
||||
*.tgz
|
||||
localesp.db
|
||||
|
||||
@@ -32,6 +32,7 @@ Sin backend en el prototipo: los datos vienen de un mock en memoria. Ver
|
||||
- [`docs/04-governance.md`](docs/04-governance.md) – cómo se gobierna el proyecto.
|
||||
- [`docs/05-contributing.md`](docs/05-contributing.md) – cómo contribuir.
|
||||
- [`docs/06-github-setup.md`](docs/06-github-setup.md) – configuración inicial del repo.
|
||||
- [`docs/07-cicd.md`](docs/07-cicd.md) – CI/CD: despliegue por rama en `<rama>.localesp.es`.
|
||||
|
||||
## Licencia
|
||||
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# CI/CD — despliegue por rama
|
||||
|
||||
Cada rama se despliega automáticamente en `https://<rama>.localesp.es`.
|
||||
|
||||
## Cómo funciona
|
||||
|
||||
```
|
||||
push a rama X
|
||||
│ Gitea Actions (workflow: .gitea/workflows/deploy.yml)
|
||||
▼
|
||||
runner self-hosted (host executor, en el propio VPS)
|
||||
│ npm ci → vite build → scripts/deploy.sh <rama>
|
||||
▼
|
||||
/opt/localesp/<slug>/ código + dist/ + node_modules (prod)
|
||||
systemd localesp-<slug>.service node server/index.js (PORT=80xx)
|
||||
nginx <slug>.localesp.es proxy_pass → localhost:80xx
|
||||
certbot *.localesp.es Let's Encrypt (si el DNS ya apunta)
|
||||
```
|
||||
|
||||
El **slug** se deriva del nombre de rama (`Feature/Foo` → `feature-foo`),
|
||||
válido como hostname DNS. Un entorno = un subdirectorio bajo `/opt/localesp/`,
|
||||
un servicio, un vhost y un puerto. La base de datos `localesp.db` de cada
|
||||
entorno **se conserva** entre despliegues (no se sobreescribe).
|
||||
|
||||
Puertos: los históricos son `8080` (prototipo-yb01) y `8081` (master); los
|
||||
nuevos se asignan desde `8082` hacia arriba. Si la rama ya tiene su
|
||||
`localesp-<slug>.service`, se reutiliza su puerto (adopta el entorno existente).
|
||||
|
||||
## Precondición manual: el registro DNS
|
||||
|
||||
El registro A de `<rama>.localesp.es` **se crea a mano** (no está automatizado).
|
||||
|
||||
1. En el proveedor DNS de `localesp.es`, añade un registro A:
|
||||
`<rama>.localesp.es A <IP-pública-del-VPS>`
|
||||
2. Espera a que propague (`dig +short <rama>.localesp.es`).
|
||||
3. Lanza el workflow (push a la rama, o *Actions → Run workflow* en Gitea).
|
||||
|
||||
Mientras el DNS no apunte al VPS, `deploy.sh` despliega la app igualmente y la
|
||||
sirve por **HTTP**; emite el cert Let's Encrypt en cuanto detecta que el DNS ya
|
||||
resuelve. No hay que tocar nada más.
|
||||
|
||||
## Añadir una rama nueva
|
||||
|
||||
```bash
|
||||
git checkout -b mi-feature
|
||||
# ... cambios ...
|
||||
git push -u origin mi-feature
|
||||
```
|
||||
|
||||
El workflow corre solo. Si quieres URL pública: crea el registro A (arriba).
|
||||
Si no, el entorno existe pero no es alcanzable por hostname (útil para tests internos
|
||||
si añades el host a `/etc/hosts`).
|
||||
|
||||
## Borrar una rama
|
||||
|
||||
Al borrar la rama en Gitea, el job `teardown` limpia el entorno (unit systemd,
|
||||
vhost, cert y `/opt/localesp-<slug>`). También manual:
|
||||
|
||||
```bash
|
||||
ssh localesp.jumpingcrab.com 'bash /opt/localesp-master/scripts/teardown.sh mi-feature'
|
||||
```
|
||||
|
||||
## Entornos existentes (ya migrados al layout unificado)
|
||||
|
||||
Los entornos legacy ya están reubicados bajo `/opt/localesp/<rama>` con sus
|
||||
systemd units renombradas al esquema `localesp-<slug>.service`, de modo que el
|
||||
CI los **adopta** sin cambios (reutiliza puerto y vhost):
|
||||
|
||||
| Rama | Dir | Unit | Puerto | Hosts |
|
||||
|---|---|---|---|---|
|
||||
| `master` | `/opt/localesp/master` | `localesp-master.service` | 8081 | `localesp.es`, `master.localesp.es` |
|
||||
| `prototipo-yb01` | `/opt/localesp/prototipo-yb01` | `localesp-prototipo-yb01.service` | 8080 | `prototipo-yb01.localesp.es` |
|
||||
|
||||
El antiguo `localesp.service` (sin slug) se retiró; ahora todas las units
|
||||
siguen el patrón `localesp-<slug>.service`. La db de cada entorno se preservó.
|
||||
|
||||
## Seguridad
|
||||
|
||||
El runner (`gitea-runner`) se ejecuta como **root en el VPS de producción** y en
|
||||
modo **host** (sin contenedor): cualquier workflow que se ejecute corresponde a
|
||||
código del repo corriendo con privilegios de root en la caja. Esto es aceptable
|
||||
mientras el repo sea propio / de baja colaboración. Si entran colaboradores
|
||||
externos, conviene migrar a:
|
||||
|
||||
- runner en contenedor + despliegue por SSH (clave como secret de Gitea), o
|
||||
- *ephemeral runners* (`gitea-runner register --ephemeral`).
|
||||
|
||||
## Evolución: estrategia B (wildcard)
|
||||
|
||||
Para eliminar la precondición manual del DNS y el cert por rama, más adelante:
|
||||
|
||||
1. Registro **wildcard `*.localesp.es`** (DNS).
|
||||
2. **Wildcard cert** `*.localesp.es` vía challenge DNS-01 (una sola vez).
|
||||
3. Un único vhost `*.localesp.es` que enruta por `Host` con un `map` rama→puerto.
|
||||
|
||||
`deploy.sh` apenas cambia: se caen los pasos de creación de vhost y certbot.
|
||||
Nada de lo hecho aquí se pierde.
|
||||
|
||||
## Infra del runner (referencia)
|
||||
|
||||
Instalado en el VPS (`localesp.jumpingcrab.com`):
|
||||
|
||||
- Binario: `/usr/local/bin/gitea-runner` (v2.2.0), host executor.
|
||||
**Cadena de suministro verificada**: el binario coincide (sha256 `d6b3f5bb…`)
|
||||
con el descomprimido del release oficial `gitea-runner-2.2.0-linux-amd64.xz`, cuya suma
|
||||
`13357e0d…` está publicada en el release. Gitea no publica firma GPG del runner; la suma
|
||||
es la garantía máxima disponible. No hay paquete `dnf` oficial (solo binario o Docker image).
|
||||
- Config: `/etc/gitea-runner/config.yaml` — etiquetas `self-hosted:host`, `linux:host`.
|
||||
- Registro: `/var/lib/gitea-runner/.runner` (instance-level, `https://git.localesp.es`).
|
||||
- Servicio: `gitea-runner.service` (systemd, `User=root`, `WorkingDirectory=/var/lib/gitea-runner`).
|
||||
|
||||
Comprobar estado: `systemctl status gitea-runner` · `journalctl -u gitea-runner -f`.
|
||||
Ver runners en Gitea: *Site administration → Actions → Runners*.
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — Despliega la rama actual a <slug>.localesp.es
|
||||
# Toda la lógica vive en lib/env.sh (modelo OOP). Éste es sólo el entrypoint.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/env.sh
|
||||
. "$SCRIPT_DIR/lib/env.sh"
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 <branch>" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
main() {
|
||||
local branch root
|
||||
|
||||
branch="${1:-${GITHUB_REF_NAME:-}}"
|
||||
if [ -z "$branch" ]; then
|
||||
usage
|
||||
fi
|
||||
root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
Logger::set_tag deploy
|
||||
Env::new env "$branch" "$root"
|
||||
Logger::info "$(Env::banner env)"
|
||||
|
||||
Env::deploy env
|
||||
|
||||
Logger::info "LISTO: $(Env::url env) ($(Env::summary env))"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,450 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/env.sh — Modelo OOP de entornos de previsualización por rama.
|
||||
#
|
||||
# Estrategia (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í.
|
||||
#
|
||||
# Representación OOP en bash:
|
||||
# - Instancia = array asociativo global, referenciado por NOMBRE.
|
||||
# - Constructor = Class::new <nombre> <args> (declara el array y establece invariantes).
|
||||
# - Método = Class::method <nombre> <args>; accede al estado vía `local -n self="$1"`.
|
||||
# - Composición = Env posee los nombres de sus colaboradores (<env>__unit, __vhost, __cert).
|
||||
# - Servicios sin estado (Slug, Host, Port, Artifacts, Deps, Logger) = métodos estáticos.
|
||||
#
|
||||
# Requiere bash >= 4.3 (namerefs). Sin efectos al sourcear: sólo define funciones/constantes.
|
||||
#
|
||||
# CONTRATO (importante): el NOMBRE de instancia pasado a Env::new (p.ej. "env") es el
|
||||
# nombre de un array asociativo global. NUNCA declares en el caller una `local` con ese
|
||||
# mismo nombre: bash usa ámbito dinámico y la local sombrearía al array global, rompiendo
|
||||
# el nameref `local -n self=env`. Pasa el nombre como literal: Env::new env ...
|
||||
|
||||
DOMAIN="localesp.es"
|
||||
PORT_BASE=8082 # 8080/8081 son entornos legacy
|
||||
LOGGER_TAG="env"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logger — servicio singleton (tag configurable según entrypoint).
|
||||
# ---------------------------------------------------------------------------
|
||||
Logger::set_tag() {
|
||||
LOGGER_TAG="$1"
|
||||
}
|
||||
|
||||
Logger::info() {
|
||||
printf '\033[1;34m[%s]\033[0m %s\n' "$LOGGER_TAG" "$*"
|
||||
}
|
||||
|
||||
Logger::error() {
|
||||
printf '\033[1;31m[%s:ERROR]\033[0m %s\n' "$LOGGER_TAG" "$*" >&2
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slug — value object estático: normaliza un nombre de rama a slug.
|
||||
# ---------------------------------------------------------------------------
|
||||
Slug::from() {
|
||||
local branch="$1"
|
||||
printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Host — servicio estático: IP pública y resolución DNS.
|
||||
# ---------------------------------------------------------------------------
|
||||
Host::public_ip() {
|
||||
curl -s4 --max-time 5 ifconfig.me || true
|
||||
}
|
||||
|
||||
Host::resolved_ip() {
|
||||
local host="$1"
|
||||
getent hosts "$host" | awk '{print $1}' | head -1 || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Port — servicio estático: asigna/reutiliza puerto para una unit+dest.
|
||||
# ---------------------------------------------------------------------------
|
||||
Port::acquire() {
|
||||
local unit="$1"
|
||||
local dest="$2"
|
||||
local unit_file="/etc/systemd/system/$unit.service"
|
||||
local used
|
||||
local port
|
||||
|
||||
# Adopta entorno existente: reutiliza el puerto de la unit ya instalada.
|
||||
if [ -f "$unit_file" ]; then
|
||||
port="$(grep -oE 'PORT=[0-9]+' "$unit_file" | head -1 | cut -d= -f2 || true)"
|
||||
if [ -n "$port" ]; then
|
||||
echo "$port"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
if [ -f "$dest/.port" ]; then
|
||||
cat "$dest/.port"
|
||||
return
|
||||
fi
|
||||
|
||||
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)"
|
||||
port="$PORT_BASE"
|
||||
while printf '%s\n' "$used" | grep -qx "$port"; do
|
||||
port=$((port+1))
|
||||
done
|
||||
echo "$port"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Artifacts — servicio estático: artefactos buildados.
|
||||
# ---------------------------------------------------------------------------
|
||||
Artifacts::require_built() {
|
||||
local root="$1"
|
||||
|
||||
if [ ! -d "$root/dist" ]; then
|
||||
Logger::error "falta dist/ — ¿se ejecutó el build?"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
Artifacts::sync() {
|
||||
local root="$1"
|
||||
local dest="$2"
|
||||
|
||||
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í.
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deps — servicio estático: dependencias de producción.
|
||||
# ---------------------------------------------------------------------------
|
||||
Deps::install() {
|
||||
local root="$1"
|
||||
local dest="$2"
|
||||
|
||||
# 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.
|
||||
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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SystemdUnit — entidad: fichero .service de un entorno.
|
||||
# ---------------------------------------------------------------------------
|
||||
SystemdUnit::new() {
|
||||
local self="$1"
|
||||
local name="$2"
|
||||
local dest="$3"
|
||||
local branch="$4"
|
||||
local host="$5"
|
||||
local port="$6"
|
||||
|
||||
declare -gA "$self"
|
||||
local -n u="$self"
|
||||
u[name]="$name"
|
||||
u[dest]="$dest"
|
||||
u[branch]="$branch"
|
||||
u[host]="$host"
|
||||
u[port]="$port"
|
||||
u[file]="/etc/systemd/system/${name}.service"
|
||||
}
|
||||
|
||||
SystemdUnit::render() {
|
||||
local self="$1"
|
||||
local -n u="$self"
|
||||
|
||||
cat <<EOF
|
||||
[Unit]
|
||||
Description=LocaleSP env rama '${u[branch]}' (${u[host]})
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=${u[dest]}
|
||||
ExecStart=/usr/bin/node server/index.js
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=${u[port]}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
}
|
||||
|
||||
SystemdUnit::write() {
|
||||
local self="$1"
|
||||
local -n u="$self"
|
||||
|
||||
Logger::info "escribiendo unit ${u[name]} (PORT=${u[port]})"
|
||||
SystemdUnit::render "$self" > "${u[file]}"
|
||||
echo "${u[port]}" > "${u[dest]}/.port"
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${u[name]}" >/dev/null 2>&1 || true
|
||||
systemctl restart "${u[name]}"
|
||||
Logger::info "${u[name]} arrancada"
|
||||
}
|
||||
|
||||
SystemdUnit::remove() {
|
||||
local self="$1"
|
||||
local -n u="$self"
|
||||
|
||||
systemctl disable --now "${u[name]}" 2>/dev/null || true
|
||||
rm -f "${u[file]}"
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NginxVhost — entidad: vhost nginx de un entorno.
|
||||
# ---------------------------------------------------------------------------
|
||||
NginxVhost::new() {
|
||||
local self="$1"
|
||||
local slug="$2"
|
||||
local host="$3"
|
||||
local port="$4"
|
||||
|
||||
declare -gA "$self"
|
||||
local -n v="$self"
|
||||
v[slug]="$slug"
|
||||
v[host]="$host"
|
||||
v[port]="$port"
|
||||
}
|
||||
|
||||
NginxVhost::find_path() {
|
||||
local self="$1"
|
||||
local -n v="$self"
|
||||
local host_re
|
||||
host_re="$(printf '%s' "${v[host]}" | sed 's/\./\\./g')"
|
||||
grep -rlE "server_name[[:space:]]+$host_re[[:space:]]*;" /etc/nginx/conf.d/*.conf 2>/dev/null | head -1 || true
|
||||
}
|
||||
|
||||
NginxVhost::render() {
|
||||
local self="$1"
|
||||
local -n v="$self"
|
||||
|
||||
cat <<EOF
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ${v[host]};
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:${v[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
|
||||
}
|
||||
|
||||
NginxVhost::create() {
|
||||
local self="$1"
|
||||
local -n v="$self"
|
||||
local path="/etc/nginx/conf.d/${v[slug]}.conf"
|
||||
|
||||
Logger::info "creando vhost HTTP $path"
|
||||
NginxVhost::render "$self" > "$path"
|
||||
}
|
||||
|
||||
NginxVhost::repoint() {
|
||||
local self="$1"
|
||||
local -n v="$self"
|
||||
local path="$2"
|
||||
|
||||
Logger::info "vhost existente: $path — forzando proxy_pass -> :${v[port]}"
|
||||
sed -i -E "s|proxy_pass http://localhost:[0-9]+;|proxy_pass http://localhost:${v[port]};|g" "$path"
|
||||
}
|
||||
|
||||
NginxVhost::configure() {
|
||||
local self="$1"
|
||||
local existing
|
||||
|
||||
existing="$(NginxVhost::find_path "$self")"
|
||||
if [ -z "$existing" ]; then
|
||||
NginxVhost::create "$self"
|
||||
else
|
||||
NginxVhost::repoint "$self" "$existing"
|
||||
fi
|
||||
nginx -t 2>&1 | tail -2
|
||||
systemctl reload nginx
|
||||
Logger::info "nginx recargado"
|
||||
}
|
||||
|
||||
NginxVhost::remove() {
|
||||
local self="$1"
|
||||
local path
|
||||
|
||||
path="$(NginxVhost::find_path "$self")"
|
||||
if [ -n "$path" ]; then
|
||||
Logger::info "borrando vhost $path"
|
||||
rm -f "$path"
|
||||
nginx -t && systemctl reload nginx
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TlsCert — entidad: certificado Let's Encrypt de un entorno.
|
||||
# ---------------------------------------------------------------------------
|
||||
TlsCert::new() {
|
||||
local self="$1"
|
||||
local host="$2"
|
||||
|
||||
declare -gA "$self"
|
||||
local -n c="$self"
|
||||
c[host]="$host"
|
||||
}
|
||||
|
||||
TlsCert::exists() {
|
||||
local self="$1"
|
||||
local -n c="$self"
|
||||
[ -d "/etc/letsencrypt/live/${c[host]}" ]
|
||||
}
|
||||
|
||||
TlsCert::ensure() {
|
||||
local self="$1"
|
||||
local -n c="$self"
|
||||
local host="${c[host]}"
|
||||
local pub_ip resolved
|
||||
|
||||
if TlsCert::exists "$self"; then
|
||||
Logger::info "cert TLS ya presente para $host"
|
||||
return
|
||||
fi
|
||||
|
||||
Logger::info "comprobando DNS de $host"
|
||||
pub_ip="$(Host::public_ip)"
|
||||
resolved="$(Host::resolved_ip "$host")"
|
||||
|
||||
if [ -n "$pub_ip" ] && [ "$resolved" = "$pub_ip" ]; then
|
||||
Logger::info "emitiendo cert con certbot --nginx"
|
||||
if certbot --nginx -d "$host" -n --redirect --keep-until-expiring; then
|
||||
Logger::info "cert emitido ✓"
|
||||
else
|
||||
Logger::error "certbot falló; $host sigue en HTTP. Revisa y vuelve a lanzar el workflow."
|
||||
fi
|
||||
else
|
||||
Logger::error "DNS de $host -> '${resolved:-<sin resolver>}', esperado $pub_ip."
|
||||
Logger::error "Crea el registro A $host -> $pub_ip y, tras propagar, re-lanza el workflow"
|
||||
Logger::error "(o ejecuta: certbot --nginx -d $host). La app ya vive en http://$host"
|
||||
fi
|
||||
}
|
||||
|
||||
TlsCert::delete() {
|
||||
local self="$1"
|
||||
local -n c="$self"
|
||||
local host="${c[host]}"
|
||||
|
||||
if TlsCert::exists "$self"; then
|
||||
Logger::info "borrando cert $host"
|
||||
certbot delete --cert-name "$host" -n 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
TlsCert::protocol() {
|
||||
local self="$1"
|
||||
|
||||
if TlsCert::exists "$self"; then
|
||||
echo "https"
|
||||
else
|
||||
echo "http"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Env — aggregate root: un entorno de rama. Compone SystemdUnit, NginxVhost, TlsCert.
|
||||
# ---------------------------------------------------------------------------
|
||||
Env::new() {
|
||||
local self="$1"
|
||||
local branch="$2"
|
||||
local root="$3"
|
||||
|
||||
declare -gA "$self"
|
||||
local -n s="$self"
|
||||
|
||||
s[branch]="$branch"
|
||||
s[root]="$root"
|
||||
s[slug]="$(Slug::from "$branch")"
|
||||
if [ -z "${s[slug]}" ]; then
|
||||
Logger::error "slug vacío para la rama '$branch'"
|
||||
exit 2
|
||||
fi
|
||||
s[host]="${s[slug]}.$DOMAIN"
|
||||
s[dest]="/opt/localesp/${s[slug]}"
|
||||
s[unit_name]="localesp-${s[slug]}"
|
||||
s[port]="$(Port::acquire "${s[unit_name]}" "${s[dest]}")"
|
||||
|
||||
# Composición: crea los colaboradores y guarda sus nombres en el agregado.
|
||||
SystemdUnit::new "${self}__unit" "${s[unit_name]}" "${s[dest]}" "$branch" "${s[host]}" "${s[port]}"
|
||||
NginxVhost::new "${self}__vhost" "${s[slug]}" "${s[host]}" "${s[port]}"
|
||||
TlsCert::new "${self}__cert" "${s[host]}"
|
||||
}
|
||||
|
||||
Env::deploy() {
|
||||
local self="$1"
|
||||
local -n s="$self"
|
||||
|
||||
Artifacts::require_built "${s[root]}"
|
||||
Logger::info "sincronizando artefactos -> ${s[dest]}"
|
||||
Artifacts::sync "${s[root]}" "${s[dest]}"
|
||||
|
||||
Logger::info "instalando dependencias (prod)"
|
||||
Deps::install "${s[root]}" "${s[dest]}"
|
||||
|
||||
SystemdUnit::write "${self}__unit"
|
||||
|
||||
Logger::info "configurando nginx para ${s[host]}"
|
||||
NginxVhost::configure "${self}__vhost"
|
||||
|
||||
TlsCert::ensure "${self}__cert"
|
||||
}
|
||||
|
||||
Env::teardown() {
|
||||
local self="$1"
|
||||
local -n s="$self"
|
||||
|
||||
SystemdUnit::remove "${self}__unit"
|
||||
TlsCert::delete "${self}__cert"
|
||||
NginxVhost::remove "${self}__vhost"
|
||||
rm -rf "${s[dest]}"
|
||||
}
|
||||
|
||||
Env::url() {
|
||||
local self="$1"
|
||||
local -n s="$self"
|
||||
local proto
|
||||
|
||||
proto="$(TlsCert::protocol "${self}__cert")"
|
||||
echo "$proto://${s[host]}"
|
||||
}
|
||||
|
||||
Env::banner() {
|
||||
local self="$1"
|
||||
local -n s="$self"
|
||||
|
||||
echo "rama=${s[branch]} slug=${s[slug]} host=${s[host]} dest=${s[dest]} puerto=${s[port]}"
|
||||
}
|
||||
|
||||
Env::identity() {
|
||||
local self="$1"
|
||||
local -n s="$self"
|
||||
|
||||
echo "${s[host]} (${s[dest]}, ${s[unit_name]})"
|
||||
}
|
||||
|
||||
Env::summary() {
|
||||
local self="$1"
|
||||
local -n s="$self"
|
||||
|
||||
echo "rama=${s[branch]} unit=${s[unit_name]} puerto=${s[port]}"
|
||||
}
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# teardown.sh — Elimina el entorno de una rama (al borrar la rama o a mano).
|
||||
# Toda la lógica vive en lib/env.sh (modelo OOP). Éste es sólo el entrypoint.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/env.sh
|
||||
. "$SCRIPT_DIR/lib/env.sh"
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 <branch>" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
main() {
|
||||
local branch root
|
||||
|
||||
branch="${1:-}"
|
||||
if [ -z "$branch" ]; then
|
||||
usage
|
||||
fi
|
||||
root="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
Logger::set_tag teardown
|
||||
Env::new env "$branch" "$root"
|
||||
Logger::info "$(Env::identity env)"
|
||||
|
||||
Env::teardown env
|
||||
|
||||
Logger::info "entorno eliminado ✓"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user