CI/CD não precisa ser complexo para ser profissional. Um pipeline bem feito com GitHub Actions pode rodar lint, testes, build e deploy numa única configuração YAML que qualquer engenheiro do time consegue entender e manter.

Este artigo constrói um pipeline real do zero, explica cada decisão, e mostra os padrões que evitam os problemas mais comuns em produção.

Estrutura básica do workflow

Um workflow GitHub Actions é um arquivo YAML em .github/workflows/. A estrutura é simples: quando rodar, em qual ambiente, com quais passos.

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

Esse é o pipeline mínimo. Simples, mas já captura a maioria dos problemas antes de chegar em produção.

Cache de dependências: o detalhe que muda a velocidade

Sem cache, cada run instala todas as dependências do zero — pode ser 2–3 minutos só nisso. Com cache, a instalação de dependências que não mudaram é pulada.

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

Para npm, use cache: 'npm'. Para yarn, cache: 'yarn'. O cache usa o hash do lockfile como chave — quando o lockfile muda, o cache é invalidado automaticamente.

Separando CI de CD: o job de deploy

CI (integração contínua) roda em cada PR. CD (entrega contínua) roda apenas quando o código entra em produção. Separe em jobs distintos com dependência explícita:

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

  deploy:
    needs: ci          # só roda se CI passar
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # só na main

    environment: production  # requer aprovação manual se configurado

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          # seu script de deploy aqui

Secrets: como fazer certo

Nunca coloque credenciais no YAML. Use GitHub Secrets — disponíveis em Settings > Secrets and variables > Actions do repositório.

- 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), use Environment Secrets — permitem configurar secrets diferentes por ambiente e adicionar revisores obrigatórios antes do deploy.

Matrix builds: testando em múltiplas versões

Para bibliotecas ou APIs com suporte a múltiplas versões do Node, matrix builds rodam o CI em paralelo em todas as combinações:

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

Se você quer usar o resultado do build no job de deploy, ou preservar relatórios de cobertura, use 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

O 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 linhas. Pipeline completo, seguro, com cache, separação CI/CD, e deploy condicional. Isso é tudo que a maioria dos projetos precisa.