Saltar al contenido principal
Volver a todos los artículos
DevOps e Infraestructura••6 min de lectura

CI con GitHub Actions: lint, test y build paso a paso

Un pipeline de integración continua con GitHub Actions ejecuta en cada push y en cada pull request tres comprobaciones en orden: lint, tests y build. Basta un archivo YAML en .github/workflows/, un runner ubuntu-latest, la caché de dependencias de actions/setup-node y una regla de protección de rama que exija que el job pase.

Qué comprueba un pipeline de CI con lint, test y build

Un pipeline de CI responde a una pregunta: ¿este commit se puede fusionar sin romper nada? Para contestarla con poco coste, cada paso descarta un tipo de error distinto y el más barato va primero:

  1. Lint y formato: ESLint y Prettier detectan errores de estilo, imports sin usar y patrones peligrosos sin ejecutar nada. Es el paso más rápido y el que más ruido quita en la revisión.
  2. Comprobación de tipos: tsc --noEmit encuentra llamadas con argumentos equivocados o propiedades que ya no existen, sin generar archivos.
  3. Tests unitarios: Vitest o Jest comprueban el comportamiento. Aquí conviene que los tests no dependan de red ni de servicios externos.
  4. Build de producción: next build (o el comando equivalente de tu stack) demuestra que la aplicación compila con las variables de entorno de producción y que no hay rutas rotas.

El orden importa: si el lint falla, no merece la pena gastar minutos de runner en compilar. En un proyecto pequeño, separar estos pasos en un único job secuencial es suficiente; dividirlos en varios jobs solo compensa cuando el build es lento y quieres ver el resultado de los tests antes.

El workflow de GitHub Actions paso a paso

Este es un workflow completo para un proyecto Node.js con npm. Se guarda como .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm test
      - run: npm run build

Qué hace cada bloque:

  • on: lanza el workflow en cada push a main y en cada pull request, que es donde interesa el aviso antes de fusionar.
  • concurrency: si haces dos pushes seguidos a la misma rama, cancela la ejecución anterior en lugar de pagar las dos.
  • permissions: contents: read: el token del workflow solo puede leer el repositorio. Un paso comprometido no podrá escribir en él.
  • timeout-minutes: corta un job colgado en vez de dejarlo consumiendo minutos hasta el límite por defecto.
  • cache: npm: actions/setup-node guarda la caché de npm usando el package-lock.json como clave, así que npm ci no descarga todo en cada ejecución.
  • npm ci: instala exactamente lo que dice el lockfile y falla si package.json y el lockfile no coinciden.

Si usas pnpm, cambia cache: npm por cache: pnpm, añade antes pnpm/action-setup y sustituye npm ci por pnpm install --frozen-lockfile.

Lint, tests y build: qué paso atrapa cada error

PasoQué detectaComando típicoCuándo falla
LintEstilo, imports sin usar, patrones peligrososnpm run lintAntes de ejecutar código
TiposFirmas y propiedades incorrectasnpx tsc --noEmitAl analizar el proyecto entero
TestsComportamiento rotonpm testAl ejecutar los casos
BuildErrores de compilación y de configuraciónnpm run buildAl generar el artefacto final

Ningún paso sustituye a otro. Un código puede pasar el lint y los tipos y aun así devolver un resultado equivocado, y unos tests verdes no garantizan que next build encuentre todas las variables de entorno que necesita.

Cómo hacer obligatoria la CI antes de fusionar en GitHub

Un pipeline que nadie mira no protege nada. Para que la CI bloquee de verdad los errores:

  1. En Settings → Branches (o Rules → Rulesets), crea una regla para main.
  2. Activa Require a pull request before merging.
  3. Activa Require status checks to pass y elige el job check del workflow anterior.
  4. Si trabajas solo, la revisión puede ser tuya: el proceso de auto pull request encaja aquí, porque la CI revisa lo mecánico y tú lo que importa.

Con la regla activa, el botón de fusionar se queda deshabilitado mientras el check esté en rojo o pendiente.

Errores frecuentes al montar CI con GitHub Actions

  • Usar npm install en lugar de npm ci: puede actualizar el lockfile en el runner y probar algo distinto de lo que se desplegará.
  • No fijar la versión de Node: si el runner cambia de versión por defecto, el build puede empezar a fallar sin que hayas tocado el código.
  • Tests que dependen de la red o de la hora: dan fallos intermitentes y acaban con un equipo que reintenta en vez de leer el error.
  • Secretos en el build de los pull requests: los workflows de pull_request desde forks no reciben secretos; si el build los necesita, usa valores de prueba en lugar de debilitar la regla.
  • Mezclar CI y despliegue en el mismo job: separa la comprobación del despliegue. Cómo encadenar ambos sin complicarse está en mi pipeline CI/CD simple y barato.

Cuándo ampliar el pipeline: matrices y monorepos

El workflow anterior cubre la mayoría de proyectos de una sola aplicación. Merece la pena ampliarlo en dos casos:

  • Librerías que se usan con varias versiones de Node: añade strategy.matrix.node-version y GitHub lanzará un job por versión.
  • Monorepos: filtra con paths para que un cambio en la documentación no recompile todo, o usa la caché de tu herramienta de monorepo para ejecutar solo los paquetes afectados. Si estás pensando en dar ese paso, en reutilizar código con un monorepo explico cuándo compensa.

Lo que no conviene es añadir pasos por si acaso. Cada comprobación nueva debe atrapar un error que hoy llega a producción; si no, solo alarga la espera antes de fusionar.

Preguntas frecuentes

¿Cuánto tiempo debería tardar un pipeline de integración continua estándar?

Un pipeline de integración continua estándar debería terminar mientras el autor todavía tiene el cambio en la cabeza. Si tarda tanto que la gente fusiona sin esperar, conviene cachear dependencias, cancelar ejecuciones repetidas con concurrency y dejar los pasos lentos para el final.

¿Es necesario levantar contenedores Docker completos dentro de la CI?

Los contenedores Docker completos no son necesarios en la CI si la aplicación solo necesita lint, comprobación de tipos y tests unitarios. Node.js instalado con actions/setup-node y la caché del gestor de paquetes arranca antes y consume menos minutos de runner que construir una imagen en cada push.

¿Cómo evitar agotar los minutos mensuales gratuitos de GitHub Actions?

Para no agotar los minutos de GitHub Actions, limita los triggers a las ramas y rutas que importan con branches y paths, cancela las ejecuciones anteriores de la misma rama con un grupo de concurrency, cachea las dependencias y pon un timeout-minutes que corte los jobs colgados.