Skip to content

rewrite-maven: new recipes for reproducible builds #7476

@timtebeek

Description

@timtebeek

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

  1. 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.
  2. 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().
  3. 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.
  4. 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

  1. 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.
  2. 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

  1. 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.
  2. 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

  1. ReproducibleBuilds — declarative composite (in maven.yml) bundling tier 1 + tier 2 in order:
    1. NoVersionRanges
    2. cleanup.ExplicitDependencyVersion (existing)
    3. cleanup.ExplicitPluginVersion (existing)
    4. NoSnapshotVersions (find-only)
    5. UpgradePluginVersionsForReproducibleBuilds
    6. AddSourceEncodingProperty
    7. 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.ymlUpgradePluginVersionsForReproducibleBuilds, 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).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions