Context
The Apache Maven Reproducible Builds guide defines reproducibility as: same source + same toolchain → byte-for-byte identical artifacts. The single most impactful change is setting project.build.outputTimestamp, but that only works if (a) versions are pinned (no SNAPSHOT, no ranges, no LATEST/RELEASE), (b) plugin versions are recent enough to honor outputTimestamp, and (c) source encoding is explicit.
rewrite-maven already covers a meaningful slice of this, but several gaps remain. This issue proposes a set of new recipes — grouped by tier — to close those gaps.
Existing coverage (do not duplicate)
| Concern |
Existing recipe |
LATEST/RELEASE keywords |
org.openrewrite.maven.cleanup.ExplicitDependencyVersion |
| Missing plugin version |
org.openrewrite.maven.cleanup.ExplicitPluginVersion |
Missing plugin groupId |
org.openrewrite.maven.cleanup.ExplicitPluginGroupId |
<scope>system</scope> paths |
org.openrewrite.maven.cleanup.NoSystemScopeDependencies |
Java version pin (<release>) |
org.openrewrite.maven.UseMavenCompilerPluginReleaseConfiguration |
| HTTPS for repos |
org.openrewrite.maven.security.UseHttpsForRepositories |
| Composite of best practices |
org.openrewrite.maven.BestPractices (declarative, in maven.yml) |
Proposed new recipes
Tier 1 — required for reproducibility
AddProjectBuildOutputTimestamp — adds <project.build.outputTimestamp> to the root POM (configurable: a fixed ISO timestamp, or \${git.commit.author.time} when a git-commit-id-style plugin is present). Reuses AddProperty / AddPropertyVisitor. Skips when already set.
NoSnapshotVersions — flags (or upgrades) SNAPSHOT versions across <dependencies>, <dependencyManagement>, <plugins>, <pluginManagement>, and <parent>. Default to find-only (data table) since auto-upgrading SNAPSHOTs is risky; opt-in upgrade mode delegates to UpgradeDependencyVersion / UpgradePluginVersion. Reuses the dated-snapshot detection in ResolvedGroupArtifactVersion.withVersionMaybeSnapshot().
NoVersionRanges — detects Maven version ranges (e.g. [1.0,2.0), (,1.0], 1.+) anywhere a version appears, replacing them with the resolved concrete version from MavenResolutionResult. Reuses org.openrewrite.maven.internal.grammar.VersionRangeParser.
UpgradePluginVersionsForReproducibleBuilds — declarative composite that calls UpgradePluginVersion for the well-known list of plugins where a minimum version is required to honor outputTimestamp (e.g. maven-jar-plugin >= 3.2.0, maven-source-plugin >= 3.2.1, maven-javadoc-plugin >= 3.2.0, maven-assembly-plugin >= 3.2.0, maven-shade-plugin >= 3.2.4, maven-war-plugin >= 3.3.1, maven-ear-plugin >= 3.1.0, maven-archetype-plugin >= 3.2.0, maven-remote-resources-plugin >= 1.7.0, maven-plugin-plugin >= 3.6.2, maven-site-plugin >= 3.9.1). Pure YAML, no new Java needed.
Tier 2 — defense in depth
AddSourceEncodingProperty — adds <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> and <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> if missing. Likely a one-line YAML wrapper around AddProperty.
AddEnforcerRule + per-rule wrappers — generic Java recipe that adds a single rule to maven-enforcer-plugin's <rules> section, creating the plugin/execution if absent. Reuses AddPlugin / ChangePluginConfiguration. Declarative wrappers for the four reproducibility-relevant rules:
enforcer.RequireReleaseDeps
enforcer.RequirePluginVersions
enforcer.RequireUpperBoundDeps
enforcer.DependencyConvergence
Tier 3 — cleanup
RemoveSnapshotRepositories — removes <repository> / <pluginRepository> entries with <snapshots><enabled>true</enabled></snapshots>, and <distributionManagement>/<snapshotRepository> when the project itself does not produce SNAPSHOTs. Visitor pattern from RemoveRepository.
ReproducibleBuildJarPluginConfig — idempotently configures maven-jar-plugin (and analogues for maven-war-plugin / maven-source-plugin) with <archive><manifest><addDefaultEntries>false</addDefaultEntries></manifest></archive>. Largely redundant once outputTimestamp is set on a recent plugin; gated for legacy projects. Reuses ChangePluginConfiguration.
Composite
ReproducibleBuilds — declarative composite (in maven.yml) bundling tier 1 + tier 2 in order:
NoVersionRanges
cleanup.ExplicitDependencyVersion (existing)
cleanup.ExplicitPluginVersion (existing)
NoSnapshotVersions (find-only)
UpgradePluginVersionsForReproducibleBuilds
AddSourceEncodingProperty
AddProjectBuildOutputTimestamp
Optionally referenced from BestPractices so users get reproducibility hygiene by default.
Critical files
rewrite-maven/src/main/java/org/openrewrite/maven/cleanup/ — NoSnapshotVersions, NoVersionRanges, AddProjectBuildOutputTimestamp.
rewrite-maven/src/main/java/org/openrewrite/maven/enforcer/ — new package for AddEnforcerRule + rule-specific recipes.
rewrite-maven/src/main/resources/META-INF/rewrite/maven.yml — UpgradePluginVersionsForReproducibleBuilds, ReproducibleBuilds, enforcer rule wrappers.
rewrite-maven/src/main/resources/META-INF/rewrite/examples.yml — examples for the docs site.
- Reusable building blocks (call, do not modify):
AddProperty, AddPlugin, AddManagedPlugin, ChangePluginConfiguration, UpgradePluginVersion, MavenResolutionResult, ResolvedPom, VersionRangeParser, Semver.
Verification
For each new recipe:
- Add a
*Test class under rewrite-maven/src/test/java/... implementing RewriteTest, using Assertions.pomXml(before, after).
- Cover: idempotency, no-op when already satisfied, multi-module (root vs. child), and parent POM inheritance.
- For composites, run end-to-end against a fixture POM with intentional violations and assert it ends reproducibility-clean.
- Run
./gradlew :rewrite-maven:test and ./gradlew licenseFormat before commit.
Out of scope
- Maven-wrapper checksum pinning (already covered by
UpdateMavenWrapper).
- JDK toolchain pinning (cross-cutting; separate initiative).
- Docker base-image pinning (handled by
rewrite-docker).
settings.xml hygiene (different file, different parser).
Context
The Apache Maven Reproducible Builds guide defines reproducibility as: same source + same toolchain → byte-for-byte identical artifacts. The single most impactful change is setting
project.build.outputTimestamp, but that only works if (a) versions are pinned (noSNAPSHOT, no ranges, noLATEST/RELEASE), (b) plugin versions are recent enough to honoroutputTimestamp, and (c) source encoding is explicit.rewrite-mavenalready covers a meaningful slice of this, but several gaps remain. This issue proposes a set of new recipes — grouped by tier — to close those gaps.Existing coverage (do not duplicate)
LATEST/RELEASEkeywordsorg.openrewrite.maven.cleanup.ExplicitDependencyVersionorg.openrewrite.maven.cleanup.ExplicitPluginVersiongroupIdorg.openrewrite.maven.cleanup.ExplicitPluginGroupId<scope>system</scope>pathsorg.openrewrite.maven.cleanup.NoSystemScopeDependencies<release>)org.openrewrite.maven.UseMavenCompilerPluginReleaseConfigurationorg.openrewrite.maven.security.UseHttpsForRepositoriesorg.openrewrite.maven.BestPractices(declarative, inmaven.yml)Proposed new recipes
Tier 1 — required for reproducibility
AddProjectBuildOutputTimestamp— adds<project.build.outputTimestamp>to the root POM (configurable: a fixed ISO timestamp, or\${git.commit.author.time}when a git-commit-id-style plugin is present). ReusesAddProperty/AddPropertyVisitor. Skips when already set.NoSnapshotVersions— flags (or upgrades)SNAPSHOTversions across<dependencies>,<dependencyManagement>,<plugins>,<pluginManagement>, and<parent>. Default to find-only (data table) since auto-upgrading SNAPSHOTs is risky; opt-in upgrade mode delegates toUpgradeDependencyVersion/UpgradePluginVersion. Reuses the dated-snapshot detection inResolvedGroupArtifactVersion.withVersionMaybeSnapshot().NoVersionRanges— detects Maven version ranges (e.g.[1.0,2.0),(,1.0],1.+) anywhere a version appears, replacing them with the resolved concrete version fromMavenResolutionResult. Reusesorg.openrewrite.maven.internal.grammar.VersionRangeParser.UpgradePluginVersionsForReproducibleBuilds— declarative composite that callsUpgradePluginVersionfor the well-known list of plugins where a minimum version is required to honoroutputTimestamp(e.g.maven-jar-plugin >= 3.2.0,maven-source-plugin >= 3.2.1,maven-javadoc-plugin >= 3.2.0,maven-assembly-plugin >= 3.2.0,maven-shade-plugin >= 3.2.4,maven-war-plugin >= 3.3.1,maven-ear-plugin >= 3.1.0,maven-archetype-plugin >= 3.2.0,maven-remote-resources-plugin >= 1.7.0,maven-plugin-plugin >= 3.6.2,maven-site-plugin >= 3.9.1). Pure YAML, no new Java needed.Tier 2 — defense in depth
AddSourceEncodingProperty— adds<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>and<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>if missing. Likely a one-line YAML wrapper aroundAddProperty.AddEnforcerRule+ per-rule wrappers — generic Java recipe that adds a single rule tomaven-enforcer-plugin's<rules>section, creating the plugin/execution if absent. ReusesAddPlugin/ChangePluginConfiguration. Declarative wrappers for the four reproducibility-relevant rules:enforcer.RequireReleaseDepsenforcer.RequirePluginVersionsenforcer.RequireUpperBoundDepsenforcer.DependencyConvergenceTier 3 — cleanup
RemoveSnapshotRepositories— removes<repository>/<pluginRepository>entries with<snapshots><enabled>true</enabled></snapshots>, and<distributionManagement>/<snapshotRepository>when the project itself does not produce SNAPSHOTs. Visitor pattern fromRemoveRepository.ReproducibleBuildJarPluginConfig— idempotently configuresmaven-jar-plugin(and analogues formaven-war-plugin/maven-source-plugin) with<archive><manifest><addDefaultEntries>false</addDefaultEntries></manifest></archive>. Largely redundant onceoutputTimestampis set on a recent plugin; gated for legacy projects. ReusesChangePluginConfiguration.Composite
ReproducibleBuilds— declarative composite (inmaven.yml) bundling tier 1 + tier 2 in order:NoVersionRangescleanup.ExplicitDependencyVersion(existing)cleanup.ExplicitPluginVersion(existing)NoSnapshotVersions(find-only)UpgradePluginVersionsForReproducibleBuildsAddSourceEncodingPropertyAddProjectBuildOutputTimestampOptionally referenced from
BestPracticesso users get reproducibility hygiene by default.Critical files
rewrite-maven/src/main/java/org/openrewrite/maven/cleanup/—NoSnapshotVersions,NoVersionRanges,AddProjectBuildOutputTimestamp.rewrite-maven/src/main/java/org/openrewrite/maven/enforcer/— new package forAddEnforcerRule+ rule-specific recipes.rewrite-maven/src/main/resources/META-INF/rewrite/maven.yml—UpgradePluginVersionsForReproducibleBuilds,ReproducibleBuilds, enforcer rule wrappers.rewrite-maven/src/main/resources/META-INF/rewrite/examples.yml— examples for the docs site.AddProperty,AddPlugin,AddManagedPlugin,ChangePluginConfiguration,UpgradePluginVersion,MavenResolutionResult,ResolvedPom,VersionRangeParser,Semver.Verification
For each new recipe:
*Testclass underrewrite-maven/src/test/java/...implementingRewriteTest, usingAssertions.pomXml(before, after)../gradlew :rewrite-maven:testand./gradlew licenseFormatbefore commit.Out of scope
UpdateMavenWrapper).rewrite-docker).settings.xmlhygiene (different file, different parser).