Todo workflow de GitHub Actions vive en .github/workflows/ y define tres cosas: el disparador, la máquina y el orden de los pasos. El YAML de abajo corre lint, typecheck, test y build en un proyecto Node con pnpm.

# .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

Cuatro pasos cubren lo que suele romperse antes del merge: el lint agarra formato fuera del estándar, el typecatch agarra tipos mal cerrados, el test agarra regresiones y el build agarra un bundle roto. El resto del archivo resuelve equipo e infraestructura.

Cache de dependencias

Sin cache, cada ejecución instala todas las dependencias desde cero y el run pierde de 2 a 3 minutos en eso. Enciendes el cache en el propio setup-node, que usa el hash del lockfile como clave:

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

Tocaste el lockfile y el runner instala de nuevo. Un proyecto con npm usa cache: 'npm' y uno con yarn usa cache: 'yarn'. La guía de caching de GitHub Actions documenta la clave, la ruta y la restauración del cache.

CI y deploy en jobs separados

La verificación corre en cada pull request. El deploy corre cuando el código llega a main. Dos jobs unidos por needs y limitados por if sostienen esa frontera:

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

  deploy:
    needs: ci          # depende del job de CI
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    environment: production  # aprobación manual si está configurada

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: ./scripts/deploy.sh

environment: production ata el deploy a los environments del repositorio. Tú defines revisores obligatorios ahí y el deploy espera la aprobación antes de arrancar. La documentación de environments cubre reglas de ambiente y reglas de rama.

Secrets fuera del YAML

Una credencial escrita en el YAML entra en el historial de Git y en cada log de ejecución. Guardas el valor en Settings > Secrets and variables > Actions y lo inyectas solo en el paso que lo necesita:

- 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

Los environment secrets separan valores por ambiente y heredan las reglas de aprobación del environment. La guía de secrets define el alcance de cada valor y qué pasa cuando imprimes un secret en un echo.

Matrix builds para varias versiones de Node

Una librería que promete soporte para Node 18, 20 y 22 tiene que probarlo en cada PR. La matriz corre el mismo job en paralelo, una ejecución por versión:

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 }}

La documentación de matrix cubre fail-fast, que cancela las ejecuciones restantes cuando una falla, y la lista de combinaciones excluidas.

Artifacts entre jobs

El deploy consume la carpeta dist/ que generó el job de CI. Upload y download mueven esa carpeta:

# en el job ci
- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/
    retention-days: 7

# en el job deploy
- uses: actions/download-artifact@v4
  with:
    name: dist
    path: dist/

El reporte de cobertura usa el mismo camino: un upload con name: coverage-report conserva el HTML del run para quien quiera abrirlo después.

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

El archivo tiene 88 líneas y cubre cache, la separación entre verificación y deploy, artifacts entre jobs y protección de environment. La mayoría de los proyectos cierra el pipeline acá.

Referencias