7 Min. Lesezeit

Maven 4, the Central Portal and a SNAPSHOT that came from nowhere

A clean CI-friendly Maven 4 release failed validation on the Sonatype Central Portal with SNAPSHOT errors nobody asked for. The real cause was two levels away from where the error message pointed.

I am publishing the RDF core of kognio as open source right now. The first step was a clean release to the Sonatype Central Portal. The Portal is the successor of OSSRH. Every library goes through it today to land in Maven Central. My project has several modules, one version scheme with ${revision}, and it builds with Maven 4. This should be routine.

It was not. The upload worked, but then the validation failed. The error list made no sense to me. I followed two wrong tracks before I found the cause. This post shows the whole way, not just the answer. If you are stuck at the same wall, this will save you an evening.

The setup

If you build many modules in one reactor, you do not want to keep the version number in every POM. The common trick is called CI friendly versions. You write <version>${revision}</version> everywhere. Then you pass the real number at build time with -Drevision=1.0.0. One placeholder, one place, done.

That is what I used here. Locally everything built fine. The artifacts had the correct version number. Only the Portal complained.

The error list that made no sense

The real CI run against the Portal used version 0.0.1. That is a normal release, not a SNAPSHOT. The Portal answered with this:

pkg:maven/io.kogn.rdf/rdf-dataset@0.0.1-SNAPSHOT:
 - The version cannot be a SNAPSHOT
 - POM matching coordinates not found in entries
 - Sources must be provided but not found in entries
 - Javadocs must be provided but not found in entries
 - File path 'io/kogn/rdf/rdf-dataset/0.0.1' is not valid
 - Filename 'rdf-dataset-0.0.1-consumer.pom' is not valid

Look at the first line: 0.0.1-SNAPSHOT. I never passed that version. I built a normal release, but the Portal said my artifacts were SNAPSHOTs. Sources and Javadoc were in the bundle, but the Portal did not find them. And almost every file in the bundle produced two errors, one for the path and one for the filename.

The number of errors was the real hint, but I saw that only later. The same modules showed up under three different versions at the same time: once with the placeholder itself, once as 0.0.1, and once as 0.0.1-SNAPSHOT.

Wrong track number one

My first idea was the ${revision} inside dependencyManagement. You find many threads that say Maven 4 has trouble with placeholders there. That sounded plausible. It also matched the error about SNAPSHOT versions.

But it was wrong. I can tell you now: the placeholder in dependencyManagement is harmless. It took me a while to accept this, because the error message pointed so clearly in that direction.

Wrong track number two

My second idea was Maven 4 itself. It is still a release candidate. Maybe this was a bug, and maybe a newer build already fixed it. So I pulled the latest 4.0.0-SNAPSHOT and built again.

Same result. The fresh build behaved exactly like the release candidate. So this was not a core bug that I could simply wait out.

The real cause

To understand the problem, you need to know one new concept in Maven 4: the consumer POM.

Maven 4 splits two things that used to be one. The build POM is the file you write. It contains ${revision} and everything special about your reactor. The consumer POM is a cleaned up version of it. In the consumer POM, placeholders are replaced by real values. What other people download from Maven Central should be the consumer POM, not your raw build POM.

This is where it went wrong. I opened the finished bundle that goes to the Portal. The official central-publishing-maven-plugin in version 0.11.0 put two POM files in there for each module: the raw .pom and a -consumer.pom next to it. And the main file, the one with the normal file name, was the raw one:

<version>${revision}</version>                    <!-- not resolved -->
<properties><revision>0.0.1-SNAPSHOT</revision>   <!-- the default, not my 1.0.0 -->

There is the SNAPSHOT from nowhere. The file name says 1.0.0, but the content says 0.0.1-SNAPSHOT. The placeholder was never replaced in the raw POM, and the default value of the revision property is a SNAPSHOT.

This single fact explains every error in the list. The content is a SNAPSHOT. The coordinates in the POM do not match the path in the bundle. The dependencyManagement resolves against the SNAPSHOT. The whole layout contradicts itself.

So my version scheme was not the problem. The plugin simply picked the wrong POM file as the main POM.

The fix

There is a community fork of the plugin: io.github.mavenplugins:central-publishing-maven-plugin. It is Apache-2.0, and version 1.3.0 came out in June 2026. The fork writes only one POM file per module, and it is the resolved consumer POM. That is what Maven 4 expects.

Only the groupId and the version change. Everything else stays as it is:

<plugin>
  <!-- was: org.sonatype.central -->
  <groupId>io.github.mavenplugins</groupId>
  <artifactId>central-publishing-maven-plugin</artifactId>
  <version>1.3.0</version>
  <extensions>true</extensions>
  <configuration>
    <publishingServerId>central</publishingServerId>
    <autoPublish>false</autoPublish>
  </configuration>
</plugin>

Here is what the main POM of each module looks like before and after:

main POM per moduleofficial 0.11.0fork 1.3.0
POM files2 (raw plus consumer)1
<version>${revision}1.0.0
revision property0.0.1-SNAPSHOT1.0.0

One detail is interesting. Inside dependencyManagement, the ${revision} still stays as literal text, even with the fork. It turned out that this does not matter at all.

What did not help

Before I found the fork, I tried the flatten-maven-plugin. It looks like the right tool, because it resolves placeholders before the deploy. The result was zero effect. The main POM in the bundle had exactly the same content as before, byte for byte. Maven 4 builds its consumer POM in its own way and ignores what flatten produces. For this problem, flatten is simply the wrong tool.

The proof, in two steps

Talk is cheap, so I uploaded it. First I used a small test project. I did a real upload to central.sonatype.com with the fork, plus sources, Javadoc and a GPG signature. I set autoPublish=false, so nothing could go public by accident. After the test, I dropped the deployment again.

This time the validation only said:

 - Project URL is not defined
 - License information is missing
 - SCM URL is not defined
 - Developers information is missing

No SNAPSHOT errors. No coordinate errors. No path errors. Only the standard required fields were missing, because my test POM did not have them. Those have nothing to do with Maven 4. The ${revision} in dependencyManagement passed without any problem. The validator resolves it correctly as soon as the main POM is resolved, and the fork takes care of that.

The second step closed the last gap. My test project only failed on missing required fields. The real release has all of them. So I put the fork into the release config of the real project and built the same tag again. This time the pipeline finished, the bundle went up, and it reached the status has been validated with no validation errors. The same tag had failed at exactly this step a few minutes earlier with the official plugin.

So a full multi module release validates cleanly with the fork. That also answers the last open question: the ${revision} in dependencyManagement is fine, as long as the main POM is the resolved consumer POM.

Conclusion

The blocker for a CI friendly Maven 4 multi module release on the Central Portal is not your ${revision}, and it is not a bug in Maven itself. It is the official central-publishing-maven-plugin 0.11.0. Under Maven 4, it puts the raw build POM into the bundle instead of the consumer POM.

The fix is one line. Use the fork io.github.mavenplugins in 1.3.0 instead of org.sonatype.central. You do not need flatten. You do not need to disable the consumer POM. You do not need to change your version scheme.

I take two things away from this. First, an error message can point very clearly in one direction and still be wrong about the cause. The SNAPSHOT error was real, but its source was two levels away. Second, with Maven 4 it pays off to open the finished bundle before you blame your own setup. The split between build POM and consumer POM is still new, and plugins handle it with different quality.


This happened during the open source release of the RDF core of kognio. All error messages and bundle contents above come from real runs on Maven 4.0.0-rc-5 and a current 4.0.0-SNAPSHOT.