tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1411Shell 141Markdown 84Rust 832898 Sutichaykuna

publicar-repo: el vhost, con validación y vuelta atrás — y publicar deja de ser compilar

Dos cosas que el ensayo del guion destapó, las dos medidas:

1. `repo-perfil.sh` NO acotaba el build. `pack --build` es instantáneo con cache-hit y una
   compilación entera si el artefacto no está sellado, así que «publicá el repo» se convertía en
   silencio en «ponete a compilar el perfil»: la corrida se puso a moler `libnftnl` en una caja de
   4 cores que ya estaba a load 46, y después venía `rust`. `build-repo.sh` llevaba `timeout` desde
   siempre; el que se corre en producción, no. Ahora BUILD_TIMEOUT (120 s) y SKIP_UNSEALED=1, con
   el vencimiento tratado como «no estaba sellado» y no como error. Resultado del perfil entero:
   173 anclados, 0 sin ancla, 2 saltados (os-release, rust), sin compilar nada.

2. `--install-vhost` pone el bloque de Caddy sin que haya que editar a mano, pero el orden es lo
   único que lo vuelve seguro: copia → añade → `caddy validate` → SÓLO ENTONCES `reload`. Si
   validate o reload fallan, restaura la copia y recarga con ella. Un reload con la config rota no
   tira el sitio nuevo: tira los 19 de la caja.

Probado en los dos caminos contra un Caddy de juguete (el de la caja no tiene el admin abierto):
con reload bueno, el admin muestra el server nuevo sirviendo el repo y el sitio previo intacto; con
reload fallido, el Caddyfile vuelve byte a byte y no queda rastro del bloque.

Y `repo-perfil.sh` pasa a respetar TAKANA del entorno: la guarda de publicar-repo.sh comprobaba un
binario y se publicaba con otro, que es peor que no tener guarda.

b7d30f2f15ff572fd17a7e1563f14d66a8114ebe · ruwaq Sergio · (1 killa ñawpaq) · tayta 7de281a79 · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

3 qillqakuna · +136 −14

diff --git a/docs/28-servidor-de-produccion.md b/docs/28-servidor-de-produccion.md
index 4045efad..63a589f8 100644
--- a/docs/28-servidor-de-produccion.md
+++ b/docs/28-servidor-de-produccion.md
@@ -570,6 +570,25 @@ firma en `/srv/repo`, comprueba el lazo y **imprime el bloque sin tocar el Caddy
 caja Caddy sirve 19 dominios y su config ya acumula 20 `.bak`; un guion que la reescribe a ciegas es
 cómo se tiran 19 sitios para levantar uno.
 
+⚠ **Publicar no es construir, y `repo-perfil.sh` no lo acotaba.** `pack --build` es instantáneo con
+cache-hit y una **compilación entera** si el artefacto no está sellado. Medido al ensayar el guion:
+la corrida se puso a compilar `libnftnl` —y después habría seguido con `rust`— en una caja de 4
+cores que ya estaba a **load 46**. `build-repo.sh` llevaba `timeout` desde siempre; el que se corre
+en producción, no. Ahora `BUILD_TIMEOUT` (120 s) y `SKIP_UNSEALED=1`: un vencimiento **no es un
+error**, significa «ése no estaba sellado». Con eso, el perfil entero publica **173 anclados, 0 sin
+ancla, 2 saltados (`os-release`, `rust`)** sin compilar nada.
+
+**El `--install-vhost`, probado en sus dos caminos** *(con un Caddy de juguete, porque el de la caja
+no tiene el admin abierto)*:
+
+| camino | qué pasó |
+|---|---|
+| reload OK | `✓ la config valida` → `✓ recargado`; el admin muestra el `srv1` nuevo sirviendo el repo **y el sitio que ya estaba sigue en pie** |
+| reload FALLA | copia restaurada **byte a byte** (`b"repo.local" not in` el fichero final), y no recarga nada |
+
+El orden es lo único que lo hace seguro: copia → añadir → `caddy validate` → **sólo entonces**
+`reload`. Un reload con la config rota no tira el sitio nuevo: tira los 19.
+
 ⚠ **Y trae su propia guarda contra el error más fácil de cometer:** si el `takana` de la caja es
 anterior al §5.5 (se detecta porque no conoce `outdated`), **aborta antes de publicar**. Con uno
 viejo el repo sale con las 16 recetas multi-parche irreproducibles y 8 paquetes prometiendo un
diff --git a/scripts/repo-perfil.sh b/scripts/repo-perfil.sh
index 514ee50c..989dd564 100755
--- a/scripts/repo-perfil.sh
+++ b/scripts/repo-perfil.sh
@@ -13,9 +13,18 @@
 # que nadie conoce y que cambia en cada corrida no lo verifica nadie: es decoración (SDD 19 §3.2,
 # «la firma sin gestión de claves es teatro»). Acá, si no está la clave, se ABORTA.
 #
+# ── PUBLICAR NO ES CONSTRUIR (2026-09-21) ───────────────────────────────────────────────────────
+# `pack --build` es instantáneo si el artefacto ya está sellado (cache-hit) y una COMPILACIÓN
+# ENTERA si no lo está. Sin acotarlo, «publicá el repo» se convierte en silencio en «ponete a
+# compilar el perfil»: medido acá, la corrida se paró sola en `libnftnl` y se quedó moliendo en una
+# caja de 4 cores que ya estaba a load 46. `build-repo.sh` ya llevaba el `timeout`; esto no, y es el
+# que se corre en producción. Un vencimiento NO es un error: significa «ése no estaba sellado», y el
+# paquete sale sin ancla (o se lo salta, con SKIP_UNSEALED=1).
+#
 # Uso:   ./scripts/repo-perfil.sh [perfil]
 # Env:   PERFIL (def servidor) · STORE (def ./store) · REPO (def dist/repo)
 #        KEY (def ~/.config/takana/keys/release.ed25519) · TRUST (def ./trust) · DISTRO (def dev)
