Skip to content

Repository files navigation

Java Parent

Maven parent POM for Java projects by Open Elements.

Inherit from this POM to get a consistent, modern Java build out of the box: pinned plugin versions, managed dependency BOMs, code formatting, an SBOM, and a complete publish-to-Maven-Central release pipeline — without copying boilerplate into every project.

Coordinates

<parent>
    <groupId>com.open-elements</groupId>
    <artifactId>java-parent</artifactId>
    <version>1.0.0</version>
</parent>

Released artifacts are published to Maven Central; -SNAPSHOT builds are published to the Central Portal snapshot repository.

Requirements

Tool Version Enforced by
Java 21 maven-enforcer-plugin (requireJavaVersion)
Maven 3.9.11+ maven-enforcer-plugin (requireMavenVersion) + wrapper

The build fails fast in the validate phase if the local toolchain does not meet these requirements. Both minimums can be raised by a child project by overriding the enforcer.requiredJavaVersion / enforcer.requiredMavenVersion properties.

A Maven Wrapper (./mvnw) pinned to 3.9.11 is included, so no local Maven installation is required.

What you get

Managed dependency versions (BOM imports)

Import-scoped BOMs so child projects can declare these dependencies without a <version>:

  • Spring Bootspring-boot-dependencies (3.5.14)
  • Testcontainerstestcontainers-bom (2.0.5)
  • OpenAPIspringdoc-openapi-bom (2.8.17) and swagger-bom (2.2.47), plus org.webjars:swagger-ui (5.32.2) — see The OpenAPI stack

The OpenAPI stack

springdoc, Swagger and the Swagger UI webjar are one coupled set, and the parent manages all three so a child gets a version-uniform stack without configuring anything.

This is not tidiness. springdoc declares its Swagger dependency without a version and inherits it from its own aggregator POM, so Swagger floats in every consuming project and is decided by nearest-wins mediation. A library that uses only the Swagger annotations declares only that artifact — the normal thing to do — and its declaration sits closer to the application than the swagger-core-jakarta springdoc contributes two levels down. The stack then splits along exactly that boundary, and Swagger's own modules call each other across incompatible releases:

io.swagger.v3.core.jackson.ModelResolver        (swagger-core-jakarta 2.2.47)
  → io.swagger.v3.oas.annotations.media.Schema.$dynamicRef()
                                                 absent in annotations 2.2.29

The result is a NoSuchMethodError at schema resolution, not a missing feature. Managing the versions in the parent removes the mediation entirely, because dependencyManagement is consulted before nearest-wins and applies at any depth.

What this does not cover. Management overrides transitive resolution only. A project that declares a Swagger artifact directly with an explicit <version> still wins over the managed version and keeps whatever it had. Dropping that <version> activates the managed one.

Retargeting the stack in a child. Setting <swagger.version> in a child moves all 14 Swagger coordinates at once. The floor is 2.2.47: swagger-bom was first published at that version, so a lower value fails the build with a non-resolvable import. Use a direct declaration with a <version> to pull an older Swagger.

Maintenance: bump the three together

springdoc.version, swagger.version and swagger-ui.version must move together. Bumping springdoc alone would publish a split stack to every child project at once, and this parent's own build would not notice — it has no Java sources and never exercises the stack.

The matching values are not exposed by springdoc-openapi-bom, which manages springdoc artifacts only. Read them from springdoc's aggregator POM:

org/springdoc/springdoc-openapi/<version>/springdoc-openapi-<version>.pom
  <swagger-api.version>   → swagger.version
  <swagger-ui.version>    → swagger-ui.version

For reference: springdoc 2.8.6 pairs with Swagger 2.2.29 and Swagger UI 5.20.1; springdoc 2.8.17 pairs with 2.2.47 and 5.32.2.

Pinned plugin versions

All common build plugins are version-managed in <pluginManagement>, so child builds are reproducible and free of "you should pin this plugin" warnings:

maven-resources, maven-compiler, maven-surefire, maven-javadoc, maven-source, maven-gpg, maven-jar, maven-deploy, maven-clean, maven-enforcer, cyclonedx, jreleaser, versions, spotless.

