diff --git a/scripts/farm/farm-worker-loop.sh b/scripts/farm/farm-worker-loop.sh index 88bfc5c3..e0180f34 100755 --- a/scripts/farm/farm-worker-loop.sh +++ b/scripts/farm/farm-worker-loop.sh @@ -60,15 +60,25 @@ DISK_HIGH="${DISK_HIGH:-82}" done ) & -echo "$(date -u +%FT%TZ) worker-loop arrancado: JOBS=$JOBS IDLE_SLEEP=$IDLE_SLEEP" +# Colas a construir (sólo SELLA, PROMOTE=0; el hub promueve+firma). `recipes/incoming` es la cola +# compartida (otro agente, stack gráfico tawasuyu); `recipes/incoming-go` es la cola AISLADA del +# frente Go (pre-sembrada para que el worker muela 24/7 sin pisar al otro agente). build-farm hace +# `rm -rf work/farm` al inicio ⇒ construirlas EN SERIE (una por vuelta) es seguro, sin colisión. +QUEUES="${QUEUES:-recipes/incoming recipes/incoming-go}" +echo "$(date -u +%FT%TZ) worker-loop arrancado: JOBS=$JOBS IDLE_SLEEP=$IDLE_SLEEP QUEUES='$QUEUES'" while :; do - n=$(ls recipes/incoming/*.toml 2>/dev/null | wc -l) - if [ "$n" -gt 0 ]; then - echo "$(date -u +%FT%TZ) ciclo: $n recetas en cola" - JOBS="$JOBS" PROMOTE=0 scripts/build-farm.sh 2>&1 | tail -50 + total=0 + for Q in $QUEUES; do + n=$(ls "$Q"/*.toml 2>/dev/null | wc -l) + [ "$n" -gt 0 ] || continue + total=$((total + n)) + echo "$(date -u +%FT%TZ) ciclo: $n recetas en $Q" + JOBS="$JOBS" PROMOTE=0 QUEUE="$Q" scripts/build-farm.sh 2>&1 | tail -50 + done + if [ "$total" -gt 0 ]; then echo "$(date -u +%FT%TZ) ciclo terminado; store sellado disponible para el hub" else - echo "$(date -u +%FT%TZ) cola vacía; durmiendo ${IDLE_SLEEP}s" + echo "$(date -u +%FT%TZ) colas vacías; durmiendo ${IDLE_SLEEP}s" fi sleep "$IDLE_SLEEP" done diff --git a/scripts/farm/harvest-go.sh b/scripts/farm/harvest-go.sh new file mode 100755 index 00000000..7b49bbb6 --- /dev/null +++ b/scripts/farm/harvest-go.sh @@ -0,0 +1,127 @@ +#!/bin/sh +# harvest-go.sh — COSECHA-ONLY automática del frente Go (modo finde desatendido). Determinista, sin +# IA: pensado para un cron en el laptop (el hub). NO elige categorías ni diagnostica fallos; sólo +# convierte en release firmado lo que el worker YA selló y que PASA un smoke-test del binario. +# +# Ciclo (idempotente, seguro de re-correr): +# 1. git pull --rebase (sincroniza con el otro agente que pushea al mismo main) +# 2. baja el store sellado del worker (rsync) +# 3. por cada receta en recipes/incoming-go/: ¿está sellada? (hammer build con `timeout` ⇒ cache-hit +# instantáneo = sí; si se pasa del timeout sería un build local ⇒ se SALTA, no compilamos en el hub) +# 4. SMOKE-TEST del binario instalado: existe + ELF estático + corre version/--help sin panic/segfault. +# - PASA ⇒ promueve (mv recipes/, pack --build) — se firma+commitea al final. +# - FALLA ⇒ mueve a tandas/needs-review-weekend/ con el motivo (no reintenta cada cosecha). +# 5. si promovió algo: firma el repo 1 vez + git add EXPLÍCITO (nunca -A: respeta al otro agente) + +# commit + push (con pull --rebase de reintento). +# Todo se loguea a work/harvest-go.log para revisar el lunes. +# +# Uso (cron): */DohubVPS cd /home/sergio/hammer && scripts/farm/harvest-go.sh root@1.2.3.4 >>work/harvest-go.log 2>&1 +# Env: SSH_KEY (def ~/.ssh/github5), REMOTE (def /opt/hammer), STORE (def ./store), REPO (def dist/repo), +# KEY (def dist/keys/release.ed25519), DISTRO (def dev), HAMMER (def target/release/hammer), +# SEAL_TIMEOUT (def 60s: techo del cache-hit; más = sería build local ⇒ skip). +set -u + +VPS="${1:?uso: harvest-go.sh user@host}" +ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" +SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" +REMOTE="${REMOTE:-/opt/hammer}" +STORE="${STORE:-$ROOT/store}" +REPO="${REPO:-$ROOT/dist/repo}" +KEY="${KEY:-$ROOT/dist/keys/release.ed25519}" +DISTRO="${DISTRO:-dev}" +HAMMER="${HAMMER:-$ROOT/target/release/hammer}" +SEAL_TIMEOUT="${SEAL_TIMEOUT:-60}" +QUEUE="recipes/incoming-go" +REVIEW="tandas/needs-review-weekend" +SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o ConnectTimeout=20" +STAMP() { date -u +%FT%TZ; } + +# Lock: una sola cosecha a la vez (el cron no se solapa). +LOCK="$ROOT/work/.harvest-go.lock" +mkdir -p "$ROOT/work" +if ! mkdir "$LOCK" 2>/dev/null; then + echo "$(STAMP) harvest-go: ya hay una cosecha en curso ($LOCK) ⇒ salgo"; exit 0 +fi +trap 'rmdir "$LOCK" 2>/dev/null' EXIT INT TERM + +echo "==================================================================" +echo "$(STAMP) harvest-go: arranco (VPS=$VPS)" + +# 1. sincronizar con el otro agente (mismo main). --autostash por si quedó algo sin commitear. +git pull --rebase --autostash origin main 2>&1 | tail -2 || echo "$(STAMP) WARN: git pull falló (sigo)" + +# 2. bajar el store sellado del worker. +echo "$(STAMP) bajando store del worker…" +rsync -az -e "$SSH" "$VPS:$REMOTE/store/" "$STORE/" 2>&1 | tail -1 || { echo "$(STAMP) ERROR: rsync store falló"; exit 1; } + +mkdir -p "$REVIEW" +promoted=""; rejected=""; skipped="" + +# 3+4. por cada receta de la cola aislada Go. +for f in "$QUEUE"/*.toml; do + [ -e "$f" ] || { echo "$(STAMP) cola vacía"; break; } + n=$(basename "$f" .toml) + + # ¿sellada? cache-hit instantáneo imprime el hash; un build real se pasa del timeout ⇒ skip. + H=$(timeout "$SEAL_TIMEOUT" "$HAMMER" --store "$STORE" build "$f" 2>/dev/null | tail -1) + case "$H" in + b3:*) : ;; + *) skipped="$skipped $n"; continue ;; # no sellada aún (o tardaría en compilar) ⇒ el worker sigue + esac + + # SMOKE-TEST: localizar el binario instalado en el seal. + d="$STORE/${H#b3:}-$n" + bin=$(find "$d" -type f -path '*/bin/*' -perm -100 2>/dev/null | head -1) + if [ -z "$bin" ]; then + { echo "# REVIEW $(STAMP): el seal no tiene binario en */bin/ (¿install raro?)"; cat "$f"; } > "$REVIEW/$n.toml" + rm -f "$f"; rejected="$rejected $n(sin-bin)"; continue + fi + # ELF estático (los del lab son musl estáticos; uno dinámico es sospechoso pero no lo rechazo solo por eso). + isstatic=$(file "$bin" 2>/dev/null | grep -c 'statically linked') + # corre sin panic/segfault: probamos varios flags benignos bajo timeout. + ran_ok=0; out="" + for sub in version --version -v --help -h; do + out=$(timeout 10 "$bin" $sub 2>&1); rc=$? + # panic de Go / segfault ⇒ malo; rc 124 = timeout (se colgó, p.ej. abrió server) ⇒ probamos otro flag. + case "$out" in *panic:*|*"runtime error"*|*SIGSEGV*|*"signal SIGABRT"*) continue ;; esac + [ "$rc" = 124 ] && continue + ran_ok=1; break + done + if [ "$ran_ok" = 1 ]; then + bn=$(basename "$bin") + echo "$(STAMP) ✓ smoke $n (bin=$bn static=$isstatic): ${out%% +*}" + mv "$f" "recipes/$n.toml" + if "$HAMMER" --store "$STORE" pack "recipes/$n.toml" --repo "$REPO" --distro-version "$DISTRO" --build >/dev/null 2>&1; then + promoted="$promoted $n" + else + echo "$(STAMP) ✗ pack falló para $n ⇒ a review" + mv "recipes/$n.toml" "$REVIEW/$n.toml"; rejected="$rejected $n(pack)" + fi + else + { echo "# REVIEW $(STAMP): smoke-test falló (panic/segfault/cuelga). bin=$(basename "$bin") última salida:"; echo "$out" | sed 's/^/# /' | head -5; cat "$f"; } > "$REVIEW/$n.toml" + rm -f "$f"; rejected="$rejected $n(smoke)" + fi +done + +echo "$(STAMP) promovidos:$promoted" +echo "$(STAMP) a-review:$rejected" +echo "$(STAMP) aún-no-sellados (worker sigue):$skipped" + +# 5. firmar + commit + push sólo si hubo promociones o movimientos a review. +if [ -n "$promoted" ] || [ -n "$rejected" ]; then + [ -n "$promoted" ] && "$HAMMER" repo sign --repo "$REPO" --key "$KEY" >/dev/null 2>&1 && echo "$(STAMP) repo re-firmado" + # add EXPLÍCITO: jamás `git add -A` (otro agente comparte el working-tree). + git add "$QUEUE" "$REVIEW" 2>/dev/null + for n in $promoted; do git add "recipes/$n.toml" 2>/dev/null; done + if ! git diff --cached --quiet; then + git commit -q -m "Etapa G: cosecha-go finde (auto) — promovidos:$promoted | review:$rejected" + # push con reintento ante el push concurrente del otro agente. + for try in 1 2 3; do + git pull --rebase --autostash origin main >/dev/null 2>&1 + if git push origin main >/dev/null 2>&1; then echo "$(STAMP) ✓ pusheado (intento $try)"; break; fi + echo "$(STAMP) push rechazado, reintento $try"; sleep 5 + done + fi +fi +echo "$(STAMP) harvest-go: fin"