Saltar al contenido

Automatizar flujos de trabajo con GitHub Actions

Patrones prácticos de GitHub Actions para automatizar CI/CD, calidad de código, actualizaciones de dependencias y releases, con workflows reutilizables.

5 min de lectura
Diagrama de flujo de trabajo de GitHub Actions que muestra jobs de CI en paralelo y etapas de despliegue

GitHub Actions empezó como una herramienta de CI sencilla, pero se ha convertido en una plataforma completa de automatización de flujos de trabajo. Más allá de ejecutar tests, los equipos la usan para imponer calidad de código, automatizar releases, mantener las dependencias actualizadas y coordinar despliegues multiservicio. La clave está en entender los patrones que hacen que los workflows sigan siendo mantenibles a medida que crecen.

La mayoría de los equipos empieza con un único archivo de workflow que lo hace todo. Eso funciona hasta que deja de funcionar. Esta guía cubre los patrones que escalan.

Pipeline básico de CI

Todo repositorio necesita un pipeline de CI que se ejecute en las pull requests. Empieza simple y añade complejidad solo cuando sea necesaria.

ymlyaml
# .github/workflows/ci.yml
name: CI
 
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

Lint y test se ejecutan en paralelo: si el linting falla, lo ves de inmediato sin esperar a los tests. Ambos jobs cachean node_modules a través de la caché integrada de la acción setup-node.

Builds con matrix para pruebas en varios entornos

Prueba con múltiples versiones de Node, sistemas operativos o versiones de bases de datos usando estrategias de matrix.

ymlyaml
# ❌ Separate jobs for each environment — repetitive and hard to maintain
jobs:
  test-node-18:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci && npm test
 
  test-node-20:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm test
ymlyaml
# ✅ Matrix strategy — one job definition, multiple environments
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
      fail-fast: false  # Don't cancel other jobs if one fails
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

fail-fast: false es importante: sin él, un fallo en una combinación cancela todos los jobs en ejecución. Conviene ver todos los fallos a la vez, no arreglarlos uno por uno.

Workflows reutilizables

Cuando varios repositorios necesitan el mismo pipeline de CI, el YAML duplicado se extiende por los repos y acaba divergiendo. Los workflows reutilizables resuelven esto.

ymlyaml
# .github/workflows/reusable-node-ci.yml (in a shared repository)
name: Reusable Node CI
 
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
      run-e2e:
        required: false
        type: boolean
        default: false
    secrets:
      NPM_TOKEN:
        required: false
 
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      - run: npm ci
      - run: npm run build
      - run: npm test
 
  e2e:
    if: ${{ inputs.run-e2e }}
    runs-on: ubuntu-latest
    needs: build-and-test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
ymlyaml
# .github/workflows/ci.yml (in consuming repositories)
name: CI
 
on:
  pull_request:
    branches: [main]
 
jobs:
  ci:
    uses: my-org/shared-workflows/.github/workflows/reusable-node-ci.yml@main
    with:
      node-version: '20'
      run-e2e: true
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Los cambios en el workflow compartido se propagan a todos los repositorios que lo referencian. Fíjalo a un SHA o tag específico para mayor estabilidad en repos de producción.

Workflow de release automatizado

Automatiza el bump de versión y la generación del changelog basándote en conventional commits.

ymlyaml
# .github/workflows/release.yml
name: Release
 
on:
  push:
    branches: [main]
 
permissions:
  contents: write
  pull-requests: write
 
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Need full history for changelog
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'
 
      - run: npm ci
      - run: npm run build
      - run: npm test
 
      - name: Determine version bump
        id: version
        run: |
          # Check commit messages since last tag
          LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
          COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s")
 
          if echo "$COMMITS" | grep -q "^feat!:\|^BREAKING CHANGE:"; then
            echo "bump=major" >> "$GITHUB_OUTPUT"
          elif echo "$COMMITS" | grep -q "^feat:"; then
            echo "bump=minor" >> "$GITHUB_OUTPUT"
          else
            echo "bump=patch" >> "$GITHUB_OUTPUT"
          fi
 
      - name: Bump version and create tag
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          npm version ${{ steps.version.outputs.bump }} -m "chore: release v%s"
          git push --follow-tags
 
      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Workflows condicionales con filtros de rutas

Los monorepos grandes no deberían ejecutar todas las verificaciones ante cualquier cambio. Los filtros de rutas garantizan que solo se disparen los workflows relevantes.

ymlyaml
# ❌ Every PR runs the full pipeline regardless of what changed
on:
  pull_request:
    branches: [main]
ymlyaml
# ✅ Only run when relevant files change
on:
  pull_request:
    branches: [main]
    paths:
      - 'src/**'
      - 'package.json'
      - 'package-lock.json'
      - '.github/workflows/ci.yml'
 
# Also useful: ignore paths
on:
  pull_request:
    branches: [main]
    paths-ignore:
      - 'docs/**'
      - '*.md'
      - '.vscode/**'

Para servicios de un monorepo, usa archivos de workflow separados con filtros de rutas por servicio. Un cambio en services/auth/** solo dispara el pipeline del servicio auth.

Caché y rendimiento

La velocidad del CI afecta directamente a la productividad del desarrollador. Cachea de forma agresiva y paraleliza todo lo posible.

ymlyaml
jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
 
      # Cache the entire node_modules for downstream jobs
      - uses: actions/cache/save@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
 
  lint:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
      - run: npm run lint
 
  test-unit:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
      - run: npm run test:unit
 
  test-integration:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
      - run: npm run test:integration

Instala una vez y luego ramifica hacia lint, tests unitarios y tests de integración en paralelo. La división cache/save y cache/restore evita descargar las dependencias tres veces.

Conclusiones clave

  1. Separa las responsabilidades en jobs paralelos: lint, test y build deberían ejecutarse de forma concurrente, no secuencial
  2. Usa builds con matrix para pruebas en varios entornos y configura fail-fast: false para ver todos los fallos
  3. Extrae workflows reutilizables cuando varios repos comparten la misma lógica de pipeline
  4. Usa filtros de rutas para evitar ejecutar workflows irrelevantes en monorepos
  5. Cachea de forma agresiva: instala las dependencias una vez y compártelas vía caché entre jobs paralelos
  6. Automatiza los releases con conventional commits y workflows de bump de versión
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX