CI para proyectos Java con Maven usando GitHub Actions
1. Introducción
La Integración Continua (CI) te ayuda a detectar errores tan pronto como haces push de código, en lugar de esperar hasta que sea demasiado tarde para hacer merge. Este artículo te guía para construir un pipeline CI para proyectos Java con Maven en GitHub Actions: build, ejecución de tests, caché de dependencias, matrix de builds con múltiples versiones de JDK, y publicación de reportes. Después de leerlo, puedes copiar el archivo del workflow en tu proyecto real y ejecutarlo en cuestión de minutos.
2. Prepara el proyecto de ejemplo
Supongamos que tienes un proyecto Spring Boot simple con una estructura Maven estándar. El archivo pom.xml mínimo es el siguiente:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nonladev</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
Las dependencias junit-jupiter y assertj-core se usan para escribir tests. El plugin maven-surefire-plugin es responsable de ejecutar tests cuando ejecutas mvn test.
Una pequeña clase de negocio para testear:
package com.nonladev.order;
public class OrderCalculator {
public double calculateTotal(double unitPrice, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity debe ser mayor que 0");
}
return unitPrice * quantity;
}
}
Y el test correspondiente:
package com.nonladev.order;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class OrderCalculatorTest {
private final OrderCalculator calculator = new OrderCalculator();
@Test
void shouldCalculateTotalCorrectly() {
double total = calculator.calculateTotal(50_000, 3);
assertThat(total).isEqualTo(150_000);
}
@Test
void shouldThrowWhenQuantityIsInvalid() {
assertThatThrownBy(() -> calculator.calculateTotal(50_000, 0))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Quantity debe ser mayor que 0");
}
}
shouldCalculateTotalCorrectly comprueba que el cálculo total sea correcto. shouldThrowWhenQuantityIsInvalid comprueba que se lance una excepción cuando quantity es inválido. Estos son los tests que CI ejecutará cada vez que hagas push de código.
3. Workflow CI básico
GitHub Actions lee archivos YAML en la carpeta .github/workflows/. Crea el archivo .github/workflows/ci.yml:
name: CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build and run tests
run: mvn -B clean verify
Explicación de cada parte:
on: el workflow se ejecuta cuando hay unpushopull_requestdirigidos a la ramamain.actions/checkout@v4: clona el código fuente en el runner.actions/setup-java@v4: instala JDK 21 versión Temurin — siempre necesitas este paso antes de ejecutarmvn.mvn -B clean verify: la flag-B(batch mode) desactiva la salida interactiva, apropiada para entornos CI.clean verifyreconstruye desde cero y ejecuta tests (la phaseverifyincluyetest).
Haz push de tu commit, ve a la pestaña Actions en GitHub y verás el job ejecutándose con un log similar a:
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.nonladev.order.OrderCalculatorTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Si los tests fallan, el job tendrá un estado rojo y verás el stack trace directamente en el log — sin necesidad de hacer SSH a ninguna máquina.
4. Cachea dependencias Maven para builds más rápidos
Cada vez que un job se ejecuta en un runner nuevo (efímero), Maven debe descargar todas las dependencias de nuevo desde .m2/repository. Con proyectos Spring Boot, esto puede tomar 1-2 minutos cada vez. setup-java tiene soporte para caché incorporado:
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
La propiedad cache: 'maven' almacena en caché automáticamente la carpeta ~/.m2/repository basándose en el hash del archivo pom.xml. En el siguiente build, si pom.xml no ha cambiado, Actions restaura el caché inmediatamente, omitiendo la descarga de cada jar de nuevo.
Puedes verificar la efectividad observando el tiempo del step “Build and run tests” antes y después de agregar el caché — generalmente se reduce entre 30-60%.
Nota: Si tu proyecto usa Maven Wrapper (
./mvnw), deberías cachear basándote enmvnwen lugar de confiar en la versión de Maven preinstalada en el runner, ya que cada vez que GitHub actualiza la imagen, la versión de Maven por defecto puede cambiar.
5. Build matrix con múltiples versiones de JDK
Si mantienes una librería que necesita ser compatible con múltiples versiones de Java, usa strategy.matrix para ejecutar múltiples jobs en paralelo:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [ '17', '21' ]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: 'maven'
- name: Build and run tests
run: mvn -B clean verify
matrix.java-version enumera la lista de versiones a probar. GitHub Actions crea automáticamente 2 jobs ejecutándose en paralelo: uno con JDK 17 y otro con JDK 21. La variable ${{ matrix.java-version }} se sustituye en cada job correspondiente.
El resultado en la pestaña Actions muestra 2 jobs separados, por ejemplo build (17) y build (21), ayudándote a saber inmediatamente en qué versión de Java falla tu proyecto.
6. Publica reportes de tests y verifica cobertura
Por defecto, los logs de tests solo se muestran como texto en Actions. Para obtener reportes más legibles, usa la action dorny/test-reporter:
- name: Build and run tests
run: mvn -B clean verify
- name: Publish test report
uses: dorny/test-reporter@v1
if: success() || failure()
with:
name: Maven Tests
path: '**/surefire-reports/*.xml'
reporter: java-junit
if: success() || failure(): este step se ejecuta tanto si los tests pasan como si fallan (pero no si el job se cancela), para que siempre veas el reporte. path apunta al archivo XML que Surefire genera después de ejecutar los tests. El reporte aparece como un check separado en el PR, listando cada test case que pasa o falla.
Si deseas medir la cobertura, agrega el plugin JaCoCo a pom.xml:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
Ejecutar mvn verify genera automáticamente un reporte HTML en target/site/jacoco/index.html. Puedes subirlo como un artifact:
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: jacoco-report
path: target/site/jacoco/
Este artifact se puede descargar desde la página de resumen del workflow run y verlo directamente en el navegador.
7. Badge de estado de construcción
Una vez que el workflow se ejecuta de manera estable, agrega un badge a tu README.md para que todos sepan el estado del CI sin necesidad de entrar a Actions:

