servidor: el aviso periódico de novedades del repo, que sólo grita cuando cambia

Corre `takana outdated --json` contra el índice firmado y deja el resultado en un fichero de
estado. No actualiza nada: instalar es reproducir y verificar, y eso no se cuelga de un cron — el
aviso dice qué comando correr.

Tres cosas deliberadas:
 · Sólo grita cuando la LISTA de novedades difiere de la del ciclo anterior. Un guardián que repite
   lo mismo cada 30 min se deja de leer, y entonces no avisa de nada.
 · La huella es nombre+versión publicada, no el JSON entero: éste trae contadores que se mueven
   solos y harían «cambiar» un estado idéntico.
 · Un repo caído NO se resume en «al día»: lo dice, sale ≠ 0 y conserva el último estado bueno.

Probado en los tres caminos contra un repo servido por HTTP en la caja.
This commit is contained in:
Sergio
2026-09-21 18:31:15 +00:00
parent 9fe1e010e2
commit 4f4fecf862
+84
View File
@@ -0,0 +1,84 @@
#!/bin/sh
# avisar-actualizaciones.sh — pregunta al repo si hay novedades para lo que esta máquina tiene
# instalado, y AVISA sólo cuando la respuesta cambia.
#
# ── QUÉ HACE Y QUÉ NO ───────────────────────────────────────────────────────────────────────────
# Corre `takana outdated --json` (que no construye, no baja `.swm` y no escribe nada: lee el índice
# firmado y la DB de instalados) y deja el resultado en un fichero de estado. **No actualiza nada**:
# instalar es reproducir y verificar, y eso no se cuelga de un cron. El aviso dice qué correr.
#
# ── POR QUÉ SÓLO AVISA CUANDO CAMBIA ────────────────────────────────────────────────────────────
# Un guardián que grita lo mismo cada 30 minutos se deja de leer, y entonces no avisa de nada — es
# la lección que ya está escrita en `vigia-subcomandos.py` ([[guardianes-el-ruido-los-mata]]).
# Así que el ciclo normal es una línea sola («al día», o «3 novedades, las mismas de antes») y el
# grito sale cuando la LISTA de novedades difiere de la del ciclo anterior.
#
# ── LA FIRMA NO ES OPCIONAL ACÁ ─────────────────────────────────────────────────────────────────
# Se pasa `--trust`: decidir «hay que actualizar» a partir de un catálogo que nadie firmó es la
# forma exacta de que te instalen lo que quieran. `outdated` aborta si el índice está manipulado
# (BAD-SIG) e informa si está sin firmar o firmado por una clave desconocida — ese informe sale en
# el log y en el estado, no se traga.
#
# Uso: scripts/servidor/avisar-actualizaciones.sh
# */30 * * * * cd /work/sergio/takana && scripts/servidor/avisar-actualizaciones.sh \
# >>work/avisar-actualizaciones.log 2>&1
#
# Env: REPO origen(es) del catálogo — dir local o lista HTTP(S) separada por comas
# DB DB de instalados (def /var/lib/hammer/installed.json)
# TRUST directorio de claves de confianza (def ./trust)
# STATE fichero de estado (def work/actualizaciones.json)
# TAKANA binario (def ./target/release/takana)
set -eu
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
REPO="${REPO:-https://repo.gioser.net}"
DB="${DB:-/var/lib/hammer/installed.json}"
TRUST="${TRUST:-$ROOT/trust}"
STATE="${STATE:-$ROOT/work/actualizaciones.json}"
TAKANA="${TAKANA:-$ROOT/target/release/takana}"
[ -x "$TAKANA" ] || { echo "!! falta $TAKANA (cargo build --release)" >&2; exit 2; }
mkdir -p "$(dirname "$STATE")"
NUEVO="$STATE.nuevo"
if ! "$TAKANA" outdated --repo "$REPO" --db "$DB" --trust "$TRUST" --json >"$NUEVO" 2>"$NUEVO.err"; then
# Un repo caído o un índice manipulado NO se resume en «al día»: se dice, y se deja el estado
# anterior intacto para no borrar la última respuesta buena.
echo "$(date -u +%FT%TZ) ✗ no pude preguntarle al repo $REPO:"
sed 's/^/ /' "$NUEVO.err"
rm -f "$NUEVO" "$NUEVO.err"
exit 1
fi
rm -f "$NUEVO.err"
# La huella es la LISTA de novedades (nombre+versión publicada), no el JSON entero: el JSON trae
# contadores que se mueven solos (p.ej. `no_instalados`) y harían «cambiar» un estado idéntico.
huella() { [ -f "$1" ] || { echo ""; return; }; python3 - "$1" <<'PY'
import json, sys
try:
d = json.load(open(sys.argv[1]))
except Exception:
print(""); raise SystemExit
print(",".join(sorted(f"{n['name']}={n['publicada']}" for n in d.get("novedades", []))))
PY
}
ANTES="$(huella "$STATE")"
AHORA="$(huella "$NUEVO")"
mv "$NUEVO" "$STATE"
N=$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1])).get("novedades",[])))' "$STATE")
SELLO="$(date -u +%FT%TZ)"
if [ "$N" = "0" ]; then
echo "$SELLO · al día contra $REPO"
exit 0
fi
if [ "$AHORA" = "$ANTES" ]; then
echo "$SELLO · $N novedad(es), las mismas del ciclo anterior (estado en $STATE)"
exit 0
fi
echo "$SELLO ⚠ HAY $N NOVEDAD(ES) en $REPO:"
"$TAKANA" outdated --repo "$REPO" --db "$DB" --trust "$TRUST" | sed 's/^/ /'