CI für Java-Projekte mit Maven mithilfe von GitHub Actions

1. Übersicht

Continuous Integration (CI) hilft dir, Fehler sofort beim Push zu entdecken, statt bis zum Merge zu warten und dann alles zu zerstören. Dieser Artikel zeigt dir, wie du eine CI-Pipeline für Java-Projekte mit Maven auf GitHub Actions aufbaust: Build, Tests ausführen, Dependencies cachen, Build-Matrix für mehrere JDK-Versionen und Report-Veröffentlichung. Nach dem Lesen kannst du die Workflow-Datei in dein echtes Projekt kopieren und alles in wenigen Minuten zum Laufen bringen.

2. Beispiel-Projekt vorbereiten

Angenommen, du hast ein einfaches Spring-Boot-Projekt mit Standard-Maven-Struktur. Eine minimale pom.xml-Datei sieht so aus:

<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>

Die Abhängigkeiten junit-jupiter und assertj-core werden zum Schreiben von Tests verwendet. Das Plugin maven-surefire-plugin ist dafür verantwortlich, Tests auszuführen, wenn du mvn test aufrufst.

Eine kleine Business-Klasse zum Testen:

package com.nonladev.order;

public class OrderCalculator {

    public double calculateTotal(double unitPrice, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity muss größer als 0 sein");
        }
        return unitPrice * quantity;
    }
}

Und der entsprechende Test:

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 muss größer als 0 sein");
    }
}

shouldCalculateTotalCorrectly überprüft die korrekte Berechnung der Summe. shouldThrowWhenQuantityIsInvalid überprüft die Exception, wenn quantity ungültig ist. Das sind die Tests, die CI jedes Mal ausführt, wenn du Code pushst.

3. Basis-CI-Workflow

GitHub Actions liest YAML-Dateien im Verzeichnis .github/workflows/. Erstelle die Datei .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

Erklärung der einzelnen Teile:

  • on: Der Workflow läuft beim push oder pull_request zum Branch main.
  • actions/checkout@v4: Klont den Quellcode auf den Runner.
  • actions/setup-java@v4: Installiert JDK 21 (Temurin-Version) – du brauchst diesen Schritt immer, bevor du mvn aufrufst.
  • mvn -B clean verify: Das Flag -B (batch mode) deaktiviert die interaktive Ausgabe, was für CI-Umgebungen geeignet ist. clean verify erstellt alles neu und führt Tests aus (die Phase verify beinhaltet test).

Pushe einen Commit hoch, gehe zum Tab Actions auf GitHub, und du wirst sehen, dass der Job läuft, mit Logs wie:

