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:
Sergio
2026-08-26 19:25:28 +00:00
co-authored by Claude Opus 5
parent f5decc26b1
commit 8ab5040a8d
5 changed files with 257 additions and 6 deletions
+46 -2
View File
@@ -155,10 +155,54 @@ artefacto tiene el contenido real. Los bytes no pudieron venir de ningún otro s
la verdad: la verdad son las recetas en git. Si el mirror se pierde, se repuebla desde cualquier
máquina que tenga `work/tarballs`.
## Fuentes git: bundles shallow por commit
Los 606 repos por commit son el 52% de las fuentes y no los cubre el mirror de tarballs. Se espejan
como **bundles**, en un espacio de nombres propio: `hammer/fuentes-git/{commit}.bundle` (571 commits
distintos — hay commits compartidos entre colas, y se espeja uno solo).
**La identidad es el commit y la verificación la hace git.** Al desempaquetar, git comprueba cada
objeto contra su SHA: un bundle alterado no pasa. No hace falta índice ni un `sha256` aparte, igual
que en los tarballs el nombre del fichero ES su verificación.
### Shallow, no clones completos
El bundle se genera desde un `git fetch --depth 1` del commit exacto. Para `act` son **9,3 MB en vez
del repositorio entero**, y con 571 fuentes esa diferencia 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`, o sea materializar un árbol.
### El detalle que costó encontrar: el fichero `shallow`
Un bundle hecho desde un repo shallow **no lleva la frontera de historia**. Al desempaquetarlo:
```text
fatal: Failed to traverse parents of commit 4f41128…
error: … did not send all necessary objects
```
El mensaje dice «no envió todos los objetos necesarios» y **es engañoso**: los objetos del commit
llegan enteros — de hecho `git archive` ya funciona pese al error. Lo que falta no es un objeto, es
decirle a git dónde termina la historia. Se arregla escribiendo el propio commit en
`<destino>/shallow` **antes** del fetch, y no hay que transportar nada: la frontera de un clon
`--depth 1` es exactamente ese commit.
### Y un bug propio que vale documentar
`hidratar_desde_bundle` pasaba al `git fetch` la ruta **relativa** del bundle. Como `run_git` invoca
`git -C <destino> …`, git la resolvía **dentro de `<destino>`** y no encontraba nada. Como el fallo
del mirror se traga a propósito (para caer a upstream), el síntoma salía lejísimos del origen: el
build moría con *«commit … no existe en \<repo\> tras fetch»*, **culpando a upstream de un error de
ruta local**. Se arregla con `canonicalize`. Es el precio de que el mirror falle en silencio, y por
eso el silencio se paga con comentarios explícitos en el código.
## Lo que este ADR NO decide
- Qué hacer con la concentración en GitHub.
- Si el mirror debe replicarse fuera de Hetzner (hoy el repo, el respaldo y el mirror están todos en
la misma cuenta y el mismo proveedor).
- El mirror de fuentes **git**: hoy sólo se espeja el tarball. Los 606 repos por commit siguen
dependiendo del remoto. Es el siguiente eslabón, y es más grande.
- El **coste de mantener el mirror git al día**: un commit nuevo en una receta es un bundle nuevo, y
hoy eso lo dispara una persona corriendo `mirror-git-poblar.sh`, no el latido.
- Qué hacer con los repos cuyo servidor **no permite fetch por SHA suelto**
(`uploadpack.allowReachableSHA1InWant` desactivado): ahí el `--depth 1` del commit exacto falla y
hay que caer a un clon completo. El poblador los reporta como `✗ upstream no da el commit`.