Reemplaza <org>/<repo> con tu nombre real de GitHub. El badge se actualiza automáticamente en verde/rojo según el resultado de la ejecución más reciente en la rama predeterminada.
8. Errores comunes
- Olvidar configurar
java-versionque coincida conmaven.compiler.source: el build local pasa pero CI falla con el errorrelease version X not supported. Siempre sincroniza la versión de JDK entrepom.xmly el workflow. - Usar
mvn testen lugar demvn verify: si tu proyecto tiene integration tests que se ejecutan en la phaseverify(por ejemplo, plugin Failsafe),mvn testlos ignorará, creando una falsa sensación de seguridad. - Caché con clave incorrecta al cambiar dependencias: si escribes manualmente la action de caché (
actions/cache) y olvidas incluirpom.xmlen el hash de la clave, el caché antiguo puede hacer que el build use dependencias desactualizadas. Usacache: 'maven'desetup-javapara evitar esto. - Ejecutar CI en un runner auto-hospedado sin Docker/red estable: Maven necesita descargar dependencias del repositorio central; si el runner está bloqueado de la red, el build se colgará en el paso de descarga. Verifica el proxy o usa un mirror de Maven interno.
Nota: Si tu proyecto es muy pequeño, solo tiene algunos tests simples y no hace deployments frecuentes, un workflow CI completo con matrix + coverage report podría ser excesivo. Comienza con el workflow básico de la sección 3 y expande gradualmente a medida que tu proyecto crece.
9. Conclusión
Con apenas unas pocas docenas de líneas de YAML, ya tienes un pipeline CI que automáticamente construye, ejecuta tests, cachea dependencias y publica reportes para tu proyecto Maven. Comienza con un workflow simple, luego agrega matrix de JDK, cobertura de JaCoCo, badges en README según tus necesidades reales. Lo más importante no es un workflow complejo, sino que CI se ejecute de manera estable y confiable cada vez que haces push de código.