Maven and Gradle Builds skill for Codex

Reproducible JVM builds with explained versions. Codex works through the wrapper and the declared Java toolchain, traces version conflicts with dependency:tree or dependencyInsight before changing anything, and fixes them with BOMs, platforms and constraints instead of piles of exclusions. It moves shared configuration into convention plugins, turns on build and configuration caching safely, and diffs dependency trees so every moved version is reported.

  • Languages
  • $maven-gradle-builds
  • 203 lines
  • 3 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- maven-gradle-builds

Installs to ~/.agents/skills/maven-gradle-builds/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $maven-gradle-builds, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Fixes and structures JVM builds in Maven and Gradle (Kotlin DSL) - wrappers, Java toolchains, BOMs and platforms, version catalogs (libs.versions.toml), dependency conflict resolution with dependency:tree and dependencyInsight, multi-module layouts, convention plugins, build and configuration cache, dependency locking and verification. Use when a Java or Kotlin build fails on a version conflict or NoSuchMethodError, when converting build.gradle to build.gradle.kts, adding a module, speeding up slow builds, or upgrading Gradle or Maven plugins.

Use it when

  • A Java or Kotlin build fails, or runtime errors point to mismatched library versions
  • You are adding a module or restructuring a multi-module Maven or Gradle project
  • Migrating Groovy Gradle scripts to Kotlin DSL or adopting libs.versions.toml
  • Builds are slow or not reproducible across machines and CI

Use something else when

How to check the result

The wrapper build passes from a clean checkout, test counts are unchanged, and a before/after dependency tree diff lists exactly the versions that moved.

The mistake it prevents

Fixing a conflict with stacked exclusions instead of a BOM or constraint, so the next transitive upgrade silently reintroduces the old version.

~/.agents/skills/maven-gradle-builds/SKILL.md

--- name: maven-gradle-builds description: Fixes and structures JVM builds in Maven and Gradle (Kotlin DSL) - wrappers, Java toolchains, BOMs and platforms, version catalogs… license: MIT ---

Done well means: the build runs from a clean checkout with only the wrapper (./mvnw or ./gradlew) and a JDK, versions are declared in one place (BOM, parent POM or version catalog), every conflict resolution is explicit and explained, and the build is reproducible on CI with the same JDK toolchain.

Inspect first

ls mvnw gradlew pom.xml build.gradle* settings.gradle* gradle/libs.versions.toml 2>/dev/null
./gradlew --version            # Gradle and the JVM running it
./mvnw -v
cat gradle/wrapper/gradle-wrapper.properties .mvn/wrapper/maven-wrapper.properties 2>/dev/null

Read AGENTS.md for the canonical build and test command. Always use the wrapper, never a globally installed mvn or gradle, so the version matches CI. First runs download the distribution and dependencies (network); with network off, try --offline (Gradle) or -o (Maven) against the local cache before asking for approval.

Which tool’s rules apply

SituationChoice
Existing projectKeep its tool. Migrating Maven to Gradle (or back) is a project, not a fix
New Kotlin or Android projectGradle with Kotlin DSL and a version catalog
New plain Java service, team values convention over flexibilityMaven 3.9.x with the wrapper
Groovy build.gradle to Kotlin DSLConvert module by module; see references/gradle-kotlin-dsl.md

Java version: toolchains, not the host JDK

// build.gradle.kts
java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
<!-- pom.xml -->
<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

Gradle toolchains can auto-provision a JDK (needs network and the foojay resolver plugin in settings). Maven compiles with the JDK running Maven unless you configure toolchains; release guarantees the API level but CI must still run a JDK at least that new.

Dependency versions in one place

Gradle version catalog (gradle/libs.versions.toml):

[versions]
spring-boot = "3.5.6"
junit = "5.13.4"

