← Todo el material
Curso Intermedio 23 de agosto de 2026

Deploy de Agentes — Módulo 8: CI con GitHub Actions — automatizar los checks

Octavo módulo del Curso de Deploy de Agentes: montar integración continua con GitHub Actions. Qué es un workflow, job, step y runner; un ci.yml real que corre en cada pull request con lint, test, eval y build; cómo usar secrets, matrix y artifacts.

☰ Índice del contenido

Octavo módulo del Curso de Deploy de Agentes. En el Módulo 7 convertimos los evals del agente en tests que pasan o fallan. Pero un test solo vale si alguien lo corre — y “acordarse de correr los tests antes de mergear” es la promesa que más rápido se rompe en un equipo. La solución no es disciplina: es automatización. Que la máquina corra todo, sola, en cada cambio, y no deje avanzar lo que no pasa. Eso es CI.

La tesis del módulo

Un check que depende de que alguien se acuerde de correrlo no existe. CI mueve la responsabilidad de la persona a la máquina: cada push abre un robot que corre lint, tests y evals, y marca el resultado en verde o rojo antes de que nadie mergee. Lo que no se automatiza, no se cumple.

CI y CD: los dos términos base

CI — integración continua (continuous integration): la práctica de que cada cambio que alguien sube se integre y se verifique de inmediato, de forma automática. En vez de juntar dos semanas de trabajo y rezar en el merge final, integras seguido y en chico, y un robot valida cada paso.

CD — puede significar dos cosas, ambas continuous: entrega continua (continuous delivery, dejar cada cambio listo para desplegar con un botón) o despliegue continuo (continuous deployment, que llegue a producción solo, sin botón). Este módulo es CI puro; el CD lo veremos con Terraform y las nubes más adelante.

La sigla que verás siempre pegada, CI/CD, es la tubería completa: del commit a producción, con verificaciones automáticas en cada etapa.

GitHub Actions: el vocabulario

GitHub Actions es el sistema de automatización que viene dentro de GitHub. No hay que instalar un servidor aparte: describes qué quieres que pase en un archivo YAML dentro del repo, y GitHub lo ejecuta. Cuatro palabras que se usan todo el tiempo:

  • Workflow — el proceso completo, descrito en un archivo .yml dentro de .github/workflows/. Se dispara con un evento (un push, un pull request, un horario).
  • Job — una tarea grande dentro del workflow (por ejemplo “correr los tests”). Cada job corre en su propia máquina limpia. Por defecto los jobs corren en paralelo, salvo que declares dependencias con needs.
  • Step — cada paso dentro de un job, en orden. Un step ejecuta un comando (run:) o llama a una acción reutilizable (uses:).
  • Runner — la máquina virtual donde corre el job. GitHub te presta runners (ubuntu-latest, etc.); también puedes traer los tuyos (self-hosted).

Y action (en minúscula): un bloque reutilizable que alguien ya escribió y publicó, que invocas con uses:. Por ejemplo actions/checkout@v4 clona tu repo dentro del runner. El @v4 fija la versión — pinear la versión es obligatorio, nunca dejes una action sin versión.


  Repo (.github/workflows/ci.yml)
  │
  ▼
  Evento: pull_request
  │
  ▼
  ┌──────────── Workflow ────────────┐
  │  ┌── Job ──┐  contiene steps:    │
  │  │ Runner  │  1. checkout        │
  │  │ (VM     │  2. setup-python    │
  │  │  limpia)│  3. run: pytest ... │
  │  └─────────┘                     │
  └──────────────────────────────────┘

El workflow real: .github/workflows/ci.yml

Este es un ci.yml completo y válido para un proyecto de agente en Python. Corre en cada pull request contra main, y encadena cuatro jobs: lint → test → eval → build. Cada uno depende del anterior con needs, así no gastas minutos de runner corriendo evals si el lint ya falló.

name: CI

# Se dispara al abrir o actualizar un PR contra main.
on:
  pull_request:
    branches: [main]

# Cancela corridas viejas del mismo PR si subes un commit nuevo.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip            # cachea las deps entre corridas
      - run: pip install -r requirements-dev.txt
      - name: Ruff (formato y linters)
        run: ruff check .
      - name: Mypy (tipos)
        run: mypy src

  test:
    needs: lint                 # solo corre si lint pasó
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -r requirements-dev.txt
      - name: Tests unitarios con cobertura
        run: pytest tests/unit --cov=src --cov-report=xml
      - name: Subir reporte de cobertura
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage.xml

  eval:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip
      - run: pip install -r requirements-dev.txt
      - name: Evals del agente (Módulo 7)
        # La API key vive en los secrets del repo, nunca en el código.
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: pytest tests/evals -m eval

  build:
    needs: eval
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build de la imagen Docker (Módulo 5)
        # Solo construye para validar que el Dockerfile compila;
        # no publica la imagen (eso es CD, más adelante).
        run: docker build -t agente:ci .