+#        BUILD_TIMEOUT (def 120 s por receta) · SKIP_UNSEALED=1 (no publicar lo que no esté sellado)
 set -eu
 ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
 
@@ -25,7 +34,12 @@ REPO="${REPO:-dist/repo}"
 KEY="${KEY:-$HOME/.config/takana/keys/release.ed25519}"
 TRUST="${TRUST:-./trust}"
 DISTRO="${DISTRO:-dev}"
-TAKANA="$ROOT/target/release/takana"
+BUILD_TIMEOUT="${BUILD_TIMEOUT:-120}"
+SKIP_UNSEALED="${SKIP_UNSEALED:-0}"
+# Overridable: `publicar-repo.sh` comprueba que el binario traiga el arreglo del SDD 28 §5.5 antes
+# de publicar, y esa guarda es mentira si acá se usa otro. Un guion que valida un binario y ejecuta
+# otro es peor que uno sin guarda: da confianza sin darla.
+TAKANA="${TAKANA:-$ROOT/target/release/takana}"
 
 [ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; }
 [ -r "$KEY" ] || {
@@ -56,12 +70,15 @@ total=$(printf '%s\n' "$LISTA" | grep -c .)
 echo "==> perfil '$PERFIL': $total paquetes en la clausura → $REPO"
 
 rm -rf "$REPO"; mkdir -p "$REPO"
-anclados=0; sin_ancla=0; fallaron=""
+anclados=0; sin_ancla=0; fallaron=""; saltados=""
 for n in $LISTA; do
     r="recipes/$n.toml"
     [ -r "$r" ] || { fallaron="$fallaron $n(sin-receta)"; continue; }
-    if "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" --distro-version "$DISTRO" --build >/dev/null 2>&1; then
+    if timeout "$BUILD_TIMEOUT" "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" \
+            --distro-version "$DISTRO" --build >/dev/null 2>&1; then
         anclados=$((anclados + 1))
+    elif [ "$SKIP_UNSEALED" = "1" ]; then
+        saltados="$saltados $n"
     elif "$TAKANA" --store "$STORE" pack "$r" --repo "$REPO" --distro-version "$DISTRO" >/dev/null 2>&1; then
         sin_ancla=$((sin_ancla + 1))
     else
@@ -78,6 +95,6 @@ echo "==> firmando el índice con $KEY"
 echo "==> ✓ el índice verifica contra $TRUST"
 
 echo
-echo "publicado: $anclados con expected_hash anclado, $sin_ancla sin ancla.${fallaron:+ FALLARON:$fallaron}"
+echo "publicado: $anclados con expected_hash anclado, $sin_ancla sin ancla.${saltados:+ SIN SELLAR (saltados):$saltados}${fallaron:+ FALLARON:$fallaron}"
 echo "repo firmado en $REPO"
 echo "  consumir:  takana install <nombre> --repo <url> --trust <trust> --require-signed"
diff --git a/scripts/servidor/publicar-repo.sh b/scripts/servidor/publicar-repo.sh
index 18695a67..1025d3c2 100755
--- a/scripts/servidor/publicar-repo.sh
+++ b/scripts/servidor/publicar-repo.sh
@@ -8,36 +8,55 @@
 #   2. comprueba que el índice verifica contra `trust/`,
 #   3. dice si Caddy ya sirve el nombre, y **si no, imprime el bloque y para**.
 #
-# ── POR QUÉ NO EDITA EL CADDYFILE ───────────────────────────────────────────────────────────────
-# 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. El bloque va a mano, una vez:
+# ── 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
 #     }
 #
-# y después `caddy reload --config /etc/caddy/Caddyfile`. El DNS ya está: `repo` A → 2.29.29.217,
-# ttl 60, en la zona de Hetzner (dado de alta el 2026-09-21).
+# 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)
+#        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}"
 
-[ "$(id -u)" = "0" ] || { echo "!! corré esto como root: la clave de release está en /root/.config/takana/keys/" >&2; exit 2; }
+# 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):
@@ -51,7 +70,56 @@ TAKANA="${TAKANA:-$ROOT/target/release/takana}"
 
 echo "==> publicando el perfil '$PERFIL' en $DESTINO"
 install -d -m 755 "$DESTINO"
-PERFIL="$PERFIL" STORE="$STORE" REPO="$DESTINO" TRUST="$TRUST" "$ROOT/scripts/repo-perfil.sh" "$PERFIL"
+PERFIL="$PERFIL" STORE="$STORE" REPO="$DESTINO" TRUST="$TRUST" TAKANA="$TAKANA" \
+    "$ROOT/scripts/repo-perfil.sh" "$PERFIL"
+
+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"
+
+        # 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 ! caddy reload --config "$CADDYFILE"; then
+            # El reload falló: 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 reintenta el reload con la buena.
+            cp -p "$COPIA" "$CADDYFILE"
+            caddy reload --config "$CADDYFILE" >/dev/null 2>&1 || true
+            echo "!! el reload falló — restaurada $COPIA y recargada" >&2
+            exit 1
+        fi
+        echo "  ✓ recargado"
+    fi
+fi
 
 echo
 echo "==> ¿lo sirve Caddy?"
@@ -63,16 +131,34 @@ if curl -fsS -m 10 "https://$NOMBRE/index.json" -o /dev/null 2>/dev/null; then
     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 tarda unos segundos; si falla,
+        `journalctl -u caddy` (o el log de arje) lo dice — casi siempre es 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. Añadí a /etc/caddy/Caddyfile:
+    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 /etc/caddy/Caddyfile
+    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