DevOps, CI/CD e contêineres
Jenkins na prática: relatórios de testes, quality gates, artefatos e imagens
Intermediário9 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
Um pipeline que só compila não diz quase nada. Este guia acrescenta o que torna um build confiável: relatórios JUnit e de cobertura, análise estática, uma quality gate do SonarQube capaz de barrar o build, artefatos com fingerprint, uma release Maven para um gerenciador de repositórios e imagens de contêiner construídas e enviadas com as tags certas; depois, junta tudo em um job multibranch.
O que você vai aprender
- Publicar resultados JUnit, cobertura de código e achados de análise estática para que as falhas sejam fáceis de entender
- Integrar o SonarQube a um pipeline e bloquear a promoção com a quality gate
- Arquivar artefatos com fingerprint e fazer deploy de releases em um gerenciador de repositórios
- Construir, taguear, escanear e enviar imagens Docker a partir de um pipeline sem vazar as credenciais do registry
- Configurar um pipeline multibranch para que branches e pull requests sejam buildadas automaticamente
Fazer um pipeline compilar o código corresponde aos primeiros dez por cento do trabalho. O resto é transformar o build em uma fonte de evidências: quais testes rodaram e quais falharam, se a cobertura mudou, se o código novo introduziu defeitos, qual binário exatamente saiu e para onde foi. Sem essas evidências, um build verde significa apenas que o compilador ficou satisfeito, e os times acabam confiando em um checklist manual.
Este guia continua de onde parou o guia de sintaxe do Jenkinsfile. Cada exemplo é um trecho que você pode colar em um pipeline declarativo, e o exemplo que acompanha o texto é um serviço Maven que termina em um gerenciador de repositórios e em um registry de contêineres.
Resultados de testes: falhas fáceis de ler
O step junit lê qualquer XML no formato JUnit, que praticamente todo framework de testes de qualquer linguagem consegue gerar, e o transforma no gráfico de tendência de testes, no histórico de cada teste e no resumo de falhas na página do build.
stage('Test') {
steps {
sh 'mvn -B -ntp verify'
}
post {
always {
junit testResults: '**/target/surefire-reports/*.xml, **/target/failsafe-reports/*.xml',
skipPublishingChecks: false,
keepLongStdio: true
}
}
}
Três detalhes fazem diferença. Coloque o step em post { always }, e não em steps; caso contrário, uma suíte de testes com falha aborta o stage antes de o relatório ser coletado, e você fica com um build vermelho sem nenhum motivo visível. Inclua tanto os padrões do Surefire (testes unitários) quanto os do Failsafe (testes de integração). E mantenha allowEmptyResults no valor padrão, false, em projetos que certamente têm testes: um padrão que silenciosamente não encontra nada é exatamente assim que um pipeline acaba reportando sucesso em um build cujos testes nunca rodaram.
Um relatório publicado muda a forma de lidar com falhas: em vez de rolar um log de console, o desenvolvedor abre o build, vê "3 falhas, 1 nova" e vai direto para o stack trace e o histórico daquele teste específico. O Test Results Analyzer e a detecção de testes instáveis das versões mais recentes do plugin usam os mesmos dados e permitem encontrar os testes que falham de forma intermitente, e não determinística.
Cobertura e análise estática
A cobertura é gerada pela ferramenta de build e publicada pelo Jenkins. Com Maven e JaCoCo, associe o agente e o goal de relatório:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution><id>prepare</id><goals><goal>prepare-agent</goal></goals></execution>
<execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
</executions>
</plugin>
Depois publique com o plugin Coverage, que substitui o antigo plugin do JaCoCo e entende os formatos Cobertura, JaCoCo e no estilo JUnit por meio de um parser comum:
recordCoverage(
tools: [[parser: 'JACOCO', pattern: '**/target/site/jacoco/jacoco.xml']],
sourceCodeRetention: 'EVERY_BUILD',
qualityGates: [
[threshold: 70.0, metric: 'LINE', baseline: 'PROJECT', criticality: 'UNSTABLE'],
[threshold: 80.0, metric: 'LINE', baseline: 'MODIFIED_LINES', criticality: 'FAILURE']
]
)
A segunda gate é a interessante: ela exige 80 % de cobertura nas linhas que a mudança tocou e tolera qualquer que seja a média atual do projeto. É a única regra de cobertura que funciona em uma base de código existente, porque pede melhoria na margem em vez de uma reescrita retroativa.
Warnings do compilador, Checkstyle, PMD, SpotBugs e dezenas de outros analisadores são reunidos pelo plugin Warnings Next Generation em uma única visão, com detecção de novos problemas em relação a um build de referência:
recordIssues(
tools: [java(), checkStyle(pattern: '**/target/checkstyle-result.xml'),
spotBugs(pattern: '**/target/spotbugsXml.xml')],
qualityGates: [[threshold: 1, type: 'NEW', criticality: 'UNSTABLE']],
referenceJob: 'shop-api/main'
)
Colocar a gate sobre os problemas novos, e não sobre o total, é o que torna a análise estática viável em uma base de código que já carrega 4.000 warnings.
A quality gate do SonarQube
O SonarQube agrega problemas, cobertura, duplicação e security hotspots, e aplica uma quality gate configurável. Integrá-lo direito exige duas peças no pipeline: rodar a análise e esperar o veredito.
Configure o servidor uma única vez em Manage Jenkins → System (nome, URL e um token de autenticação armazenado como credencial do tipo secret text). Depois, no pipeline:
stage('Analyse') {
steps {
withSonarQubeEnv('sonarqube-main') {
sh '''mvn -B -ntp sonar:sonar \
-Dsonar.projectKey=shop-api \
-Dsonar.qualitygate.wait=false'''
}
}
}
stage('Quality gate') {
steps {
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
O withSonarQubeEnv injeta a URL do servidor e o token, de modo que nenhuma credencial aparece no comando. A análise é assíncrona: o scanner envia os dados e retorna, e o SonarQube calcula o resultado depois. O waitForQualityGate fica bloqueado até o servidor chamar o Jenkins de volta, o que exige um webhook configurado no SonarQube apontando para https://jenkins.example.com/sonarqube-webhook/. Sem esse webhook, o step espera até o timeout e todo mundo conclui que a integração está quebrada.
Separar a análise e a gate em stages diferentes permite rodar outras tarefas no meio, e colocar o stage da gate em agent none libera o executor durante a espera.
Configure a gate em si sobre a definição de código novo: nenhum novo problema bloqueante ou crítico, cobertura do código novo acima de um limite, duplicação do código novo abaixo de outro, nenhum novo security hotspot sem revisão. Uma gate definida assim pode ser imposta na main desde o primeiro dia, sem um projeto de limpeza prévio.
Artefatos: archive, stash, fingerprint
Três mecanismos movimentam arquivos, e eles não são intercambiáveis.
stash e unstash passam arquivos entre stages dentro de um mesmo build. Eles ficam armazenados no controller, então mantenha-os pequenos: um jar e alguns manifestos, não um workspace.
stage('Build') { steps { sh 'mvn -B -ntp package -DskipTests'; stash name: 'app', includes: 'target/*.jar' } }
stage('Deploy') { agent { label 'deploy' }; steps { unstash 'app'; sh './deploy.sh' } }
archiveArtifacts anexa arquivos ao build de forma permanente, disponíveis para download na página do build e pela API REST. Adicione fingerprint: true para que o Jenkins registre o checksum do arquivo e possa dizer depois qual build produziu determinado binário e quais builds o consumiram.
archiveArtifacts artifacts: 'target/*.jar, target/classes/META-INF/build-info.properties',
fingerprint: true,
onlyIfSuccessful: true
O lugar certo das releases é um gerenciador de repositórios: Nexus, Artifactory ou um registry de pacotes na nuvem. Os arquivos do Jenkins servem para depuração e retenção de curto prazo; o gerenciador de repositórios é o sistema de registro oficial, com políticas de retenção, imutabilidade e controle de acesso.
Publicando releases em um gerenciador de repositórios
Forneça ao pipeline um settings.xml cujas credenciais de servidor venham de variáveis de ambiente, para que o arquivo em si não contenha nenhum segredo:
<settings>
<servers>
<server>
<id>nexus-releases</id>
<username>${env.NEXUS_USR}</username>
<password>${env.NEXUS_PSW}</password>
</server>
</servers>
</settings>
Em seguida, vincule a credencial e faça o deploy. Evite o clássico maven-release-plugin dentro da CI: ele faz dois commits, uma tag e dois builds completos, e falha no meio do caminho com frequência suficiente para deixar um repositório em um estado estranho. A alternativa pensada para CI define a versão de fora:
environment { NEXUS = credentials('nexus-deploy') }
stage('Release') {
when { buildingTag() }
steps {
sh '''
VERSION="${TAG_NAME#v}"
mvn -B -ntp -s ci/settings.xml \
-Drevision="$VERSION" \
-DskipTests \
deploy
'''
}
}
Com <version>${revision}</version> no POM, o pipeline fornece a versão, a tag a define, e um único build produz uma única release imutável. Os snapshots da main vão para o repositório de snapshots; só os builds de tags chegam ao repositório de releases, que a maioria dos gerenciadores de repositórios permite configurar para rejeitar um novo deploy de uma versão existente.
Construindo e enviando imagens de contêiner
O plugin Docker Pipeline encapsula o daemon em steps práticos e cuida da autenticação no registry a partir do armazenamento de credenciais:
stage('Image') {
agent { label 'docker' }
environment {
REGISTRY = 'registry.example.com'
IMAGE = "${REGISTRY}/acme/shop-api"
TAG = "${env.GIT_COMMIT.take(12)}"
}
steps {
unstash 'app'
script {
def image = docker.build("${IMAGE}:${TAG}", "--build-arg JAR_FILE=target/app.jar .")
docker.withRegistry("https://${REGISTRY}", 'registry-credentials') {
image.push()
if (env.BRANCH_NAME == 'main') { image.push('latest') }
}
}
}
}
Para imagens multiarquitetura, ou para construir sem um daemon Docker no agente, use o buildx diretamente:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag "$IMAGE:$TAG" \
--label "org.opencontainers.image.revision=$GIT_COMMIT" \
--label "org.opencontainers.image.source=https://git.example.com/acme/shop-api" \
--provenance=true \
--push .
Vale a pena adicionar esses labels OCI: eles permitem que qualquer pessoa que inspecione uma imagem em execução encontre o commit que a gerou. O guia de fundamentos do Docker cobre o lado do Dockerfile, incluindo os builds multi-stage que mantêm pequena a imagem enviada.
Adicione um scan antes do push e faça o build falhar diante de achados graves:
sh 'trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed "$IMAGE:$TAG"'
O --ignore-unfixed mantém a gate acionável: ela só falha em vulnerabilidades que já têm uma versão corrigida disponível, ou seja, em algo que o time consegue resolver hoje mesmo.
Pipelines multibranch e pull requests
Um pipeline multibranch varre um repositório, cria um job para cada branch que contenha um Jenkinsfile, faz o build a cada push e remove o job quando a branch é apagada. A variante organization folder faz o mesmo para todos os repositórios de uma organização Git. É isso que substitui o hábito de copiar um job e trocar o nome da branch.
Aponte-o para o repositório, forneça as credenciais e configure:
- Discover branches: normalmente "excluir branches que também estão abertas como PR", para que uma pull request não seja buildada duas vezes.
- Discover pull requests from origin and forks: a estratégia útil é fazer o merge da PR com a branch de destino, porque ela testa o que estaria na
maindepois do merge, e não o que está na branch agora. - Orphaned item strategy: apagar os jobs alguns dias depois que a branch desaparece.
- Scan triggers: varredura periódica como rede de segurança, com um webhook fazendo o trabalho de verdade.
Dentro do Jenkinsfile, env.BRANCH_NAME, env.CHANGE_ID, env.CHANGE_TARGET e env.TAG_NAME permitem que um único arquivo atenda a todos os contextos:
stage('Publish snapshot') {
when { allOf { branch 'main'; not { changeRequest() } } }
steps { sh 'mvn -B -ntp -s ci/settings.xml deploy -DskipTests' }
}
Os builds de pull request devem ser rápidos e rigorosos quanto à corretude; os builds da branch main acrescentam o trabalho mais lento: suítes de integração completas, build e push da imagem, deploy em um ambiente compartilhado. Mantenha o ciclo de feedback das pull requests abaixo de dez minutos, ou as pessoas param de esperar por ele.
Juntando tudo
Um pipeline realista para um serviço acaba tendo, em linhas gerais, esta cara: build e testes unitários em um agente em contêiner; publicação dos resultados e da cobertura no post; análise estática e Sonar em paralelo com os testes de integração; espera pela quality gate; build e scan da imagem; push com uma tag imutável; deploy automático em staging; deploy em produção por trás de uma aprovação input ou a partir de um build de tag.
Cada uma dessas etapas produz evidências anexadas ao build: tendências de testes, variação da cobertura, veredito do Sonar, artefatos com fingerprint, o digest da imagem. Quando alguém pergunta "o que está rodando em produção e de onde veio?", a resposta é um link, e não uma investigação.
Erros comuns
- Relatórios coletados em
stepsem vez depost. Um stage com falha pula essa coleta, e o build mostra uma falha sem nenhum detalhe. - Gates de cobertura sobre o projeto inteiro. São inatingíveis em código legado e acabam desligadas. Aplique a gate às linhas novas ou modificadas.
waitForQualityGatesem o webhook do SonarQube. O step fica bloqueado até o timeout e o time culpa o Jenkins.- Tags Docker que mudam. Fazer deploy de
:latesttorna impossível identificar a imagem que roda em produção. Envie uma tag imutável e faça o deploy dela. - Credenciais interpoladas em strings Groovy. Use strings de shell com aspas simples para que o shell expanda a variável e o valor nunca entre no comando que o Jenkins monta.
- Arquivar workspaces inteiros. Builds de vários gigabytes enchem o disco do controller. Arquive o artefato e os relatórios, e nada mais.
- Publicar releases a partir de um build de branch. Dois builds da mesma branch podem produzir dois binários diferentes com a mesma versão. Publique a partir de tags e faça o deploy do artefato que você já construiu.
Principais pontos
- Publique os resultados do
junitem um blocopost { always }e faça um padrão de relatório vazio resultar em falha, e não em um sucesso silencioso. - Registre a cobertura e os achados de análise estática com gates sobre código novo ou modificado, a única formulação que funciona em uma base de código existente.
- Integre o SonarQube com
withSonarQubeEnvmais um stagewaitForQualityGateseparado, e configure o webhook que faz a espera terminar. - Use
stashdentro de um build,archiveArtifacts ... fingerprint: truepara rastreabilidade e um gerenciador de repositórios como sistema de registro oficial das releases. - Forneça a versão a partir do pipeline em vez de deixar um plugin de release reescrever o POM, e faça deploy de releases apenas a partir de builds de tags.
- Construa as imagens em um agente Docker dedicado ou com um builder sem daemon, tagueie-as pelo commit, adicione labels OCI, escaneie com
--ignore-unfixede faça o push antes do deploy. - Um pipeline multibranch dá automaticamente um build a cada branch e a cada pull request, e apaga o job quando a branch deixa de existir.
Fontes e leituras complementares
Perguntas frequentes
- Por que meu build continua verde quando os testes falham?
- Quase sempre porque o comando de build engoliu a falha: um passo de shell que termina em um pipe, uma execução do Maven com -Dmaven.test.failure.ignore=true ou um step junit cujo padrão não encontra nada enquanto allowEmptyResults está como true. Confira se a ferramenta de testes retorna um código de saída diferente de zero e se o step junit reporta o número de testes esperado. Um padrão de relatório que não encontra nada deve fazer o build falhar, e não passar.
- A quality gate deve quebrar o build?
- Na branch principal, sim, desde que a gate esteja configurada sobre o código novo, e não sobre a base de código inteira. Uma gate sobre a cobertura geral de um projeto legado falha para sempre e acaba desativada em menos de um mês. Uma gate sobre código novo (cobertura das linhas adicionadas, nenhum novo problema bloqueante, nenhum novo bloco duplicado) é viável desde o primeiro dia e impede que a base de código piore.
- O que o fingerprinting de um artefato faz de fato?
- O Jenkins registra o MD5 do arquivo e guarda qual build o produziu e quais builds o usaram depois. Diante de um jar encontrado em um servidor, você pode perguntar ao Jenkins qual build o gerou e, a partir de um build, ver todos os lugares por onde a saída dele passou. Ativar isso no archiveArtifacts não custa nada, e é a forma mais barata de rastreabilidade que um pipeline interno pode ter.
- Como as imagens de contêiner devem ser tagueadas na CI?
- Tagueie cada imagem com algo imutável e rastreável, como o hash curto do commit ou a versão semântica do build, e envie essa tag. Acrescente uma tag móvel, como o nome da branch ou latest, apenas por conveniência para as pessoas. Os deploys devem referenciar a tag imutável ou, melhor ainda, o digest, para que refazer o deploy da mesma referência gere sempre exatamente os mesmos bytes.