gradle-developer

SkillProductivity

Guides your agent to write and edit Gradle build scripts correctly in the apache/grails-core repository.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the gradle-developer skill

About this capability

Expert guide for Gradle 9 builds in apache/grails-core on the 8.0.x line - multi-project topology, convention plugins, BOM platforms, dependency rules, task configuration hygiene, and repo-specific patterns that override generic Gradle docs

What this skill tells your AI

The instructions your AI receives, as published by apache/grails-core in .agents/skills/gradle-developer/SKILL.md and read by ahel’s review.

What I Do

  • Write and change Gradle build scripts the way this repository already does them on 8.0.x (Gradle 9.7.x).
  • Keep agents off generic Gradle "best practice" when it conflicts with established monorepo patterns.
  • Cover composite builds (build-logic, grails-gradle, grails-forge, end-to-end), convention plugins, BOM/platform() dependency management, test wiring, publishing hooks, and Gradle 9 task-configuration traps learned from recent PRs.
  • Make Gradle changes boring, copy-paste consistent, and correct on the first try.

When to Use Me

MANDATORY before any of the following:

  • Editing build.gradle, settings.gradle, gradle.properties, dependencies.gradle, or anything under gradle/, build-logic/, grails-gradle/
  • Adding or renaming a module / subproject
  • Bumping a dependency version or the Gradle wrapper
  • Changing test, publish, SBOM, code-style, or JaCoCo Gradle wiring
  • Touching Grails Gradle plugins used by apps (org.apache.grails.gradle.*)
  • Diagnosing configuration-cache, resolution, afterEvaluate, or task-graph failures

Also load when a change looks like application code but requires build script updates (new module, new published artifact, CLI companion jar, functional test app).

Related skills (do not substitute this one):

NeedSkill
CodeNarc / Checkstyle / PMD / SpotBugs reportsviolation-fixer
Failing tests / aggregate reportstest-fixer
Merging an external plugin repo into the monorepomono-repo-integration
App-facing Grails 8 upgrade guidancegrails-8-upgrade

