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
This commit is contained in:
Executable
+101
@@ -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"
|
||||
Reference in New Issue
Block a user