Post

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 Build: Lifecycle, Build Files, Scanners and CI

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

CategoryCommon tools
Build toolApache Maven 3.9+, Maven Wrapper (mvnw)
Package registryMaven Central, private Artifactory / Nexus
Build filespom.xml, mvnw, .mvn/wrapper/maven-wrapper.properties, .mvn/maven.config, ~/.m2/settings.xml
Lint and formatCheckstyle, PMD, Spotless, Error Prone
SAST (open source)SpotBugs + FindSecBugs, Semgrep, CodeQL, SonarQube Community Build
SAST (commercial)Checkmarx One, OpenText Fortify, Veracode, Coverity, SonarQube Server
SCAOWASP Dependency-Check, Trivy, Snyk, Mend, Black Duck, JFrog Xray
FrameworksSpring Boot, Quarkus, Micronaut, Jakarta EE
Test and coverageJUnit 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:

  1. Source + pom.xml: A commit triggers CI. pom.xml declares dependencies and plugins, and the committed Maven Wrapper (mvnw) pins the Maven version.
  2. Resolve dependencies: Maven downloads dependencies and plugins from Maven Central or a private Nexus / Artifactory into ~/.m2/repository, which CI caches.
  3. Lint and format: ./mvnw -B checkstyle:check fails on style violations. PMD, Spotless and Error Prone are common additions.
  4. Compile: maven-compiler-plugin:compile runs in the compile phase. Asking for a later phase, such as verify, runs every phase before it.
  5. Test and coverage: maven-surefire-plugin runs the JUnit 5 tests in the test phase, and JaCoCo records coverage (./mvnw -B test jacoco:report).
  6. 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.
  7. Quality gate: ./mvnw -B -ntp verify is the CI target: unit tests, integration tests (Failsafe) and every check bound to verify must pass.
  8. Fail the build (fail): A failing test or check fails the verify phase, so nothing is packaged, signed or deployed.
  9. Package (pass): The package phase writes the .jar (or .war / .ear) to target/.
  10. SBOM and signing (pass): The CycloneDX Maven plugin (makeAggregateBom) writes target/bom.json, and the artifact is signed with Sigstore cosign.
  11. Deploy to the repository (pass): ./mvnw -B deploy uploads to the repository in <distributionManagement>. install is only for local multi-project builds, not CI.
  12. 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

FilePurposeCommit?
pom.xmlProject model: coordinates, dependencies, plugins, profilesYes
mvnw, mvnw.cmdMaven Wrapper scripts, pin the Maven versionYes
.mvn/wrapper/maven-wrapper.propertiesMaven distribution URL and version for the wrapperYes
.mvn/maven.configDefault CLI flags (for example -B -ntp)Yes
.mvn/jvm.configJVM options for Maven itselfYes
~/.m2/settings.xmlMirrors, server credentials, proxiesNo (CI generates it)
~/.m2/toolchains.xmlJDK locations for the toolchains pluginNo
target/Build outputNo (.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

FrameworkStart a Maven project
Spring Bootcurl https://start.spring.io/starter.zip -d type=maven-project -d dependencies=web -o demo.zip
Quarkusquarkus create app com.example:demo
Micronautmn 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 ./mvnw everywhere so developers and CI use the same Maven version.
  • Run verify, not install, in CI; bind quality checks (coverage, SpotBugs, Checkstyle) to verify so 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); add maven-enforcer-plugin with requireMavenVersion and dependencyConvergence.
  • Mirror everything through Artifactory or Nexus in settings.xml so builds do not hit Maven Central directly and credentials never live in pom.xml.
  • Never release -SNAPSHOT dependencies; the enforcer requireReleaseDeps rule catches them.
  • Set project.build.outputTimestamp for reproducible jars.
This post is licensed under CC BY 4.0 by the author.