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]
45 34. Necesitamos que nadie rompa main
Si depende de la memoria, fallará
“¿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
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 esnpm 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
- ¿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?
- Ordena el CD: ¿por qué el deploy va después del build de imagen y no en paralelo?
- 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.
- 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.
- 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: verifyes la cola ordenada. - 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.