← Todos los temas
Día 7 · Cómo se publica y se conecta

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

EtapaQué haceEjemploSi falla
Fuente (source)Se dispara por un evento del repositoriopush, pull_requestNo hay ejecución
Compilación (build)Prepara el código y las dependenciaspip install, npm ci, construir una imagenSe detiene
Verificación (test)Pruebas automáticas, linters, escaneo de seguridadpytest, ruff, análisis de dependenciasSe detiene; el cambio no se puede integrar
Artefacto (artifact)Produce el resultado versionado y reutilizableimagen de contenedor, paquete, .zipSe detiene
Despliegue (deploy)Publica el artefacto en un entornostaging, producciónSe 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érminoSignificado
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 a main y en cada pull request hacia main. Así el cambio se verifica antes de fusionarse.
  • permissions: contents: read: el token automático GITHUB_TOKEN queda con el mínimo privilegio (solo lectura). Es la práctica recomendada; se amplía solo en los jobs que lo necesiten.
  • concurrency con cancel-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 job pruebas se ejecuta dos veces, una por versión de Python. fail-fast: false deja que ambas terminen aunque una falle, para ver el panorama completo.
  • uses: actions/setup-python@v5 con cache: pip: instala Python y guarda en caché las dependencias descargadas.
  • needs: pruebas: el job desplegar no empieza hasta que todas las combinaciones de pruebas hayan pasado. Este es el «inspector»: sin éxito, no hay despliegue.
  • if: desplegar solo corre en un push a main, 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:

NivelQué verificaVelocidadCantidad recomendada
UnitariasUna función o clase aisladaMilisegundosMuchas
De integraciónVarios componentes juntos (API + base de datos)SegundosAlgunas
De extremo a extremo (end to end)El sistema completo desde la perspectiva del usuarioMinutosPocas, 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

EstrategiaCómo funcionaVentajaCosto
Reemplazo directo (recreate)Se apaga la versión vieja y se arranca la nuevaSimpleHay interrupción
Continuo (rolling)Se reemplazan las instancias de a pocasSin interrupciónConviven dos versiones un rato
Azul/verde (blue/green)Dos entornos completos; el tráfico se conmuta de uno a otroReversión inmediataDoble infraestructura
Canario (canary)La nueva versión recibe un pequeño porcentaje del tráfico y se amplía si las métricas son sanasLimita el daño de un errorRequiere métricas y enrutado fino
Banderas de funcionalidad (feature flags)El código se despliega apagado y se enciende por gruposSepara «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 echo mal puesto puede filtrarlos de otras formas.
  • Mínimo privilegio: declara permissions explí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ónRecomendación
Proyecto personal con pruebasCI mínima: instalar y probar en cada push
Más de una persona toca el códigoCI obligatoria como condición para fusionar (branch protection)
Usuarios reales que dependen del servicioEntrega continua con entorno de staging y aprobación
Equipo maduro, pruebas sólidas, buen monitoreoDespliegue continuo con canario o banderas
Sin pruebas automáticasPrimero 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

En el reel lo explicamos así

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 Integrationintegración continua (probar cada cambio)
CD · Continuous Deploymentdespliegue continuo (publicar solo)
Pipelinecadena de pasos
Deploydespliegue (publicar)