CI cho dự án Java với Maven bằng GitHub Actions
1. Tổng quan
Continuous Integration (CI) giúp bạn phát hiện lỗi ngay khi push code, thay vì đợi đến lúc merge mới “vỡ trận”. Bài này hướng dẫn bạn dựng pipeline CI cho dự án Java dùng Maven trên GitHub Actions: build, chạy test, cache dependency, build matrix nhiều phiên bản JDK, và publish report. Đọc xong, bạn có thể copy file workflow vào dự án thật và chạy ngay trong vài phút.
2. Chuẩn bị dự án mẫu
Giả sử bạn có một dự án Spring Boot đơn giản với cấu trúc Maven chuẩn. File pom.xml tối thiểu như sau:
<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>
Dependency junit-jupiter và assertj-core dùng để viết test. Plugin maven-surefire-plugin chịu trách nhiệm chạy test khi gọi mvn test.
Một class nghiệp vụ nhỏ để test:
package com.nonladev.order;
public class OrderCalculator {
public double calculateTotal(double unitPrice, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity phải lớn hơn 0");
}
return unitPrice * quantity;
}
}
Và test tương ứng:
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 phải lớn hơn 0");
}
}
shouldCalculateTotalCorrectly kiểm tra tính tổng đúng. shouldThrowWhenQuantityIsInvalid kiểm tra exception khi quantity không hợp lệ. Đây là test mà CI sẽ chạy mỗi lần bạn push code.
3. Workflow CI cơ bản
GitHub Actions đọc file YAML trong thư mục .github/workflows/. Tạo file .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
Giải thích từng phần:
on: workflow chạy khi cópushhoặcpull_requestnhắm vào branchmain.actions/checkout@v4: clone source code vào runner.actions/setup-java@v4: cài JDK 21 bản Temurin — bạn luôn cần bước này trước khi gọimvn.mvn -B clean verify: cờ-B(batch mode) tắt output tương tác, phù hợp môi trường CI.clean verifybuild lại từ đầu và chạy test (phaseverifybao gồmtest).
Push commit lên, vào tab Actions trên GitHub, bạn sẽ thấy job chạy và log tương tự:
[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
Nếu test fail, job sẽ có status đỏ và bạn thấy stack trace ngay trong log — không cần SSH vào máy nào cả.
4. Cache dependency Maven để build nhanh hơn
Mỗi lần job chạy trên runner mới (ephemeral), Maven phải tải lại toàn bộ dependency từ .m2/repository. Với dự án Spring Boot, việc này có thể tốn 1–2 phút mỗi lần. setup-java hỗ trợ cache sẵn:
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
Thuộc tính cache: 'maven' tự động cache thư mục ~/.m2/repository dựa trên hash của file pom.xml. Lần build sau, nếu pom.xml không đổi, Actions phục hồi cache ngay, bỏ qua bước download lại từng jar.
Bạn có thể kiểm tra hiệu quả bằng cách xem thời gian step “Build and run tests” trước và sau khi thêm cache — thường giảm 30–60%.
Lưu ý: Nếu dự án dùng Maven Wrapper (
./mvnw) thì nên cache theomvnwthay vì tin tưởng phiên bản Maven cài sẵn trên runner, vì mỗi lần GitHub update image, phiên bản Maven mặc định có thể thay đổi.
5. Build matrix với nhiều phiên bản JDK
Nếu bạn maintain thư viện cần tương thích nhiều phiên bản Java, dùng strategy.matrix để chạy song song nhiều job:
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 liệt kê danh sách phiên bản muốn test. GitHub Actions tự tạo 2 job chạy song song: một với JDK 17, một với JDK 21. Biến ${{ matrix.java-version }} được thay vào từng job tương ứng.
Kết quả trên tab Actions hiển thị 2 job riêng biệt, ví dụ build (17) và build (21), giúp bạn biết ngay dự án fail ở phiên bản Java nào.
6. Publish test report và kiểm tra coverage
Mặc định, log test chỉ hiển thị dạng text trong Actions. Để có report dễ đọc hơn, dùng 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(): step này chạy cả khi test fail (nhưng không chạy nếu job bị cancel), để bạn luôn thấy report. path chỉ tới file XML mà Surefire sinh ra sau khi chạy test. Report hiện ra như một check riêng trong PR, liệt kê từng test case pass/fail.
Nếu muốn đo coverage, thêm plugin JaCoCo vào 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>
Chạy mvn verify sẽ tự sinh report HTML tại target/site/jacoco/index.html. Bạn có thể upload nó làm artifact:
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: jacoco-report
path: target/site/jacoco/
Artifact này tải về được từ trang summary của workflow run, xem trực tiếp bằng browser.
7. Badge trạng thái build
Sau khi workflow chạy ổn định, thêm badge vào README.md để mọi người biết trạng thái CI mà không cần vào Actions:

Thay <org>/<repo> bằng tên GitHub thật. Badge tự cập nhật xanh/đỏ theo kết quả lần chạy gần nhất trên branch mặc định.
8. Lỗi thường gặp
- Quên set
java-versionkhớp vớimaven.compiler.source: build local pass nhưng CI fail với lỗirelease version X not supported. Luôn đồng bộ phiên bản JDK giữapom.xmlvà workflow. - Dùng
mvn testthay vìmvn verify: nếu dự án có integration test chạy ở phaseverify(ví dụ Failsafe plugin),mvn testsẽ bỏ qua chúng, tạo cảm giác an toàn giả. - Cache sai key khi đổi dependency: nếu bạn tự viết cache action thủ công (
actions/cache) mà quên đưapom.xmlvào key hash, cache cũ có thể khiến build dùng dependency lỗi thời. Dùngcache: 'maven'củasetup-javađể tránh việc này. - Chạy CI trên self-hosted runner không có Docker/network ổn định: Maven cần tải dependency từ repository trung tâm; nếu runner bị chặn mạng, build sẽ treo ở bước download. Kiểm tra proxy hoặc dùng Maven mirror nội bộ.
Lưu ý: Nếu dự án của bạn rất nhỏ, chỉ có vài test đơn giản và không deploy thường xuyên, một workflow CI đầy đủ với matrix + coverage report có thể là overkill. Bắt đầu với workflow cơ bản ở mục 3, rồi mở rộng dần khi dự án lớn lên.
9. Kết luận
Với vài chục dòng YAML, bạn đã có pipeline CI tự động build, test, cache dependency và publish report cho dự án Maven. Bắt đầu từ workflow đơn giản, rồi thêm matrix JDK, JaCoCo coverage, badge README theo nhu cầu thực tế. Điều quan trọng nhất không phải là workflow phức tạp, mà là CI chạy ổn định và đáng tin cậy mỗi lần bạn push code.