agentleFS
Sign inSign up

gradle-developer

apache/grails-core/.agents/skills/gradle-developer/SKILL.md

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

Skill2.9k starsChanged yesterday

What's in it

  1. What I Do
  2. When to Use Me
  3. Prime Directive: This Repo Wins
  4. Change workflow
  5. Topology (Know Where You Are)
  6. Project path != directory name
  7. Micronaut "island"
  8. Gradle Version Sync (Hard Rule)
  9. Preferred bump workflow
  10. Root gradle.properties Flags (Do Not "Fix" Blindly)
  11. Standard Published Library Module
  12. Rules for module scripts
  13. CLI companion modules
  14. Functional / Test-Example Apps
  15. Dependency Management (BOM Is Law)
  16. Single source of versions
  17. Map naming contract
  18. Platform usage in modules
  19. validateDependencyVersions (AGENTS.md rule 14 / dependency-validator plugin)
  20. validateBomProperties (parent BOM property rules)
  21. Adding or bumping a dependency
  22. Exclusions
  23. Convention Plugins (build-logic)
  24. CompilePlugin behaviors you must not fight
  25. Published Grails Gradle plugins (grails-gradle)
  26. Shared Scripts Under gradle/
  27. Test flags (test-config.gradle)
  28. Configuration Hygiene (Gradle 9.x Landmines)
  29. Build Cacheability
  30. Configuration Cache
