#!/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 [--host root@host] [--remote /opt/takana] set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" RECETA="${1:?uso: sembrar-fuente.sh [--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 ` ` 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"