Prime Directive: This Repo Wins

  1. Match neighboring modules. Before inventing structure, open 2-3 similar build.gradle files and copy their plugin block, dependency style, and apply { from ... } scripts.
  2. Prefer existing convention plugins over inline configuration. If CompilePlugin already sets encoding, release, jars, and reproducibility, do not re-declare those in the module script.
  3. Gradle docs are secondary. Official Gradle 9 docs are useful for APIs and deprecations, but this monorepo intentionally diverges (configuration cache off, no Spring DM plugin, heavy projectDir remapping, presence-based -P flags, custom BOM validator). When docs and this repo disagree, follow this repo unless you are deliberately fixing a known issue with a tracked reason.
  4. Do not use io.spring.dependency-management / Spring Dependency Management plugin in core modules or applications. The intentional regression fixture at grails-test-examples/spring-dependency-management is the only exception. Grails 8 otherwise uses native platform() / enforcedPlatform() plus org.apache.grails.gradle.bom-property-overrides (see PR #15467).
  5. Scope Gradle invocations to the touched subproject (:module:test, not root test) unless the change is cross-cutting.

Change workflow

  1. Inspect 2-3 sibling modules and the relevant convention plugin or shared script.
  2. Confirm the Gradle project path in settings.gradle, including any projectDir mapping.
  3. Edit the smallest appropriate build file or convention plugin.
  4. Run scoped compile, test, and validateDependencyVersions tasks for the changed project.
  5. For grails-gradle changes, run the relevant plugin TestKit tests from grails-gradle/ with its wrapper.

Topology (Know Where You Are)

This git repo is several independent Gradle builds, not one flat multiproject:

BuildPathRoleHow to run
Root frameworkrepo root60+ published modules, BOMs, test-examples, profiles, docs./gradlew … from root
build-logicbuild-logic/Shared convention plugins via includeBuildcd build-logic && ./gradlew … (or root pluginManagement includeBuild)
grails-gradlegrails-gradle/Published Grails Gradle plugins for appscd grails-gradle && ./gradlew …
grails-forgegrails-forge/App generator (own wrapper, own deps)cd grails-forge && ./gradlew …
end-to-endend-to-end/Tests against published artifacts in build/local-mavenFull 3-step flow below (see end-to-end/README.md)
gradle-bootstrapgradle-bootstrap/Regenerates shared wrappers from .sdkmanrcgradle -p gradle-bootstrap (see wrapper section)

end-to-end is not a composite consumer of the root build. It resolves real published coordinates from <repo>/build/local-maven (the TestCaseMavenRepo), not ~/.m2 and not via publishAllToMavenLocal. Do not add includeBuild('..') substitution - that defeats validating consumer metadata, CLI companions, and POM/BOM shape.

Full local run (three steps, in order):

# 1) Publish grails-gradle + root into build/local-maven (both required)
(cd grails-gradle && ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository)
./gradlew publishAllPublicationsToTestCaseMavenRepoRepository

# 2) Build the standalone Grails 7 fixture jar (JDK 17 / Gradle 8.x via its .sdkmanrc)
cd end-to-end/legacy-g7-command-plugin
sdk env
./gradlew jar
cd ../..

# 3) Run the suite on the root JDK 21 environment
sdk env   # from repository root (.sdkmanrc)
cd end-to-end
./gradlew check

Leave legacy-g7-command-plugin's wrapper on its pinned Grails 7 Gradle version when bumping the main line.

Root settings.gradle wires:

pluginManagement {
    includeBuild('./grails-gradle') { name = 'grails-gradle' }
    includeBuild('./build-logic') { name = 'build-logic-root' }
    // ...
}

build-logic exists because composite builds do not share buildSrc plugins. Internal conventions live there so root, grails-gradle, and forge can consume them.

Project path != directory name

Root settings heavily remaps projectDir (100+ entries). Examples:

Gradle pathDirectory
:grails-bomgrails-bom/default
:grails-base-bomgrails-bom/base
:grails-hibernate7-bomgrails-bom/hibernate7
:grails-controllersgrails-controllers (often 1:1)
:grails-data-hibernate7-coregrails-data-hibernate7/core
:grails-test-examples-app1grails-test-examples/app1

Always use the Gradle project path in task names (./gradlew :grails-data-hibernate7-core:test). Confirm with settings.gradle include + projectDir when unsure. Do not invent paths from folder names alone.

Micronaut "island"

grails-micronaut*, micronaut BOMs, and related test-examples are gated in settings.gradle:

  • Auto-excluded on JDK < 25 (Micronaut 5 targets JVM 25 bytecode)
  • Auto-included on JDK 25+
  • -PskipMicronautProjects forces exclude (used by groovy-joint CI)
  • -PincludeMicronautProjects forces include on older JDKs (still may not compile)

Presence-based flags (property present, value optional) match skipFunctionalTests / skipCodeStyle style elsewhere.


Gradle Version Sync (Hard Rule)

Current line: Gradle 9.7.1 (distributionUrl + gradleToolingApiVersion=9.7.1). Upstream may already ship a newer 9.7.x patch - this repo rides close to latest only after a deliberate multi-location bump PR. Do not "helpfully" jump one wrapper ahead of the rest.

Two Groovy stacks: Gradle itself embeds Groovy 4 for build logic. Application/runtime code on 8.0.x is Groovy 5. That is why dependencies.gradle keeps separate maps:

  • gradleBomDependencyVersions / gradle-groovy.version / gradle-spock.version → build tooling (Groovy 4 / Spock groovy-4)
  • bomDependencyVersions / groovy.version / spock.version → apps and framework modules (Groovy 5 / Spock groovy-5.0)

Never unify those casually.

Preferred bump workflow

  1. Set the new Gradle version in .sdkmanrc (gradle=…).

  2. Run the bootstrap project (uses a system gradle to regenerate shared wrappers from .sdkmanrc):

    gradle -p gradle-bootstrap
    

    Bootstrap generates the wrapper under gradle-bootstrap/, copies it to grails-forge, grails-gradle, and end-to-end, then moves the generated wrapper into the repository root. It also runs legacyG7Wrapper so end-to-end/legacy-g7-command-plugin stays on its pinned Grails 7 Gradle version (currently 8.x - do not force it to 9). Root is therefore bootstrap-covered; do not re-list it as a manual step.

  3. Manually refresh the locations bootstrap does not cover. For each tree, keep the full wrapper set in sync (gradle-wrapper.properties, gradle-wrapper.jar, gradlew, gradlew.bat) - not properties alone:

    • build-logic/ - run its wrapper task or copy the complete set from root after bootstrap
    • grails-profiles/base/skeleton/ and grails-profiles/profile/skeleton/
    • grails-shell-cli/src/test/resources/gradle-sample/ (and bin/test copy if present)
    • Forge generated-app wrapper assets (all of these - properties alone is not enough):
      • grails-forge/grails-forge-core/.../gradleWrapperProperties.rocker.raw (properties template)
      • grails-forge/grails-forge-core/src/main/resources/gradle/gradlew
      • grails-forge/grails-forge-core/src/main/resources/gradle/gradlew.bat
      • grails-forge/grails-forge-core/src/main/resources/gradle/wrapper/gradle-wrapper.jar
    • gradle.propertiesgradleToolingApiVersion (must match the new Gradle version)
  4. Verify every main-line tree matches on properties and scripts/jars. The only intentional holdout is end-to-end/legacy-g7-command-plugin (Gradle 8.x).

Also keep gradlew.bat LF line endings on this line (PR #15709). Comment at top of root gradle-wrapper.properties remains a human checklist.


Root gradle.properties Flags (Do Not "Fix" Blindly)

PropertyValue / note
org.gradle.cachingtrue
org.gradle.paralleltrue
org.gradle.daemontrue
org.gradle.configuration-cachefalse until #15497 resolved - do not enable casually
org.gradle.configureondemandcommented off - Gradle issue #9489
org.gradle.jvmargs-Xmx5G (raise only with reason; groovydoc is hungry)
javaVersion21 (CompilePlugin reads this for --release)
projectVersionframework version
slf4jPreventExclusiontrue - Grails Gradle plugin POM behavior

CI vs local behavior is branched on System.getenv('CI') and SOURCE_DATE_EPOCH (reproducible builds disable remote cache).


Standard Published Library Module

Canonical pattern (see grails-core/build.gradle, grails-controllers/build.gradle, grails-services/build.gradle):

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  ... Apache header ...
 */

plugins {
    id 'groovy'
    id 'java-library'
    id 'project-report'                                      // optional but common
    id 'org.apache.grails.buildsrc.properties'
    id 'org.apache.grails.buildsrc.dependency-validator'
    id 'org.apache.grails.buildsrc.compile'
    id 'org.apache.grails.buildsrc.publish'
    id 'org.apache.grails.buildsrc.sbom'
    id 'org.apache.grails.buildsrc.vulnerability-scan'      // when appropriate
    id 'org.apache.grails.gradle.grails-code-style'
    id 'org.apache.grails.gradle.grails-jacoco'
}

version = projectVersion
group = 'org.apache.grails'   // or org.apache.grails.web / .data / etc. - match siblings

dependencies {
    implementation platform(project(':grails-bom'))   // or :grails-hibernate7-bom, etc.

    api project(':grails-core')
    api 'org.apache.groovy:groovy'
    // versions come from the platform - do NOT hardcode versions here

    compileOnly 'jakarta.servlet:jakarta.servlet-api'

    testImplementation 'org.spockframework:spock-core'
    testImplementation 'org.apache.groovy:groovy-test-junit5'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
    // junit-platform-launcher is added by gradle/test-config.gradle
}

apply {
    from rootProject.layout.projectDirectory.file('gradle/docs-config.gradle')
    from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
}

Rules for module scripts

  • Apache license header on every new .gradle / .gradle.kts file.
  • Prefer Groovy DSL (this repo is almost entirely .gradle, not .kts).
  • version = projectVersion and explicit group - do not invent version schemes per module.
  • Use rootProject.layout.projectDirectory.file('gradle/…') for shared scripts (lazy layout API), not brittle rootProject.file string soup in new code.
  • Prefer tasks.named('x') / tasks.withType(T).configureEach over eager task x << or bare tasks.x { } mutation when touching existing modernized code.
  • Configuration avoidance: do not call .get() on providers during configuration unless required; do not resolve configurations at configuration time.
  • api vs implementation vs compileOnly vs runtimeOnly vs testImplementation - follow Java Library plugin semantics; public types in your API surface → api.
  • Project deps: project(':grails-foo') using the settings path.
  • External deps: coordinate without version when the BOM manages them.

CLI companion modules

Command-bearing modules may apply org.apache.grails.gradle.grails-plugin-cli and declare cliApi / cliImplementation configurations (PR #15948). Framework modules that wire CLI with project deps set ext.grailsCliAutoProvision = false (root build.gradle does this for non-test-example projects). Do not dump CLI-only deps back onto the main runtime classpath.


Functional / Test-Example Apps

See grails-test-examples/app1/build.gradle:

  • Apply Grails app plugins: org.apache.grails.gradle.grails-web, often org.apache.grails.gradle.grails-gsp, and cloud.wondrify.asset-pipeline
  • Still use implementation platform(project(':grails-bom'))
  • Depend on published coordinates (org.apache.grails:grails-dependencies-starter-web) - root applies dependency substitution via gradle/functional-test-config.gradle so local projects replace Maven coordinates
  • Apply gradle/functional-test-config.gradle (and datastore-specific scripts like hibernate7-test-config.gradle when needed)
  • Do not disable substitution without understanding multi-project resolution

Dependency Management (BOM Is Law)

Single source of versions

FileWhat it owns
Root dependencies.gradleApplication/runtime BOM versions (bomDependencyVersions, bomDependencies, bomPlatformDependencies) and gradle-tooling maps (gradleBomDependencyVersions, etc.). grails-gradle applies this same file via ../dependencies.gradle - there is no separate grails-gradle/dependencies.gradle on this line
gradle.propertiesNon-BOM pins (tool versions, javaVersion, gradleToolingApiVersion, checkstyle/codenarc/pmd/jacoco versions)

No gradle/libs.versions.toml. This monorepo does not use Gradle version catalogs. Do not introduce a catalog "because Gradle docs recommend it." Dependabot and the published BOM pipeline are built around the root dependencies.gradle maps (see comment at top of that file).

Map naming contract

For POM property generation, map key must be the dependency name prefix:

bomDependencyVersions = [
    'groovy.version': '5.0.7',
]
bomDependencies = [
    'groovy': "org.apache.groovy:groovy:${bomDependencyVersions['groovy.version']}",
]

Break this and published BOM properties / docs extraction break.

Platform usage in modules

// Default
implementation platform(project(':grails-bom'))

// Hibernate 7 stack
implementation platform(project(':grails-hibernate7-bom'))

// Micronaut variants use enforcedPlatform in app-facing plugin logic

grails-bom/base (:grails-base-bom) is a java-platform that:

  • Imports Spring Boot BOM via api platform(...) (with deliberate excludes for groovy/spock/hibernate/liquibase where Grails owns the line)
  • Adds constraints from dependencies.gradle maps
  • Adds constraints for published subprojects
  • Applies gradle/cli-companion-bom-constraints.gradle for CLI companion versions under enforcedPlatform

validateDependencyVersions (AGENTS.md rule 14 / dependency-validator plugin)

Applied via org.apache.grails.buildsrc.dependency-validator.

Implementation detail (GrailsDependencyValidatorPlugin): for each resolved coordinate that the BOM also manages, it fails when bomVersion != resolvedVersion - any mismatch, not only "resolved is newer."

How to fix by direction:

SituationFix
Transitive resolved newer than BOMBump the pin in dependencies.gradle so the BOM is >= the winner (usual case; AGENTS.md rule 14)
Resolved older / forced / strict conflictFind the force, strict constraint, or second platform pulling the other version; remove the force, align platforms, or document a deliberate override
Intentional divergence that must stayext.allowedBomOverrides = ['group:name', …] with a commented reason - last resort
Whole project cannot validatePrefer ext.skipDependencyValidation = true in the build script. CLI: -PskipDependencyValidation is presence-based and skips validation; -PskipDependencyValidation=true is also valid. Use the documented form that best communicates intent.

Also:

  • Prefer inheriting Spring Boot managed versions over re-pinning duplicates (PR #15730). Only pin when diverging (security override, missing from Boot BOM, lockstep companion like graphql-java-extended-scalars).
  • Same coordinate managed in multiple BOM maps must use the same version everywhere or enforcedPlatform resolution explodes.
  • Do not silence validation with exclusions as a shortcut to avoid a BOM bump.

Adding or bumping a dependency

  1. Decide if Spring Boot already manages it - if same version, omit pin.
  2. If Grails must manage it, add/bump in the correct map in dependencies.gradle.
  3. Use the unversioned coordinate in module dependencies {}.
  4. Run ./gradlew :that-module:validateDependencyVersions (and affected consumers).
  5. Security overrides: comment with CVE and previous Boot version (see existing httpcore5/jackson/logback pins).

Exclusions

Use sparingly, always with a reason. Common pattern for Hibernate:

api 'org.hibernate.orm:hibernate-core', {
    exclude group: 'commons-logging', module: 'commons-logging'
    // ...
}

Do not exclude your way out of a BOM version fight. Prefer the direction-aware fixes in the validator table above (usually bump the BOM when a transitive is newer; otherwise align forces/platforms).


Convention Plugins (build-logic)

Plugin IDs (implementation under build-logic/plugins/…/buildsrc/):

Plugin IDPurpose
org.apache.grails.buildsrc.propertiesLoad root/local.properties into ext
org.apache.grails.buildsrc.compileJava 21 --release, UTF-8, fork memory, parameters, sources/javadoc jars, reproducible archives, Groovy config script, isolated build, per-project base.dir
org.apache.grails.buildsrc.dependency-validatorvalidateDependencyVersions
org.apache.grails.buildsrc.publishPublishing conventions (grails-publish integration)
org.apache.grails.buildsrc.sbomCycloneDX / SBOM reproducibility
org.apache.grails.buildsrc.vulnerability-scanOSS Index style scanning hooks
org.apache.grails.buildsrc.groovydocGroovydoc
org.apache.grails.buildsrc.groovydoc-enhancerGroovydoc enhancer
org.apache.grails.buildsrc.repoSettings plugin: Apache snapshot/staging repo content filters
org.apache.grails.gradle.grails-code-styleCheckstyle + CodeNarc
org.apache.grails.gradle.grails-code-analysisPMD + SpotBugs (opt-in props)
org.apache.grails.gradle.grails-jacocoJaCoCo per project
org.apache.grails.gradle.grails-violation-aggregationRoot only - aggregateViolations
org.apache.grails.gradle.grails-ij-formatterIntelliJ formatter wiring

CompilePlugin behaviors you must not fight

  • JavaCompile.options.release from javaVersion (21) - not outdated sourceCompatibility/targetCompatibility pairs in new code
  • UTF-8 everywhere
  • -parameters for reflection/IDE
  • Forked compilation with -Dgrails.isolated.build=true and a per-project BaseDirArgumentProvider supplying -Dbase.dir=<projectDir>. It is marked @Internal, not @InputDirectory, because projectDir contains build outputs. Do not remove it: it prevents grails.factories leaking between compiler daemons (#15799 / CompilePlugin comments).
  • The compile-time base.dir provider is required, but absolute base.dir values are forbidden in Test.systemProperties: they make cache keys machine-specific. Never "simplify" by removing the compiler provider or adding an absolute test property.
  • Jar.duplicatesStrategy = FAIL - duplicate entries are configuration bugs
  • Reproducible archives: no timestamps, fixed order, unix 0644/0755
  • Groovy configurationScriptgradle/groovy-compile-configscript.groovy (annotation member order / GROOVY-12146 workaround, PR #15963)

Published Grails Gradle plugins (grails-gradle)

Use the fully qualified plugin IDs in plugins { id '…' } blocks. Most are registered in grails-gradle/plugins/build.gradle. org.apache.grails.gradle.grails-publish is supplied by the external grails-publish-plugin implementation dependency (not listed in that file's gradlePlugin {} block) but is still the correct ID for publishing.

Plugin ID
org.apache.grails.gradle.grails-app
org.apache.grails.gradle.grails-web
org.apache.grails.gradle.grails-plugin
org.apache.grails.gradle.grails-gsp
org.apache.grails.gradle.grails-gson
org.apache.grails.gradle.grails-markup
org.apache.grails.gradle.grails-profile
org.apache.grails.gradle.grails-publish-profile
org.apache.grails.gradle.grails-cli
org.apache.grails.gradle.grails-plugin-cli
org.apache.grails.gradle.grails-cli-library
org.apache.grails.gradle.grails-exploded
org.apache.grails.gradle.grails-integration-test
org.apache.grails.gradle.grails-test-phases
org.apache.grails.gradle.bom-property-overrides
org.apache.grails.gradle.grails-publish (via grails-publish-plugin dependency)

Never paste bare suffixes like grails-web into a plugins block - resolution will fail.

When changing these plugins:

  • Prefer lazy task configuration; never resolve configurations inside configureEach at configuration time (PR #16076 - Gradle 9.5+ markAsObserved failures).
  • Be careful with nested afterEvaluate ordering (PR #16009 - BOM apply vs CLI detect race).
  • Declare Copy/processResources filter values as task inputs (PR #16006 - ReplaceTokens up-to-date bug).
  • Functional tests live under grails-gradle/plugins/src/test with TestKit projects - update them with behavior changes.
  • Build/test from grails-gradle/ directory with its wrapper.

Shared Scripts Under gradle/

Apply with:

apply {
    from rootProject.layout.projectDirectory.file('gradle/test-config.gradle')
}

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
3k
Forks
975
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
gradle-developer
Source
github.com/apache/grails-core