Build conventions

  • Java 21, source/target via maven.compiler.*.
  • UTF-8 for sources and reporting.
  • -parameters compiler flag (parameter names retained — useful for frameworks like Spring and Jackson).
  • Reproducible builds via an inherited project.build.outputTimestamp (see Reproducible builds).
  • Code formatting via Spotless using Google Java Format.
  • Surefire pre-configured with the --add-opens flags commonly needed by reflection-based test/mocking libraries.
  • Toolchain enforcement (see Requirements).
  • LF line endings enforced for the formatter, so spotless:apply never writes CRLF (see Line endings).

Line endings

This repository pins line endings, and it does so for a concrete reason: with packaging=pom and no flatten-maven-plugin, its pom.xml is deployed to Maven Central verbatim. Checked out on a machine with core.autocrlf=true — the Windows default — every text file arrives with CRLF, and the published .pom would differ from one built elsewhere. For a project with Java sources the same variance lands in the sources jar and in every copied resource.

.gitattributes forces LF in the working tree on every platform, overriding whatever core.autocrlf or core.eol the developer has set. Windows batch files are the single exception, because cmd.exe is fragile with LF-only scripts. .editorconfig mirrors the same rules for editors.

This eliminates line endings as a source of byte-level variance. It does not by itself make builds reproducible across operating systems — that involves further variables and is not claimed here.

What child projects need to do

Git attributes are repository-local: Maven inheritance cannot deliver them. A child project inherits the Spotless setting below, but must carry its own .gitattributes. Copy this file into the repository root:

# Force LF in the working tree on every platform, overriding local core.autocrlf.
*               text=auto eol=lf

# Windows batch files are the exception: cmd.exe is fragile with LF-only scripts.
*.bat           text eol=crlf
*.cmd           text eol=crlf

# Truly binary files: never normalized, never diffed as text.
*.class         binary
*.eot           binary
*.gif           binary
*.gz            binary
*.ico           binary
*.jar           binary
*.jks           binary
*.jpeg          binary
*.jpg           binary
*.p12           binary
*.pdf           binary
*.png           binary
*.ttf           binary
*.woff          binary
*.woff2         binary
*.zip           binary

Then run git add --renormalize . once and commit whatever it stages. In a repository that never received CRLF this is a no-op.

Take this project's .editorconfig along with it. Its [*.java] block is set to 2 spaces and a 100 column limit, matching the Google Java Format this parent enforces via Spotless — a .editorconfig specifying anything else makes the editor fight the formatter on every save. The [*.{cmd,bat}] block keeps editors in agreement with the CRLF pin above.

What the parent enforces on its own

The parent sets <lineEndings>UNIX</lineEndings> on the Spotless plugin, which every child inherits. Spotless defaults to GIT_ATTRIBUTES, so without this a spotless:apply run on a Windows machine in a repository lacking .gitattributes would actively write CRLF into Java sources.

The reach of that setting is narrow, and worth stating plainly: it applies only to files Spotless formats, and only when spotless:apply or spotless:check is invoked — neither is bound to a lifecycle phase. It guarantees the mandated formatter never introduces CRLF. It does not make a child's sources jar LF-clean; only the child's own .gitattributes does that.

Reproducible builds

Building the same source twice yields byte-identical artifacts — jar, sources jar, javadoc jar and the CycloneDX SBOM.

This works because java-parent declares a fixed timestamp that every child project inherits:

<properties>
    <project.build.outputTimestamp>2026-08-28T00:00:00Z</project.build.outputTimestamp>
</properties>

Child projects set nothing. They inherit the value by pinning a java-parent version, and release.sh updates it whenever a new java-parent release is cut.

What is promised

The same source, built with the same toolchain, produces byte-identical artifacts — regardless of when or where the build runs.

"Same toolchain" is part of the promise, not a footnote. Reproducibility across differing JDK patch versions, Maven versions, operating systems or locales has not been measured and is not claimed. Javadoc output in particular is known to vary between JDK builds. Use the Java and Maven versions this project enforces.

Verifying a release yourself

No build flags and no insider knowledge are needed. Apart from its deployment arguments, this is the same command CI runs:

