45  34. Necesitamos que nadie rompa main

Si depende de la memoria, fallará

NotaEn una frase

“¿Pasaste los tests antes del merge?” — en un equipo real nadie pregunta: el pipeline corre lint + typecheck + tests + build en cada PR, y si algo falla, el merge está bloqueado. CI no es burocracia: es la memoria del equipo convertida en máquina — lo que se olvida en la cabeza, no se olvida en el YAML.

45.1 El problema

El flujo manual: tú corres los tests (cuando te acuerdas), el compañero pushea sin correrlos “porque era un cambio chiquito”, main se rompe el viernes y nadie sabe desde qué commit. Y el deploy: ssh + git pull desde la laptop de una persona — si esa laptop se pierde, nadie despliega. Todo lo que depende de acordarse acaba fallando.

Un deploy es poner tu código a correr en el servidor: copiar la imagen, iniciar el proceso, verificar que responde. El objetivo es que sea aburrido — automático, repetible, con rollback.

Un commit es una foto guardada del código con un mensaje que dice por qué cambió. La historia del proyecto es una cadena de commits: puedes volver a cualquiera, ver qué cambió y quién lo hizo.

45.2 Cómo lo resuelve un equipo

Un equipo serio convierte las verificaciones en eventos automáticos: cada PR dispara el pipeline — instala deps, corre migraciones sobre un Postgres efímero, corre la suite completa, buildea — y GitHub marca el PR en verde o rojo. El gate de merge (“CI verde obligatorio”) convierte la regla de equipo en regla de máquina. Y CD continúa: merge → build de imagen → registry → deploy — ningún ssh desde laptops.

Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.

El build (construcción) es el proceso que convierte tu código fuente en algo ejecutable/entregable: compilar TypeScript a JS, empaquetar el frontend, armar la imagen Docker. Lo que se despliega es el resultado del build, no tu código crudo.

45.3 Conceptos nuevos

  • Ops CI con GitHub Actions: lint + typecheck + tests + build por stack
  • Ops La BD en CI: Postgres como servicio del pipeline — el esquema es reproducible
  • U Gates de merge: cobertura mínima, CI verde obligatorio — la conversación de equipo
  • Ops CD: build de imagen → registry → deploy — las piezas y su orden

45.4 La explicación visual

flowchart LR
  PR[Pull Request] --> CI{Pipeline CI}
  CI --> L[lint + typecheck]
  CI --> T[tests<br/>+ Postgres de servicio]
  CI --> B[build]
  L & T & B -->|todo verde| M[merge a main]
  L & T & B -->|algo rojo| X[merge bloqueado]
  M --> CD{Pipeline CD}
  CD --> IMG[build imagen :sha]
  IMG --> REG[registry]
  REG --> DEP[deploy a prod]

El pipeline es el mismo para los tres stacks — lo que cambia son los comandos de cada paso (npm run lint / ruff / go vet), no la forma.

45.5 Implementación

Capítulo agnóstico — el pipeline de GitHub Actions que sirve para los tres (los comandos por stack van comentados):

# .github/workflows/ci.yml
name: ci
on:
  pull_request:
  push: { branches: [main] }

jobs:
  verify:
    runs-on: ubuntu-latest
    services:
      postgres:                              # a REAL Postgres for the integration tests
        image: postgres:16
        env: { POSTGRES_PASSWORD: ci, POSTGRES_DB: taskflow_test }
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready" --health-interval 2s --health-retries 10
    env:
      DATABASE_URL: postgres://postgres:ci@localhost:5432/taskflow_test
      JWT_SECRET: ci-secret-key-for-tests-only-32chars
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4          # TS: setup-node · Py: setup-python · Go: setup-go
        with: { node-version: 20, cache: npm }

      - run: npm ci                          # Py: pip install -r requirements.txt
      - run: npm run lint                    # Py: ruff check · Go: go vet ./...
      - run: npm run typecheck               # Py: mypy · Go: (compiler does it)
      - run: npm run migrate                 # the schema is reproducible — that's the point
      - run: npm test                        # Py: pytest · Go: go test ./...
      - run: npm run build                   # proves it compiles, not just that tests pass

  image:
    needs: verify
    if: github.ref == 'refs/heads/main'      # only after merge to main
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t registry.example.com/taskflow:${{ github.sha }} .
      - run: docker push registry.example.com/taskflow:${{ github.sha }}
      # deploy step: platform-specific (ECS update / kubectl set image / Render hook)
Crea el workflow de GitHub Actions para mi API [Express+TS/FastAPI/Go]
+ Postgres. Requisitos: en cada PR y push a main correr lint, typecheck,
migraciones y tests contra un Postgres 16 de servicio con healthcheck
y DATABASE_URL+JWT_SECRET de test como env vars; un segundo job que solo
corre tras merge a main: build de la imagen Docker taggeada con el sha
del commit y push al registry; caché de dependencias; y los comandos
correctos para mi stack en cada paso.

45.6 ¿Por qué así y no “cada quien corre los tests en su máquina”?

Lo único que necesitas llevarte: CI convierte la regla en infraestructura — no hay que acordarse ni confiar en que cada uno corrió la suite: el PR lo dice, y main solo acepta código verificado. La laptop que despliega es un riesgo; el pipeline que despliega es un proceso.

Un proceso es un programa en ejecución: el sistema operativo le da memoria propia y un lugar en la fila del CPU. Tu API es un proceso; Postgres es otro; el navegador es varios. Que “se caiga el proceso” = el programa murió.

