Pular para o conteúdo
EngenhariaIniciante

CI/CD com GitHub Actions: pipeline do zero ao deploy

Monte um pipeline CI/CD com GitHub Actions: lint, testes em matriz, build de imagem Docker, cache, secrets, permissões mínimas e deploy com aprovação.

Por Equipe IAUAI Estudos · 7 de agosto de 2026 · 9 min de leitura

Nesta página

Ao final deste artigo você vai conseguir montar um pipeline de CI/CD no GitHub Actions que roda lint e testes a cada mudança, publica uma imagem Docker e faz deploy em produção só depois de uma aprovação humana. Os exemplos usam Node, mas a estrutura é a mesma para qualquer linguagem.

Por que automatizar

Sem CI, cada pessoa testa do seu jeito, e o "funciona na minha máquina" vira rotina. Sem CD, o deploy depende de alguém lembrar de cada passo, e uma mudança de uma linha pode esperar meia hora de checagem manual. Com Actions, toda mudança que chega ao repositório passa pelas mesmas verificações, e o resultado aparece no próprio pull request.

Anatomia de um workflow

Um workflow é um arquivo YAML em .github/workflows/. Ele tem gatilhos (on), jobs, e cada job tem steps que rodam em uma máquina (runs-on).

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test

Duas coisas já acontecem aqui. O setup-node com cache: npm guarda o cache de downloads do npm entre execuções, sem precisar de um step de cache separado. E o npm ci instala exatamente o que está no package-lock.json, o que é o correto em CI (o npm install pode alterar o lock).

As versões das actions (@v7) mudam com o tempo. Confira a última no repositório de cada action, na aba Releases, e deixe o Dependabot abrir pull requests de atualização com um .github/dependabot.yml para o ecossistema github-actions.

Pipeline completo

Agora com lint, testes em matriz, build da imagem e deploy:

name: CI/CD

on:
  push:
    branches: [main]
    paths-ignore:
      - "docs/**"
      - "**.md"
  pull_request:

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run lint

  test:
    needs: lint
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: [22, 24]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

  build:
    needs: test
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      tag: ${{ steps.meta.outputs.version }}
    steps:
      - uses: actions/checkout@v7
      - uses: docker/setup-buildx-action@v4
      - uses: docker/login-action@v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v6
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=raw,value=latest
      - uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    steps:
      - name: Disparar deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          curl -fsS -X POST "${{ vars.DEPLOY_URL }}" \
            -H "Authorization: Bearer $DEPLOY_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{"tag":"${{ needs.build.outputs.tag }}"}'

Lendo de cima para baixo:

  1. permissions: contents: read no topo reduz o poder do GITHUB_TOKEN para o mínimo. O job build pede packages: write só porque precisa publicar no GitHub Container Registry.
  2. concurrency cancela execuções antigas do mesmo pull request quando chega um commit novo, o que poupa minutos.
  3. needs encadeia os jobs: o test só roda se o lint passar, e assim por diante.
  4. A matriz roda os testes nas versões 22 e 24 do Node. Use as versões que seu projeto realmente suporta, e consulte a tabela de releases do Node para não testar em versão sem suporte.
  5. O build só roda em push, não em pull request, para não publicar imagem de código ainda não revisado.
  6. O cache-from: type=gha reaproveita as camadas do Docker entre execuções, usando o cache do próprio GitHub.

O job deploy aqui chama uma URL de deploy como exemplo. Na prática, ele pode rodar kubectl, helm, terraform apply ou acionar o seu provedor. Para ver como gerar a imagem enxuta que o build publica, leia Docker multi-stage.

Cache: o que ele resolve

O cache reaproveita o que não mudou entre execuções. Com o cache: npm do setup-node, a chave é calculada a partir do lock file, então o cache é renovado quando as dependências mudam. Um acerto de cache costuma encurtar bastante a instalação, mas o ganho depende do tamanho do projeto. Veja nos logs da sua execução quanto tempo o step npm ci leva com e sem cache, em vez de confiar em números genéricos.

