Files
takana/scripts/servidor/publicar-repo.sh
T
Sergio a66346fb05 puerta 5 cerrada: repo.gioser.net sirve el repo FIRMADO, y el guion aprende cómo se recarga esta caja
`takana install zsh --repo https://repo.gioser.net --trust ./trust --require-signed` ⇒
`release: trusted (by release)`, 1331 ficheros en 0,286 s, zsh 5.9 corriendo. 173 paquetes
anclados, 0 sin ancla, certificado CN=repo.gioser.net de Let's Encrypt.

Tres cosas que sólo se supieron al poder entrar como root, y que el guion ahora sabe:

· `caddy reload` NO funciona acá: el Caddyfile lleva `admin off` y el reload habla por ese admin
  (de ahí que el :2019 estuviera siempre cerrado). La recarga real es `arjectl restart caddy` —
  caddy es un Ente de arje con Restart{1000,30000}, no un proceso suelto.
· Un restart levanta los 19 dominios o ninguno, así que el guion saca un TESTIGO de la config
  previa (acá gitea.gioser.net), espera a que responda, y si no vuelve restaura y revierte.
  Comprobado después: los 8 dominios vivos siguen en pie.
· El certificado tarda más que la comprobación: el guion dio «todavía no responde» y la misma URL
  daba 200 segundos después. El mensaje ahora manda reintentar antes de ir a leer logs.

Y dos fallos míos, medidos: backticks dentro de comillas y de un heredoc sin entrecomillar se
EJECUTAN (salió un `journalctl: not found` en mitad de un mensaje de ayuda), y `[ -n "$X" ] && echo`
como última orden mata el guion entero bajo `set -e`.

El espejo provisional sin firmar de takana.gioser.net/repo se borró: con el firmado en pie, un
origen sin firma no es un origen.
2026-09-21 19:45:41 +00:00

223 lines
11 KiB
Bash
Executable File

#!/bin/sh
# publicar-repo.sh — deja el repo FIRMADO servido en `repo.gioser.net`. Se corre EN LA CAJA, como
# root (la clave privada de release vive en `/root/.config/takana/keys/`, SDD 28 §2116).
#
# ── QUÉ HACE ────────────────────────────────────────────────────────────────────────────────────
# 1. publica la clausura del perfil en `/srv/repo` con `repo-perfil.sh` (que ABORTA si no está la
# clave: un índice firmado con una efímera que nadie conoce no lo verifica nadie),
# 2. comprueba que el índice verifica contra `trust/`,
# 3. dice si Caddy ya sirve el nombre, y **si no, imprime el bloque y para**.
#
# ── EL CADDYFILE: SÓLO CON `--install-vhost`, Y CON VUELTA ATRÁS ───────────────────────────────
# Por defecto NO lo toca: imprime el bloque y para. En esta caja Caddy sirve 19 dominios y su
# Caddyfile ya acumula 20 `.bak` (SDD 28 §1); un guion que lo reescribe a ciegas es cómo se tiran
# 19 sitios para levantar uno.
#
# repo.gioser.net {
# root * /srv/repo
# file_server browse
# }
#
# Con `--install-vhost` lo hace, pero **el orden importa y es lo único que lo vuelve seguro**:
# copia de seguridad → añade → `caddy validate` → sólo entonces `caddy reload`. Si validate o
# reload fallan, **restaura la copia y recarga con ella** antes de salir: el estado final es el que
# había, no uno a medias. Y si el nombre ya está en la config, no añade nada (idempotente).
#
# El DNS ya está: `repo` A → 2.29.29.217, ttl 60, en la zona de Hetzner (alta del 2026-09-21).
#
# ⚠ **Un solo origen es exactamente lo que el ADR 0014 existe para no tener.** Esta caja sirviéndose
# a sí misma está bien como camino normal, pero el `--repo` de los clientes debería llevar también
# el Storage Box: el primer origen roto, si es el único, es el último.
#
# Uso: sudo scripts/servidor/publicar-repo.sh [perfil]
# sudo scripts/servidor/publicar-repo.sh --install-vhost [perfil]
# Env: PERFIL (def servidor) · STORE (def /store) · DESTINO (def /srv/repo)
# NOMBRE (def repo.gioser.net) · TRUST (def ./trust) · CADDYFILE (def /etc/caddy/Caddyfile)
set -eu
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
INSTALL_VHOST=0
case "${1:-}" in --install-vhost) INSTALL_VHOST=1; shift ;; esac
PERFIL="${1:-${PERFIL:-servidor}}"
STORE="${STORE:-/store}"
DESTINO="${DESTINO:-/srv/repo}"
NOMBRE="${NOMBRE:-repo.gioser.net}"
TRUST="${TRUST:-$ROOT/trust}"
TAKANA="${TAKANA:-$ROOT/target/release/takana}"
CADDYFILE="${CADDYFILE:-/etc/caddy/Caddyfile}"
# La guarda pregunta por lo que de verdad hace falta —poder LEER la clave— y no por `id -u == 0`,
# que es sólo su causa habitual: así el guion se puede ensayar contra un repo y una clave de
# prueba sin ser root, y el día que la clave viva en otro sitio no hay que tocar la condición.
KEY="${KEY:-$HOME/.config/takana/keys/release.ed25519}"
[ -r "$KEY" ] || {
echo "!! no puedo leer la clave de release: $KEY" >&2
echo " En la caja vive en /root/.config/takana/keys/ ⇒ esto va como root (SDD 28 §2116)." >&2
exit 2
}
export KEY
[ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; }
# ⚠ El binario TIENE que traer el arreglo de los parches múltiples y del target_bin (SDD 28 §5.5):
# con uno anterior, las 16 recetas con ≥2 parches se publican irreproducibles y 8 paquetes más
# (bash, sed, tar, parted, dhcpcd, squid, wpa_supplicant, zsh) prometen un binario que no existe.
"$TAKANA" outdated --help >/dev/null 2>&1 || {
echo "!! este $TAKANA es anterior al arreglo del SDD 28 §5.5 (no conoce 'outdated')." >&2
echo " Reconstruilo —cargo build --release— antes de publicar, o el repo sale roto." >&2
exit 2
}
# ⚠ ROOT NO ESCRIBE EN EL ÁRBOL DEL USUARIO. `work_root` sale de `dirname(store)/work`, o sea `/work`
# con el store en `/store` — y ahí `sources` y `out` son ENLACES a `/work/sergio/work/…`, que es del
# usuario. Publicar como root dejaría directorios de root en su árbol y el siguiente build suyo
# moriría con un permiso denegado que no menciona esto. Ya pasó con otro guion: root hizo `git pull`
# en el clon de tawasuyu, dejó 24 entradas suyas dentro del `.git` y congeló el clon para el dueño
# sin que nada fallara del lado de root (ver `publicar-webs.sh`). Así que el scratch va aparte.
if [ "$(id -u)" = "0" ] && [ -z "${TAKANA_WORK:-}" ]; then
# `/var/tmp` no existe en la caja (sistema mínimo), así que se elige el que haya en vez de
# crear un directorio que ese sistema decidió no tener.
[ -d /var/tmp ] && _tmp=/var/tmp || _tmp=/tmp
TAKANA_WORK="${TAKANA_WORK_ROOT:-$_tmp/takana-publicar-work}"
export TAKANA_WORK
install -d -m 755 "$TAKANA_WORK"
echo "==> como root: scratch de build en $TAKANA_WORK (no en el árbol del usuario)"
fi
echo "==> publicando el perfil '$PERFIL' en $DESTINO"
install -d -m 755 "$DESTINO"
PERFIL="$PERFIL" STORE="$STORE" REPO="$DESTINO" TRUST="$TRUST" TAKANA="$TAKANA" \
"$ROOT/scripts/repo-perfil.sh" "$PERFIL"
# ── CÓMO SE APLICA UNA CONFIG NUEVA, QUE NO ES `caddy reload` ───────────────────────────────────
# El Caddyfile de la caja lleva `admin off` (por eso el `:2019` está cerrado), y **`caddy reload`
# habla por el admin**: sin él no recarga nada y falla. Lo que hay es un supervisor: caddy es un
# Ente de arje (`/etc/arje/cards.d/caddy.json`, `Restart{initial:1000,max:30000}`), así que la
# recarga de verdad es `arjectl restart caddy` y esperar a que vuelva a escuchar.
#
# ⚠ Y no alcanza con que el proceso vuelva: hay que comprobar que **sigue sirviendo lo de antes**.
# Un restart con la config nueva levanta los 19 dominios o ninguno, así que el testigo es un
# dominio que YA funcionaba —sacado de la config previa— y si no responde, se revierte.
TESTIGO="${TESTIGO:-}"
aplicar_config() {
if caddy reload --config "$CADDYFILE" 2>/dev/null; then
echo " · recargado por el admin de Caddy"
return 0
fi
command -v arjectl >/dev/null 2>&1 || {
echo ' !! caddy reload falló (¿admin off?) y no hay arjectl para reiniciar' >&2
return 1
}
echo " · el admin de Caddy está apagado ⇒ reinicio por el supervisor (arjectl)"
arjectl restart caddy >/dev/null 2>&1 || return 1
[ -n "$TESTIGO" ] || return 0
i=0
while [ "$i" -lt 25 ]; do
if curl -sk -o /dev/null -m 3 "https://$TESTIGO/"; then
echo " · testigo $TESTIGO responde: Caddy volvió con todo lo que ya servía"
return 0
fi
i=$((i + 1)); sleep 1
done
echo " !! $TESTIGO no volvió a responder tras el reinicio" >&2
return 1
}
if [ "$INSTALL_VHOST" = "1" ]; then
echo
echo "==> vhost de Caddy en $CADDYFILE"
[ -w "$CADDYFILE" ] || { echo "!! no puedo escribir $CADDYFILE" >&2; exit 2; }
command -v caddy >/dev/null || { echo "!! no encuentro el binario 'caddy' en el PATH" >&2; exit 2; }
if grep -qE "^[[:space:]]*(https?://)?$NOMBRE[[:space:],{]" "$CADDYFILE"; then
echo " = $NOMBRE ya está en la config; no toco nada"
else
COPIA="$CADDYFILE.antes-de-$NOMBRE-$(date -u +%Y%m%dT%H%M%SZ)"
cp -p "$CADDYFILE" "$COPIA"
echo " copia de seguridad: $COPIA"
# El testigo sale de la config que YA funcionaba: el primer dominio con nombre propio.
[ -n "$TESTIGO" ] || TESTIGO=$(grep -oE "^[a-z0-9][a-z0-9.-]+\.[a-z]+" "$COPIA" | head -1)
# Sin `|| true` esto mataría el guion con `set -e` cuando no hay testigo: el `&&` con la
# condición falsa devuelve 1 y `set -e` no distingue «no aplica» de «falló».
[ -n "$TESTIGO" ] && echo " testigo para comprobar que no rompí nada: $TESTIGO" || true
# Un site block va al FINAL: las opciones globales de Caddy tienen que ser lo primero del
# fichero, así que añadir al final nunca las invalida.
cat >> "$CADDYFILE" <<EOF
# repo de paquetes takana — SDD 28 §5.7. Lo publica scripts/servidor/publicar-repo.sh.
$NOMBRE {
root * $DESTINO
file_server browse
}
EOF
# ⚠ VALIDAR ANTES DE RECARGAR. Un reload con la config rota se lleva los 19 dominios de la
# caja, no sólo el que estoy añadiendo. Si no valida, vuelvo atrás y ni recargo.
if ! caddy validate --config "$CADDYFILE" >/dev/null 2>&1; then
cp -p "$COPIA" "$CADDYFILE"
echo "!! la config NO valida con el bloque añadido — restaurada $COPIA, no recargué" >&2
caddy validate --config "$CADDYFILE" >/dev/null 2>&1 \
|| echo "!! y la ORIGINAL tampoco valida: eso ya venía roto, miralo antes de recargar" >&2
exit 1
fi
echo " ✓ la config valida"
if ! aplicar_config; then
# No se pudo aplicar: Caddy sigue con lo que tenía cargado, pero el fichero en disco ya
# no es eso. Se restaura para que el próximo arranque no levante una config que no probó
# nadie, y se vuelve a aplicar la buena.
cp -p "$COPIA" "$CADDYFILE"
aplicar_config >/dev/null 2>&1 || true
echo "!! no pude aplicar la config — restaurada $COPIA y revertida" >&2
exit 1
fi
echo " ✓ aplicada"
fi
fi
echo
echo "==> ¿lo sirve Caddy?"
if curl -fsS -m 10 "https://$NOMBRE/index.json" -o /dev/null 2>/dev/null; then
echo " ✓ https://$NOMBRE/index.json responde"
echo
echo " probalo desde cualquier hub (necesita el lab: install reproduce desde fuente):"
echo " takana install zsh --repo https://$NOMBRE --trust ./trust --require-signed"
echo " y el aviso periódico:"
echo " REPO=https://$NOMBRE scripts/servidor/avisar-actualizaciones.sh"
else
if [ "$INSTALL_VHOST" = "1" ]; then
# Decir «falta el vhost» justo después de haberlo puesto es la clase de mensaje que manda a
# buscar el problema donde no está.
cat <<EOF
✗ el vhost está puesto y Caddy recargado, pero https://$NOMBRE todavía no responde.
Lo normal a mirar, en este orden:
· el CERTIFICADO: Caddy lo pide en la PRIMERA petición y puede tardar unos segundos más de
los que espera esta comprobación — reintentá el curl antes de buscar nada. Si de verdad
falla, el log de caddy (arjectl status caddy) lo dice: casi siempre el :80 tapado o el DNS.
· el DNS: $NOMBRE tiene que resolver a ESTA caja (hoy: A → 2.29.29.217).
· el repo: $DESTINO tiene que tener el index.json que acaba de firmarse.
EOF
exit 1
fi
cat <<EOF
✗ https://$NOMBRE todavía no responde. El repo YA está publicado y firmado en $DESTINO;
lo que falta es el vhost. O lo pone este mismo guion:
sudo $0 --install-vhost $PERFIL
(copia de seguridad → añade → valida → recarga, y si algo falla vuelve atrás)
o a mano, en $CADDYFILE:
$NOMBRE {
root * $DESTINO
file_server browse
}
y recargá: caddy reload --config $CADDYFILE
Después volvé a correr este guion: comprueba el lazo entero en vez de darlo por hecho.
EOF
exit 1
fi