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 beimpushoderpull_requestzum Branchmain.actions/checkout@v4: Klont den Quellcode auf den Runner.actions/setup-java@v4: Installiert JDK 21 (Temurin-Version) – du brauchst diesen Schritt immer, bevor dumvnaufrufst.mvn -B clean verify: Das Flag-B(batch mode) deaktiviert die interaktive Ausgabe, was für CI-Umgebungen geeignet ist.clean verifyerstellt alles neu und führt Tests aus (die Phaseverifybeinhaltettest).
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 nachmvnwcachen, 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:

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-versionmitmaven.compiler.sourceabzustimmen: Der lokale Build funktioniert, aber CI schlägt mit Fehlerrelease version X not supportedfehl. Synchronisiere die JDK-Version immer zwischenpom.xmlund Workflow. mvn teststattmvn verifyverwenden: Wenn dein Projekt Integration Tests in der Phaseverifyhat (zum Beispiel Failsafe plugin), ignoriertmvn testdiese 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.xmlin den Schlüssel-Hash einzubeziehen, könnte der alte Cache dazu führen, dass der Build veraltete Abhängigkeiten verwendet. Verwendecache: 'maven'vonsetup-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.