farm: sembrar-fuente.sh — el worker construye lo de tawasuyu SIN tener la clave
El worker no puede clonar el gitea de tawasuyu y **no debe poder**: esa clave es el SSH de todo y
él sólo necesita leer un repo. Lo que viaja es el ÁRBOL, no la credencial: el hub —que sí la
tiene— clona, y el guión manda el mirror.
⚠ Y copiar el mirror tal cual NO alcanza, que es lo que costó descubrir: el fetch de takana clona
con `--filter=blob:none`, así que el mirror copiado **no tiene los blobs** y los va a buscar a un
remoto que allá no responde. El síntoma no nombra la causa:
tar: This does not look like a tar archive
Error: git archive | tar -x falló (tar exit Some(2))
Por eso el guión HIDRATA (`fetch --refetch --no-filter`, 17 M → 210 M) y verifica con la prueba del
CONSUMIDOR —`git archive` de verdad, en el worker— y no con `cat-file -e`, que pasa igual sobre un
mirror parcial. Más el `chown -R root:root`, sin el cual git rechaza el repo por «dubious
ownership» cuando el builder corre como root: falla al construir, no al copiar.
Dos bugs propios, cazados corriéndolo:
· `grep -q .` sobre la salida de `git archive` decía «vacío» con un archive perfectamente bueno: es
un tar BINARIO y puede no traer un salto de línea en los primeros bytes. Va `wc -c`.
· `head -c` cierra el pipe, `git archive` muere con SIGPIPE y con `pipefail` eso hacía fallar el
pipeline entero: el guión se cortaba EN SILENCIO justo en la línea que dice que verifica. Un
verificador que aborta sin decir nada es peor que no verificar.
Probado con `recipes/arjectl.toml`: idempotente (segunda pasada no reclona) y el worker extrae el
árbol. Y el control del contrato: con una receta de tarball dice que no es una receta git y sale.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Tf9T4vGzsMoT7eS8YzMFn
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
1 archivos · +101 −0
- scripts/farm/sembrar-fuente.sh +101 −0
diff --git a/scripts/farm/sembrar-fuente.sh b/scripts/farm/sembrar-fuente.sh
new file mode 100755
index 00000000..45009105
--- /dev/null
+++ b/scripts/farm/sembrar-fuente.sh
@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+# sembrar-fuente.sh — lleva al worker el ÁRBOL DE FUENTES de una receta que él no puede clonar.
+#
+# ── POR QUÉ EXISTE (medido 2026-09-14) ──────────────────────────────────────────────────────────
+# El worker no tiene `~/.ssh/tawasuyu`, así que cualquier receta cuyo `[source]` apunte al gitea de
+# tawasuyu muere allá con `Please make sure you have the correct access rights`. Y **no debe
+# tenerla**: esa clave es el SSH de todo; el worker sólo necesita LEER un repo. Lo que viaja acá es
+# el árbol ya clonado, no la credencial.
+#
+# ⚠ Y NO ALCANZA CON COPIAR EL MIRROR: el fetch de takana clona con `--filter=blob:none`, así que un
+# mirror copiado tal cual **no tiene los blobs** y los va a buscar al remoto — que allá no responde.
+# El síntoma no nombra la causa:
+#
+# tar: This does not look like a tar archive
+# Error: git archive | tar -x falló (tar exit Some(2))
+#
+# Por eso este guión HIDRATA el mirror (`fetch --refetch --no-filter`) antes de mandarlo, y verifica
+# del otro lado con la prueba del CONSUMIDOR —`git archive` de verdad—, no con `cat-file -e`, que
+# pasa igual sobre un mirror parcial.
+#
+# Uso: scripts/farm/sembrar-fuente.sh <receta.toml> [--host root@host] [--remote /opt/takana]
+set -euo pipefail
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
+
+RECETA="${1:?uso: sembrar-fuente.sh <receta.toml> [--host user@host] [--remote /opt/takana]}"; shift || true
+HOST=""; REMOTE="${REMOTE:-/opt/takana}"; SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --host) HOST="$2"; shift;;
+ --remote) REMOTE="$2"; shift;;
+ *) echo "opción desconocida: $1" >&2; exit 2;;
+ esac; shift
+done
+# Sin `--host`: el primero de la flota. `.fleet` es `<nombre> <ip>` por línea y está GITIGNORED —
+# un hub recién clonado la tiene vacía y la granja queda desconectada sin que nada falle.
+if [ -z "$HOST" ]; then
+ ip=$(awk 'NF{print $2; exit}' scripts/farm/.fleet 2>/dev/null || true)
+ [ -n "$ip" ] || { echo "!! no hay --host y scripts/farm/.fleet está vacía o no existe" >&2; exit 2; }
+ HOST="root@$ip"
+fi
+SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o BatchMode=yes"
+
+[ -r "$RECETA" ] || { echo "!! no puedo leer $RECETA" >&2; exit 2; }
+eval "$(python3 - "$RECETA" <<'PY'
+import sys, tomllib
+d = tomllib.load(open(sys.argv[1], "rb"))
+src = d.get("source", {})
+print(f'NOMBRE={d.get("name", "")!r}'.replace("'", '"'))
+print(f'REPO={src.get("repo", "")!r}'.replace("'", '"'))
+print(f'COMMIT={src.get("commit", "")!r}'.replace("'", '"'))
+PY
+)"
+[ -n "$REPO" ] && [ -n "$COMMIT" ] || { echo "!! $RECETA no es una receta git (necesita source.repo + source.commit)" >&2; exit 2; }
+
+MIRROR="work/repos/$NOMBRE.git"
+echo "==> receta : $NOMBRE"
+echo "==> repo : $REPO"
+echo "==> commit : ${COMMIT:0:12}"
+
+# 1. el mirror en el HUB, que es quien tiene la credencial
+if [ ! -d "$MIRROR" ]; then
+ echo "==> clonando en el hub (mirror completo, con blobs)"
+ git clone --mirror "$REPO" "$MIRROR"
+else
+ echo "==> mirror ya existe en el hub"
+fi
+# Hidratar: si viene de un fetch de takana es PARCIAL. Es idempotente y barato cuando ya está lleno.
+if git -C "$MIRROR" config --get remote.origin.partialclonefilter >/dev/null 2>&1; then
+ echo "==> el mirror es PARCIAL (blob:none) ⇒ hidratando los blobs"
+ git -C "$MIRROR" fetch --refetch --no-filter
+ git -C "$MIRROR" config --unset remote.origin.partialclonefilter || true
+ git -C "$MIRROR" config --unset remote.origin.promisor || true
+fi
+git -C "$MIRROR" cat-file -e "$COMMIT" 2>/dev/null || {
+ echo "==> el commit no está ⇒ fetch"; git -C "$MIRROR" fetch --all --tags
+ git -C "$MIRROR" cat-file -e "$COMMIT" || { echo "!! el commit $COMMIT no existe en $REPO" >&2; exit 1; }
+}
+# La prueba del consumidor, EN EL HUB, antes de gastar la red: `git archive` es lo que corre el fetch
+# de takana, y es lo único que distingue un mirror completo de uno parcial.
+# (`wc -c`, no `grep -q .`: la salida es un tar BINARIO y puede no traer un salto de línea en los
+# primeros bytes — `grep` decía «vacío» sobre un archive perfectamente bueno.)
+# `set +o pipefail` acá y no fuera: `head -c` cierra el pipe y `git archive` muere con SIGPIPE, que
+# con pipefail hace fallar el pipeline ENTERO — el guión se cortaba en silencio justo donde dice
+# que verifica. Un verificador que aborta sin decir nada es peor que no verificar.
+bytes=$(set +o pipefail; git -C "$MIRROR" archive "$COMMIT" 2>/dev/null | head -c 512 | wc -c)
+[ "$bytes" -gt 0 ] || { echo "!! git archive vacío en el hub — el mirror no sirve" >&2; exit 1; }
+echo "==> mirror listo: $(du -sh "$MIRROR" | cut -f1)"
+
+# 2. al worker
+echo "==> enviando a $HOST:$REMOTE/$MIRROR"
+rsync -a --delete -e "$SSH" "$MIRROR/" "$HOST:$REMOTE/$MIRROR/"
+# `chown`: el rsync deja el dueño del hub y git rechaza el repo con «dubious ownership» cuando el
+# builder corre como root. Falla al construir, no al copiar.
+$SSH "$HOST" "chown -R root:root '$REMOTE/$MIRROR'"
+
+# 3. verificar ALLÁ con la prueba del consumidor, no con `cat-file`
+echo "==> verificando en el worker (git archive de verdad)"
+entradas=$(set +o pipefail; $SSH "$HOST" "cd '$REMOTE' && git -C '$MIRROR' archive '$COMMIT' 2>/dev/null | tar -t 2>/dev/null | head -3" || true)
+[ -n "$entradas" ] || { echo "!! el árbol NO se puede extraer en el worker" >&2; exit 1; }
+echo "$entradas" | sed 's/^/ /'
+echo "✓ $NOMBRE: el worker ya puede construirla sin credenciales"