Maven Build: Lifecycle, Build Files, Scanners and CI
How Maven builds Java projects, which files it needs, how to lint, scan and test with it, and a complete GitHub Actions pipeline.
Maven is the most widely used build tool for Java. It is declarative: you describe the project in pom.xml and Maven runs a fixed lifecycle of phases, with plugins doing the actual work in each phase. That predictability is why Maven is still the default in many enterprises and why it is easy to standardise in CI.
This post is part of Build Ecosystems by Programming Language. The other common Java build tool is covered in Gradle Build.
At a glance
| Category | Common tools |
|---|---|
| Build tool | Apache Maven 3.9+, Maven Wrapper (mvnw) |
| Package registry | Maven Central, private Artifactory / Nexus |
| Build files | pom.xml, mvnw, .mvn/wrapper/maven-wrapper.properties, .mvn/maven.config, ~/.m2/settings.xml |
| Lint and format | Checkstyle, PMD, Spotless, Error Prone |
| SAST (open source) | SpotBugs + FindSecBugs, Semgrep, CodeQL, SonarQube Community Build |
| SAST (commercial) | Checkmarx One, OpenText Fortify, Veracode, Coverity, SonarQube Server |
| SCA | OWASP Dependency-Check, Trivy, Snyk, Mend, Black Duck, JFrog Xray |
| Frameworks | Spring Boot, Quarkus, Micronaut, Jakarta EE |
| Test and coverage | JUnit 5, Mockito, Testcontainers, Surefire / Failsafe, JaCoCo |
| Build output | .jar, .war, .ear, container image via Jib or Buildpacks |
| CI cache | ~/.m2/repository |
How the build works
Maven has three built-in lifecycles (clean, default, site). Running a phase runs every phase before it in the same lifecycle, so mvn verify also compiles, tests and packages. Plugins bind goals to phases: for example maven-compiler-plugin:compile runs in compile, and maven-surefire-plugin:test runs in test.
In CI, each stage of the generic build pipeline maps to a Maven command, plugin or scanner. Press play, or step through it:
- Source + pom.xml: A commit triggers CI. pom.xml declares dependencies and plugins, and the committed Maven Wrapper (mvnw) pins the Maven version.
- Resolve dependencies: Maven downloads dependencies and plugins from Maven Central or a private Nexus / Artifactory into ~/.m2/repository, which CI caches.
- Lint and format: ./mvnw -B checkstyle:check fails on style violations. PMD, Spotless and Error Prone are common additions.
- Compile: maven-compiler-plugin:compile runs in the compile phase. Asking for a later phase, such as verify, runs every phase before it.
- Test and coverage: maven-surefire-plugin runs the JUnit 5 tests in the test phase, and JaCoCo records coverage (./mvnw -B test jacoco:report).
- Security scans in parallel: SpotBugs with FindSecBugs checks our code, OWASP Dependency-Check fails on CVSS 7 or higher, and Gitleaks looks for committed keys and tokens.
- Quality gate: ./mvnw -B -ntp verify is the CI target: unit tests, integration tests (Failsafe) and every check bound to verify must pass.
- Fail the build (fail): A failing test or check fails the verify phase, so nothing is packaged, signed or deployed.
- Package (pass): The package phase writes the .jar (or .war / .ear) to target/.
- SBOM and signing (pass): The CycloneDX Maven plugin (makeAggregateBom) writes target/bom.json, and the artifact is signed with Sigstore cosign.
- Deploy to the repository (pass): ./mvnw -B deploy uploads to the repository in <distributionManagement>. install is only for local multi-project builds, not CI.
- Container image (pass): Jib or Cloud Native Buildpacks build the container image without a Dockerfile, and the registry scans it again.
- verify is the right CI target: it runs unit tests, integration tests (Failsafe) and any quality checks bound to
verify(Checkstyle, SpotBugs, JaCoCo coverage rules). - install is only needed when another local project depends on this one; avoid it in CI.
- deploy publishes to the repository in
<distributionManagement>.
Build files
| File | Purpose | Commit? |
|---|---|---|
pom.xml | Project model: coordinates, dependencies, plugins, profiles | Yes |
mvnw, mvnw.cmd | Maven Wrapper scripts, pin the Maven version | Yes |
.mvn/wrapper/maven-wrapper.properties | Maven distribution URL and version for the wrapper | Yes |
.mvn/maven.config | Default CLI flags (for example -B -ntp) | Yes |
.mvn/jvm.config | JVM options for Maven itself | Yes |
~/.m2/settings.xml | Mirrors, server credentials, proxies | No (CI generates it) |
~/.m2/toolchains.xml | JDK locations for the toolchains plugin | No |
target/ | Build output | No (.gitignore) |
Commands by category
1
2
3
4
5
6
7
8
./mvnw -B -ntp clean verify # build + test + bound checks
./mvnw -B checkstyle:check # lint
./mvnw -B com.github.spotbugs:spotbugs-maven-plugin:check # SAST (FindSecBugs configured in pom)
./mvnw -B org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7 # SCA
./mvnw -B test jacoco:report # tests + coverage report
./mvnw -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom # SBOM (target/bom.json)
./mvnw -B dependency:tree # inspect the dependency graph
./mvnw -B deploy -DskipTests # publish to the remote repository
Minimal project
The pom.xml below builds a Java 21 jar, runs JUnit 5 tests, enforces 80% line coverage with JaCoCo and runs SpotBugs with the FindSecBugs security rules during verify.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- reproducible builds: fixed timestamp inside the jar -->
<project.build.outputTimestamp>2026-01-01T00:00:00Z</project.build.outputTimestamp>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution><id>prepare</id><goals><goal>prepare-agent</goal></goals></execution>
<execution><id>report</id><phase>verify</phase><goals><goal>report</goal></goals></execution>
<execution>
<id>check</id>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>0.80</minimum></limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.6.6</version>
<configuration>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.13.0</version>
</plugin>
</plugins>
</configuration>
<executions>
<execution><phase>verify</phase><goals><goal>check</goal></goals></execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Plugin versions move quickly; check Maven Central for the latest release before copying.
Frameworks
| Framework | Start a Maven project |
|---|---|
| Spring Boot | curl https://start.spring.io/starter.zip -d type=maven-project -d dependencies=web -o demo.zip |
| Quarkus | quarkus create app com.example:demo |
| Micronaut | mn create-app com.example.demo --build=maven |
| Jakarta EE (WAR) | mvn archetype:generate -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=com.example -DartifactId=demo |
CI with GitHub Actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
name: maven-ci
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven # caches ~/.m2/repository
- name: Build, test, coverage gate, SpotBugs
run: ./mvnw -B -ntp verify
- name: Dependency scan (SCA)
run: ./mvnw -B -ntp org.owasp:dependency-check-maven:check -DfailBuildOnCVSS=7 -DnvdApiKey=${{ secrets.NVD_API_KEY }}
- name: SBOM
run: ./mvnw -B -ntp org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
- uses: actions/upload-artifact@v4
with:
name: demo
path: |
target/*.jar
target/bom.json
OWASP Dependency-Check downloads the NVD database on first run. Without an NVD API key this is very slow; request a free key and store it as a repository secret.
Best practices
- Commit the wrapper and call
./mvnweverywhere so developers and CI use the same Maven version. - Run
verify, notinstall, in CI; bind quality checks (coverage, SpotBugs, Checkstyle) toverifyso the build fails in one place. - Use
-B -ntp(batch mode, no transfer progress) for readable CI logs. - Centralise versions with
<dependencyManagement>and BOMs (spring-boot-dependencies,junit-bom); addmaven-enforcer-pluginwithrequireMavenVersionanddependencyConvergence. - Mirror everything through Artifactory or Nexus in
settings.xmlso builds do not hit Maven Central directly and credentials never live inpom.xml. - Never release
-SNAPSHOTdependencies; the enforcerrequireReleaseDepsrule catches them. - Set
project.build.outputTimestampfor reproducible jars.
