This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch docs/gradle-developer-skill
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 01b1c9150b788e643dd0e6af29c1db5237fe1f2a
Author: t <t@t>
AuthorDate: Sun Aug 2 11:57:30 2026 -0400

    docs: add Gradle 9 developer skill for monorepo builds
    
    Capture 8.0.x Gradle conventions so agents write build scripts the way
    this repository already does: composite builds, BOM platforms, wrapper
    sync via gradle-bootstrap, end-to-end TestCaseMavenRepo flow, and
    Gradle 9 configuration hygiene from recent PRs.
    
    Assisted-by: Sisyphus:xai/grok-4.5
---
 .agents/skills/gradle-developer/SKILL.md | 645 +++++++++++++++++++++++++++++++
 1 file changed, 645 insertions(+)

diff --git a/.agents/skills/gradle-developer/SKILL.md 
b/.agents/skills/gradle-developer/SKILL.md
new file mode 100644
index 0000000000..fff0248a0a
--- /dev/null
+++ b/.agents/skills/gradle-developer/SKILL.md
@@ -0,0 +1,645 @@
+---
+name: gradle-developer
+description: 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
+license: Apache-2.0
+---
+<!--
+SPDX-License-Identifier: Apache-2.0
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor 
license agreements; and to You under the Apache License, Version 2.0.
+-->
+
+## What I Do
+
+- Write and change Gradle build scripts the way **this repository** already 
does them on `8.0.x` (Gradle **9.6.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):
+
+| 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
+
+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. **Never reintroduce** `io.spring.dependency-management` / Spring Dependency 
Management plugin. Grails 8 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.
+
+---
+
+## 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):
+
+```bash
+# 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:
+
+```groovy
+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 &lt; 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.6.0** (`distributionUrl` + 
`gradleToolingApiVersion=9.6.0`). Upstream may already ship a newer 9.6.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`):
+
+   ```bash
+   gradle -p gradle-bootstrap
+   ```
+
+   That refreshes wrappers/scripts for **root**, **grails-gradle**, 
**grails-forge**, and **end-to-end**, and separately 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).
+
+3. Still refresh locations **not** fully covered by bootstrap. 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.properties` → `gradleToolingApiVersion` (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)
+
+| 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`):
+
+```groovy
+/*
+ *  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
+
+| 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**:
+
+```groovy
+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
+
+```groovy
+// 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` (PR #15689 family / AGENTS.md rule 14)
+
+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=true`** is the safe 
form. Bare `-PskipDependencyValidation` is treated as skip only when the 
property value is `null` (see `shouldSkip`); prefer `=true` so behavior does 
not depend on how the CLI materializes the property |
+
+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:
+
+```groovy
+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.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 **per-project** 
`BaseDirArgumentProvider` so `grails.factories` never leak across modules via 
reused compiler daemons
+- `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 
(registered in `grails-gradle/plugins/build.gradle`):
+
+| 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` |
+
+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:
+
+```groovy
+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 |
+| `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-tasks`
+- `debug.tests` system prop - attach debugger args
+- `SUPPRESS_DEPRECATION_WARNINGS=true` - strip some `-Xlint` noise
+
+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:
+
+1. **No configuration-time classpath resolution** in task `configureEach` 
callbacks. Defer probes to execution (`doFirst` / task actions) or use proper 
providers (PR #16076).
+2. **No nested `afterEvaluate` that mutates configurations** after another 
plugin may have resolved a related configuration (PR #16009). Prefer 
`withPlugin` / `plugins.withId` / lazy `configurations.configureEach` before 
observation.
+3. **Task inputs must include filter/token maps** and any other non-file data 
that affects outputs (PR #16006).
+4. **All custom task types** must declare caching intent 
(`@DisableCachingByDefault` or correct cacheable annotations) - required since 
Gradle 9 upgrade (PR #15365).
+5. Prefer **`JavaPluginExtension`** over deprecated `JavaPluginConvention`; 
**`destinationFile`** over `outputFile` on `WriteProperties`; avoid 
`ConfigureUtil` / old `convention` APIs.
+6. Tests need **`junit-platform-launcher`** on `testRuntimeOnly` (shared 
script adds it) - Gradle 9 requirement.
+7. Do not enable **configuration cache** or **configure-on-demand** in this 
repo without an issue-linked plan.
+8. **`evaluationDependsOn`** appears in BOM/docs scripts for a reason - do not 
cargo-cult it into random modules; it couples configuration order and slows 
builds.
+
+---
+
+## Adding a New Subproject (Checklist)
+
+1. Choose directory layout consistent with family (`grails-foo/…` or nested 
under existing tree).
+2. `include 'grails-foo'` (or nested name) in root `settings.gradle`.
+3. If path != default, set `project(':grails-foo').projectDir = file('…')`.
+4. Copy a sibling `build.gradle` plugin/dependency skeleton; set 
`group`/`ext.pom*` as needed.
+5. Wire `platform(project(':grails-bom'))` or the correct variant BOM.
+6. If published: ensure publish plugin + BOM constraint inclusion (base BOM 
auto-discovers published projects with grails-publish plugins).
+7. If it has commands: plan CLI companion artifact (`grails-plugin-cli`), not 
runtime leakage.
+8. If test-example: use functional-test-config + external coordinates + 
substitution.
+9. Register in any root aggregators if required (docs, publish-root-config, CI 
matrices).
+10. Run:
+    ```bash
+    ./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
+
+```bash
+# 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 |
+| 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 |
+| 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.6 release notes](https://docs.gradle.org/9.6.0/release-notes.html)
+- [Upgrading major version 
9](https://docs.gradle.org/9.6.0/userguide/upgrading_major_version_9.html)
+- [Upgrading within Gradle 
9.x](https://docs.gradle.org/9.6.0/userguide/upgrading_version_9.html)
+- [Java Library 
plugin](https://docs.gradle.org/9.6.0/userguide/java_library_plugin.html)
+- [Java Platform / 
BOM](https://docs.gradle.org/9.6.0/userguide/java_platform_plugin.html)
+- [Platforms](https://docs.gradle.org/9.6.0/userguide/platforms.html)
+- [Sharing build logic via included 
builds](https://docs.gradle.org/9.6.0/userguide/sharing_build_logic_between_subprojects.html)
+- [Task configuration 
avoidance](https://docs.gradle.org/9.6.0/userguide/task_configuration_avoidance.html)
+- [Configuration 
cache](https://docs.gradle.org/9.6.0/userguide/configuration_cache.html) (read 
for compatibility - still **off** here)
+- [Version 
catalogs](https://docs.gradle.org/9.6.0/userguide/version_catalogs.html) (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 |
+| #15365 | Gradle 9.4 API cleanup; caching annotations; junit launcher |
+| #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 |
+
+When fixing a new Gradle failure, search merged PR titles for the exception 
text before inventing a workaround.
+
+---
+
+## 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`
+- [ ] `validateDependencyVersions` clean 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
+- [ ] Apache headers on new files
+- [ ] User-facing behavior documented if it affects app builds (`grails-doc`)
+
+If you only remember three things: **copy siblings**, **BOM owns versions**, 
**never resolve configurations while configuring tasks**.

Reply via email to