Alternativa Qué es Cuándo
GitHub Actions YAML en el repo, runners de GitHub El default si el repo está en GitHub
GitLab CI / Azure DevOps Lo mismo en otras plataformas Si el equipo ya vive ahí
Pre-commit hooks Verificación antes de pushear Complemento: atrapa lo rápido (lint) sin esperar CI
Jenkins self-hosted CI que tú operas Empresas con requisitos de infra propia — pagas el ops

45.7 Errores comunes

Error Por qué pasa Fix
CI que no bloquea el merge “Está para informar” Branch protection: status check requerido — el gate es la mitad del valor
Tests conectando a una BD compartida “Ya existe el staging” Postgres efímero del pipeline — reproducible, aislado, sin estado
Secretos de prod en el CI “Necesitaba el deploy” Secrets del repo + OIDC — nunca en el YAML ni en logs
Pipeline de 20 minutos Todo en un solo job Paralelizar lint/typecheck/test + caché de deps — CI lento es CI que se salta
Tests de CI distintos a los locales “En mi máquina pasan” Mismos comandos en ambos — el pipeline corre lo que tú corres

45.8 Buenas prácticas

  • El pipeline corre lo mismo que tú corres — si tu test local es npm test, el paso es npm test: una sola forma de verificar, no dos dialectos.
  • Las migraciones corren en CI sobre la BD efímera — el test “¿el esquema se construye de cero?” se responde en cada PR, gratis.

El esquema es el plano de la base de datos: qué tablas hay, qué columnas tiene cada una y de qué tipo. Es contrato: una fila que no cumple el esquema no entra. Se cambia con migraciones, no a mano.

  • CD separado del CI: verificar ≠ desplegar — el build de imagen solo tras merge a main, taggeado con el sha (rollback = tag anterior, cap. 35).
  • Secretos solo en el almacén de secretos del repo/plataforma — el YAML referencia, nunca contiene.

45.9 Ejercicio

  1. ¿Por qué el pipeline corre migraciones + tests y no solo tests? ¿Qué bug del mundo real detecta eso que “tests con BD ya poblada” no detecta?
  2. Ordena el CD: ¿por qué el deploy va después del build de imagen y no en paralelo?
  3. Tu compañero propone correr la suite contra staging “porque ya tiene datos reales”. ¿Dos problemas con eso?

Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.

45.10 Mini reto

Hacer fallar el build a propósito con un test roto y ver el gate funcionar.

Ejercicio.

  1. Las migraciones en CI verifican que el esquema se construye desde cero — detecta la migración que funciona sobre tu BD de dev “porque la columna ya existía” pero falla en una base vacía. El bug clásico: migración que asume estado que solo existe en tu máquina.
  2. El deploy consume el artefacto del build — deployar sin imagen es deployar nada, y deployar una imagen del build que aún corre es una carrera: puedes publicar algo que no compiló. needs: verify es la cola ordenada.
    1. Los tests escriben y borran — correrlos contra datos reales de staging contamina lo que otros ven/usan; (b) staging es compartido: dos PRs corriendo se pisan datos entre sí → falsos rojos que enseñan a ignorar CI. La BD efímera es aislada y gratis.

Mini reto. Cambia un expect a algo falso, abre un PR, y mira: CI rojo → merge bloqueado por el gate → el botón gris que dice “checks failing”. Revierte el test: verde. El experimento de 5 minutos que convierte “el gate existe” en algo que viste funcionar — y demuestra por qué ningún “era un cambio chiquito” pasa sin tests.

45.11 Vocabulario técnico del capítulo

Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.

Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.

Un log es el diario del programa: qué pasó, cuándo, con qué request. Estructurado = en JSON con campos (request_id, user_id), para filtrar por máquina y no con los ojos.

Una variable de entorno es configuración que vive fuera del código: DATABASE_URL, JWT_SECRET. El mismo binario corre en dev y prod con distintas vars — los secretos nunca se escriben en el código.

Un secreto es un dato que no puede publicarse: contraseñas de BD, llaves de API, el secreto que firma los JWT. Viven en .env (fuera de git) o en un secret manager — nunca en el código ni en el repo.

Un caché es una copia rápida de algo costoso de obtener: el resultado de una query pesada, la sesión. Redis es el caché estándar. La regla de oro: cachear es fácil, invalidar (saber cuándo el caché ya no vale) es lo difícil.

Docker empaqueta tu app con todo lo que necesita (runtime, librerías, config) en una imagen — el mismo paquete corre idéntico en tu laptop y en producción. Es la respuesta a “en mi máquina sí funcionaba”.

Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.

Un compilador traduce código a otra forma: TypeScript → JavaScript (transpila), Go → binario (compila). Atrapa errores antes de ejecutar. tsc, esbuild, go build son compiladores.

Un repositorio (repo) es la carpeta del proyecto con todo su historial Git adentro. GitHub/GitLab son servicios que hospedan repos para compartirlos y respaldarlos.

Un puerto es una puerta numerada dentro de una computadora: la IP llega al edificio, el puerto al departamento. :3000 = “la app que escucha en el departamento 3000”. Postgres suele usar 5432, HTTP el 80, HTTPS el 443.

localhost significa “esta misma máquina” — es la dirección que tu computadora usa para hablarse a sí misma. Cuando desarrollas, el “servidor” y el “cliente” viven en tu laptop: por eso todo es localhost:3000.

La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.

45.12 Lo que deberías saber hacer ahora