# Siete deploys en verde que nunca entraron

- Fecha: 13 de julio de 2026
- Proyecto: restoman
- Tags: DevOps, Postgres, CI

> La API de Restoman sirvió durante un día el build de la noche anterior mientras siete deploys daban éxito. Una migración trabada y un comando que salía con 0 ante un fallo se ocultaban entre sí.

Durante unas veinticuatro horas, nada de lo que mergeamos llegó a producción.
Corrieron siete deploys, los siete quedaron en verde y la API siguió sirviendo
el build de la noche anterior. Nadie se dio cuenta porque nada estaba caído: el
contenedor viejo seguía respondiendo. Lo encontramos de casualidad mientras
aplicábamos migraciones en producción.

## Dos bugs, cada uno tapando al otro

### El contenedor nuevo no podía arrancar

El contenedor corre `./migrate && exec ./server`. El migrador de Drizzle decide
qué está pendiente **por nombre**, y saltea las filas que no tienen nombre.

Una migración se había aplicado por fuera con `drizzle-kit`, que la registró sin
nombre. En cada arranque, el migrador la creía pendiente y volvía a correr su
`ALTER TABLE ... ADD COLUMN`. Postgres contestaba que la columna ya existía,
`migrate` salía con 1, el `&&` cortaba ahí y el servidor nunca llegaba a
escuchar. El health check fallaba, así que la herramienta de deploy conservaba
el contenedor anterior — exactamente lo que tenía que hacer.

### El rollout fallido pasaba el CI

`haloy deploy` sale con **0** aunque un rollout falle el health check. El paso
de GitHub Actions era un `run: haloy deploy` pelado, así que el job quedaba en
verde. Siete veces.

El primer bug frenaba cada versión nueva. El segundo reportaba que había
entrado. Juntos dejaron a producción lo bastante sana como para ocultar que
llevaba un día de atraso.

## ¿Por qué no borrar la fila?

Borrar la fila sin nombre, o reescribir la migración con `IF NOT EXISTS`,
también habría dejado arrancar al contenedor. Las dos opciones contestan la
única pregunta que importa —_¿esta migración se aplicó?_— con una suposición
sobre la base de producción.

En cambio, reparamos el estado:

- Antes de migrar, el script ahora relaciona las filas sin nombre con nuestras
  migraciones **por hash** y les devuelve el nombre. Un hash que coincide es
  evidencia de que la migración ya corrió; registrar ese dato evita volver a
  ejecutar el DDL o modificar datos de la aplicación.
- El workflow de deploy lee la salida de haloy y **falla** cuando el rollout
  falla o nunca reporta un health check sano.

Verificamos el arreglo reproduciendo el estado exacto en desarrollo — una fila
sin nombre para una migración que ya había corrido — y ejecutando el mismo
binario que corre el contenedor. Salió con 0, reparó la fila y no ejecutó ningún
DDL dos veces.

## Lo que me llevo

- **El estado de las migraciones se verifica por hash, nunca por nombre.** El
  nombre es una etiqueta; el hash es la evidencia.
- **Un deploy que no puede hacer fallar el CI no es un deploy verificado.** Si
  la herramienta sale con 0 ante un rollout fallido, hay que leer su salida y
  hacer fallar el job.
- **Un health check en verde no prueba que esté corriendo el código nuevo.** La
  confirmación barata es pedirle a la API algo que solo la versión nueva sepa
  contestar.

---

Página: https://eitanf.com/es/notes/seven-green-deploys · English: https://eitanf.com/notes/seven-green-deploys.md · llms.txt: https://eitanf.com/llms.txt
