ADR 0013: mirror de fuentes git — bundles shallow por commit
Los 606 repos por commit son el 52% de las fuentes y el mirror de tarballs no los cubría. Se espejan
como `hammer/fuentes-git/{commit}.bundle` — 571 commits distintos, porque hay commits compartidos
entre colas y se espeja uno solo.
La identidad es el commit y la verificación la hace git: al desempaquetar comprueba cada objeto
contra su SHA, así que un bundle alterado no pasa. No hace falta índice ni sha256 aparte.
SHALLOW, NO CLONES COMPLETOS. El bundle sale de un `fetch --depth 1` del commit exacto: para `act`
son 9,3 MB en vez del repo entero, y con 571 fuentes eso decide si el mirror cabe. Es legítimo
porque hammer NUNCA usa la historia — lo único que hace con un repo es `git archive <commit> |
tar -x`, materializar un árbol.
EL DETALLE QUE COSTÓ ENCONTRAR. Un bundle hecho desde un repo shallow no lleva la frontera de
historia, y al desempaquetarlo git aborta con «Failed to traverse parents … did not send all
necessary objects». El mensaje dice que faltan objetos y es ENGAÑOSO: llegan enteros —`git archive`
ya funciona pese al error—; lo que falta es decirle a git dónde termina la historia. Se escribe el
propio commit en `<destino>/shallow` antes del fetch. No hay nada que transportar: la frontera de un
`--depth 1` es exactamente ese commit.
Y UN BUG PROPIO QUE VALE DOCUMENTAR: se pasaba al `git fetch` la ruta RELATIVA del bundle, y como
`run_git` invoca `git -C <destino>`, git la resolvía dentro de `<destino>`. Como el fallo del mirror
se traga a propósito para caer a upstream, el síntoma salía lejísimos: el build moría con «commit …
no existe en <repo> tras fetch», culpando a upstream de un error de ruta local. Ése es el precio de
que el mirror falle en silencio, y por eso el silencio se paga con comentarios explícitos.
Verificado igual que el de tarballs, con receta EFÍMERA para que no haya cache-hit: commit de `act`
ya espejado + un repo cuyo host no resuelve por DNS. Sin HAMMER_MIRROR_GIT falla en el clone; con
él, sella — y el artefacto trae el README.md real de act.
`cargo test -p hammer-build`: 5/5. Población de los 571 bundles corriendo aparte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
This commit is contained in:
@@ -12,4 +12,7 @@
|
||||
# ruta ABSOLUTA del servidor, así que `…:23/hammer/fuentes` busca en la raíz y devuelve
|
||||
# «(78) Could not open remote file for reading» aunque el fichero exista. `/~/` lo ancla al home.
|
||||
export HAMMER_MIRROR="${HAMMER_MIRROR:-sftp://u647150@u647150.your-storagebox.de:23/~/hammer/fuentes}"
|
||||
# Fuentes git: bundles shallow por commit. Variable APARTE de HAMMER_MIRROR porque son dos espacios
|
||||
# de nombres distintos —`{sha256}.tar` vs `{commit}.bundle`— y se pueblan y se pierden por separado.
|
||||
export HAMMER_MIRROR_GIT="${HAMMER_MIRROR_GIT:-sftp://u647150@u647150.your-storagebox.de:23/~/hammer/fuentes-git}"
|
||||
export HAMMER_MIRROR_KEY="${HAMMER_MIRROR_KEY:-$HOME/.ssh/github5}"
|
||||
|
||||
Executable
+99
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Genera y sube bundles shallow de las fuentes git del corpus. Ver ADR 0013 y el .sh hermano.
|
||||
|
||||
Es INCREMENTAL: consulta primero qué commits ya están en el mirror y sólo trabaja los que faltan.
|
||||
Interrumpible sin penalización — cada bundle es independiente.
|
||||
"""
|
||||
import os, re, subprocess, sys, glob, tempfile, shutil
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
os.chdir(ROOT)
|
||||
SB_USER = os.environ.get("SB_USER", "u647150")
|
||||
SB_HOST = os.environ.get("SB_HOST", "u647150.your-storagebox.de")
|
||||
SB_PORT = os.environ.get("SB_PORT", "23")
|
||||
KEY = os.environ.get("KEY", os.path.expanduser("~/.ssh/github5"))
|
||||
LIMITE = int(os.environ.get("LIMITE", "0"))
|
||||
SOLO = os.environ.get("SOLO", "")
|
||||
DEST = "hammer/fuentes-git"
|
||||
SSH = ["ssh", "-4", "-p", SB_PORT, "-i", KEY, "-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "ConnectTimeout=30"]
|
||||
|
||||
def fuentes_git():
|
||||
vistos, out = set(), []
|
||||
for f in sorted(glob.glob("recipes/**/*.toml", recursive=True)):
|
||||
s = open(f, errors="ignore").read()
|
||||
repo = re.search(r'^\s*repo\s*=\s*"([^"]+)"', s, re.M)
|
||||
com = re.search(r'^\s*commit\s*=\s*"([0-9a-fA-F]{7,40})"', s, re.M)
|
||||
if not (repo and com):
|
||||
continue
|
||||
nombre = (re.search(r'^\s*name\s*=\s*"([^"]+)"', s, re.M) or [None, os.path.basename(f)[:-5]])[1]
|
||||
c = com.group(1).lower()
|
||||
if c in vistos: # el mismo commit desde dos colas = un solo bundle
|
||||
continue
|
||||
vistos.add(c)
|
||||
out.append({"receta": nombre, "repo": repo.group(1), "commit": c})
|
||||
return out
|
||||
|
||||
def ya_en_mirror():
|
||||
r = subprocess.run(SSH + [f"{SB_USER}@{SB_HOST}", f"ls {DEST}"],
|
||||
capture_output=True, text=True, timeout=180)
|
||||
return {l.strip()[:-7] for l in r.stdout.splitlines() if l.strip().endswith(".bundle")}
|
||||
|
||||
def bundle(e, tmpdir):
|
||||
"""Clona --depth 1 el commit exacto y emite un bundle. None si upstream no lo da."""
|
||||
d = os.path.join(tmpdir, "r")
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
os.makedirs(d)
|
||||
def git(*a, **kw):
|
||||
return subprocess.run(["git", "-C", d] + list(a), capture_output=True, text=True,
|
||||
timeout=kw.get("t", 900))
|
||||
if git("init", "-q").returncode != 0: return None
|
||||
if git("remote", "add", "origin", e["repo"]).returncode != 0: return None
|
||||
# --depth 1 del SHA exacto: el servidor debe permitir fetch por sha (uploadpack.allowReachableSHA1).
|
||||
if git("fetch", "-q", "--depth", "1", "origin", e["commit"], t=1800).returncode != 0:
|
||||
return None
|
||||
if git("update-ref", "refs/heads/hammer", e["commit"]).returncode != 0: return None
|
||||
out = os.path.join(tmpdir, f'{e["commit"]}.bundle')
|
||||
if git("bundle", "create", out, "refs/heads/hammer", t=1800).returncode != 0: return None
|
||||
return out if os.path.exists(out) else None
|
||||
|
||||
def main():
|
||||
fs = fuentes_git()
|
||||
if SOLO: fs = [e for e in fs if e["receta"] == SOLO]
|
||||
print(f"== fuentes git en el corpus: {len(fs)} commits distintos", flush=True)
|
||||
try:
|
||||
hay = ya_en_mirror()
|
||||
except Exception as ex:
|
||||
print(f"!! no pude listar el mirror ({ex}); asumo vacío", flush=True); hay = set()
|
||||
print(f"== ya en el mirror: {len(hay)}", flush=True)
|
||||
faltan = [e for e in fs if e["commit"] not in hay]
|
||||
if LIMITE: faltan = faltan[:LIMITE]
|
||||
print(f"== a espejar ahora: {len(faltan)}", flush=True)
|
||||
|
||||
subprocess.run(SSH + [f"{SB_USER}@{SB_HOST}", f"mkdir hammer {DEST}"],
|
||||
capture_output=True, text=True)
|
||||
ok = fallo = 0
|
||||
tmpdir = tempfile.mkdtemp(prefix="hammer-git-mirror-")
|
||||
try:
|
||||
for i, e in enumerate(faltan, 1):
|
||||
b = bundle(e, tmpdir)
|
||||
if not b:
|
||||
fallo += 1
|
||||
print(f" [{i}/{len(faltan)}] {e['receta']:<24} ✗ upstream no da el commit", flush=True)
|
||||
continue
|
||||
mb = os.path.getsize(b) / 1e6
|
||||
r = subprocess.run(["rsync", "-a", "-e", " ".join(SSH), b,
|
||||
f"{SB_USER}@{SB_HOST}:{DEST}/"], capture_output=True, text=True, timeout=3600)
|
||||
if r.returncode == 0:
|
||||
ok += 1
|
||||
print(f" [{i}/{len(faltan)}] {e['receta']:<24} ✓ {mb:6.1f} MB", flush=True)
|
||||
else:
|
||||
fallo += 1
|
||||
print(f" [{i}/{len(faltan)}] {e['receta']:<24} ✗ subida: {r.stderr.strip()[:60]}", flush=True)
|
||||
os.remove(b)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
print(f"== espejados {ok} fallidos {fallo}")
|
||||
return 0
|
||||
|
||||
sys.exit(main())
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# mirror-git-poblar.sh — espeja las fuentes GIT del corpus como bundles (ADR 0013).
|
||||
#
|
||||
# ── POR QUÉ BUNDLES SHALLOW Y NO CLONES COMPLETOS ──────────────────────────────────────────────
|
||||
# Hammer no usa la historia: lo único que hace con un repo es `git archive <commit> | tar -x`, o sea
|
||||
# materializar UN árbol. Un clon `--depth 1` del commit exacto basta, y para `act` son 8,9 MB en
|
||||
# lugar del repositorio entero. Con 606 fuentes git, la diferencia decide si el mirror cabe.
|
||||
#
|
||||
# El objeto se nombra por CONTENIDO igual que los tarballs: `hammer/fuentes-git/{commit}.bundle`.
|
||||
# El commit ES su verificación — git comprueba cada objeto contra su SHA al desempaquetar, así que
|
||||
# un bundle alterado no pasa. No hace falta índice ni sha256 aparte.
|
||||
#
|
||||
# ── EL FICHERO `shallow` NO SE SUBE ────────────────────────────────────────────────────────────
|
||||
# Un bundle hecho desde un repo shallow no lleva la frontera de historia, y al desempaquetarlo git
|
||||
# aborta con «Failed to traverse parents». Suena a bundle incompleto y NO lo es: los objetos llegan
|
||||
# enteros. La frontera de un clon `--depth 1` es exactamente el commit pedido, así que el lado que
|
||||
# consume la reconstruye sola (lo hace `hidratar_desde_bundle` en fetch.rs). Nada que transportar.
|
||||
#
|
||||
# Uso: scripts/fuentes/mirror-git-poblar.sh # todas las que falten
|
||||
# LIMITE=20 scripts/fuentes/mirror-git-poblar.sh # sólo las 20 primeras
|
||||
# SOLO=mesa scripts/fuentes/mirror-git-poblar.sh # una receta concreta
|
||||
set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
|
||||
exec python3 scripts/fuentes/mirror-git-poblar.py "$@"
|
||||
Reference in New Issue
Block a user