---
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.8.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 (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:

| 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.8.0** (`distributionUrl` + `gradleToolingApiVersion=9.8.0`). Upstream may already ship a newer 9.8.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
   ```

   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.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` | **`true`** in every build (root, `build-logic`, `grails-gradle`, `grails-forge`) - build logic must stay compatible, see [Configuration Cache](#configuration-cache) |
| `org.gradle.configureondemand` | **commented off** - Gradle issue #9489 |
| `org.gradle.jvmargs` | `-Xmx3G` (daemon only; groovydoc and the guide run in their own JVMs) |
| `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).

`groovydocMaxHeapSize` and `guideMaxHeapSize` are **not** keys in `gradle.properties` - do not add
them. Each documentation JVM carries its own default (1g per groovydoc task, raised to 3g/2g by the
two aggregates; 1500m for the guide), and these exist only as `-P` overrides that beat the build
script when a documentation run will not fit.

---

## 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.1.3',
]
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` (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 `enforcedPlatform` resolution explodes.
- Do **not** silence validation with exclusions as a shortcut to avoid a BOM bump.

### `validateBomProperties` (parent BOM property rules)

Registered by its own plugin, `org.apache.grails.buildsrc.bom-property-validator`, which every BOM applies (on `java-platform` projects only). Like `validateDependencyVersions` it is not attached to `check` or `build`; CI runs it by name in its own `Validate BOM Properties` job (root BOMs and `grails-gradle-bom`). It has its own opt-out, `skipBomPropertyValidation`, which works like `skipDependencyValidation` (`-PskipBomPropertyValidation`, `-PskipBomPropertyValidation=true`, or `ext.skipBomPropertyValidation = true`); each property skips only its own task. It reads the POM a BOM publishes (`generatePomFileForMavenPublication`). Every version property the BOM owns (`ext.bomVersionProperties`: `gradleBomDependencyVersions` for `grails-gradle-bom`, `bomDependencyVersions` for `grails-base-bom`, `customBomVersions` for each variant) must be used by some published entry. It also compares the POM with the parent BOMs named by `ext.parentBoms` (`spring-boot-dependencies`, set in `dependencies.gradle`), including the BOMs they import, and for every module both manage it fails when:

| Rule | Example failure | Fix |
|------|-----------------|-----|
| (any owned property) No published entry uses the version key | `liquibase-hibernate5.version = 4.27.0` | Delete the key, or add the dependency that should use it |
| The pin uses a different property than the parent controls the module with | `${jackson3.version} should be ${jackson-bom.version}` | Rename the version key, and the dependency keys that prefix it (map naming contract) |
| The pin overrides a version the parent writes literally (all of Spring Boot's own `org.springframework.boot:*` entries) | `${foo.version}, where only ${spring-boot.version} moves the parent's version` | Drop the pin, or add a documented exemption - renaming it to the parent's import property would only repeat the parent's version |
| The pin repeats the parent's version | `graphql-java.version = 25.0` | Drop the pin and inherit the parent's version |

A module the parent manages through an imported BOM belongs to the property the parent imports that BOM with (`jackson-bom.version` for all of `tools.jackson:jackson-bom`), because that is the property a consumer sets to move the family. Coordinates and versions are resolved from each parent POM's properties, its own parents' and the Maven built-ins (`${project.groupId}`, as the Kotlin and Brave BOMs write their group, `${project.version}`, `${project.parent.version}`); an entry that cannot be resolved fails the task with `Cannot resolve <entry>, managed by <bom>` instead of going unchecked, so teach `PomVersions.properties` the missing built-in. Deliberate exceptions go in `bomUnusedVersionExemptions` / `bomPropertyNameExemptions` / `bomRedundantVersionExemptions` in `dependencies.gradle`, each with its reason.

### Adding or bumping a dependency

1. Decide if Spring Boot already manages it - if same version, omit pin; if a different version, name the version key after Spring Boot's property (`validateBomProperties` enforces both).
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.bom-property-validator` | `validateBomProperties` (BOM projects only) |
| `org.apache.grails.buildsrc.publish` | Publishing conventions (grails-publish integration) |
| `org.apache.grails.buildsrc.sbom` | CycloneDX / SBOM reproducibility |
| `org.apache.grails.buildsrc.vulnerability-scan` | `vulnerabilityScan`: looks up the resolved `runtimeClasspath` and `compileClasspath` in OSV and, when `SONATYPE_GUIDE_USERNAME`/`SONATYPE_GUIDE_TOKEN` are set, Sonatype Guide (read at execution, never stored in the configuration cache); answers cached for an hour in the Gradle user home (`vulnerabilityScan.cacheTtl`) since Sonatype lookups cost credits; exclusions need a reason |
| `org.apache.grails.buildsrc.vulnerability-scan-report` | Root only: `vulnerabilityScanReport` collects every project's scan into `build/reports/vulnerability-scan/summary.md` |
| `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.buildsrc.agent-skills` | Packages `skills/<skill>/` into the jar at `META-INF/skills/apache/grails-core/<skill>/` (SkillsJars layout); validates each skill's frontmatter `name` |
| `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 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 `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 `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 |
| `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-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 **configure-on-demand** in this repo without an issue-linked plan. The **configuration cache** is on: follow [Configuration Cache](#configuration-cache).
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.

### 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: the action `GroovydocEnhancerPlugin` replaces Groovydoc's with 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.

---

## Configuration Cache

The configuration cache is enabled in every build of this repository, and generated Grails applications and plugins enable it too, so the build logic here and the Grails Gradle plugins must stay compatible. When an entry is reused, Gradle runs the tasks from what it stored: nothing that runs while a task executes may reach the build model.

| Rule | Why / practice |
|------|----------------|
| Task actions, `onlyIf`, `outputs.cacheIf`/`upToDateWhen`, copy-spec and test-listener closures must not use `project`, other tasks, configurations, `rootProject`/`subprojects`, script `ext` properties, script methods or `logger` of the script | Read the values while configuring into locals or providers (`boolean skipped = [...].any { project.hasProperty(it) }; onlyIf { !skipped }`). Use the action's own parameter (`doLast { Task task -> task.logger... }`) |
| Do not rely on a closure's owner or delegate when it runs | They are not restored: a statically compiled closure that reaches the task through its owner fails with `ClosureCodec$BrokenObject`, and a dynamic one calling a method of the declaring class fails with `Could not find method`. Qualify static calls with the class name; put script helpers in a class defined in the script and call it qualified |
| A `project.provider {}` / `providers.provider {}` value is computed when the entry is stored | Fine for model values (they are final then), wrong for anything a task produces: derive it from the task's output property instead (`tasks.named('findMainClass', FindMainClassTask).flatMap { it.mainClassCacheFile }.map { ... }`), which stays lazy until execution |
| Do not keep `Project`, `Task`, `Configuration`, `SourceSet`, publications, extension objects that hold the project, or `AntBuilder` in task state or captured closures | Keep `FileCollection`s, providers and plain values. Use injected `FileSystemOperations`/`ExecOperations` (`objects.newInstance(SomeInterface)` in scripts) instead of `project.copy`/`exec`/`delete` |
| Resolution a task needs at execution goes into its inputs | `ExtractDependenciesTask` computes its rows in a memoized provider, as `validateBomProperties` does with the parent BOMs |
| Do not probe other projects' build outputs while configuring | A missing file is recorded as an input, so building it later invalidates the entry (`GrailsCliGradlePlugin` skips the jar probe for projects of the same build) |
| Closures with primitive-typed parameters cannot be restored | Use `Boolean`, or a static method of a script-defined class |
| Third-party plugins without support | Replace them (the asciidoctor plugin became `grails.doc.gradle.ConvertAsciidocTask`, forge's nohttp plugin a plain `Checkstyle` task), or mark their tasks `notCompatibleWithConfigurationCache(...)` as a last resort |

Verify a change with:

```bash
# store-time problems of everything a CI lane runs, without executing it
./gradlew build bootJar groovydoc publishAllPublicationsToTestCaseMavenRepoRepository aggregateViolations \
    --dry-run --configuration-cache-problems=warn -Dorg.gradle.configuration-cache.max-problems=100000
# execution-time problems: run the affected tasks for real; run them a second time and expect
# "Configuration cache entry reused"
```

The report is `build/reports/configuration-cache/*/*/configuration-cache-report.html`; its JSON between `// begin-report-data` and `// end-report-data` names the class, closure and field behind each problem.

The `grails-gradle` TestKit specs run every fixture build with the configuration cache and `problems=fail` (`GradleSpecification` writes it into the TestKit Gradle user home). Fixture tasks that inspect the model must read it into providers while configuring and only print in `doLast`; pass `--no-configuration-cache` only to test behavior without it.

---

## 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 - org.gradle.jvmargs sizes the daemon, so a bare -Xmx here is ignored
export GRADLE_OPTS='-Dorg.gradle.jvmargs=-Xmx4G'
```

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` |
| Use `project`, other tasks, configurations or script properties in a task action | Capture values while configuring; see [Configuration Cache](#configuration-cache) |
| Read a file another task writes through `provider {}` | Derive the value from that task's output property |
| 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.8 release notes](https://docs.gradle.org/9.8.0/release-notes.html)
- [Upgrading major version 9](https://docs.gradle.org/9.8.0/userguide/upgrading_major_version_9.html)
- [Upgrading within Gradle 9.x](https://docs.gradle.org/9.8.0/userguide/upgrading_version_9.html)
- [Java Library plugin](https://docs.gradle.org/9.8.0/userguide/java_library_plugin.html)
- [Java Platform / BOM](https://docs.gradle.org/9.8.0/userguide/java_platform_plugin.html)
- [Platforms](https://docs.gradle.org/9.8.0/userguide/platforms.html)
- [Sharing build logic via included builds](https://docs.gradle.org/9.8.0/userguide/sharing_build_logic_between_subprojects.html)
- [Task configuration avoidance](https://docs.gradle.org/9.8.0/userguide/task_configuration_avoidance.html)
- [Configuration cache](https://docs.gradle.org/9.8.0/userguide/configuration_cache.html) and its [requirements](https://docs.gradle.org/9.8.0/userguide/configuration_cache_requirements.html)
- [Version catalogs](https://docs.gradle.org/9.8.0/userguide/version_catalogs.html) (docs like them - **this repo does not**)

### Docs say X - we do Y

| Gradle docs lean | This repo |
|------------------|-----------|
| 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`
- [ ] `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
- [ ] No absolute machine paths in `Test.systemProperties`; compiler `base.dir` remains isolated in `BaseDirArgumentProvider`
- [ ] No overlapping task outputs; classpath normalization considered for packed generated files
- [ ] Apache headers on new files
- [ ] User-facing behavior documented if it affects app builds (`grails-doc`)
- [ ] For commit-ready work, use `violation-fixer` to run the AGENTS.md gate: `./gradlew clean aggregateViolations :grails-test-report:check --continue`. This is not required merely to read this skill.

If you only remember three things: **copy siblings**, **BOM owns versions**, **never resolve configurations while configuring tasks**.

More agent context in apache/grails-core

11 other files this repository gives its agents.

AGENTS.md

CLAUDE.md

Skill

Discussion

Did it work?

Say what you used it for and what you changed. People and their agents can both post here.

No reports yet. Be the first to say whether it worked.

Posts are public. Sign in to say whether it worked for you.Sign in to post

Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.