[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

Wenn Tests fehlschlagen, wird der Job rot und du siehst den Stack Trace direkt im Log – ohne SSH auf eine Maschine.

4. Maven-Dependencies cachen für schnellere Builds

Jedes Mal, wenn ein Job auf einem neuen Runner (ephemeralisch) läuft, muss Maven alle Abhängigkeiten aus .m2/repository erneut herunterladen. Bei einem Spring-Boot-Projekt kann das 1–2 Minuten pro Mal dauern. setup-java unterstützt bereits Caching:

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: 'maven'

Das Attribut cache: 'maven' speichert automatisch das Verzeichnis ~/.m2/repository basierend auf dem Hash der pom.xml-Datei. Beim nächsten Build, falls sich pom.xml nicht geändert hat, wird der Cache sofort wiederhergestellt, sodass der Download aller JAR-Dateien übersprungen wird.

Du kannst die Effizienz überprüfen, indem du die Zeit des Schritts „Build and run tests” vor und nach dem Hinzufügen des Caches vergleichst – normalerweise reduziert sich die Zeit um 30–60 %.

Hinweis: Wenn dein Projekt Maven Wrapper (./mvnw) verwendet, solltest du nach mvnw cachen, anstatt dich auf die vorinstallierte Maven-Version auf dem Runner zu verlassen, da sich die Standard-Maven-Version ändern kann, wenn GitHub das Image aktualisiert.

5. Build-Matrix mit mehreren JDK-Versionen

Wenn du eine Bibliothek pflegst, die mit mehreren Java-Versionen kompatibel sein muss, verwende strategy.matrix, um mehrere Jobs parallel auszuführen:

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 listet die Versionen auf, die du testen möchtest. GitHub Actions erstellt automatisch 2 Jobs, die parallel laufen: eine mit JDK 17, eine mit JDK 21. Die Variable ${{ matrix.java-version }} wird in jedem entsprechenden Job ersetzt.

Das Ergebnis im Tab Actions zeigt 2 separate Jobs, zum Beispiel build (17) und build (21), sodass du sofort sehen kannst, bei welcher Java-Version dein Projekt fehlschlägt.

6. Test-Reports veröffentlichen und Coverage überprüfen

Standardmäßig werden Test-Logs nur als Text in Actions angezeigt. Für einen besseren lesbaren Report verwendest du die 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(): Dieser Schritt läuft auch, wenn Tests fehlschlagen (aber nicht, wenn der Job abgebrochen wird), damit du immer einen Report siehst. path zeigt auf die XML-Datei, die Surefire nach dem Test-Lauf generiert. Der Report erscheint als separate Überprüfung im PR und listet jeden Test-Fall auf (Pass/Fail).

Wenn du Coverage messen möchtest, füge das JaCoCo-Plugin zu pom.xml hinzu:

<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>

Das Ausführen von mvn verify generiert automatisch einen HTML-Report unter target/site/jacoco/index.html. Du kannst ihn als Artifact hochladen:

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: jacoco-report
          path: target/site/jacoco/

Dieses Artifact kann auf der Summary-Seite des Workflow-Runs heruntergeladen und direkt im Browser angesehen werden.

7. Build-Status-Badge

Nachdem der Workflow stabil läuft, füge ein Badge zu README.md hinzu, damit jeder den CI-Status sieht, ohne Actions zu öffnen:

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

Ersetze <org>/<repo> mit deinem echten GitHub-Namen. Das Badge aktualisiert sich automatisch auf grün/rot, je nachdem wie der letzte Lauf auf dem Standard-Branch war.

8. Häufige Fehler

  • Vergessen, java-version mit maven.compiler.source abzustimmen: Der lokale Build funktioniert, aber CI schlägt mit Fehler release version X not supported fehl. Synchronisiere die JDK-Version immer zwischen pom.xml und Workflow.
  • mvn test statt mvn verify verwenden: Wenn dein Projekt Integration Tests in der Phase verify hat (zum Beispiel Failsafe plugin), ignoriert mvn test diese und erzeugt ein falsches Sicherheitsgefühl.
  • Falscher Cache-Schlüssel beim Ändern von Abhängigkeiten: Wenn du eine Cache-Action manuell schreibst (actions/cache) und vergisst, pom.xml in den Schlüssel-Hash einzubeziehen, könnte der alte Cache dazu führen, dass der Build veraltete Abhängigkeiten verwendet. Verwende cache: 'maven' von setup-java, um das zu vermeiden.
  • CI auf selbst gehosteten Runnern ohne Docker/stabiles Netzwerk ausführen: Maven muss Abhängigkeiten aus dem zentralen Repository herunterladen; wenn der Runner netzwerkblockiert ist, hängt der Build beim Download. Überprüfe den Proxy oder verwende einen internen Maven-Mirror.

Hinweis: Wenn dein Projekt sehr klein ist, nur ein paar einfache Tests hat und nicht regelmäßig deployt, könnte ein vollständiger CI-Workflow mit Matrix + Coverage Report zu viel sein. Beginne mit dem Basis-Workflow aus Abschnitt 3 und erweitere ihn schrittweise, wenn dein Projekt wächst.

9. Fazit

Mit ein paar Dutzend Zeilen YAML hast du eine CI-Pipeline, die automatisch dein Maven-Projekt built, testet, Dependencies cached und Reports veröffentlicht. Beginne mit einem einfachen Workflow und füge dann Matrix JDK, JaCoCo Coverage und README-Badges nach deinen praktischen Anforderungen hinzu. Das Wichtigste ist nicht ein komplexer Workflow, sondern dass CI stabil und zuverlässig läuft, jedes Mal wenn du Code pushst.

github-actionsjavamavenci-cdtesting