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
.ymldentro 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
| Job | Qué corre | Qué protege | Si falla, significa |
|---|---|---|---|
| lint | ruff + mypy | Estilo y tipos consistentes | El código no sigue las convenciones o tiene errores de tipo |
| test | pytest tests/unit con cobertura | La lógica no se rompió | Un test unitario cambió de resultado |
| eval | pytest tests/evals (M7) | El agente sigue respondiendo bien | El comportamiento del agente se degradó |
| build | docker build (M5) | La imagen compila | El 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
needsy usarconcurrencypara cancelar corridas viejas te ahorra plata. Y ojo: por seguridad, lossecretsno 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
needsno 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/checkoutsin@v4te 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.ymlestá 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. Elcache: pipdesetup-pythonlo 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
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.