DevOps, CI/CD y contenedores
Jenkins en la práctica: informes de tests, quality gates, artefactos e imágenes
Intermedio9 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Un pipeline que solo compila apenas te dice nada. Esta guía añade lo que hace que un build sea fiable: informes JUnit y de cobertura, análisis estático, una quality gate de SonarQube capaz de detener el build, artefactos con huella, una release de Maven a un gestor de repositorios e imágenes de contenedor construidas y subidas con tags correctos; y después lo conecta todo en un job multibranch.
Lo que aprenderás
- Publicar resultados JUnit, cobertura de código y hallazgos de análisis estático para que los fallos se entiendan
- Integrar SonarQube en un pipeline y bloquear la promoción con la quality gate
- Archivar artefactos con su huella y desplegar releases en un gestor de repositorios
- Construir, etiquetar, escanear y subir imágenes Docker desde un pipeline sin filtrar las credenciales del registro
- Configurar un pipeline multibranch para que ramas y pull requests se construyan automáticamente
Conseguir que un pipeline compile el código es el primer diez por ciento del trabajo. El resto consiste en convertir un build en una fuente de evidencias: qué tests se ejecutaron y cuáles fallaron, si la cobertura varió, si el código nuevo introdujo defectos, qué binario exacto salió y adónde fue. Sin esas evidencias, un build en verde solo significa que el compilador estaba contento, y los equipos acaban confiando en una checklist manual.
Esta guía retoma el hilo donde lo deja la guía de sintaxis del Jenkinsfile. Cada ejemplo es un fragmento que puedes pegar en un pipeline declarativo, y el ejemplo que recorre la guía es un servicio Maven que termina en un gestor de repositorios y en un registro de contenedores.
Resultados de tests: fallos que se entienden
El step junit lee cualquier XML en formato JUnit, que casi cualquier framework de tests de cualquier lenguaje sabe generar, y lo convierte en el gráfico de tendencia de tests, el historial de cada test y el resumen de fallos en la página del 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
}
}
}
Hay tres detalles importantes. Pon el step en post { always } y no en steps; si no, una suite de tests que falla aborta el stage antes de recoger el informe y obtienes un build en rojo sin ningún motivo visible. Incluye tanto los patrones de Surefire (tests unitarios) como los de Failsafe (tests de integración). Y deja allowEmptyResults en su valor por defecto, false, en los proyectos que seguro tienen tests: un patrón que en silencio no encuentra nada es exactamente así como un pipeline acaba informando de éxito en un build cuyos tests nunca se ejecutaron.
Un informe publicado cambia la forma de trabajar con los fallos: en lugar de recorrer un log de consola, el desarrollador abre el build, ve «3 fallidos, 1 nuevo» y salta directamente a la stack trace y al historial de ese test concreto. El Test Results Analyzer y la detección de tests inestables de las versiones recientes del plugin se apoyan en los mismos datos y te permiten localizar los tests que fallan de forma intermitente y no determinista.
Cobertura y análisis estático
La cobertura la genera la herramienta de build y la publica Jenkins. Con Maven y JaCoCo, vincula el agente y el goal de informe:
<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>
Después publica con el plugin Coverage, que sustituye al antiguo plugin de JaCoCo y entiende los formatos Cobertura, JaCoCo y de estilo JUnit mediante un parser común:
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']
]
)
La segunda gate es la interesante: exige un 80 % de cobertura en las líneas que toca el cambio y tolera la media que tenga el proyecto, sea cual sea. Es la única regla de cobertura que funciona en un código existente, porque pide mejorar en el margen en lugar de una reescritura retroactiva.
Los warnings del compilador, Checkstyle, PMD, SpotBugs y decenas de analizadores más se reúnen con el plugin Warnings Next Generation en una única vista, con detección de problemas nuevos respecto a un build de referencia:
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'
)
Poner la gate sobre los problemas nuevos y no sobre el total es lo que hace que el análisis estático sea adoptable en un código que ya arrastra 4.000 warnings.
La quality gate de SonarQube
SonarQube agrega problemas, cobertura, duplicación y security hotspots, y aplica una quality gate configurable. Integrarlo bien requiere dos piezas en el pipeline: lanzar el análisis y esperar el veredicto.
Configura el servidor una sola vez en Manage Jenkins → System (nombre, URL y un token de autenticación guardado como credencial de tipo secret text). Luego, en el 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
}
}
}
withSonarQubeEnv inyecta la URL del servidor y el token, así que ninguna credencial aparece en el comando. El análisis es asíncrono: el scanner sube los datos y termina, y SonarQube calcula el resultado después. waitForQualityGate se queda bloqueado hasta que el servidor avisa a Jenkins, lo que exige un webhook configurado en SonarQube que apunte a https://jenkins.example.com/sonarqube-webhook/. Sin ese webhook, el step espera hasta el timeout y la gente concluye que la integración está rota.
Separar el análisis y la gate en stages distintos permite ejecutar otro trabajo entre medias, y poner el stage de la gate en agent none libera el executor durante la espera.
Configura la gate en sí sobre la definición de código nuevo: ningún problema bloqueante o crítico nuevo, cobertura del código nuevo por encima de un umbral, duplicación del código nuevo por debajo de otro, ningún security hotspot nuevo sin revisar. Una gate definida así se puede imponer en main desde el primer día sin un proyecto de limpieza previo.
Artefactos: archive, stash, fingerprint
Hay tres mecanismos para mover ficheros, y no son intercambiables.
stash y unstash pasan ficheros entre stages dentro de un mismo build. Se guardan en el controller, así que mantenlos pequeños: un jar y unos cuantos manifiestos, no un 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 adjunta ficheros al build de forma permanente, descargables desde la página del build y a través de la API REST. Añade fingerprint: true para que Jenkins registre el checksum del fichero y pueda decirte más adelante qué build produjo un binario determinado y qué builds lo consumieron.
archiveArtifacts artifacts: 'target/*.jar, target/classes/META-INF/build-info.properties',
fingerprint: true,
onlyIfSuccessful: true
El sitio de las releases es un gestor de repositorios: Nexus, Artifactory o un registro de paquetes en la nube. Los archivos de Jenkins sirven para depurar y para una retención a corto plazo; el gestor de repositorios es el sistema de referencia, con políticas de retención, inmutabilidad y control de acceso.
Publicar releases en un gestor de repositorios
Dale al pipeline un settings.xml cuyas credenciales de servidor salgan de variables de entorno, para que el propio fichero no contenga ningún secreto:
<settings>
<servers>
<server>
<id>nexus-releases</id>
<username>${env.NEXUS_USR}</username>
<password>${env.NEXUS_PSW}</password>
</server>
</servers>
</settings>
Después vincula la credencial y despliega. Evita el clásico maven-release-plugin dentro de la CI: hace dos commits, un tag y dos builds completos, y falla a medio camino con la frecuencia suficiente como para dejar un repositorio en un estado extraño. La alternativa pensada para CI fija la versión desde fuera:
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
'''
}
}
Con <version>${revision}</version> en el POM, el pipeline aporta la versión, el tag la define y un único build produce una única release inmutable. Los snapshots de main van al repositorio de snapshots; solo los builds de tags llegan al repositorio de releases, que la mayoría de gestores de repositorios permiten configurar para rechazar el redespliegue de una versión existente.
Construir y subir imágenes de contenedor
El plugin Docker Pipeline envuelve el daemon en steps cómodos y gestiona la autenticación en el registro a partir del almacén de credenciales:
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 imágenes multiarquitectura, o para construir sin un daemon de Docker en el agente, usa buildx directamente:
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 .
Merece la pena añadir esas etiquetas OCI: permiten que cualquiera que inspeccione una imagen en ejecución encuentre el commit que la generó. La guía de fundamentos de Docker cubre la parte del Dockerfile, incluidos los builds multi-stage que mantienen pequeña la imagen que se sube.
Añade un escaneo antes del push y haz que falle ante hallazgos graves:
sh 'trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed "$IMAGE:$TAG"'
--ignore-unfixed mantiene la gate accionable: solo falla con vulnerabilidades que ya tienen una versión corregida disponible, es decir, con algo que el equipo puede resolver hoy mismo.
Pipelines multibranch y pull requests
Un pipeline multibranch escanea un repositorio, crea un job por cada rama que contenga un Jenkinsfile, lo construye en cada push y elimina el job cuando se borra la rama. Su variante organization folder hace lo mismo con todos los repositorios de una organización de Git. Esto es lo que sustituye la costumbre de copiar un job y cambiarle el nombre de la rama.
Apúntalo al repositorio, dale credenciales y configura:
- Discover branches: normalmente «excluir las ramas que también están abiertas como PR», para que una pull request no se construya dos veces.
- Discover pull requests from origin and forks: la estrategia útil es fusionar la PR con la rama destino, porque prueba lo que habría en
maintras el merge y no lo que hay ahora en la rama. - Orphaned item strategy: borrar los jobs unos días después de que desaparezca la rama.
- Scan triggers: un escaneo periódico como red de seguridad, con un webhook haciendo el trabajo de verdad.
Dentro del Jenkinsfile, env.BRANCH_NAME, env.CHANGE_ID, env.CHANGE_TARGET y env.TAG_NAME permiten que un solo fichero sirva para todos los contextos:
stage('Publish snapshot') {
when { allOf { branch 'main'; not { changeRequest() } } }
steps { sh 'mvn -B -ntp -s ci/settings.xml deploy -DskipTests' }
}
Los builds de pull request deben ser rápidos y exhaustivos en cuanto a corrección; los builds de rama en main añaden el trabajo más lento: suites de integración completas, construcción y push de la imagen, despliegue en un entorno compartido. Mantén el ciclo de feedback de las pull requests por debajo de diez minutos o la gente dejará de esperarlo.
Todo junto
Un pipeline realista para un servicio acaba teniendo, a grandes rasgos, esta forma: build y tests unitarios en un agente en contenedor; publicación de resultados y cobertura en post; análisis estático y Sonar en paralelo con los tests de integración; espera de la quality gate; construcción y escaneo de la imagen; push con un tag inmutable; despliegue automático en staging; despliegue en producción tras una aprobación input o desde un build de tag.
Cada uno de esos pasos produce evidencias adjuntas al build: tendencias de tests, variación de cobertura, veredicto de Sonar, artefactos con huella, el digest de la imagen. Cuando alguien pregunta «¿qué está corriendo en producción y de dónde ha salido?», la respuesta es un enlace y no una investigación.
Errores comunes
- Informes recogidos en
stepsen lugar de enpost. Un stage que falla se los salta, y el build muestra un fallo sin ningún detalle. - Gates de cobertura sobre todo el proyecto. Son inalcanzables en código legacy y acaban desactivadas. Aplica la gate a las líneas nuevas o modificadas.
waitForQualityGatesin el webhook de SonarQube. El step se bloquea hasta el timeout y el equipo culpa a Jenkins.- Tags de Docker que se mueven. Desplegar
:latesthace imposible identificar la imagen que corre en producción. Sube un tag inmutable y despliega ese. - Credenciales interpoladas en strings de Groovy. Usa strings de shell con comillas simples para que sea el shell quien expanda la variable y el valor nunca entre en el comando que monta Jenkins.
- Archivar workspaces enteros. Builds de varios gigabytes llenan el disco del controller. Archiva el artefacto y los informes, nada más.
- Publicar releases desde un build de rama. Dos builds de la misma rama pueden producir dos binarios distintos con la misma versión. Publica desde tags y despliega el artefacto que ya construiste.
Ideas clave
- Publica los resultados de
juniten un bloquepost { always }y haz que un patrón de informe vacío sea un fallo y no un éxito silencioso. - Registra la cobertura y los hallazgos de análisis estático con gates sobre código nuevo o modificado, la única formulación que funciona en un código existente.
- Conecta SonarQube con
withSonarQubeEnvmás un stagewaitForQualityGateseparado, y configura el webhook que hace que la espera termine. - Usa
stashdentro de un build,archiveArtifacts ... fingerprint: truepara la trazabilidad y un gestor de repositorios como sistema de referencia para las releases. - Aporta la versión desde el pipeline en lugar de dejar que un plugin de release reescriba el POM, y despliega releases solo desde builds de tags.
- Construye las imágenes en un agente Docker dedicado o con un builder sin daemon, etiquétalas por commit, añade etiquetas OCI, escanea con
--ignore-unfixedy sube la imagen antes de desplegar. - Un pipeline multibranch da automáticamente un build a cada rama y a cada pull request, y elimina el job cuando la rama desaparece.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Por qué mi build sigue en verde cuando fallan los tests?
- Casi siempre porque el comando de build se tragó el fallo: un paso de shell que termina en un pipe, una ejecución de Maven con -Dmaven.test.failure.ignore=true o un step junit cuyo patrón no encuentra nada mientras allowEmptyResults está a true. Comprueba que la herramienta de tests devuelve un código de salida distinto de cero y que el step junit informa del número de tests esperado. Un patrón de informe que no encuentra nada debe hacer fallar el build, no darlo por bueno.
- ¿Debe la quality gate romper el build?
- En la rama principal, sí, siempre que la gate se configure sobre el código nuevo y no sobre todo el código. Una gate sobre la cobertura global de un proyecto legacy falla para siempre y acaba desactivada en menos de un mes. Una gate sobre código nuevo (cobertura de las líneas añadidas, ningún problema bloqueante nuevo, ningún bloque duplicado nuevo) es alcanzable desde el primer día e impide que el código empeore.
- ¿Qué hace realmente el fingerprinting de un artefacto?
- Jenkins registra el MD5 del fichero y recuerda qué build lo generó y qué builds lo usaron después. Si encuentras un jar en un servidor, puedes preguntarle a Jenkins qué build lo produjo, y desde un build puedes ver todos los sitios por los que pasó su salida. Activarlo en archiveArtifacts no cuesta nada y es la forma más barata de trazabilidad que puede tener un pipeline interno.
- ¿Cómo se deben etiquetar las imágenes de contenedor desde la CI?
- Etiqueta cada imagen con algo inmutable y trazable, como el hash corto del commit o la versión semántica del build, y sube ese tag. Añade un tag móvil, como el nombre de la rama o latest, solo por comodidad para las personas. Los despliegues deben referenciar el tag inmutable o, mejor aún, el digest, para que volver a desplegar la misma referencia produzca siempre exactamente los mismos bytes.