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:
permissions: contents: readno topo reduz o poder doGITHUB_TOKENpara o mínimo. O jobbuildpedepackages: writesó porque precisa publicar no GitHub Container Registry.concurrencycancela execuções antigas do mesmo pull request quando chega um commit novo, o que poupa minutos.needsencadeia os jobs: otestsó roda se olintpassar, e assim por diante.- 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.
- O
buildsó roda em push, não em pull request, para não publicar imagem de código ainda não revisado. - O
cache-from: type=ghareaproveita 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