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 un push o pull_request dirigidos a la rama main.
  • 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 ejecutar mvn.
  • mvn -B clean verify: la flag -B (batch mode) desactiva la salida interactiva, apropiada para entornos CI. clean verify reconstruye desde cero y ejecuta tests (la phase verify incluye test).

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 en mvnw en 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:

![CI](https://github.com/<org>/<repo>/actions/workflows/ci.yml/badge.svg)

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-version que coincida con maven.compiler.source: el build local pasa pero CI falla con el error release version X not supported. Siempre sincroniza la versión de JDK entre pom.xml y el workflow.
  • Usar mvn test en lugar de mvn verify: si tu proyecto tiene integration tests que se ejecutan en la phase verify (por ejemplo, plugin Failsafe), mvn test los 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 incluir pom.xml en el hash de la clave, el caché antiguo puede hacer que el build use dependencias desactualizadas. Usa cache: 'maven' de setup-java para 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.

github-actionsjavamavenci-cdtesting