Matriz de versões

A strategy.matrix cria um job por combinação. Com duas versões de Node e dois sistemas, são quatro jobs em paralelo:

strategy:
  matrix:
    node-version: [22, 24]
    os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}

Cada combinação consome minutos. Para aplicações que só rodam em Linux, uma matriz de versões em Ubuntu basta.

Secrets e variáveis

Valores sensíveis ficam em Settings > Secrets and variables > Actions e entram no workflow por ${{ secrets.NOME }}. O GitHub mascara esses valores nos logs, trocando por ***, mas isso não é uma garantia absoluta: um valor transformado (por exemplo, em base64) pode escapar da máscara. Por isso evite imprimir segredos.

Duas boas práticas. Passe o segredo por env e use $VARIAVEL no script, como no job de deploy acima, em vez de interpolar ${{ secrets.X }} direto no comando, o que reduz risco de injeção de comando. E, quando a nuvem permitir, prefira autenticação por OIDC, que troca o token de longa duração por credenciais temporárias emitidas a cada execução (AWS, Google Cloud e Azure suportam, com permissions: id-token: write).

Para configurações não sensíveis, como a URL de deploy, use vars.NOME (Variables), que ficam visíveis e editáveis.

Se um segredo vazou num commit, apague o arquivo não resolve: troque o segredo. O histórico do Git guarda tudo.

Ambientes e aprovação manual

O bloco environment: production conecta o job a um ambiente configurado em Settings > Environments. Lá você define revisores obrigatórios, e o job fica pausado até alguém aprovar. Também dá para restringir quais branches podem fazer deploy no ambiente e ter segredos específicos de produção, separados dos de staging. Alguns recursos de proteção de ambiente dependem do plano do repositório (repositórios privados em planos gratuitos têm limitações), então confira a documentação oficial do GitHub para o seu caso.

Armadilhas comuns

Gastar minutos à toa. Repositórios privados têm uma cota mensal de minutos conforme o plano, e execuções em Windows e macOS consomem mais por minuto que em Linux. Consulte a página de preços do GitHub Actions para os valores atuais, sem confiar em número de artigo. Use paths-ignore, concurrency e cache para manter o consumo baixo.

Pull request de fork. Por padrão, workflows disparados por forks não recebem seus secrets, e o token tem só leitura. Evite usar o gatilho pull_request_target com checkout do código do fork, porque ele roda com privilégios e já foi origem de vazamentos.

Actions de terceiros sem fixar. Uma tag como @v7 pode ser movida. Para ações de fontes menos confiáveis, fixe pelo SHA do commit (uses: org/acao@<sha>).

Deploy sem volta. Tenha um caminho de rollback, nem que seja reaplicar a tag anterior, e um health check depois do deploy:

- name: Health check
  run: curl -fsS https://app.example.com/health

Quando considerar outra ferramenta

Se o build leva horas ou precisa de máquinas especiais (GPU, muita memória), use runners próprios (self-hosted) ou os runners maiores do GitHub. Se sua empresa já usa GitLab CI ou Jenkins e tem muito investimento neles, a troca pode não valer o esforço. Para quem já está no GitHub, o Actions costuma ser o caminho mais curto.

Próximos passos

Teste o fluxo num repositório novo: coloque o workflow, abra um pull request e quebre um teste de propósito para ver o pipeline falhar. Depois, leve a infraestrutura para código com Terraform: primeiros passos e chame o terraform plan dentro do próprio pipeline.

Quer aplicar isso na sua empresa? Marque uma conversa de 45 minutos em https://iauaicloud.com.br/consultoria

Teste seu conhecimento

Teste o que você aprendeu sobre CI/CD com GitHub Actions

Pergunta 1 de 5

Por que usar npm ci em vez de npm install no CI?

Um conteúdo prático por quinzena

Deixe o seu e-mail para receber um conteúdo prático de IA e cloud a cada quinzena. Sem spam, e você pede a exclusão quando quiser.

Continue lendo