O que CI e CD significam na prática
Continuous integration (integração contínua) é a prática de mesclar o trabalho de cada desenvolvedor em um branch compartilhado com frequência e verificá-lo automaticamente. Cada merge dispara um build, uma passagem de lint e a suíte de testes em uma máquina onde ninguém desenvolve. O objetivo é encontrar problemas de integração em minutos, enquanto a alteração ainda é pequena, em vez de enfrentar um merge doloroso ao final de um branch longo.
Continuous delivery (entrega contínua) estende essa ideia: o branch principal está sempre em um estado passível de release, e o pipeline pode implantá-lo em produção a qualquer momento. O artefato é construído e testado a cada alteração; um humano decide quando fazer o release. Continuous deployment (implantação contínua) remove essa decisão humana, enviando cada alteração que passa pelos critérios de validação diretamente para a produção.
A diferença entre delivery e deployment é o portão de aprovação, e essa é a única diferença. Tudo o que vem antes disso — o build, os testes, o artefato — é idêntico.
Equipes frequentemente afirmam “fazer CI/CD” sem fazer nenhum dos dois. A integração contínua exige que o build seja executado em uma máquina compartilhada e autoritativa, não apenas no laptop antes de um commit. A entrega contínua exige que o artefato produzido pelo pipeline seja aquele que vai para produção, e não algo reconstruído manualmente no momento do release. Se qualquer um desses pontos estiver faltando, o ciclo fica aberto e os benefícios se perdem.
A anatomia de um pipeline
Todo pipeline, desde um arquivo de GitHub Actions de dez linhas até uma configuração empresarial de mil jobs, é construído a partir do mesmo vocabulário.
- Trigger — o evento que inicia uma execução: um push, um pull request, uma tag, um agendamento ou um disparo manual.
- Workflow — o arquivo que declara os triggers e jobs. No GitHub Actions, ele fica em
.github/workflows/. - Job — uma unidade de trabalho que roda em um único runner. Os jobs rodam em paralelo, a menos que você declare dependências com
needs. - Step — um comando único ou uma action reutilizável dentro de um job. Os steps rodam em ordem e o job para na primeira falha.
- Runner — a máquina que executa um job. Runners hospedados (hosted) são descartáveis; runners self-hosted são máquinas que você gerencia.
- Artifact — um arquivo produzido por um job e armazenado para jobs posteriores ou para humanos: um binário, um bundle, um relatório de testes.
- Cache — um diretório restaurado entre execuções para evitar a repetição de tarefas custosas, como a instalação de dependências.
- Environment — um alvo nomeado, como
stagingouproduction, com seus próprios secrets, regras de proteção e URL. - Secret — um valor criptografado injetado em um job durante a execução e mascarado nos logs.
- Status check — o resultado de sucesso ou falha reportado ao commit, que pode ser exigido por regras de branch protection.
Uma vez que esses termos estejam claros, qualquer sistema de CI torna-se legível. Jenkins, GitLab CI, CircleCI e GitHub Actions utilizam os mesmos conceitos, apenas com nomenclaturas diferentes.
Um workflow do GitHub Actions, linha por linha
O workflow útil mais simples faz o checkout do código, instala as dependências e executa os testes.
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
on declara os gatilhos. Um push para main e qualquer pull request iniciam uma execução. jobs.test roda em um runner ubuntu-latest limpo. uses executa uma action reutilizável; run executa um comando de shell. npm ci instala exatamente o que o lockfile define, o que torna a execução reproduzível. cache: npm instrui a action de setup a fazer o cache do diretório do npm, utilizando o lockfile como chave.
Adicione concurrency para cancelar execuções superadas, o que economiza tempo e dinheiro em branches ativas:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Quando um desenvolvedor faz push de três commits seguidos, apenas a execução mais recente sobrevive. As anteriores são canceladas, e o pull request exibe um único status atual em vez de uma fila de status obsoletos.
Build once, deploy o mesmo artefato
O hábito mais valioso em um pipeline é buildar exatamente uma vez. O pipeline compila, testa e empacota a aplicação uma única vez, e cada ambiente, do staging à produção, executa esse mesmo output.
A alternativa — rebuildar para cada ambiente — parece inofensiva, mas não é. Builds diferentes podem resolver versões de dependências distintas, capturar digests de imagens base diferentes ou embutir um NODE_ENV diferente. O staging, então, testa um binário que a produção jamais executará, e o “passou no staging” não prova nada.
Duas regras tornam o build-once prático. Primeiro, tagueie o artefato pelo commit, nunca por um nome mutável como latest:
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA
Segundo, injete a configuração em runtime. A imagem não deve saber se está rodando em staging ou produção. URLs de banco de dados, feature flags e chaves de API chegam como variáveis de ambiente quando o container inicia. O build é idêntico em todos os lugares; apenas o ambiente difere.
Para promover, referencie o digest imutável em vez da tag, para que mesmo uma imagem retagueada não sofra drift:
kubectl set image deploy/app \
app=ghcr.io/me/app@sha256:...
Esta é a base que torna os rollbacks triviais. O release anterior é simplesmente o digest anterior, ainda presente no registry, pronto para ser deployado em segundos.
Testes, lint e typecheck como gates
Um gate é uma verificação que deve ser aprovada antes que o pipeline continue. Os gates essenciais são rápidos e baratos: linting, type checking e a suíte de testes unitários. Eles rodam em cada pull request e impedem que o artefato seja buildado quando falham.
Ordene-os do mais rápido para o mais lento. Lint e typecheck geralmente terminam em segundos e capturam uma grande classe de erros. Os testes unitários vêm em seguida. Testes de integração e end-to-end, que são mais lentos, podem rodar em paralelo ou apenas na branch main.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm test
Dois jobs rodam em paralelo, cada um em seu próprio runner. O lint falha em vinte segundos sem precisar esperar pela suíte de testes. A contrapartida é que ambos os jobs instalam as dependências; um cache compartilhado mantém esse processo barato.
Gates só fazem sentido se forem obrigatórios. No forge, marque as verificações como obrigatórias na proteção de branch para que um pull request não possa ser mergeado enquanto qualquer um deles estiver vermelho.
Matrizes e paralelismo
Uma matriz executa a definição de um job em diversas combinações de inputs. É assim que você testa múltiplas versões do Node e sistemas operacionais sem duplicar o YAML.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
Isso se expande para quatro jobs. O fail-fast: false permite que os demais terminem mesmo que um falhe, o que é útil quando você quer saber se a falha é específica de uma versão.
O paralelismo não é gratuito. Cada job provisiona um runner e instala as dependências, portanto, uma matriz ampla custa mais tempo e dinheiro do que uma estreita. Teste as versões que você realmente suporta, não todas as versões que existem.
Para suítes grandes, faça o shard dos próprios testes: passe um índice de shard e o total para o test runner para que cada um dos quatro jobs execute um quarto dos testes. A suíte termina em aproximadamente um quarto do tempo.
Secrets e variáveis de ambiente
Secrets são valores criptografados que a pipeline injeta em tempo de execução. Eles devem ficar no armazenamento de secrets da plataforma, nunca no repositório e nunca na imagem.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
A plataforma mascara valores de secrets conhecidos nos logs, mas esse mascaramento é baseado em “best-effort”. Ele não consegue capturar um secret que foi transformado, codificado em base64 ou impresso por uma ferramenta que o reformata. A regra de segurança é simples: nunca imprima um secret. Não execute env em um passo de debug, não faça echo de um token e não passe um secret para um comando que registre seus argumentos nos logs.
Limite o escopo dos secrets ao ambiente que precisa deles. Um deploy de staging não tem motivo para ter a senha do banco de dados de produção. No GitHub, secrets de ambiente estão disponíveis apenas para jobs que declaram esse ambiente e podem exigir aprovação prévia.
Para provedores de nuvem, prefira OIDC em vez de chaves de longa duração. A pipeline troca um token de identidade de curta duração por credenciais temporárias, portanto, não há um secret estático para vazar ou rotacionar.
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy
aws-region: eu-west-1
O token é válido por alguns minutos e limitado a apenas um role. Se ele vazar, o raio de impacto fica restrito a uma única execução.
Um aviso merece destaque: pull requests de forks executam código não confiável. Nunca exponha secrets para esse código. Use pull_request em vez de pull_request_target para qualquer coisa que faça o checkout e o build de código de contribuidores, e exija aprovação antes de executar workflows de contribuidores de primeira viagem.
Ambientes, aprovações e branches protegidas
Um environment é um alvo de deploy nomeado com seus próprios secrets, regras de proteção e URL. É o mecanismo que mantém a produção distinta de todo o resto.
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- run: ./deploy.sh
Environments podem exigir uma aprovação manual antes que um job seja executado, restringir quais branches podem fazer deploy neles e limitar quanto tempo um deployment aguarda antes de sofrer timeout. É aqui que reside o “botão humano” da entrega contínua: o pipeline roda automaticamente até o portão de segurança, e um revisor o libera.
Branches protegidas complementam os environments. Na branch main, exija um pull request, exija a aprovação de status checks, exija revisão e proíba force pushes. Juntos, eles garantem que a única maneira de chegar à produção seja através do pipeline, que é exatamente o objetivo.
Estratégias de deployment e rollbacks
A maneira como uma nova versão substitui a antiga é uma decisão de design com consequências reais para os usuários.
O Rolling deployment substitui as instâncias aos poucos. A capacidade diminui levemente durante a troca, mas não é necessária infraestrutura extra. É o padrão na maioria das plataformas e o ponto de partida ideal.
O Blue-green deployment executa dois ambientes completos. O tráfego é direcionado para o “blue” enquanto o “green” recebe a nova versão; assim que o “green” está saudável, uma única alteração de roteamento alterna todo o tráfego. O rollback consiste em alternar de volta, o que é quase instantâneo. O custo é manter dois ambientes rodando simultaneamente.
O Canary deployment envia uma pequena fatia do tráfego — um por cento, depois cinco, depois cinquenta — para a nova versão e monitora as taxas de erro e a latência antes de prosseguir. Isso detecta problemas que um health check não consegue, ao custo de um roteamento e monitoramento mais complexos.
Independentemente da estratégia, o deployment deve ser reversível. Como o artefato anterior é imutável e ainda está no registry, um rollback é um redeploy do digest anterior, e não um rebuild:
kubectl rollout undo deploy/app
Dois detalhes tornam os rollbacks seguros. As migrações de banco de dados devem ser retrocompatíveis por pelo menos uma release, para que o código antigo ainda consiga rodar com o novo schema. Além disso, feature flags permitem desativar novos comportamentos sem fazer deploy de nada, o que é a forma mais rápida de rollback que existe.
Registries de artefatos e imagens de container
Um registry de artefatos armazena a saída do build: imagens de container, pacotes npm, binários, tarballs. Ele é o ponto de entrega entre o build e o deploy, e deve ser imutável.
Imagens de container são o artefato mais comum porque carregam seu próprio runtime. Um registry como GitHub Container Registry, Amazon ECR ou Docker Hub armazena camadas, e a pipeline faz o push para ele uma vez por build. Os deploys, então, baixam o digest exato.
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
As opções cache-from e cache-to persistem o cache de camadas do Docker entre as execuções, para que uma camada de dependência inalterada não seja reconstruída do zero todas as vezes.
Mantenha uma política de retenção. Registries acumulam gigabytes rapidamente, e deletar imagens antigas faz parte de manter a pipeline rápida. Mantenha pelo menos as últimas versões para que o rollback continue sendo possível.
Verificações de status e proteção de branch
Uma verificação de status (status check) é o veredito do pipeline sobre um commit. O forge a anexa ao pull request, e a proteção de branch decide se ela é apenas consultiva ou obrigatória.
Configure a branch main para exigir:
- Um pull request antes do merge, com pelo menos uma aprovação.
- Verificações de status aprovadas, incluindo lint, typecheck e testes.
- Que a branch esteja atualizada com a main antes do merge, para que as verificações sejam executadas contra o código que será efetivamente integrado.
- Proibição de force pushes e de deleções.
O resultado é que a branch main está sempre “verde”. Cada commit nela passou pelos mesmos critérios, e cada deploy a partir dela é um artefato comprovadamente estável.
Mantenha a lista de exigências curta. Exigir uma suíte de testes end-to-end lenta em cada pull request faz os desenvolvedores esperarem; exigi-la na merge queue ou na branch main oferece a mesma segurança sem gerar atrito.
Cache e builds incrementais
O cache é a diferença entre um pipeline de dois minutos e um de doze minutos. O princípio é nunca repetir um trabalho cujos inputs não tenham mudado.
O cache mais valioso são as dependências. Utilize o hash do lockfile como chave para que ele seja invalidado exatamente quando as dependências mudarem:
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: npm-
restore-keys permite uma correspondência parcial: mesmo quando a chave exata não é encontrada, o cache mais recente é restaurado e atualizado incrementalmente, o que é muito mais rápido do que uma instalação do zero.
As ferramentas de build adicionam seus próprios caches. O build incremental do TypeScript, o pre-bundling de dependências do Vite e o cache de camadas do Docker beneficiam-se de serem persistidos entre as execuções. Para o Docker, utilize o registry ou o backend de cache do provedor de CI em vez do daemon local, que é descartado junto com o runner.
Um cache é uma otimização, nunca uma fonte da verdade. Se um cache estiver corrompido ou desatualizado, a execução ainda deve estar correta. Ferramentas de build que confiam cegamente em um cache podem produzir outputs errados, portanto, defina as chaves de cache de forma conservadora e trate a ausência de cache (cache miss) como algo normal.
Pipelines para monorepos
Um monorepo armazena diversos pacotes em um único repositório. Um pipeline ingênuo reconstrói e testa todos eles a cada alteração, o que se torna mais lento à medida que o repositório cresce.
A solução é a detecção de mudanças. Filtros de caminho e grafos de dependência informam ao pipeline quais pacotes são afetados por um diff, e apenas esses são buildados.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run test --filter='...[origin/main]'
O filtro seleciona os pacotes alterados desde main, além de tudo que dependa deles. Uma alteração em um utilitário compartilhado dispara seus dependentes; uma alteração em um pacote folha dispara apenas a si mesmo.
Adicione um remote cache para que a saída compilada e os resultados dos testes sejam compartilhados entre máquinas e execuções. Se as entradas de um pacote não foram alteradas, seu build é restaurado em vez de ser recomputado. Em um monorepo, isso geralmente traz um ganho maior do que o cache de dependências.
Mantenha o pipeline rigoroso: uma alteração no lockfile da raiz ou em configurações compartilhadas ainda deve executar a suíte completa. Testes seletivos são uma otimização que nunca deve permitir que um pacote quebrado passe despercebido.
Segurança do pipeline
O pipeline detém credenciais de produção, o que o torna um alvo de alto valor. Trate os arquivos de workflow como código sensível e revise cuidadosamente qualquer alteração neles.
Fixe as actions em uma versão. Uma tag como @v4 é um ponteiro móvel; um commit SHA completo é imutável. Fixar em um SHA é a opção mais rigorosa e evita que uma tag comprometida execute código malicioso em seu pipeline.
Conceda o menor privilégio possível. Defina permissions explicitamente em cada workflow e job, mantendo como padrão o acesso apenas de leitura, e adicione escopos de escrita apenas onde for necessário.
permissions:
contents: read
O GITHUB_TOKEN padrão geralmente é mais amplo do que qualquer job necessita. Restringi-lo significa que uma etapa comprometida não poderá fazer push de commits ou publicar pacotes.
Prefira OIDC em vez de chaves estáticas, conforme descrito acima. Credenciais de curta duração vinculadas a uma função específica eliminam o segredo de longa duração que os atacantes mais desejam.
Não execute código não confiável com secrets. Pull requests de forks são o vetor clássico. Use pull_request para build e teste, exija aprovação para contribuidores de primeira viagem e nunca faça o checkout e a execução de código de forks em um workflow que tenha acesso a secrets.
Revise actions de terceiros. Uma action é uma dependência que roda com as suas credenciais. Prefira actions do próprio forge ou de publicadores conhecidos, e audite as demais.
Melhores práticas
- Gere o artefato exatamente uma vez e promova-o através de cada ambiente.
- Identifique artefatos por commit SHA ou digest, nunca apenas por
latest. - Injete a configuração em tempo de execução; mantenha o build idêntico em todos os lugares.
- Execute o lint e o typecheck antes da suíte de testes (que é mais lenta) para que as falhas sejam detectadas rapidamente.
- Torne as verificações importantes obrigatórias na proteção de branch.
- Faça o cache de dependências usando o hash do lockfile como chave, com restore keys.
- Restrinja segredos a ambientes específicos e prefira credenciais OIDC de curta duração.
- Nunca imprima segredos e nunca os exponha a pull requests não confiáveis.
- Fixe a versão de actions de terceiros e defina permissões de token de privilégio mínimo.
- Use
concurrencypara cancelar execuções superadas. - Mantenha o pipeline rápido; um pipeline lento acaba sendo ignorado.
- Faça com que os rollbacks sejam um redeploy do digest anterior.
Erros comuns
- Reconstruir a imagem de forma diferente para cada ambiente e chamar isso de promoção.
- Embutir a configuração do ambiente na imagem durante o build.
- Usar
latestcomo a única tag e perder a rastreabilidade. - Exibir secrets em um passo de debug e assumir que o mascaramento captura tudo.
- Expor secrets para workflows disparados por pull requests de forks.
- Referenciar actions de terceiros através de uma branch instável.
- Deixar as permissões do token padrão totalmente abertas.
- Marcar cada check opcional como obrigatório e tornar os merges extremamente lentos.
- Deixar testes flaky ficarem vermelhos até que ninguém mais leia os resultados.
- Fazer cache agressivamente sem uma chave que invalide com mudanças reais nos inputs.
- Reconstruir tudo em um monorepo a cada alteração.
- Não ter um caminho de rollback além de reverter um commit e esperar.
Próximos passos
Um pipeline termina com a implantação de um artefato, portanto, o próximo passo natural são as Plataformas de Nuvem, onde a release, os health checks e os rollbacks realmente acontecem. O artefato geralmente é um container, e o guia de Docker aborda as imagens e os multi-stage builds que o pipeline produz. A maioria dos runners são máquinas Linux, então o guia de Linux explica o shell e as ferramentas das quais seus steps dependem, e o de Node.js fundamenta o runtime e a toolchain que estão sendo construídos.