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 undergradle/,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):
| Need | Skill |
|---|---|
| CodeNarc / Checkstyle / PMD / SpotBugs reports | violation-fixer |
| Failing tests / aggregate reports | test-fixer |
| Merging an external plugin repo into the monorepo | mono-repo-integration |
| App-facing Grails 8 upgrade guidance | grails-8-upgrade |
Prime Directive: This Repo Wins
- Match neighboring modules. Before inventing structure, open 2-3 similar
build.gradlefiles and copy their plugin block, dependency style, andapply { from ... }scripts. - Prefer existing convention plugins over inline configuration. If
CompilePluginalready sets encoding, release, jars, and reproducibility, do not re-declare those in the module script. - 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
projectDirremapping, presence-based-Pflags, custom BOM validator). When docs and this repo disagree, follow this repo unless you are deliberately fixing a known issue with a tracked reason. - Do not use
io.spring.dependency-management/ Spring Dependency Management plugin in core modules or applications. The intentional regression fixture atgrails-test-examples/spring-dependency-managementis the only exception. Grails 8 otherwise uses nativeplatform()/enforcedPlatform()plusorg.apache.grails.gradle.bom-property-overrides(see PR #15467). - Scope Gradle invocations to the touched subproject (
:module:test, not roottest) unless the change is cross-cutting.
Change workflow
- Inspect 2-3 sibling modules and the relevant convention plugin or shared script.
- Confirm the Gradle project path in
settings.gradle, including anyprojectDirmapping. - Edit the smallest appropriate build file or convention plugin.
- Run scoped compile, test, and
validateDependencyVersionstasks for the changed project. - For
grails-gradlechanges, run the relevant plugin TestKit tests fromgrails-gradle/with its wrapper.
Topology (Know Where You Are)
This git repo is several independent Gradle builds, not one flat multiproject:
| Build | Path | Role | How to run |
|---|---|---|---|
| Root framework | repo root | 60+ published modules, BOMs, test-examples, profiles, docs | ./gradlew … from root |
| build-logic | build-logic/ |
Shared convention plugins via includeBuild |
cd build-logic && ./gradlew … (or root pluginManagement includeBuild) |
| grails-gradle | grails-gradle/ |
Published Grails Gradle plugins for apps | cd grails-gradle && ./gradlew … |
| grails-forge | grails-forge/ |
App generator (own wrapper, own deps) | cd grails-forge && ./gradlew … |
| end-to-end | end-to-end/ |
Tests against published artifacts in build/local-maven |
Full 3-step flow below (see end-to-end/README.md) |
| gradle-bootstrap | gradle-bootstrap/ |
Regenerates shared wrappers from .sdkmanrc |
gradle -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 path | Directory |
|---|---|
:grails-bom |
grails-bom/default |
:grails-base-bom |
grails-bom/base |
:grails-hibernate7-bom |
grails-bom/hibernate7 |
:grails-controllers |
grails-controllers (often 1:1) |
:grails-data-hibernate7-core |
grails-data-hibernate7/core |
:grails-test-examples-app1 |
grails-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+
-PskipMicronautProjectsforces exclude (used by groovy-joint CI)-PincludeMicronautProjectsforces 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
Set the new Gradle version in
.sdkmanrc(gradle=…).Run the bootstrap project (uses a system
gradleto regenerate shared wrappers from.sdkmanrc):gradle -p gradle-bootstrapBootstrap 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 runslegacyG7Wrappersoend-to-end/legacy-g7-command-pluginstays 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.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 bootstrapgrails-profiles/base/skeleton/andgrails-profiles/profile/skeleton/grails-shell-cli/src/test/resources/gradle-sample/(andbin/testcopy 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/gradlewgrails-forge/grails-forge-core/src/main/resources/gradle/gradlew.batgrails-forge/grails-forge-core/src/main/resources/gradle/wrapper/gradle-wrapper.jar
gradle.properties→gradleToolingApiVersion(must match the new Gradle version)
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)
| Property | Value / note |
|---|---|
org.gradle.caching |
true |
org.gradle.parallel |
true |
org.gradle.daemon |
true |
org.gradle.configuration-cache |
false until #15497 resolved - do not enable casually |
org.gradle.configureondemand |
commented off - Gradle issue #9489 |
org.gradle.jvmargs |
-Xmx5G (raise only with reason; groovydoc is hungry) |
javaVersion |
21 (CompilePlugin reads this for --release) |
projectVersion |
framework version |
slf4jPreventExclusion |
true - 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.ktsfile. - Prefer Groovy DSL (this repo is almost entirely
.gradle, not.kts). version = projectVersionand explicitgroup- do not invent version schemes per module.- Use
rootProject.layout.projectDirectory.file('gradle/…')for shared scripts (lazy layout API), not brittlerootProject.filestring soup in new code. - Prefer
tasks.named('x')/tasks.withType(T).configureEachover eagertask x <<or baretasks.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. apivsimplementationvscompileOnlyvsruntimeOnlyvstestImplementation- 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, oftenorg.apache.grails.gradle.grails-gsp, andcloud.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 viagradle/functional-test-config.gradleso local projects replace Maven coordinates - Apply
gradle/functional-test-config.gradle(and datastore-specific scripts likehibernate7-test-config.gradlewhen needed) - Do not disable substitution without understanding multi-project resolution
Dependency Management (BOM Is Law)
Single source of versions
| File | What it owns |
|---|---|
Root dependencies.gradle |
Application/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.properties |
Non-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.1.2',
]
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.gradlemaps - Adds constraints for published subprojects
- Applies
gradle/cli-companion-bom-constraints.gradlefor CLI companion versions underenforcedPlatform
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:
| Situation | Fix |
|---|---|
| Transitive resolved newer than BOM | Bump the pin in dependencies.gradle so the BOM is >= the winner (usual case; AGENTS.md rule 14) |
| Resolved older / forced / strict conflict | Find 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 stay | ext.allowedBomOverrides = ['group:name', …] with a commented reason - last resort |
| Whole project cannot validate | Prefer 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
enforcedPlatformresolution explodes. - Do not silence validation with exclusions as a shortcut to avoid a BOM bump.
Adding or bumping a dependency
- Decide if Spring Boot already manages it - if same version, omit pin.
- If Grails must manage it, add/bump in the correct map in
dependencies.gradle. - Use the unversioned coordinate in module
dependencies {}. - Run
./gradlew :that-module:validateDependencyVersions(and affected consumers). - 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 ID | Purpose |
|---|---|
org.apache.grails.buildsrc.properties |
Load root/local.properties into ext |
org.apache.grails.buildsrc.compile |
Java 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-validator |
validateDependencyVersions |
org.apache.grails.buildsrc.publish |
Publishing conventions (grails-publish integration) |
org.apache.grails.buildsrc.sbom |
CycloneDX / SBOM reproducibility |
org.apache.grails.buildsrc.vulnerability-scan |
OSS Index style scanning hooks |
org.apache.grails.buildsrc.groovydoc |
Groovydoc |
org.apache.grails.buildsrc.groovydoc-enhancer |
Groovydoc enhancer |
org.apache.grails.buildsrc.repo |
Settings plugin: Apache snapshot/staging repo content filters |
org.apache.grails.gradle.grails-code-style |
Checkstyle + CodeNarc |
org.apache.grails.gradle.grails-code-analysis |
PMD + SpotBugs (opt-in props) |
org.apache.grails.gradle.grails-jacoco |
JaCoCo per project |
org.apache.grails.gradle.grails-violation-aggregation |
Root only - aggregateViolations |
org.apache.grails.gradle.grails-ij-formatter |
IntelliJ formatter wiring |
CompilePlugin behaviors you must not fight
JavaCompile.options.releasefromjavaVersion(21) - not outdatedsourceCompatibility/targetCompatibilitypairs in new code- UTF-8 everywhere
-parametersfor reflection/IDE- Forked compilation with
-Dgrails.isolated.build=trueand a per-projectBaseDirArgumentProvidersupplying-Dbase.dir=<projectDir>. It is marked@Internal, not@InputDirectory, becauseprojectDircontains build outputs. Do not remove it: it preventsgrails.factoriesleaking between compiler daemons (#15799 /CompilePlugincomments). - The compile-time
base.dirprovider is required, but absolutebase.dirvalues are forbidden inTest.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
configurationScript→gradle/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
configureEachat configuration time (PR #16076 - Gradle 9.5+markAsObservedfailures). - Be careful with nested
afterEvaluateordering (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/testwith 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')
}
| Script | Use |
|---|---|
test-config.gradle |
JUnit Platform, parallel forks, heap, cache policy, skip flags, launcher deps |
functional-test-config.gradle |
Dependency substitution for test-examples |
docs-config.gradle / docs-dependencies.gradle |
Groovydoc / docs classpaths |
publish-root-config.gradle |
Root publishing orchestration |
rat-root-config.gradle |
Apache RAT |
cli-companion-bom-constraints.gradle |
CLI artifact constraints on BOMs |
hibernate5-test-config.gradle / hibernate7-test-config.gradle |
Datastore test stacks |
spring-security-test-config.gradle |
Spring Security functional and integration tests |
grails-data-tck-config.gradle |
GORM data TCK wiring and test filters |
grails-extension-gradle-config.gradle |
Gradle extension module conventions |
test-webjar-asset-config.gradle |
WebJar asset test setup |
mongodb-forked-test-config.gradle |
Forked MongoDB test configuration |
mongodb-*-test-config.gradle / redis-test-config.gradle |
External service tests |
plugin-repositories.gradle |
Shared plugin repo config for settings |
groovy-compile-configscript.groovy |
Groovy compiler config script (not applied via apply from in modules - referenced by CompilePlugin) |
Test flags (test-config.gradle)
Presence of project properties skips/selects suites, e.g.:
skipTests, skipCoreTests, onlyFunctionalTests, onlyHibernate5Tests, onlyHibernate7Tests, onlyMongodbTests, onlyRedisTests, onlySpringSecurityTests
Parallelism: configuredTestParallel from -PmaxTestParallel or CI default 3 / local availableProcessors * 3/4.
Env:
DO_NOT_CACHE_TESTS=1- force test re-run without full--rerun-tasksdebug.testssystem prop - attach debugger argsSUPPRESS_DEPRECATION_WARNINGS=true- strip some-Xlintnoise
CI disables build cache for GroovyCompile and Test so AST transforms and tests stay honest.
Configuration Hygiene (Gradle 9.x Landmines)
These bit this repo in production PRs. Treat as hard rules when writing plugin or build code:
- No configuration-time classpath resolution in task
configureEachcallbacks. Defer probes to execution (doFirst/ task actions) or use proper providers (PR #16076). - No nested
afterEvaluatethat mutates configurations after another plugin may have resolved a related configuration (PR #16009). PreferwithPlugin/plugins.withId/ lazyconfigurations.configureEachbefore observation. - Task inputs must include filter/token maps and any other non-file data that affects outputs (PR #16006).
- All custom task types must declare caching intent (
@DisableCachingByDefaultor correct cacheable annotations) - required since Gradle 9 upgrade (PR #15365). - Prefer
JavaPluginExtensionover deprecatedJavaPluginConvention;destinationFileoveroutputFileonWriteProperties; avoidConfigureUtil/ oldconventionAPIs. - Tests need
junit-platform-launcherontestRuntimeOnly(shared script adds it) - Gradle 9 requirement. - Do not enable configuration cache or configure-on-demand in this repo without an issue-linked plan.
evaluationDependsOnappears in BOM/docs scripts for a reason - do not cargo-cult it into random modules; it couples configuration order and slows builds.
Build Cacheability
The local build cache is enabled. Treat cacheability as a correctness constraint, not a later optimization.
| Rule | Why / practice |
|---|---|
Do not put absolute machine paths, especially base.dir, in Test.systemProperties |
They poison cache keys across machines (#15483). The compiler's @Internal BaseDirArgumentProvider is the separate, required compile-time case. |
Avoid doFirst and custom actions on cacheable tasks |
They can make a task ineligible for the build cache. Prefer a dedicated task with declared inputs and outputs or model the I/O properly. Some tradeoffs are intentional: GroovyDoc compatibility with configuration cache and selected GroovyCompile doFirst actions in GrailsGradlePlugin. |
| Give compiler configuration and reports task-specific paths | Overlapping outputs disable caching. Use names such as grailsGroovyCompilerConfig-{taskName}.groovy and separate Checkstyle / CodeNarc report paths (#15532). |
Do not use outputs.upToDateWhen to bypass work when it also prevents cache loading |
Model inputs and outputs instead. Keep GSP outputs separate, for example gsp-classes/main versus webapp (#15537). |
| Normalize generated unstable files packed into jars or classpaths | SbomPlugin uses normalization.runtimeClasspath.ignore("META-INF/sbom.json"). Add comparable normalization so unstable generated contents do not cascade cache misses. |
Use Develocity experiments to prove a change: populate the cache, then delete task outputs (or clean) and rebuild - cacheable tasks should report FROM-CACHE. A plain second build with outputs still present usually reports UP-TO-DATE, which does not prove remote/local cache loading.
Adding a New Subproject (Checklist)
- Choose directory layout consistent with family (
grails-foo/…or nested under existing tree). include 'grails-foo'(or nested name) in rootsettings.gradle.- If path != default, set
project(':grails-foo').projectDir = file('…'). - Copy a sibling
build.gradleplugin/dependency skeleton; setgroup/ext.pom*as needed. - Wire
platform(project(':grails-bom'))or the correct variant BOM. - If published: ensure publish plugin + BOM constraint inclusion (base BOM auto-discovers published projects with grails-publish plugins).
- If it has commands: plan CLI companion artifact (
grails-plugin-cli), not runtime leakage. - If test-example: use functional-test-config + external coordinates + substitution.
- Register in any root aggregators if required (docs, publish-root-config, CI matrices).
- Run:
./gradlew :grails-foo:compileGroovy :grails-foo:test :grails-foo:validateDependencyVersions
For importing an entire external plugin repository, use mono-repo-integration skill instead of this checklist alone.
Commands Cheatsheet
# Always from the owning build root (usually repo root)
./gradlew :grails-core:compileGroovy
./gradlew :grails-core:test
./gradlew :grails-core:test --tests 'org.example.SomeSpec'
./gradlew :grails-core:validateDependencyVersions
# Build without tests
./gradlew build -PskipTests
# Style / analysis (see violation-fixer)
./gradlew :grails-core:codeStyle
./gradlew aggregateViolations
# Publish to ~/.m2 (maintainers / forge local cascade) - NOT what end-to-end uses
./gradlew publishAllToMavenLocal # grails-gradle → root → forge
# Publish to build/local-maven for end-to-end (and forge TestCaseMavenRepo consumers)
(cd grails-gradle && ./gradlew publishAllPublicationsToTestCaseMavenRepoRepository)
./gradlew publishAllPublicationsToTestCaseMavenRepoRepository
# Force re-run tests
./gradlew :module:test --rerun-tasks
# or
DO_NOT_CACHE_TESTS=1 ./gradlew :module:test
# Parallelism override / flake bisect
./gradlew :module:test -PmaxTestParallel=1
./gradlew :module:test -PtestBisect
# Memory
export GRADLE_OPTS='-Xms2G -Xmx5G'
Work in grails-gradle or grails-forge only with that directory's ./gradlew.
Develocity: https://develocity.apache.org - build scans publish when authenticated; remote cache push is CI-only.
Anti-Patterns (Reject These)
| Do not | Do instead |
|---|---|
| Apply Spring Dependency Management plugin | platform / enforcedPlatform + bom-property-overrides |
Apply Spring Dependency Management outside grails-test-examples/spring-dependency-management |
Native platforms + bom-property-overrides |
Hardcode versions in module dependencies {} |
BOM maps in root dependencies.gradle |
Introduce libs.versions.toml catalogs |
Keep dependencies.gradle maps |
Silence validateDependencyVersions without comment |
Fix the mismatch (usually bump BOM if transitive is newer; else remove force / align platforms / document override) |
Root ./gradlew test after a one-module edit |
./gradlew :module:test |
| Enable configuration cache "because Gradle says so" | Leave off until #15497 |
Resolve configuration.files in configureEach |
Defer to execution / providers |
Add an absolute machine path to Test.systemProperties |
Use portable task inputs; keep compiler-only base.dir in its @Internal argument provider |
| Give multiple tasks the same compiler config or report output path | Make every output path task-specific |
Use outputs.upToDateWhen in a way that prevents cache loading |
Declare inputs and outputs or use a dedicated task |
| Pack unstable generated files without classpath normalization | Add an explicit normalization ignore when appropriate |
| Duplicate CompilePlugin settings in module scripts | Trust convention plugins |
| Invent new plugin IDs without build-logic registration | Add descriptor + tests in build-logic |
| Bump one wrapper only | Sync all wrapper locations |
| Put CLI-only deps on runtime classpath | CLI companion artifact / cli* configurations |
buildscript { classpath … } for plugins already on pluginManagement |
plugins { id '…' } |
| Kotlin DSL for a one-off module in a Groovy DSL repo | Groovy DSL build.gradle |
| Wildcard imports in buildsrc Groovy | Explicit imports (same as app code style) |
Add repositories {} in a subproject |
Root/settings repo management (FAIL_ON_PROJECT_REPOS) |
includeBuild the root into end-to-end |
Publish to build/local-maven via publishAllPublicationsToTestCaseMavenRepoRepository |
Use publishAllToMavenLocal to feed end-to-end |
That fills ~/.m2; end-to-end reads <repo>/build/local-maven |
Bump legacy-g7-command-plugin wrapper to Gradle 9 |
Leave it on the Grails 7-pinned Gradle 8.x |
| Mix Gradle-embedded Groovy 4 pins into app Groovy 5 BOM | Keep gradleBom* vs bom* maps separate |
Gradle 9 Doc Notes (Useful, Not Absolute)
Use official docs for API signatures and deprecations (pin URLs to the version you are bumping toward):
- Gradle 9.7 release notes
- Upgrading major version 9
- Upgrading within Gradle 9.x
- Java Library plugin
- Java Platform / BOM
- Platforms
- Sharing build logic via included builds
- Task configuration avoidance
- Configuration cache (read for compatibility - still off here)
- Version catalogs (docs like them - this repo does not)
Docs say X - we do Y
| Gradle docs lean | This repo |
|---|---|
| Turn on configuration cache | Off (#15497) |
Version catalogs (libs.versions.toml) |
dependencies.gradle maps + published BOM |
enforcedPlatform is dangerous for libraries |
Used deliberately for Micronaut-variant BOMs + validator |
| Prefer toolchains everywhere | options.release from javaVersion=21 via CompilePlugin (toolchains optional elsewhere) |
| JVM Test Suite plugin for extra suites | Existing test / integrationTest + shared gradle/*-test-config.gradle |
Avoid afterEvaluate |
Still present in plugins - change carefully; prefer plugins.withId for new code |
| Configure on demand | Explicitly disabled (Gradle #9489) |
Removed/deprecated APIs AI still emits - reject on sight in new code: jcenter(), Project.exec / Project.javaexec (use injected ExecOperations), JavaPluginConvention, ConfigureUtil, old convention APIs, WriteProperties.outputFile (use destinationFile), bare tasks.create / eager getByName when register/named suffice.
Repositories
GrailsRepoSettingsPlugin configures settings-level repos and RepositoriesMode.FAIL_ON_PROJECT_REPOS. Adding repositories { mavenCentral() } inside a random subproject will fail the build. Fix repo needs in settings / the repo settings plugin, not per-module.
Lessons From Recent 8.0.x Gradle PRs
| PR | Takeaway |
|---|---|
| #15467 | Spring DM removed; platforms + bom-property-overrides |
| #15483 | Cacheable tasks need portable inputs; avoid absolute test properties and normalize packed unstable SBOM metadata |
| #15365 | Gradle 9.4 API cleanup; caching annotations; junit launcher |
| #15532 | Overlapping task outputs disable caching; use per-task compiler and report paths |
| #15537 | Bad outputs.upToDateWhen predicates block cache loads; keep GSP output directories distinct |
| #15672 / #15763 | Wrapper multi-location sync discipline |
| #15730 | Prefer Boot BOM inheritance over duplicate pins |
| #15686 / #15687 | code-style / analysis / jacoco / violation aggregation plugins |
| #15948 | CLI split from runtime; companion artifacts |
| #15963 | Groovy compile config script / annotation order |
| #16006 | processResources tokens must be task inputs |
| #16009 | afterEvaluate ordering vs configuration observation |
| #16076 | No compile classpath probes at GroovyCompile configuration time |
| #16069 | Dependency substitution for functional tests is hot-path config work - keep it cheap and cached |
| #16078 | Keep CLI/test-tier deps off the production runtime classpath; do not ship unfixed native tooling (Jansi) |
When fixing a new Gradle failure, search merged PR titles for the exception text before inventing a workaround.
Agent Quality Bar
Strong 8.0.x Gradle PRs are small, sibling-consistent, and evidence-backed:
- State the Problem, Fix, Why, and Verification in the PR or handoff.
- Match the closest existing module, script, or convention plugin before adding a new pattern.
- Avoid drive-by reformatting, dependency churn, or unrelated modernization.
- Verify with the narrowest commands that cover the changed project: compile, targeted tests, and dependency validation as applicable.
- For
grails-gradle, include the relevant TestKit coverage. - Use dual review when available, especially for build, dependency, and publishing changes.
- Report exact commands and results. Evidence beats confidence.
Files to Read First (By Task)
| Task | Read |
|---|---|
| New library module | Sibling build.gradle, settings.gradle include section, CompilePlugin.groovy |
| Version bump | dependencies.gradle, maybe Spring Boot BOM notes in PR #15730 |
| Wrapper bump | gradle/wrapper/gradle-wrapper.properties comment checklist |
| Test wiring | gradle/test-config.gradle, gradle/functional-test-config.gradle |
| BOM / platform | grails-bom/base/build.gradle, GrailsDependencyValidatorPlugin.groovy |
| App plugin behavior | grails-gradle/plugins/.../GrailsGradlePlugin.groovy |
| Convention plugin change | build-logic/plugins/... + its Spock tests |
| Publishing | PublishPlugin.groovy, gradle/publish-root-config.gradle |
| Style gates | violation-fixer skill + code-style plugins |
Definition of Done (Gradle Change)
- Matches sibling module patterns (plugins, group, platform, apply scripts)
- No hardcoded versions that belong in
dependencies.gradle -
validateDependencyVersionsclean for touched modules (or documented override) - Scoped Gradle verify command green (
:module:compileGroovy,:module:test, plugin TestKit as applicable) - Wrapper/version sync complete if Gradle version changed
- No configuration-time resolution / nested afterEvaluate hazards introduced
- No absolute machine paths in
Test.systemProperties; compilerbase.dirremains isolated inBaseDirArgumentProvider - No overlapping task ou
…(truncated)