Files
takana/scripts/farm/farm-sync.sh
T
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00:00

131 lines
7.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# farm-sync.sh <user@host> — orquesta el worker VPS desde el HUB (laptop). Una sola pasada:
# 1. sube código + recipes/incoming/ al worker (rsync, el worker no toca gitea)
# 2. baja el store/ sellado que el worker construyó
# 3. promueve+firma lo cosechado (cache-hit instantáneo en los artefactos bajados) + commit/push
#
# El acceso es UNO solo: laptop->VPS por SSH (la clave de firma y gitea nunca salen del laptop).
# Idempotente y seguro de re-correr. Pensado para correr cada vez que el laptop está online.
#
# Uso: scripts/farm/farm-sync.sh root@1.2.3.4
# scripts/farm/farm-sync.sh root@1.2.3.4 --no-promote # sólo sincroniza, no firma
# Env: SSH_KEY (def ~/.ssh/github5), REMOTE (def /opt/hammer)
set -euo pipefail
VPS="${1:?uso: farm-sync.sh user@host [--no-promote]}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
REMOTE="${REMOTE:-/opt/hammer}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
# El MANIFIESTO de lo ya sellado en el hub, para que el worker no rehaga trabajo hecho. Es la otra
# mitad del arreglo del sync unidireccional (ver la nota en `scripts/build-farm.sh`): el store no
# viaja hub→worker porque son gigabytes, pero la lista de nombres son kilobytes y evita que el worker
# reintente y falle cada ciclo lo que el hub ya tiene. Se regenera acá, en cada sync, para que no se
# quede vieja sola.
#
# ── ⚠ EL MANIFIESTO NO ES `ls ./store` ─────────────────────────────────────────────────────────
# Lo fue hasta el 2026-08-09, y desde que el store se mudó al VOLUMEN eso pasó a ser una mentira
# cara: el disco local se quedó con 220 artefactos (los de fuente privada + lo aún no respaldado),
# así que `ls ./store` le habría dicho al worker «el hub tiene 220» y el worker habría reconstruido
# ~950 artefactos que ya existen. Es exactamente el bucle que este fichero documenta más abajo, pero
# al revés y multiplicado: no devolver basura, sino rehacer lo hecho.
#
# La pregunta que el worker hace es «¿esto ya está sellado?», no «¿esto está en el disco del hub?».
# Se responde con la UNIÓN de lo que hay en el disco y lo que registran los manifiestos — el mismo
# criterio que usa `build-state.py`, y por la misma razón: la presencia de los bytes y el hecho de
# estar construido dejaron de ser lo mismo el día que el store dejó de vivir acá.
mkdir -p work
{
ls ./store 2>/dev/null | grep -E '^[0-9a-f]{64}-' || true
# `respaldo-sellados.txt` (lo que hay en el Storage Box) y el propio manifiesto del ciclo anterior,
# que arrastra lo que el worker selló y todavía no llegó a ningún otro registro.
cat work/respaldo-sellados.txt work/farm-sellados.txt 2>/dev/null || true
} | grep -E '^[0-9a-f]{64}-' | sort -u > work/farm-sellados.txt.new
# Guardián: el manifiesto sólo puede CRECER salvo poda explícita. Si encoge, algo se perdió y es más
# barato abortar el sync que mandar al worker a rehacer un corpus.
VIEJO=$(wc -l < work/farm-sellados.txt 2>/dev/null || echo 0)
NUEVO=$(wc -l < work/farm-sellados.txt.new)
if [ "$NUEVO" -lt "$VIEJO" ]; then
echo "⚠ el manifiesto ENCOGIÓ ($VIEJO$NUEVO) — no sincronizo; revisá antes de que el worker rehaga trabajo" >&2
rm -f work/farm-sellados.txt.new; exit 1
fi
mv work/farm-sellados.txt.new work/farm-sellados.txt
echo "==> 0. manifiesto de sellados: $NUEVO artefactos (disco local: $(ls ./store 2>/dev/null | grep -cE '^[0-9a-f]{64}-'))"
echo "==> 1. subiendo código + cola al worker ($VPS:$REMOTE)"
rsync -az --delete -e "$SSH" \
--include '/work/' --include '/work/farm-sellados.txt' \
--exclude /work --exclude /store --exclude '/store-*' --exclude /target \
--exclude /dist --exclude /.dev-fs --exclude /.git --exclude /.scratch \
--exclude '*.png' --exclude '/content*' \
./ "$VPS:$REMOTE/"
echo "==> 2. bajando artefactos sellados del worker"
# ── ⚠ NO BAJAR LO QUE YA PODAMOS: era un BUCLE DE CHURN DE 24 G ────────────────────────────────
# Medido el 2026-08-07: `store-gc.sh` borró 362 artefactos superados (24 G) por la mañana y por la
# tarde volvió a borrar **los mismos 362** — solapamiento del 100%. No era casualidad: este rsync
# bajaba el store del worker ENTERO, y el worker nunca se poda, así que conservaba los superados y
# nos los devolvía en cada cosecha. Podar → cosechar → vuelven → podar. El disco pagaba 24 G por
# vuelta y la métrica de espacio libre mentía sobre el progreso.
#
# El registro `work/store-gc-superados.txt` es la unión de todos los manifiestos de poda: nombres
# `<hash>-<paquete>` de artefactos que YA decidimos que sobran. Como el store es CAS —los nombres
# son inmutables y un artefacto superado no vuelve a ser vigente salvo que una receta retroceda—
# excluirlos es seguro; y si una receta retrocediera, su artefacto se reconstruye, que es barato.
LEDGER="work/store-gc-superados.txt"
# ── ⚠ `-H` Y EXCLUIR `.dmerge`: SIN ESTO LA COSECHA LLENA EL DISCO ─────────────────────────────
# Medido el 2026-09-01: esta bajada murió con `No space left on device (28)` escribiendo
# `store/.dmerge/b8bc5ecc…/usr/share/locale/az/LC_MESSAGES/kio6.mo`, con 95 G libres y un store
# remoto de 25 G. Dos causas que se multiplican:
#
# 1. **`rsync -a` NO preserva hardlinks** (eso es `-H`, y no está incluido en `-a`). El store es
# CAS y `.dmerge` hardlinkea los artefactos: en el worker, 25 G medidos con `du` —que deduplica
# por inodo— son MUCHOS más en el cable, porque cada enlace se transfiere y se escribe como una
# copia entera. El tamaño que informa `du` del origen NO es el que hace falta en el destino.
# 2. **`.dmerge` es CACHÉ PURA y no tiene por qué viajar.** Es la capa de merge que takana
# reconstruye sola; copiarla no aporta un solo artefacto y es la parte más pesada del árbol
# (en el hub llegó a 132 G con 111,7 GiB exclusivos). Cosechar es traerse ARTEFACTOS.
#
# El fallo además es a mitad de camino: rsync limpia sus temporales al abortar, pero los directorios
# que ya creó quedan — y un directorio de artefacto sin ficheros es EXACTAMENTE un cache-hit
# envenenado (`mise` llegó así en esa pasada). Ver la regla 3 del CLAUDE.md.
RSOPTS=(-azH --exclude '/.dmerge')
if [ -s "$LEDGER" ]; then
echo " (excluyendo $(wc -l < "$LEDGER") artefactos ya podados — ver la nota del bucle de churn)"
rsync "${RSOPTS[@]}" -e "$SSH" --exclude-from="$LEDGER" "$VPS:$REMOTE/store/" ./store/
else
rsync "${RSOPTS[@]}" -e "$SSH" "$VPS:$REMOTE/store/" ./store/
fi
# ── GUARDIÁN: ningún artefacto cosechado puede llegar VACÍO ────────────────────────────────────
# Un directorio sin ficheros no es un artefacto, es un nombre — y `takana build` lo lee como
# presencia y sella sin construir. Barrerlos acá es barato; detectarlos tres eslabones más abajo
# (manifiesto → grafo → build) cuesta una campaña.
VACIOS=0
for d in ./store/*/; do
case "$d" in ./store/.dmerge/) continue ;; esac
[ -d "$d" ] || continue
if [ -z "$(find "$d" -mindepth 1 -print -quit 2>/dev/null)" ]; then
echo " ⚠ artefacto VACÍO cosechado, lo quito: $d"; rmdir "$d" 2>/dev/null; VACIOS=$((VACIOS+1))
fi
done
[ "$VACIOS" -gt 0 ] && echo " ($VACIOS vacíos barridos — la bajada quedó a medias, re-corré el sync)"
if [ "${2:-}" = "--no-promote" ]; then
echo "✓ store sincronizado (sin promote)"; exit 0
fi
echo "==> 3. promote + firma (cache-hit en lo que el worker selló)"
scripts/build-farm.sh
echo "==> 4. commit + push de la cosecha"
git add recipes/ tandas/ 2>/dev/null || true
if ! git diff --cached --quiet; then
git commit -q -m "Etapa G: cosecha del worker VPS — promote + firma"
git push -q origin main && echo "✓ pusheado"
else
echo "(nada nuevo que commitear)"
fi