Files
takana/scripts/farm/sembrar-fuente.sh
T
SergioandClaude Opus 5 79a9ac5bda 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
2026-09-14 18:17:46 +00:00

102 lines
5.5 KiB
Bash
Executable File

#!/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"