Tres cosas que ese archivo introduce y conviene aislar:

Secrets. Nunca escribas una API key en el YAML. La guardas en Settings → Secrets and variables → Actions del repo, y la lees con la sintaxis ${{ secrets.NOMBRE }}. GitHub la inyecta como variable de entorno y la censura (***) en los logs. Es el mismo principio de secretos del Módulo 4.

Matrix. Si necesitas probar el mismo job en varias versiones de Python a la vez, no copias el job cuatro veces: declaras una matrix y GitHub lanza una corrida por combinación, en paralelo.

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pytest

Artifacts. Los archivos que un job produce (un reporte de cobertura, un binario, logs) se guardan con actions/upload-artifact@v4 y quedan descargables desde la corrida. Los usaremos en el Módulo 9 para que el quality gate lea la cobertura.

El flujo completo


 PUSH / PR
   │
   ▼
 ┌─────────┐
 │  GitHub │ ──► asigna un runner limpio
 └─────────┘
   │
   ▼
 ┌────────┐     ┌────────┐     ┌────────┐
 │  lint  │ ──► │  test  │ ──► │  eval  │
 └────────┘     └────────┘     └────────┘
                                    │
                                    ▼
                                ┌────────┐
                                │  build │
                                └────────┘
                                    │
                                    ▼
                       ┌──────────────────────┐
                       │ todos verdes ──► OK  │
                       │ un rojo ──► BLOQUEA  │
                       └──────────────────────┘

Si el último check queda verde, el PR muestra el tic verde y se puede mergear. Si uno falla, aparece el aspa roja con el log del error — y en el Módulo 9 haremos que ese aspa bloquee el merge, no solo lo señale.

Los cuatro jobs y qué valida cada uno

JobQué correQué protegeSi falla, significa
lintruff + mypyEstilo y tipos consistentesEl código no sigue las convenciones o tiene errores de tipo
testpytest tests/unit con coberturaLa lógica no se rompióUn test unitario cambió de resultado
evalpytest tests/evals (M7)El agente sigue respondiendo bienEl comportamiento del agente se degradó
builddocker build (M5)La imagen compilaEl Dockerfile o las deps quedaron rotas

Cuidado con los minutos y los secretos en PRs de forks. Los runners de GitHub se cobran por minuto (gratis hasta un tope en repos privados). Encadenar con needs y usar concurrency para cancelar corridas viejas te ahorra plata. Y ojo: por seguridad, los secrets no se exponen en workflows disparados desde un fork externo, así que un PR de alguien de afuera no podrá correr el job de eval con tu API key — es a propósito, para que nadie te robe la credencial abriendo un PR.

Cuidado con el orden de los jobs. Poner el job más lento (eval, que llama a un LLM y cuesta plata) al final de la cadena needs no es estético: es económico. Un typo que rompe el lint no debería gastarte una corrida completa de evals.

Errores comunes

  • No pinear la versión de las actions. Usar actions/checkout sin @v4 te deja a merced de cambios que rompen tu CI sin que tú tocaras nada. Siempre fija la versión.
  • Meter secretos en el YAML. El archivo ci.yml está en el repo, a la vista de todos. Cualquier credencial va en Secrets, se lee con ${{ secrets.X }} y nunca se imprime.
  • Correr todo en un solo job gigante. Sin needs, pierdes el paralelismo y la señal: cuando falla, no sabes si fue lint o eval. Jobs separados te dan un diagnóstico limpio.
  • Olvidar el cache de dependencias. Sin cache: pip, cada corrida reinstala todo desde cero: más lento y más minutos gastados. El cache: pip de setup-python lo resuelve en una línea.
  • Confundir que CI pase con que el merge esté blindado. Que el check corra no impide que alguien mergee en rojo saltándoselo. Eso se arregla en el Módulo 9.

La frase para llevarse

“CI no es correr los tests: es quitarle a la memoria humana el trabajo de acordarse. El robot corre todo en cada cambio, y lo que está en rojo se ve antes de que haga daño.”

Qué viene

Módulo 9, el que cierra el bloque intermedio: GitHub a nivel enterprise. Los checks que montamos hoy son opcionales — se pueden ignorar. Los volveremos obligatorios con branch protection, CODEOWNERS y environments, y sumaremos los guardianes de seguridad (Dependabot, CodeQL, secret scanning) y el quality gate (SonarQube Cloud) para que nada llegue a main sin pasar por todo.

Material relacionado

DeployCI/CDGitHub ActionsDevOpsTesting

Preguntas sobre este curso

Regístrate gratis para hacer preguntas y guardar tu progreso.

✉ Entérate de lo nuevo

Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.