CI/CD no tiene que ser complejo para ser profesional. Un pipeline bien hecho con GitHub Actions puede correr lint, tests, build y deploy en una única configuración YAML que cualquier ingeniero del equipo puede entender y mantener.

Este artículo construye un pipeline real desde cero, explica cada decisión y muestra los patrones que evitan los problemas más comunes en producción.

Estructura básica del workflow

Un workflow de GitHub Actions es un archivo YAML en .github/workflows/. La estructura es simple: cuándo correr, en qué ambiente, con qué pasos.

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - run: pnpm lint
      - run: pnpm typecheck
      - run: pnpm test --coverage
      - run: pnpm build

Ese es el pipeline mínimo. Simple, pero ya captura la mayoría de los problemas antes de llegar a producción.

Cache de dependencias: el detalle que cambia la velocidad

Sin cache, cada run instala todas las dependencias desde cero. Pueden ser 2-3 minutos solo en eso. Con cache, la instalación de dependencias que no cambiaron se salta.

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'pnpm'  # ← hash del lockfile como clave de cache

Para npm, usa cache: 'npm'. Para yarn, cache: 'yarn'. El cache usa el hash del lockfile como clave. Cuando el lockfile cambia, el cache se invalida.

Separando CI de CD: el job de deploy

CI (integración continua) corre en cada PR. CD (entrega continua) corre solo cuando el código entra en producción. Sepáralo en jobs distintos con dependencia explícita:

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      # ... lint, test, build

  deploy:
    needs: ci          # solo corre si CI pasa
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # solo en main

    environment: production  # requiere aprobación manual si está configurado

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          # tu script de deploy aquí

Secrets: cómo hacerlo bien

Nunca pongas credenciales en el YAML. Usa GitHub Secrets, disponibles en Settings > Secrets and variables > Actions del repositorio.

- name: Deploy
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  run: ./scripts/deploy.sh

Para ambientes (production, staging), usa Environment Secrets. Permiten configurar secrets diferentes por ambiente y agregar revisores obligatorios antes del deploy.

Matrix builds: testeando en múltiples versiones

Para librerías o APIs con soporte de múltiples versiones de Node, los matrix builds corren el CI en paralelo en todas las combinaciones:

jobs:
  ci:
    strategy:
      matrix:
        node-version: [18, 20, 22]

    runs-on: ubuntu-latest
    name: Test on Node ${{ matrix.node-version }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

Artifacts: preservando outputs entre jobs

Si quieres usar el resultado del build en el job de deploy, o preservar reportes de cobertura, usa artifacts:

- name: Upload coverage report
  uses: actions/upload-artifact@v4
  with:
    name: coverage-report
    path: coverage/

- name: Upload build
  uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/
    retention-days: 7

El pipeline completo

name: CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck
      - run: pnpm test --coverage
      - run: pnpm build

      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  deploy:
    needs: ci
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production

    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./scripts/deploy.sh

88 líneas. Pipeline completo, seguro, con cache, separación CI/CD y deploy condicional. Eso es todo lo que la mayoría de los proyectos necesita.