Setup for maven projects#
See github-maven-release-test as an example project.
Requirements of Maven Central#
Releases are published to Maven Central via the Central Portal. Some rules are checked there. To comply with them, the pom.xml must contain:
<url>: a URL for the project, e.g. the URL of the GitHub project.<licenses>,<scm>and at least one<developers><developer>.- Sources and Javadoc artifacts and a signature for every artifact.
The release action provides the GPG key and activates the profile publish-release. Signing is skipped for local builds with gpg.skip:
<properties>
<gpg.skip>true</gpg.skip>
</properties>
<profiles>
<profile>
<id>publish-release</id>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.11.0</version>
<extensions>true</extensions>
<configuration>
<autoPublish>true</autoPublish>
<publishingServerId>central</publishingServerId>
<waitUntil>published</waitUntil>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
Quality assurance#
All Maven projects use the same QA tools. The action (📡) Verify runs mvn verify, which executes all of them. A violation fails the build.
| Tool | Purpose | Configuration |
|---|---|---|
| Error Prone | bug patterns at compile time | -Werror for the main code: every warning is an error |
| NullAway | null safety at compile time | JSpecify mode, annotated packages com.sitepark, off for test code |
| JSpecify | null annotations | the code is @NullMarked, nullable points use @Nullable |
| PMD | static analysis | pmd-ruleset.xml of the project, failurePriority 5, includes tests |
| Spotless | formatting | google-java-format, sorted pom.xml |
| JaCoCo | test coverage | whole project: 85 % lines, 80 % branches |
| Maven Enforcer | build environment | Java 25, Maven 3.8, no duplicate dependencies |
SpotBugs is no longer used. Error Prone and NullAway cover its checks.
Java version#
The projects are compiled for Java 25:
<properties>
<maven.compiler.release>25</maven.compiler.release>
<errorprone.version>2.50.0</errorprone.version>
<nullaway.version>0.14.2</nullaway.version>
<pmd.version>7.28.0</pmd.version>
</properties>
Error Prone needs access to internal packages of the compiler. The file .mvn/jvm.config of the project provides it:
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
Null safety#
Every package is @NullMarked: parameters, fields and return values are non-null unless they are annotated with org.jspecify.annotations.Nullable. In a project with a module-info.java the module is annotated, otherwise every package in its package-info.java:
import org.jspecify.annotations.NullMarked;
@NullMarked
module com.sitepark.ies.example {
requires static org.jspecify;
}
The project depends on org.jspecify:jspecify. Builders whose fields are only set by fluent setters suppress the initialization check with @SuppressWarnings("NullAway.Init").
Formatting#
Spotless formats the code locally in the phase process-sources and checks it in the phase verify. In CI the profile ci skips the formatting, so that unformatted code fails the build:
<profile>
<id>ci</id>
<activation>
<property>
<name>env.CI</name>
</property>
</activation>
<properties>
<spotless.apply.skip>true</spotless.apply.skip>
</properties>
</profile>
Test coverage#
JaCoCo requires 85 % line and 80 % branch coverage for the whole project. If a project does not reach these values yet, its measured values are set as minimum together with a comment. They may only be raised, never lowered.
Tests#
Surefire loads Mockito as a Java agent, because the JDK will block its dynamic self-attach. The goal dependency:properties provides the path of the agent as the property ${org.mockito:mockito-core:jar}.
Complete build section#
<build> section of the pom.xml
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
<arg>-Xplugin:ErrorProne -Xep:NullAway:ERROR -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:AnnotatedPackages=com.sitepark</arg>
<!-- Treat every Error Prone / javac warning as a build error. -->
<arg>-Werror</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>${nullaway.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
<executions>
<!-- Test code: Error Prone on, NullAway off (tests pass null intentionally) -->
<execution>
<id>default-testCompile</id>
<configuration>
<compilerArgs combine.self="override">
<arg>-XDcompilePolicy=simple</arg>
<arg>--should-stop=ifError=FLOW</arg>
<arg>-Xplugin:ErrorProne -Xep:NullAway:OFF</arg>
</compilerArgs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<!-- Provides the property ${org.mockito:mockito-core:jar} for the Mockito agent -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.11.0</version>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<!-- Load Mockito as agent, the JDK blocks its dynamic self-attach in the future -->
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar}</argLine>
<consoleOutputReporter>
<disable>true</disable>
</consoleOutputReporter>
<statelessTestsetInfoReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5StatelessTestsetInfoTreeReporter">
<hideResultsOnSuccess>true</hideResultsOnSuccess>
<theme>UNICODE</theme>
</statelessTestsetInfoReporter>
</configuration>
<dependencies>
<dependency>
<groupId>me.fabriciorby</groupId>
<artifactId>maven-surefire-junit5-tree-reporter</artifactId>
<version>1.5.1</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<configuration>
<doclint>all,-missing</doclint>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
<execution>
<id>sign-artifacts</id>
<goals>
<goal>sign</goal>
</goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<scmCommentPrefix>ci(release):</scmCommentPrefix>
<tagNameFormat>@{project.version}</tagNameFormat>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.3</version>
<dependencies>
<dependency>
<groupId>io.github.thefolle</groupId>
<artifactId>glowing-waffle</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>enforce-maven</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<banDuplicatePomDependencyVersions/>
<requireJavaVersion>
<version>25</version>
</requireJavaVersion>
<requireMavenVersion>
<version>3.8</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
<execution>
<id>verify-release</id>
<goals>
<goal>enforce</goal>
</goals>
<phase>none</phase>
<configuration>
<fail>true</fail>
<rules>
<requireReleaseDeps>
<failWhenParentIsSnapshot>false</failWhenParentIsSnapshot>
<message>No Snapshots Allowed!</message>
</requireReleaseDeps>
<requireReleaseDepsInPlugins implementation="org.apache.maven.enforcer.rule.requireReleaseDepsInPlugins"/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.85</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
<execution>
<id>generate-code-coverage-report</id>
<goals>
<goal>report</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.10.3</version>
<configuration>
<java>
<!-- apply a specific flavor of google-java-format and reflow long strings -->
<googleJavaFormat>
<version>1.36.1</version>
<style>GOOGLE</style>
<reflowLongStrings>true</reflowLongStrings>
<formatJavadoc>false</formatJavadoc>
</googleJavaFormat>
<removeUnusedImports>
<engine>google-java-format</engine>
</removeUnusedImports>
</java>
<pom>
<includes>
<include>pom.xml</include>
</includes>
<sortPom>
<!-- value of -1 indicates that a tab character should be used instead -->
<nrOfIndentSpace>-1</nrOfIndentSpace>
</sortPom>
</pom>
</configuration>
<executions>
<!-- Formats locally; skipped in the ci profile, so that the check below can fail -->
<execution>
<id>spotless-apply</id>
<goals>
<goal>apply</goal>
</goals>
<phase>process-sources</phase>
<configuration>
<skip>${spotless.apply.skip}</skip>
</configuration>
</execution>
<execution>
<id>spotless-check</id>
<?m2e ignore?>
<goals>
<goal>check</goal>
</goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.28.0</version>
<configuration>
<failOnViolation>true</failOnViolation>
<failurePriority>5</failurePriority>
<includeTests>true</includeTests>
<linkXRef>false</linkXRef>
<printFailingErrors>true</printFailingErrors>
<rulesets>
<ruleset>pmd-ruleset.xml</ruleset>
</rulesets>
</configuration>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-core</artifactId>
<version>${pmd.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-java</artifactId>
<version>${pmd.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>pmd</id>
<goals>
<goal>check</goal>
</goals>
<phase>verify</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
The execution verify-release of the enforcer is not bound to a phase. mvn enforcer:enforce@verify-release checks that a release does not depend on snapshots.
Register to app.snyk.io#
Log in with a user who has administration rights for the GitHub organization sitepark. Add the project.
GitHub Actions#
The following GitHub Actions are available for Maven projects. The corresponding workflow files can be found here: GitHub Workflows for Maven Projects