CI/CD: publicar sin romper nada
Qué es CI/CD y cómo las empresas prueban y publican cambios de forma automática.
Definición
CI/CD agrupa dos prácticas que automatizan el camino del código desde el editor hasta los usuarios:
- Integración continua (Continuous Integration, CI): cada cambio que un desarrollador sube al repositorio se integra con el código de los demás y se verifica automáticamente (compilación, pruebas, análisis estático). El objetivo es detectar errores de integración en minutos y no semanas después.
- Entrega continua (Continuous Delivery, CD): todo cambio que pasa la verificación queda en un estado desplegable; publicarlo es una decisión humana que se ejecuta con un clic.
- Despliegue continuo (Continuous Deployment, también CD): variante en la que el cambio que pasa todas las verificaciones se publica en producción sin intervención humana.
Las dos «CD» se confunden con frecuencia. La diferencia es una sola pregunta: ¿hay una aprobación manual antes de producción (entrega continua) o no (despliegue continuo)?
Un pipeline (cadena o tubería) es la secuencia automatizada de pasos que implementa estas prácticas. Cada ejecución del pipeline parte de un evento (un push, un pull request), corre en una máquina limpia y termina en uno de dos estados: éxito o fallo. Un fallo en cualquier paso detiene la cadena: ese es su valor central.
Anatomía de un pipeline
| Etapa | Qué hace | Ejemplo | Si falla |
|---|---|---|---|
| Fuente (source) | Se dispara por un evento del repositorio | push, pull_request | No hay ejecución |
| Compilación (build) | Prepara el código y las dependencias | pip install, npm ci, construir una imagen | Se detiene |
| Verificación (test) | Pruebas automáticas, linters, escaneo de seguridad | pytest, ruff, análisis de dependencias | Se detiene; el cambio no se puede integrar |
| Artefacto (artifact) | Produce el resultado versionado y reutilizable | imagen de contenedor, paquete, .zip | Se detiene |
| Despliegue (deploy) | Publica el artefacto en un entorno | staging, producción | Se detiene; se evalúa reversión (rollback) |
Dos principios sostienen todo el diseño: construir una vez, desplegar muchas (el mismo artefacto que pasó las pruebas es el que llega a producción, no uno reconstruido) y fallar rápido (las verificaciones baratas van primero).
Conceptos de GitHub Actions
GitHub Actions es el servicio de automatización integrado en GitHub. Se configura con archivos YAML en .github/workflows/. Su vocabulario:
| Término | Significado |
|---|---|
| Workflow (flujo de trabajo) | Un archivo YAML; el proceso automatizado completo |
| Event (evento) | Lo que lo dispara: push, pull_request, schedule (cron), workflow_dispatch (manual) |
| Job (trabajo) | Conjunto de pasos que corre en un mismo runner; los jobs corren en paralelo salvo que declaren needs |
| Step (paso) | Un comando (run) o una acción reutilizable (uses) |
| Action (acción) | Componente reutilizable, p. ej. actions/checkout; se fija a una versión (@v4) |
| Runner (ejecutor) | La máquina virtual que ejecuta el job; ubuntu-latest es un runner alojado por GitHub |
| Matrix (matriz) | Repite un job con combinaciones de valores (versiones de Python, sistemas operativos) |
| Secret (secreto) | Valor cifrado (credenciales, tokens) disponible como ${{ secrets.NOMBRE }}; nunca se escribe en el YAML |
| Environment (entorno) | Destino de despliegue con reglas propias: aprobadores, secretos por entorno |
| Cache (caché) | Conserva dependencias entre ejecuciones para acelerar el pipeline |
Un workflow completo, línea por línea
El archivo .github/workflows/ci.yml del proyecto de la práctica:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
pruebas:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.12"]
steps:
- name: Descargar el código
uses: actions/checkout@v4
- name: Instalar Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Instalar dependencias
run: python -m pip install -r requirements.txt
- name: Ejecutar pruebas
run: python -m pytest -v
desplegar:
needs: pruebas
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: produccion
steps:
- name: Descargar el código
uses: actions/checkout@v4
- name: Publicar
run: echo "Aquí iría el comando de despliegue (por ejemplo, subir a un hosting)"
Qué hace cada bloque:
on: el workflow corre al hacer push amainy en cada pull request haciamain. Así el cambio se verifica antes de fusionarse.permissions: contents: read: el token automáticoGITHUB_TOKENqueda con el mínimo privilegio (solo lectura). Es la práctica recomendada; se amplía solo en los jobs que lo necesiten.concurrencyconcancel-in-progress: si llega un cambio nuevo a la misma rama, se cancela la ejecución anterior en lugar de acumular ejecuciones obsoletas.strategy.matrix: el jobpruebasse ejecuta dos veces, una por versión de Python.fail-fast: falsedeja que ambas terminen aunque una falle, para ver el panorama completo.uses: actions/setup-python@v5concache: pip: instala Python y guarda en caché las dependencias descargadas.needs: pruebas: el jobdesplegarno empieza hasta que todas las combinaciones depruebashayan pasado. Este es el «inspector»: sin éxito, no hay despliegue.if:desplegarsolo corre en un push amain, nunca en un pull request.environment: produccion: asocia el job a un entorno de GitHub. Si el entorno exige aprobadores, el job espera su autorización: así se obtiene entrega continua (aprobación manual). Sin esa regla, es despliegue continuo.
Pruebas: lo que el pipeline necesita para decidir
Un pipeline solo es tan fiable como sus pruebas. La pirámide de pruebas orienta la proporción:
| Nivel | Qué verifica | Velocidad | Cantidad recomendada |
|---|---|---|---|
| Unitarias | Una función o clase aislada | Milisegundos | Muchas |
| De integración | Varios componentes juntos (API + base de datos) | Segundos | Algunas |
| De extremo a extremo (end to end) | El sistema completo desde la perspectiva del usuario | Minutos | Pocas, solo flujos críticos |
Las pruebas inestables (flaky tests, que a veces pasan y a veces fallan sin cambios en el código) son el enemigo principal: enseñan al equipo a ignorar los fallos. Se corrigen o se aíslan; no se reintentan indefinidamente.
Estrategias de despliegue
| Estrategia | Cómo funciona | Ventaja | Costo |
|---|---|---|---|
| Reemplazo directo (recreate) | Se apaga la versión vieja y se arranca la nueva | Simple | Hay interrupción |
| Continuo (rolling) | Se reemplazan las instancias de a pocas | Sin interrupción | Conviven dos versiones un rato |
| Azul/verde (blue/green) | Dos entornos completos; el tráfico se conmuta de uno a otro | Reversión inmediata | Doble infraestructura |
| Canario (canary) | La nueva versión recibe un pequeño porcentaje del tráfico y se amplía si las métricas son sanas | Limita el daño de un error | Requiere métricas y enrutado fino |
| Banderas de funcionalidad (feature flags) | El código se despliega apagado y se enciende por grupos | Separa «desplegar» de «liberar» | Deuda técnica si no se limpian |
Toda estrategia necesita una reversión (rollback) ensayada: volver a la versión anterior debe ser tan automático como avanzar.
Seguridad del pipeline
El pipeline tiene acceso a producción, así que es un objetivo valioso.
- Secretos: guárdalos en Settings → Secrets, nunca en el repositorio. GitHub los enmascara en los registros, pero un comando como
echomal puesto puede filtrarlos de otras formas. - Mínimo privilegio: declara
permissionsexplícitos; para desplegar en la nube, prefiere OIDC (OpenID Connect, credenciales temporales emitidas por el proveedor) en lugar de llaves de larga duración. - Acciones de terceros: son código ajeno que corre con tus permisos. Fíjalas a una versión; las de mayor riesgo, al hash completo del commit, que no puede reescribirse como sí puede un tag.
- Pull requests de forks: no reciben secretos por defecto. No lo cambies sin entender el riesgo.
- Entornos protegidos: aprobadores obligatorios y ramas permitidas para producción.
Criterios de adopción
| Situación | Recomendación |
|---|---|
| Proyecto personal con pruebas | CI mínima: instalar y probar en cada push |
| Más de una persona toca el código | CI obligatoria como condición para fusionar (branch protection) |
| Usuarios reales que dependen del servicio | Entrega continua con entorno de staging y aprobación |
| Equipo maduro, pruebas sólidas, buen monitoreo | Despliegue continuo con canario o banderas |
| Sin pruebas automáticas | Primero escribe pruebas: automatizar la ausencia de verificación solo publica errores más rápido |
Práctica: CI de un proyecto Python con pytest
Se usa un proyecto mínimo que calcula el importe de una estimación de obra. Requisitos: Python 3.10 o superior y Git. No hace falta una cuenta de GitHub para la parte local.
1. Estructura.
tmp-ci-demo/
├── .github/workflows/ci.yml
├── estimaciones.py
├── pytest.ini
├── requirements.txt
└── tests/test_estimaciones.py
2. El código (estimaciones.py):
def importe(cantidad, precio_unitario):
"""Importe = cantidad x precio unitario, redondeado a centavos."""
if cantidad < 0 or precio_unitario < 0:
raise ValueError("cantidad y precio deben ser no negativos")
return round(cantidad * precio_unitario, 2)
def total_con_retencion(importes, retencion=0.05):
"""Devuelve (subtotal, retención, total) a partir de una lista de importes."""
if not 0 <= retencion < 1:
raise ValueError("la retención debe estar entre 0 y 1")
subtotal = round(sum(importes), 2)
ret = round(subtotal * retencion, 2)
return subtotal, ret, round(subtotal - ret, 2)
3. Las pruebas (tests/test_estimaciones.py):
import pytest
from estimaciones import importe, total_con_retencion
def test_importe_basico():
assert importe(10, 25.5) == 255.0
def test_importe_rechaza_negativos():
with pytest.raises(ValueError):
importe(-1, 10)
def test_total_con_retencion():
assert total_con_retencion([100.0, 50.0]) == (150.0, 7.5, 142.5)
def test_retencion_invalida():
with pytest.raises(ValueError):
total_con_retencion([100.0], retencion=1)
4. Configuración. requirements.txt contiene pytest>=8,<10. pytest.ini indica dónde buscar el código, para que python -m pytest funcione igual en tu equipo y en el runner:
[pytest]
pythonpath = .
testpaths = tests
5. Ejecuta la verificación localmente, exactamente el comando que correrá el pipeline:
python -m pytest -v
Resultado esperado: 4 passed. En la verificación de este artículo se ejecutó con Python 3.12 y las cuatro pruebas pasaron.
6. Comprueba que el inspector detiene la banda. Cambia cantidad * precio_unitario por cantidad + precio_unitario y vuelve a ejecutar: test_importe_basico falla (assert 35.5 == 255.0) y el comando termina con código de salida distinto de cero. Ese código es lo que GitHub Actions interpreta como «paso fallido» y lo que impide que el job desplegar arranque. Revierte el cambio.
7. Añade el workflow. Crea .github/workflows/ci.yml con el contenido de la sección anterior. La sintaxis YAML se validó con un analizador (parser): el archivo carga, define los jobs pruebas y desplegar, la matriz ["3.10", "3.12"] y la dependencia needs: pruebas.
8. Súbelo a un repositorio de práctica de GitHub (uno tuyo, no el de un proyecto real):
git init
git add .
git commit -m "ci: primera version"
git branch -M main
git remote add origin https://github.com/TU-USUARIO/ci-practica.git
git push -u origin main
9. Observa la ejecución en la pestaña Actions del repositorio: verás dos ejecuciones de pruebas (Python 3.10 y 3.12) y, al terminar bien, el job desplegar.
10. Protege main. En Settings → Branches crea una regla que exija que el estado pruebas pase antes de fusionar un pull request. Con ella, el flujo de trabajo es: rama → pull request → CI en verde → fusión → despliegue.
Alcance de la verificación de este artículo. Se ejecutaron las pruebas con pytest en local y se validó que el YAML es sintácticamente correcto. El workflow no se ejecutó en los servidores de GitHub (no se subió a ningún repositorio); su comportamiento en Actions se describe según la documentación oficial.
Preguntas frecuentes
¿Qué es CI/CD?
Integración continua (CI) y entrega o despliegue continuo (CD): cada cambio en el código se prueba automáticamente y, si pasa las pruebas, se prepara o se publica sin pasos manuales.
¿Qué es un pipeline?
Es la secuencia automática de pasos que recorre cada cambio: instalar dependencias, revisar el estilo, ejecutar pruebas, construir y desplegar. Si un paso falla, el proceso se detiene y el cambio no llega a producción.
¿Qué es GitHub Actions?
Es el servicio de automatización de GitHub. Los pipelines se describen como workflows en archivos YAML dentro de .github/workflows/ y se ejecutan al hacer push, abrir un pull request o en un horario.
¿CI/CD sirve si trabajo solo?
Sí. Las pruebas automáticas detectan errores antes de publicar y el despliegue deja de depender de recordar cada paso a mano.
En el reel lo explicamos así
Imagina una banda de fábrica con un inspector en cada estación. Cada pieza (cada cambio de código) pasa por las estaciones en orden: una revisa las medidas, otra prueba que funcione, otra la empaca. Si una pieza sale mala, la banda se detiene antes de llegar al cliente. Eso es CI/CD: una cadena automática que prueba cada cambio y, solo si todo sale bien, lo publica.
Referencias
- Documentación de GitHub Actions: punto de entrada oficial.
- Sintaxis de workflows:
on,jobs,needs,strategy,concurrency,permissions. - Compilar y probar con Python:
setup-python, caché y matrices. - Gestión de entornos para despliegue: aprobadores y secretos por entorno.
- Endurecimiento de seguridad para Actions: secretos, permisos y acciones de terceros.
- Documentación de pytest.
- Nota de verificación: las URLs de GitHub Docs respondieron correctamente (HTTP 200) al comprobarse con una petición directa el 9-oct-2026; GitHub reorganiza su documentación con frecuencia, por lo que conviene revisar los enlaces antes de publicar.
Una banda de fábrica con un inspector en cada estación: si una pieza sale mala, la banda se detiene antes de llegar al cliente.
Glosario: del inglés al español
| CI · Continuous Integration | integración continua (probar cada cambio) |
|---|---|
| CD · Continuous Deployment | despliegue continuo (publicar solo) |
| Pipeline | cadena de pasos |
| Deploy | despliegue (publicar) |