git checkout vA.B.C          # any release that carries the timestamp property
./mvnw -Pfull-build clean verify

Then compare the result against the artifacts published on Maven Central, for example with sha256sum. They must match.

Do not pass -Dproject.build.outputTimestamp, and do not override the property in a child POM. Both take precedence over the inherited value (-D beats a child POM, which beats the parent), so either one silently produces artifacts that nobody else can reproduce.

The timestamp is not a build time

project.build.outputTimestamp records which java-parent release an artifact was built against. It is deliberately not the time the build ran — a real build time cannot be reproduced, which is the whole point.

If an application needs to answer "which state is this?", use the Git metadata that the full-build profile writes into every jar's META-INF/MANIFEST.MF:

Manifest entry Meaning
Git-Commit-Time When the source state came into being (UTC, commit-derived)
Git-Commit Abbreviated commit id
Git-Branch Branch the build came from
Git-Tag Tags pointing at the commit
Git-Dirty Whether the working tree had uncommitted changes

Git-Commit-Time is the correct replacement for any buildTime / build.time field. Such a field sourced from project.build.outputTimestamp would be misleading and must not be introduced.

When the build runs outside a Git checkout — for example from a published source archive — these entries are present but empty. The build deliberately does not fail (failOnNoGitDirectory is false) so that reproducing from sources stays possible, and the empty values are themselves deterministic.

Known limitations

Tracked in docs/TODO.md:

  • No automated check guards reproducibility — a plugin upgrade could reintroduce non-determinism unnoticed.
  • Cross-toolchain reproducibility is unmeasured — see What is promised.
  • Snapshot builds are not reproducible, by decision. A project pinning a -SNAPSHOT parent inherits a value that moves when the snapshot is republished.
  • A project pinning a java-parent version older than the first release carrying the property inherits nothing and stays non-reproducible. Raising the parent version is what actually switches this on for a downstream project.

Common commands

# Build and test
./mvnw clean verify

# Apply code formatting / check formatting
./mvnw spotless:apply
./mvnw spotless:check

# Full build: also attaches Javadoc jar, sources jar and a CycloneDX SBOM
./mvnw -Pfull-build clean verify

# Check for newer dependency, plugin and property versions
./check-dependencies.sh

Checking for updates

check-dependencies.sh runs the versions-maven-plugin and writes three reports to target/:

  • dependency-updates.txt
  • plugin-updates.txt
  • property-updates.txt

Build profiles

Profile Purpose
full-build Attaches the Javadoc jar, sources jar, and generates a CycloneDX SBOM. Used for releases & CI.
deploy-release Signs artifacts (GPG) and publishes to Maven Central + creates the GitHub release via JReleaser.

Releasing

Releases are cut with release.sh and finished by CI — the script only prepares git state, it never deploys:

./release.sh <release-version> <next-snapshot-version>
# e.g.
./release.sh 1.1.0 1.2.0-SNAPSHOT

The script:

  1. Sets the release version in the POM.
  2. Pins project.build.outputTimestamp to the release date and verifies the rewrite took effect — a stale timestamp would publish artifacts that no rebuild could match, so the release aborts rather than continuing.
  3. Runs ./mvnw -Pfull-build clean verify locally — so a broken build, missing Javadoc link, or SBOM error fails here, not after the tag is pushed. Because the timestamp is already pinned, these are the bytes CI will publish.
  4. Best-effort generates upgrade documentation under docs/releases/ (requires the Claude Code CLI; skipped with a warning if absent).
  5. Commits version and timestamp together, tags vA.B.C, pushes, then bumps to the next -SNAPSHOT (leaving the timestamp at the release date).

Pushing the vA.B.C tag triggers the release workflow, which verifies the POM version matches the tag, stages artifacts, and publishes to Maven Central while creating the GitHub release.

Continuous integration

Workflow Trigger What it does
build.yml Pull requests to main ./mvnw clean verify
snapshot.yml Push to main Publishes -SNAPSHOT artifacts to the Central Portal snapshot repo.
release.yml Push of a v*.*.* tag Builds, signs, and deploys to Maven Central; creates a GitHub release.

License

Licensed under the Apache License, Version 2.0.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages