DevOps, CI/CD e contêineres
Integração contínua: práticas, pipeline de build e uma configuração funcional
Iniciante10 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Integração contínua significa que cada desenvolvedor integra na branch principal compartilhada pelo menos uma vez por dia e que um build automatizado prova que cada merge compila e passa nos testes. Este guia cobre as práticas por trás da CI, a anatomia de um pipeline de build, um workflow de CI completo para um projeto Java, como manter os builds rápidos e confiáveis e os hábitos que separam um time que pratica CI de um time que apenas tem um servidor de CI.
O que você vai aprender
- Enumerar as práticas que definem a integração contínua e por que cada uma existe
- Projetar as etapas de um build de CI: compilação, testes unitários, análise estática, empacotamento
- Escrever um workflow de CI que rode o build a cada push e a cada pull request
- Manter o build rápido e confiável com cache, paralelismo e uma boa higiene contra testes flaky
- Escolher uma política de merge — trunk-based ou branches de vida curta — que sustente a integração diária
A integração costumava ser uma fase. Os times trabalhavam por semanas em branches separadas e depois passavam dias — às vezes semanas — fazendo merge, descobrindo que duas pessoas tinham alterado a mesma interface de formas incompatíveis e caçando bugs que só apareciam quando tudo era juntado. A integração contínua (CI) dissolve essa fase ao tornar a integração tão frequente que ela nunca dói: todo mundo integra pequenas mudanças em uma branch principal compartilhada todo dia, e um build automatizado prova, depois de cada merge, que o software ainda compila e que os testes ainda passam. A prática é simples de descrever e surpreendentemente exigente de cumprir. Este guia mostra o que ela requer, como se monta um pipeline de build e como manter tudo rápido o bastante para que as pessoas confiem nele.
O que a integração contínua exige
CI é uma prática, e o servidor de CI é só um dos ingredientes. A lista completa:
Um único repositório de código com tudo o que é preciso para buildar o sistema: código, testes, scripts de build, migrações de banco de dados, templates de configuração. Alguém com uma máquina nova e um checkout deve conseguir buildar sem perguntar a um colega o que mais precisa instalar.
Um build automatizado que roda com um único comando. mvn verify, ./gradlew build, npm ci && npm test — a ferramenta exata importa menos do que a garantia de que nenhuma etapa manual está envolvida. O build precisa rodar em qualquer máquina, não só naquela em que foi configurado pela primeira vez.
Um build autotestável. O build executa os testes automatizados e falha se qualquer teste falhar. Um build que só compila prova muito pouco; são os testes que dão significado a um resultado verde.
Todo mundo faz commit na branch principal todo dia. É a regra que os times mais pulam. Uma branch que vive duas semanas representa duas semanas de trabalho não integrado, não importa quão verde esteja o build dela. Merges pequenos e diários mantêm a divergência entre os desenvolvedores pequena o bastante para ser resolvida em minutos.
Cada commit dispara um build em uma máquina de integração. Nada de “na minha máquina funciona”: um ambiente limpo, um checkout novo, os mesmos passos toda vez. Esse é o trabalho do servidor de CI.
Builds quebrados são consertados imediatamente. A branch principal é um patrimônio compartilhado do time; quando ela está vermelha, ninguém consegue fazer merge com segurança, então consertá-la (ou reverter) é a prioridade máxima.
O build é rápido. Feedback que chega uma hora depois não é feedback; é interrupção. Dez minutos é o teto mais citado para o build principal.
Todo mundo consegue ver o resultado. Um dashboard, uma mensagem no Slack, um badge de status no pull request. Esconder o status do build em um servidor que ninguém visita é o jeito certo de deixar builds vermelhos passarem despercebidos.
Anatomia de um build de CI
O build de um serviço típico passa por etapas de custo crescente e frequência decrescente. Cada etapa funciona como um portão: se ela falha, as seguintes não rodam, o que mantém o feedback rápido para os tipos de falha mais comuns.
| Etapa | O que faz | Tempo típico | Falha por |
|---|---|---|---|
| Checkout | Clone limpo do commit em teste | segundos | Repositório inacessível |
| Compilação | Compila o código principal e o de teste | de segundos a um minuto | Erros de sintaxe e de tipo |
| Testes unitários | Testes rápidos e isolados, sem I/O | de um a cinco minutos | Regressões de lógica |
| Análise estática | Linters, verificação de estilo, varredura de vulnerabilidades nas dependências | de um a três minutos | Code smells, CVEs conhecidas |
| Empacotamento | Gera o jar ou a imagem de contêiner | um minuto | Empacotamento mal configurado |
| Testes de integração | Testes contra um banco de dados ou broker reais (muitas vezes em contêiner) | de cinco a quinze minutos | Bugs de wiring, SQL e serialização |
| Publicação | Envia o artefato para um repositório, com a tag da versão | segundos | Permissões do repositório |
O Maven codifica quase tudo isso no seu ciclo de vida: compile, test (testes unitários via Surefire), package, verify (testes de integração via Failsafe, além de verificações como limites de cobertura), install e deploy. Rodar mvn verify na CI, portanto, executa compilação, testes unitários, empacotamento e testes de integração na ordem certa e para na primeira falha.
<!-- pom.xml: run *IT classes as integration tests during "verify" -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
Separar os testes unitários (Surefire, *Test) dos testes de integração (Failsafe, *IT) faz diferença na velocidade: os desenvolvedores rodam mvn test localmente em segundos, enquanto o servidor de CI roda o verify completo.
Um workflow de CI completo
Aqui está um workflow do GitHub Actions para um projeto Maven com Java 21. A mesma estrutura se traduz diretamente em um pipeline declarativo do Jenkins (abordado no guia do Jenkinsfile) ou no GitLab CI.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
# Cancel an older run of the same branch when a new push arrives
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres: # real database for the integration tests
image: postgres:16
env:
POSTGRES_PASSWORD: ci
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-timeout 5s --health-retries 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven # caches ~/.m2 keyed on pom.xml hashes
- name: Build and test
run: mvn -B --no-transfer-progress verify
env:
DB_URL: jdbc:postgresql://localhost:5432/postgres
DB_PASSWORD: ci
- name: Publish test report
if: always() # upload even when tests failed
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: |
target/surefire-reports
target/failsafe-reports
Alguns pontos que merecem atenção. -B (modo batch) impede o Maven de imprimir barras de progresso e códigos de cor que poluem os logs. concurrency com cancel-in-progress evita enfileirar cinco builds para cinco pushes seguidos no mesmo pull request. timeout-minutes impede que um teste travado ocupe um runner por seis horas. O if: always() no upload dos relatórios garante que um build com falha ainda entregue os relatórios de que você precisa para diagnosticá-lo. Fazer cache do repositório Maven com uma chave baseada no conteúdo do pom.xml transforma cinco minutos de download de dependências em poucos segundos quando elas não mudaram.
Mantendo o build rápido
Um build lento é o motivo mais comum para a CI se deteriorar. Estratégias, mais ou menos em ordem de retorno:
Faça cache das dependências. O ~/.m2 do Maven, o cache do Gradle, o node_modules do npm restaurado de um cache com chave no lock file. É o maior ganho isolado em runners hospedados, que começam de uma máquina limpa.
Separe o rápido do lento. Testes unitários no job principal; testes de integração e end-to-end em um segundo job que roda depois, ou em um pipeline noturno se levarem uma hora. Os desenvolvedores recebem primeiro o veredito rápido.
Rode os testes em paralelo. O Surefire e o JUnit 5 conseguem executar classes de teste em paralelo em vários núcleos (<parallel> / junit.jupiter.execution.parallel.enabled), e uma suíte grande pode ser dividida entre vários runners.
Construa só o que mudou. Em um monorepo, ferramentas como o build cache do Gradle, Nx ou Bazel pulam os módulos cujas entradas não mudaram.
Não teste pela interface o que um teste unitário consegue cobrir. Uma pirâmide de testes com muitos testes unitários rápidos, menos testes de integração e um punhado de testes end-to-end é mais rápida e menos flaky do que o inverso. O guia de estratégia de automação de testes mostra como chegar a esse formato.
Acompanhe a tendência. Registre a duração dos builds e coloque em um gráfico. Os builds ficam lentos um teste de cada vez; ninguém percebe até o build de dez minutos passar a levar quarenta.
Testes flaky e confiança
Um teste flaky passa e falha com o mesmo código. O custo dele vai muito além dos minutos desperdiçados: quando os desenvolvedores aprendem que um build vermelho pode ser “só aquele teste flaky”, param de investigar builds vermelhos, e as falhas reais escapam junto com as falsas.
As causas típicas são testes dependentes de ordem que compartilham estado estático, testes que dependem do relógio ou do fuso horário, race conditions em código assíncrono testado com Thread.sleep, colisões de porta quando os testes rodam em paralelo e dependência de serviços externos. A solução nunca é colocar um loop de retry em volta do build inteiro; é colocar o teste em quarentena (movê-lo para um job separado e não bloqueante), abrir uma issue e corrigi-lo ou apagá-lo em poucos dias. Uma suíte de testes só é útil se um resultado vermelho significa alguma coisa.
Estratégia de merge: trunk versus branches
A exigência de integração diária da CI impõe uma restrição à forma como você usa branches.
Trunk-based development — todo mundo faz commit na main, diretamente ou via pull requests que vivem poucas horas — é a abordagem mais alinhada com a CI. Funcionalidades inacabadas ficam escondidas atrás de feature flags (uma chave de configuração que mantém aquele caminho de código desligado em produção) em vez de ficarem em uma branch, de modo que o código é integrado e compilado todo dia enquanto a funcionalidade continua invisível para os usuários.
Feature branches de vida curta com pull requests funcionam bem quando cada branch dura um ou dois dias e o build dela precisa estar verde antes do merge. Exija que a branch esteja atualizada com a main antes do merge (ou use uma merge queue) para que o resultado do build reflita de fato a combinação.
Branches de vida longa — uma branch develop semanas atrás da main, branches de release que recebem correções em paralelo, branches por desenvolvedor integradas uma vez por mês — é aí que a dor da integração reaparece. Às vezes são necessárias (para dar suporte a uma release antiga), mas devem ser a exceção. Os trade-offs entre esses modelos são explorados em fluxos de trabalho com branches no Git.
Verificações em pull requests
A CI moderna roda em pull requests, não só em merges, e as plataformas permitem condicionar o botão de merge a verificações. Verificações obrigatórias sensatas para um serviço:
- O build e os testes unitários passam.
- A análise estática não aponta nenhum problema crítico novo (quality gate do SonarQube, ESLint, Checkstyle).
- A varredura de dependências não aponta nenhuma vulnerabilidade nova de alta severidade (OWASP Dependency-Check, Dependabot, Trivy).
- A cobertura de código nas linhas alteradas não caiu abaixo do limite combinado — uma regra sobre código novo, já que impor um número global a uma base de código legada leva as pessoas a burlar a métrica.
- Pelo menos um revisor aprovou.
Mantenha a lista curta o bastante para que uma mudança rotineira passe em minutos; cada verificação que você adiciona é uma verificação que as pessoas vão tentar contornar se ela for lenta ou barulhenta.
Erros comuns
- Fazer commit na main uma vez por semana a partir de uma branch longa. O servidor de CI roda, mas a integração não é contínua. Integre todo dia; use feature flags para o trabalho inacabado.
- Buildar só no servidor de CI. Se o
mvn verifynão roda localmente, os desenvolvedores fazem push para descobrir se a mudança compila, e o pipeline vira um compilador lento. O build precisa funcionar em um notebook. - Deixar o build vermelho. Depois de um dia, todo mundo supõe que outra pessoa está cuidando disso. Atribua explicitamente ou reverta.
- Testes que batem em ambientes compartilhados. Uma suíte de testes que precisa do banco de staging só consegue rodar uma execução por vez e quebra quando outra pessoa usa o staging. Em vez disso, suba um banco de dados em um contêiner a cada build.
- Segredos no arquivo do pipeline. Uma senha de banco em
ci.ymlfica para sempre no histórico do Git. Use o cofre de segredos do sistema de CI e injete-a como variável de ambiente em tempo de execução. - Ignorar warnings. Um build que imprime trezentos warnings de depreciação esconde aquele que importa. Faça o build falhar com warnings novos ou, pelo menos, acompanhe a contagem.
- Gerar um artefato diferente para cada ambiente. O build de CI deve produzir um único artefato que as etapas seguintes promovem; rebuildar por ambiente significa que aquilo que você testou não é aquilo que você implantou. Essa é a ponte para a entrega contínua.
Principais pontos
- Integração contínua é uma prática — merges diários em uma branch principal compartilhada, verificados por um build automatizado e autotestável — e um servidor de CI sozinho não a entrega.
- Estruture o build em etapas de custo crescente para que as falhas comuns (erros de compilação, regressões em testes unitários) sejam reportadas em minutos.
- Mantenha o build principal abaixo de dez minutos com cache de dependências, testes em paralelo e uma pirâmide de testes sensata; leve as suítes lentas para uma etapa posterior.
- Testes flaky destroem a confiança mais rápido do que builds lentos; coloque-os em quarentena e corrija-os em vez de simplesmente rodar de novo.
- Trunk-based development ou branches curtas com verificações obrigatórias sustentam a integração diária; branches de vida longa recriam a fase de integração que a CI existe para eliminar.
- Uma branch principal vermelha é a maior prioridade do time: corrija em minutos ou reverta.
Fontes e leituras complementares
Perguntas frequentes
- Temos um servidor Jenkins que roda um build toda noite. Isso é integração contínua?
- Não exatamente. Um build noturno avisa na manhã seguinte que algo integrado ontem quebrou o código, depois que todo mundo já foi para casa e, possivelmente, depois que mais mudanças se acumularam por cima. CI significa que o build roda a cada push, termina em minutos e quem quebrou descobre enquanto a mudança ainda está fresca. O servidor é necessário, mas o que faz disso CI é a frequência e a reação.
- Quanto tempo um build de CI deve levar?
- Mire em menos de dez minutos entre o push e o resultado no ciclo de feedback principal. Passando disso, os desenvolvedores param de esperar o resultado, começam a próxima tarefa e tratam as falhas como interrupções em vez de feedback. Suítes mais longas — integração, end-to-end, performance — ficam em uma etapa posterior do pipeline, que roda depois que a rápida passou.
- A CI deve rodar nas feature branches ou só na branch principal?
- Nas duas. Rodar em toda branch e todo pull request pega os problemas antes do merge e mantém a main verde. Mas builds de branch não substituem integrar com frequência: uma branch que está verde há três semanas ainda pode ter conflitos sérios com a main. Mantenha as branches curtas e faça rebase ou merge a partir da main todo dia.
- O que fazemos quando o build está vermelho?
- Consertá-lo vira a maior prioridade de quem quebrou, e ninguém mais faz merge em cima de um build vermelho. Se a correção não ficar óbvia em dez minutos, reverta o commit culpado e investigue em uma branch. Um time que tolera a branch principal vermelha por dias perdeu completamente o benefício da CI.