Como construir pipelines de CI/CD para projetos monorepo com Nx e Turborepo

1. Fundamentos do Monorepo e Ferramentas de Build

Monorepo é uma estratégia de gerenciamento de código onde múltiplos projetos (apps, libs, packages) residem em um único repositório. Em pipelines CI/CD, isso oferece vantagens como compartilhamento de dependências, refatoração atômica e visibilidade unificada. No entanto, desafios surgem: builds lentos, execução desnecessária de testes e complexidade de orquestração.

Nx e Turborepo são ferramentas de build modernas que resolvem esses problemas com cache inteligente, paralelismo e execução baseada em dependências. Nx oferece um ecossistema mais completo, com geradores de código e plugins para frameworks específicos. Turborepo foca em simplicidade e desempenho, com cache remoto nativo e integração fácil com monorepos baseados em npm/yarn/pnpm.

Uma estrutura típica de monorepo:

meu-monorepo/
├── apps/
│   ├── web/          # Aplicação React
│   ├── api/          # Backend Node.js
│   └── mobile/       # App React Native
├── libs/
│   ├── shared-ui/    # Componentes compartilhados
│   └── utils/        # Funções utilitárias
├── packages/
│   └── eslint-config/ # Configuração compartilhada
├── nx.json           # Configuração Nx
├── turbo.json        # Configuração Turborepo
└── package.json

2. Configuração Inicial do Ambiente CI/CD

Vamos usar GitHub Actions como provedor CI/CD. A configuração básica envolve instalar a ferramenta escolhida, configurar cache e definir triggers.

Exemplo de pipeline com Nx no GitHub Actions:

name: CI/CD Pipeline

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

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci

Para Turborepo, a configuração é similar, mas com foco em cache remoto:

name: CI/CD com Turborepo

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

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npx turbo run build test --filter=[main]

3. Estratégias de Cache e Dependências

O cache é o coração da eficiência em pipelines monorepo. Para dependências, utilizamos o cache nativo do GitHub Actions:

- uses: actions/cache@v3
  with:
    path: |
      node_modules
      .npm
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

Para cache de artefatos de build:

Nx:

- uses: actions/cache@v3
  with:
    path: |
      .nx/cache
      node_modules/.cache/nx
    key: ${{ runner.os }}-nx-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-nx-

Turborepo:

- uses: actions/cache@v3
  with:
    path: |
      .turbo/cache
      node_modules/.cache/turbo
    key: ${{ runner.os }}-turbo-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-turbo-

O hashing incremental é automático: Nx e Turborepo calculam hashes dos arquivos de entrada de cada tarefa e reutilizam resultados se nada mudou.

4. Pipeline de Testes e Linting no Monorepo

A execução apenas nos projetos afetados é o diferencial. Com Nx, usamos affected:

jobs:
  test-affected:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npx nx affected --target=test --parallel=3 --base=main
      - run: npx nx affected --target=lint --parallel=3 --base=main

Com Turborepo, usamos --filter:

  test-affected:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npx turbo run test lint --filter=[main]

Para relatórios de cobertura agregados:

- run: npx nx affected --target=test --coverage --parallel=3
- uses: codecov/codecov-action@v3
  with:
    directory: coverage/
    flags: unittests

5. Build e Publicação de Artefatos

Build otimizado com dependências entre projetos:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npx nx run-many --target=build --all --parallel=3
      - uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: |
            dist/
            apps/*/dist/

Para versionamento semântico com changesets:

- uses: changesets/action@v1
  with:
    publish: npm run release
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

6. Deploy Contínuo e Ambientes Múltiplos

Estratégia de deploy por ambiente:

deploy-staging:
  if: github.ref == 'refs/heads/develop'
  runs-on: ubuntu-latest
  needs: [build, test-affected]
  steps:
    - uses: actions/download-artifact@v3
      with:
        name: build-artifacts
    - name: Deploy to Vercel (Staging)
      run: |
        npx vercel --token ${{ secrets.VERCEL_TOKEN }} --prod --scope staging

deploy-production:
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  needs: [build, test-affected]
  steps:
    - uses: actions/download-artifact@v3
      with:
        name: build-artifacts
    - name: Deploy to AWS
      run: |
        aws s3 sync dist/apps/web s3://meu-bucket-producao/

Para rollback automatizado:

- name: Rollback on failure
  if: failure()
  run: |
    echo "Falha detectada. Iniciando rollback..."
    aws s3 sync s3://meu-bucket-backup/last-good/ s3://meu-bucket-producao/

7. Monitoramento e Otimização do Pipeline

Métricas de desempenho podem ser extraídas com comandos específicos:

# Nx: relatório de cache
- run: npx nx report

# Turborepo: estatísticas de execução
- run: npx turbo run build --dry-run

Configuração de timeouts e paralelismo:

jobs:
  test:
    timeout-minutes: 30
    strategy:
      matrix:
        project: [web, api, mobile]
    steps:
      - run: npx nx test ${{ matrix.project }} --parallel=2

Boas práticas finais:

  • Separe build, teste e deploy em stages independentes
  • Use concurrency para cancelar execuções antigas no mesmo branch
  • Configure notificações via Slack ou e-mail com actions/github-script
- uses: actions/github-script@v6
  if: failure()
  with:
    script: |
      github.rest.issues.create({
        owner: context.repo.owner,
        repo: context.repo.repo,
        title: `Pipeline falhou em ${context.sha}`,
        body: 'Verifique os logs do workflow para detalhes.'
      })

Referências