Todo workflow do GitHub Actions mora em .github/workflows/ e define três coisas: o gatilho, a máquina e a ordem dos passos. O YAML abaixo roda lint, typecheck, teste e build num projeto Node com 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

Quatro passos cobrem o que costuma quebrar antes do merge: o lint pega formatação fora do padrão, o typecatch pega tipo fechado, o teste pega regressão e o build pega bundle quebrado. O restante do arquivo resolve time e infraestrutura.

Cache de dependências

Sem cache, cada execução instala todas as dependências do zero e o run perde de 2 a 3 minutos nisso. Você liga o cache no próprio setup-node, que usa o hash do lockfile como chave:

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

Mexeu no lockfile, o runner refaz a instalação. Projeto em npm usa cache: 'npm' e projeto em yarn usa cache: 'yarn'. O guia de caching do GitHub Actions documenta a chave, o caminho e a restauração do cache.

CI e deploy em jobs separados

Verificação roda em todo pull request. Deploy roda quando o código chega na main. Dois jobs ligados por needs e limitados por if mantêm essa fronteira:

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

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

    environment: production  # aprovação manual quando configurada

    steps:
      - uses: actions/checkout@v4

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

environment: production amarra o deploy aos environments do repositório. Você define revisores obrigatórios ali, e o deploy espera alguém aprovar antes de rodar. O documento de environments cobre proteção por ambiente e proteção por branch.

Secrets fora do YAML

Credencial escrita no YAML entra no histórico do Git e em todo log de execução. Você guarda o valor em Settings > Secrets and variables > Actions e injeta só no passo que precisa dele:

- 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

Environment secrets separam valores por ambiente e herdam as aprovações do environment. O guia de secrets define o escopo de cada valor e o que acontece quando você imprime um secret num echo.

Matrix builds para várias versões do Node

Biblioteca que promete suporte a Node 18, 20 e 22 precisa provar isso a cada PR. A matriz executa o mesmo job em paralelo, uma execução por versão:

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

O documento de matrix cobre fail-fast, que cancela as execuções restantes quando uma falha, e a lista de combinações excluídas.

Artifacts entre jobs

O deploy consome o dist/ que o job de CI gerou. Upload e download passam esse diretório adiante:

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

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

Relatório de cobertura usa o mesmo caminho: um upload com name: coverage-report preserva o HTML do run para quem quiser abrir depois.

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

O arquivo tem 88 linhas e cobre cache, separação entre verificação e deploy, artefatos entre jobs e proteção de environment. A maioria dos projetos fecha o pipeline aqui.

Referências