[libraries]
spring-boot-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
dependencies {
    implementation(platform(libs.spring.boot.bom))   // versions come from the BOM
    implementation(libs.jackson.databind)
    testImplementation(platform(libs.junit.bom))
    testImplementation(libs.junit.jupiter)
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Maven: import BOMs in dependencyManagement and omit versions in dependencies:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Version numbers above are examples; check the latest stable release before writing them.

Resolving conflicts

ToolResolution ruleInspect
MavenNearest definition wins (shortest path), then first declared./mvnw dependency:tree -Dverbose -Dincludes=com.fasterxml.jackson.core
GradleHighest requested version wins (plus constraints, platforms, rich versions)./gradlew :app:dependencyInsight --dependency jackson-databind --configuration runtimeClasspath

Symptoms of a conflict: NoSuchMethodError, ClassNotFoundException, NoClassDefFoundError, IncompatibleClassChangeError at runtime while compilation passed.

Fix in this order:

  1. Align with a BOM/platform for the library family (Jackson, Netty, gRPC, AWS SDK).
  2. Upgrade the direct dependency that drags the old version.
  3. Maven: declare the version in dependencyManagement. Gradle: a constraints {} block with a because("...") reason.
  4. Exclusions only for genuinely unwanted artifacts (duplicate logging bindings), with a comment saying why.

Guard against regressions: Maven Enforcer dependencyConvergence or requireUpperBoundDeps; Gradle failOnVersionConflict() only for small graphs (it is noisy).

Multi-module structure

  • Maven: parent POM with <modules>, shared pluginManagement and dependencyManagement. Build one module plus what it needs: ./mvnw -pl app -am verify.
  • Gradle: settings.gradle.kts with include(":app", ":core"); shared configuration in convention plugins under an included build build-logic/, never in allprojects {} / subprojects {} blocks (they break configuration cache and isolated projects).
  • Gradle java-library: api for types exposed in the module’s public signatures, implementation for everything else. Overusing api slows recompilation and leaks dependencies.

Build speed

# gradle.properties
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC
  • Configuration cache is the preferred mode in Gradle 9; plugins that are incompatible are reported by the first run. Fix or upgrade them rather than disabling the cache.
  • Find slow tasks with ./gradlew build --profile (local HTML report). Build scans (--scan) upload data to a third party and need the user’s agreement.
  • Maven: ./mvnw -T 1C verify for parallel modules; skip nothing in CI that protects correctness (-DskipTests only locally).

Reproducibility and supply chain

  • Commit the wrapper files (gradlew, gradle/wrapper/*, mvnw, .mvn/wrapper/*). Update with ./gradlew wrapper --gradle-version <x.y> (run twice: once to update the properties, once to regenerate scripts with the new version) or ./mvnw wrapper:wrapper -Dmaven=<x.y.z>.
  • Gradle dependency locking: dependencyLocking { lockAllConfigurations() } then ./gradlew dependencies --write-locks.
  • Gradle dependency verification: ./gradlew --write-verification-metadata sha256 help writes gradle/verification-metadata.xml; review it, do not regenerate it blindly after a checksum failure.
  • Avoid dynamic versions (1.+, latest.release, Maven version ranges) in applications.

Gotchas

  • Gradle implementation(platform(...)) aligns versions; enforcedPlatform overrides even higher transitive versions and leaks to consumers of libraries. Use it only in applications.
  • Maven <scope>provided</scope> dependencies are absent at runtime outside a container; Spring Boot fat jars need them as compile scope.
  • Kotlin DSL tasks.test { useJUnitPlatform() } is required for JUnit 5; without it Gradle runs zero tests and reports success.
  • Gradle 9 requires Java 17+ to run Gradle itself; compile for older targets via toolchains.
  • A plugin version in both the catalog and pluginManagement causes confusing resolution; declare it once.
  • mvn install to share modules locally hides reactor problems; build from the root with -pl ... -am instead.

Verification

./gradlew clean build --warning-mode all     # deprecations surface before the next major
./gradlew :app:dependencies --configuration runtimeClasspath > deps-after.txt
./mvnw -q clean verify
./mvnw dependency:tree -Dverbose > deps-after.txt

Diff dependency trees before and after any version change and list every resolved version that moved. Confirm the test count did not drop to zero.

Report template

**Build:** Gradle 9.x wrapper, Kotlin DSL, toolchain Java 21, configuration cache on
**Problem:** NoSuchMethodError in JsonMapper: jackson-databind 2.15 forced by legacy-sdk
**Fix:** imported jackson-bom 2.19.x as platform; removed manual exclusion
**Resolved changes:** jackson-* 2.15.4 -> 2.19.2 (6 artifacts); nothing else moved
**Checks:** ./gradlew build green, 1,284 tests run (unchanged)

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Maven and Gradle Builds"
  short_description: "Reproducible JVM builds with explained versions"
  default_prompt: "$maven-gradle-builds We get NoSuchMethodError from Jackson at runtime, find and fix the version conflict"
references/gradle-kotlin-dsl.md93 lines, 4.3 KB
# Groovy build.gradle to Kotlin DSL (build.gradle.kts)

Convert one module at a time: rename the file, fix compile errors in the build script,
run `./gradlew help` and then the module's build. Keep the diff free of unrelated changes.

## Syntax mapping

| Groovy | Kotlin DSL |
|---|---|
| `apply plugin: 'java-library'` | `plugins { `java-library` }` |
| `id 'org.springframework.boot' version '3.5.6'` | `id("org.springframework.boot") version "3.5.6"` (or `alias(libs.plugins.spring.boot)`) |
| `implementation 'g:a:1.0'` | `implementation("g:a:1.0")` |
| `sourceCompatibility = 21` | `java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }` |
| `ext.foo = 'bar'` | `extra["foo"] = "bar"`; read with `val foo: String by extra` (prefer gradle.properties or the catalog) |
| `test { useJUnitPlatform() }` | `tasks.test { useJUnitPlatform() }` |
| `tasks.withType(JavaCompile) { options.encoding = 'UTF-8' }` | `tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }` |
| `task hello { doLast { println 'hi' } }` | `tasks.register("hello") { doLast { println("hi") } }` |
| `configurations { integrationTestImplementation.extendsFrom testImplementation }` | `val integrationTestImplementation by configurations.creating { extendsFrom(configurations.testImplementation.get()) }` |
| `jar { manifest { attributes 'Main-Class': 'app.Main' } }` | `tasks.jar { manifest { attributes("Main-Class" to "app.Main") } }` |
| `exclude group: 'commons-logging'` | `exclude(group = "commons-logging")` |
| String interpolation `"$version"` with single quotes elsewhere | Always double quotes in Kotlin |
| `def x = 1` | `val x = 1` |

Since Gradle 8.2 the Kotlin DSL supports `=` assignment for lazy `Property` types
(`options.release = 21`); older scripts use `.set(21)`. Both are valid on current Gradle.

## Lazy configuration

- Use `tasks.register` and `tasks.named`, not `tasks.create` or `tasks.getByName`; eager
  APIs configure tasks that never run and break configuration avoidance.
- `configureEach` instead of `all` / `each` on task collections.
- Wire task outputs as providers (`from(tasks.named("generate"))`), not file paths, so
  Gradle infers dependencies.

## Settings file

```kotlin
// settings.gradle.kts
pluginManagement {
    includeBuild("build-logic")
    repositories { gradlePluginPortal(); mavenCentral() }
}
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"   // check latest
}
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories { mavenCentral() }
}
rootProject.name = "shop"
include(":app", ":core", ":persistence")
```

`FAIL_ON_PROJECT_REPOS` stops modules adding their own repositories; put every repository
here.

## Convention plugin in build-logic

```
build-logic/
├── settings.gradle.kts         # dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } }
├── build.gradle.kts            # plugins { `kotlin-dsl` }
└── src/main/kotlin/shop.java-conventions.gradle.kts
```

```kotlin
// build-logic/src/main/kotlin/shop.java-conventions.gradle.kts
plugins { java }

java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror"))
}
tasks.withType<Test>().configureEach {
    useJUnitPlatform()
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
}
```

Modules then apply `plugins { id("shop.java-conventions") }`. Accessing the version catalog
inside precompiled script plugins needs `the<VersionCatalogsExtension>().named("libs")`
(type-safe `libs` accessors are not generated there).

## Common errors after conversion

| Error | Fix |
|---|---|
| `Unresolved reference: implementation` | The plugin providing the configuration is not applied in `plugins {}` |
| `Type mismatch: inferred type is Int but Property<Int>` | Use `.set(..)` or upgrade Gradle to 8.2+ for assignment |
| `Unresolved reference: libs` in build-logic | Declare the catalog in build-logic's settings file |
| `Cannot add task 'x' as a task with that name already exists` | Use `tasks.named("x")` to configure an existing task |
| Extremely slow first sync | Kotlin DSL compiles scripts once; later runs are cached. Keep logic in build-logic, not in each script |

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc