You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/05/28 16:23:55 UTC

(grails-core) 01/01: Add grails-code-analysis convention plugin and violation aggregation

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

jamesfredley pushed a commit to branch feat/grails-code-analysis-plugin
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 29a24649f14f5f662b0fb5627c84dd24ddd130f2
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Thu May 28 12:23:09 2026 -0400

    Add grails-code-analysis convention plugin and violation aggregation
    
    Splits static code analysis out of the code-style plugin into a dedicated
    grails-code-analysis convention plugin (PMD + SpotBugs), and adds a
    root-only grails-violation-aggregation plugin that consolidates the
    per-module Checkstyle/CodeNarc/PMD XML reports into Markdown summaries
    under build/reports/violations/ via an aggregateStyleViolations task.
    
    Extracted from the Hibernate 7 staging branch (PR #15654) so it can be
    reviewed as a focused, single-topic change against 8.0.x, per review
    feedback that the analysis tooling should land in its own PR.
    
    Includes:
    
    - GrailsCodeAnalysisPlugin + GrailsCodeAnalysisExtension
    - GrailsViolationAggregationPlugin (aggregateStyleViolations)
    - GrailsCodeStylePlugin improvements: enable/disable properties,
      codenarcFix task, test-styling toggle; resource path renamed to
      grails-code-style, PMD config under grails-code-analysis
    - codeanalysis.yml workflow; codestyle.yml switched to aggregated reports
    - pmd/spotbugs versions; violation-fixer skill
    - Applies grails-code-analysis to 96 subprojects and wires forge
    
    Assisted-by: claude-code:claude-4.7-opus
---
 .agents/skills/violation-fixer/SKILL.md            | 231 ++++++++++
 .github/workflows/codeanalysis.yml                 |  95 +++++
 .github/workflows/codestyle.yml                    |  75 ++--
 build-logic/plugins/build.gradle                   |  19 +-
 .../org/apache/grails/buildsrc/GradleUtils.groovy  |   7 +
 ...n.groovy => GrailsCodeAnalysisExtension.groovy} |  29 +-
 .../buildsrc/GrailsCodeAnalysisPlugin.groovy       | 183 ++++++++
 .../buildsrc/GrailsCodeStyleExtension.groovy       |   9 +-
 .../grails/buildsrc/GrailsCodeStylePlugin.groovy   | 141 +++++--
 .../GrailsViolationAggregationPlugin.groovy        | 467 +++++++++++++++++++++
 .../pmd/pmd.xml}                                   |  15 +-
 .../checkstyle/checkstyle-suppressions.xml         |   0
 .../checkstyle/checkstyle.xml                      |   0
 .../codenarc/codenarc.groovy                       |   0
 .../buildsrc/GrailsCodeStylePluginSpec.groovy      | 135 ++++++
 .../GrailsViolationAggregationPluginSpec.groovy    | 177 ++++++++
 build.gradle                                       |   4 +
 gradle.properties                                  |   2 +
 grails-async/core/build.gradle                     |   1 +
 grails-async/gpars/build.gradle                    |   1 +
 grails-async/plugin/build.gradle                   |   1 +
 grails-async/rxjava/build.gradle                   |   1 +
 grails-async/rxjava2/build.gradle                  |   1 +
 grails-async/rxjava3/build.gradle                  |   1 +
 grails-bootstrap/build.gradle                      |   1 +
 grails-cache/build.gradle                          |   1 +
 grails-codecs-core/build.gradle                    |   1 +
 grails-codecs/build.gradle                         |   1 +
 grails-common/build.gradle                         |   1 +
 grails-console/build.gradle                        |   1 +
 grails-controllers/build.gradle                    |   1 +
 grails-converters/build.gradle                     |   1 +
 grails-core/build.gradle                           |   1 +
 grails-data-hibernate5/boot-plugin/build.gradle    |   1 +
 grails-data-hibernate5/core/build.gradle           |   1 +
 grails-data-hibernate5/dbmigration/build.gradle    |   1 +
 grails-data-hibernate5/grails-plugin/build.gradle  |   1 +
 grails-data-hibernate5/spring-orm/build.gradle     |   1 +
 grails-data-mongodb/boot-plugin/build.gradle       |   1 +
 grails-data-mongodb/bson/build.gradle              |   1 +
 grails-data-mongodb/core/build.gradle              |   1 +
 grails-data-mongodb/ext/build.gradle               |   1 +
 grails-data-mongodb/grails-plugin/build.gradle     |   1 +
 grails-data-mongodb/gson-templates/build.gradle    |   1 +
 grails-data-simple/build.gradle                    |   1 +
 grails-databinding-core/build.gradle               |   1 +
 grails-databinding/build.gradle                    |   1 +
 grails-datamapping-async/build.gradle              |   1 +
 grails-datamapping-core/build.gradle               |   1 +
 grails-datamapping-rx/build.gradle                 |   1 +
 grails-datamapping-support/build.gradle            |   1 +
 grails-datamapping-tck/build.gradle                |   1 +
 grails-datamapping-validation/build.gradle         |   1 +
 grails-datasource/build.gradle                     |   1 +
 grails-datastore-async/build.gradle                |   1 +
 grails-datastore-core/build.gradle                 |   1 +
 grails-datastore-web/build.gradle                  |   1 +
 grails-domain-class/build.gradle                   |   1 +
 grails-encoder/build.gradle                        |   1 +
 grails-events/core/build.gradle                    |   1 +
 grails-events/gpars/build.gradle                   |   1 +
 grails-events/plugin/build.gradle                  |   1 +
 grails-events/rxjava/build.gradle                  |   1 +
 grails-events/rxjava2/build.gradle                 |   1 +
 grails-events/rxjava3/build.gradle                 |   1 +
 grails-events/spring/build.gradle                  |   1 +
 grails-events/transforms/build.gradle              |   1 +
 grails-fields/build.gradle                         |   1 +
 grails-forge/build.gradle                          |   1 +
 grails-forge/gradle/code-style-config.gradle       |   1 +
 grails-geb/build.gradle                            |   1 +
 grails-gradle/common/build.gradle                  |   1 +
 grails-gradle/model/build.gradle                   |   1 +
 grails-gradle/plugins/build.gradle                 |   1 +
 grails-gradle/tasks/build.gradle                   |   1 +
 grails-gsp/core/build.gradle                       |   1 +
 grails-gsp/grails-layout/build.gradle              |   1 +
 grails-gsp/grails-sitemesh3/build.gradle           |   1 +
 grails-gsp/grails-taglib/build.gradle              |   1 +
 grails-gsp/grails-web-gsp-taglib/build.gradle      |   1 +
 grails-gsp/grails-web-gsp/build.gradle             |   1 +
 grails-gsp/grails-web-jsp/build.gradle             |   1 +
 grails-gsp/grails-web-taglib/build.gradle          |   1 +
 grails-gsp/plugin/build.gradle                     |   1 +
 grails-i18n/build.gradle                           |   1 +
 grails-interceptors/build.gradle                   |   1 +
 grails-logging/build.gradle                        |   1 +
 grails-mimetypes/build.gradle                      |   1 +
 grails-rest-transforms/build.gradle                |   1 +
 grails-scaffolding/build.gradle                    |   1 +
 grails-services/build.gradle                       |   1 +
 grails-shell-cli/build.gradle                      |   1 +
 grails-spring/build.gradle                         |   1 +
 grails-test-core/build.gradle                      |   1 +
 grails-test-suite-base/build.gradle                |   1 +
 grails-testing-support-core/build.gradle           |   1 +
 grails-testing-support-datamapping/build.gradle    |   1 +
 grails-testing-support-dbcleanup-core/build.gradle |   1 +
 grails-testing-support-dbcleanup-h2/build.gradle   |   1 +
 .../build.gradle                                   |   1 +
 grails-testing-support-http-client/build.gradle    |   1 +
 grails-testing-support-mongodb/build.gradle        |   1 +
 grails-testing-support-views-gson/build.gradle     |   1 +
 grails-testing-support-web/build.gradle            |   1 +
 grails-url-mappings/build.gradle                   |   1 +
 grails-validation/build.gradle                     |   1 +
 grails-views-core/build.gradle                     |   1 +
 grails-views-gson/build.gradle                     |   1 +
 grails-views-markup/build.gradle                   |   1 +
 grails-web-boot/build.gradle                       |   1 +
 grails-web-common/build.gradle                     |   1 +
 grails-web-core/build.gradle                       |   1 +
 grails-web-databinding/build.gradle                |   1 +
 grails-web-mvc/build.gradle                        |   1 +
 grails-web-url-mappings/build.gradle               |   1 +
 grails-wrapper/build.gradle                        |   1 +
 116 files changed, 1579 insertions(+), 108 deletions(-)

diff --git a/.agents/skills/violation-fixer/SKILL.md b/.agents/skills/violation-fixer/SKILL.md
new file mode 100644
index 0000000000..07ed784d35
--- /dev/null
+++ b/.agents/skills/violation-fixer/SKILL.md
@@ -0,0 +1,231 @@
+---
+name: violation-fixer
+description: Guide for running, interpreting, and fixing code style and analysis violations in grails-core using GrailsCodeStylePlugin, GrailsCodeAnalysisPlugin, and GrailsViolationAggregationPlugin — covering CodeNarc, Checkstyle, PMD, SpotBugs, and JaCoCo
+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
+
+- Explain how `GrailsCodeStylePlugin`, `GrailsCodeAnalysisPlugin`, and `GrailsViolationAggregationPlugin` enforce code quality across all 60+ modules.
+- Guide you through running style and analysis checks, interpreting the per-tool Markdown violation reports, and fixing each class of violation.
+- Describe which tools are always-on vs. opt-in, how to configure them via Gradle properties, and which violations can be auto-fixed.
+
+## When to Use Me
+
+Activate this skill when:
+
+- Running `./gradlew aggregateViolations` and interpreting the resulting `*_VIOLATIONS.md` files.
+- Fixing CodeNarc, Checkstyle, PMD, SpotBugs, or Spotless violations reported in those files.
+- Configuring code style or analysis tools across the repo (enabling/disabling tools or adjusting rule files).
+- Preparing a commit — the plugin output must be clean before merging.
+
+---
+
+## Plugin Overview
+
+| Plugin | Applied to | Responsibility |
+|--------|-----------|----------------|
+| `org.apache.grails.gradle.grails-code-style` | Every subproject | Applies Checkstyle and CodeNarc; registers per-project `codeStyle` task; redirects XML reports to root `build/reports/codestyle/` |
+| `org.apache.grails.gradle.grails-code-analysis` | Every subproject | Applies PMD and SpotBugs (both opt-in); registers per-project `codeAnalysis` task; redirects XML reports to root `build/reports/codestyle/` |
+| `org.apache.grails.gradle.grails-jacoco` | Every subproject | Applies JaCoCo; wires `jacocoTestReport` to run after each `test` task |
+| `org.apache.grails.gradle.grails-violation-aggregation` | **Root project only** | Registers `aggregateViolations` and `aggregateJacocoCoverage` tasks; writes Markdown summaries to `build/reports/violations/` |
+
+---
+
+## Key Tasks
+
+| Task | Scope | Description |
+|------|-------|-------------|
+| `./gradlew codeStyle` | per-project | Runs Checkstyle and CodeNarc for that project |
+| `./gradlew codeAnalysis` | per-project | Runs PMD and/or SpotBugs for that project (when enabled) |
+| `./gradlew aggregateViolations` | root | Runs all checks across every module, then writes `*_VIOLATIONS.md` to `build/reports/violations/` |
+| `./gradlew aggregateJacocoCoverage` | root | Runs JaCoCo reports across every module, then writes `JACOCO_COVERAGE.md` to `build/reports/violations/` |
+| `./gradlew codenarcFix` | per-project | Auto-fixes a subset of CodeNarc violations |
+
+### Quick commands
+
+```bash
+# Check a single module (style only)
+./gradlew :grails-core:codeStyle
+
+# Check a single module (analysis — must be enabled via properties)
+./gradlew :grails-core:codeAnalysis -Pgrails.codeanalysis.enabled.pmd=true
+
+# Full multi-module check + report
+./gradlew aggregateViolations
+
+# Include test sources in style checks
+./gradlew aggregateViolations -Pgrails.codestyle.enabled.tests=true
+
+# Include test sources in analysis
+./gradlew aggregateViolations -Pgrails.codeanalysis.enabled.tests=true
+
+# Ignore failures (collect reports without failing the build)
+./gradlew aggregateViolations -Pgrails.codestyle.ignoreFailures=true -Pgrails.codeanalysis.ignoreFailures=true
+
+# Auto-fix some CodeNarc violations before running checks
+./gradlew codenarcFix codeStyle
+
+# JaCoCo coverage report
+./gradlew aggregateJacocoCoverage
+```
+
+---
+
+## Output Files
+
+After running `aggregateViolations`, these files appear under `build/reports/violations/` in the **root project build directory**:
+
+| File | Tool | Always generated |
+|------|------|-----------------|
+| `build/reports/violations/CODENARC_VIOLATIONS.md` | CodeNarc | Yes |
+| `build/reports/violations/CHECKSTYLE_VIOLATIONS.md` | Checkstyle | Yes |
+| `build/reports/violations/PMD_VIOLATIONS.md` | PMD | Yes — contains `No violations found!` when PMD is disabled |
+| `build/reports/violations/SPOTBUGS_VIOLATIONS.md` | SpotBugs | Yes — contains `No violations found!` when SpotBugs is disabled |
+
+After running `aggregateJacocoCoverage`:
+
+| File | Tool | Generated |
+|------|------|-----------|
+| `build/reports/violations/JACOCO_COVERAGE.md` | JaCoCo | Only when at least one subproject has a JaCoCo CSV report |
+
+All reports are inside `build/` and are excluded from version control via `.gitignore`. A clean run produces `No violations found! 🎉` in each style file. **The build must be clean before committing.**
+
+Each file is a Markdown table grouped by module, with columns: **Class**, **Tool**, **Violation**, **Line**, **Message**.
+
+---
+
+## Tool Details
+
+### CodeNarc (Groovy — always enabled)
+
+Rule file: `build/codestyle/codenarc/codenarc.groovy` (generated by the plugin during setup; not intended to be edited directly).
+
+Most common violations and how to fix them:
+
+| Rule | Fix |
+|------|-----|
+| `UnnecessaryGString` | Replace `"plain string"` with `'plain string'` |
+| `UnnecessarySemicolon` | Remove trailing `;` |
+| `SpaceBeforeOpeningBrace` | Add space before `{` → `method() {` |
+| `SpaceAroundMapEntryColon` | `[key: value]` not `[key:value]` |
+| `ConsecutiveBlankLines` | Collapse 3+ blank lines to 2 |
+| `ClassStartsWithBlankLine` | Remove blank line right after `class Foo {` |
+| `NoWildcardImports` | Expand `import org.foo.*` to explicit imports |
+| `UnusedImport` | Remove imports not referenced in the file |
+| `MethodName` | Method names must be camelCase (not `snake_case`) |
+| `VariableName` | Variable names must be camelCase |
+| `LineLength` | Keep lines ≤ 200 chars (default) |
+
+Auto-fixable via `codenarcFix`: `ClassStartsWithBlankLine`, `SpaceAroundMapEntryColon`, `UnnecessaryGString`, `UnnecessarySemicolon`, `SpaceBeforeOpeningBrace`, `ConsecutiveBlankLines`.
+
+### Checkstyle (Java — always enabled)
+
+Rule file: `build/codestyle/checkstyle/checkstyle.xml`.
+
+Common violations:
+
+| Rule | Fix |
+|------|-----|
+| `ImportOrder` | Re-order imports: `java|javax`, then `groovy`, then `jakarta`, then blank, then `io.spring|org.springframework`, then `grails|org.apache.grails|org.grails`, then static imports |
+| `AvoidStarImport` | Use explicit class imports |
+| `UnusedImports` | Remove unused imports |
+| `WhitespaceAround` | Add spaces around operators and keywords |
+| `NeedBraces` | Add `{}` to single-statement `if`/`for`/`while` |
+| `FileTabCharacter` | Replace tabs with 4 spaces |
+| `NewlineAtEndOfFile` | Ensure file ends with `\n` |
+
+### PMD (Java/Groovy — opt-in)
+
+Enable: `-Pgrails.codeanalysis.enabled.pmd=true`
+
+Rule file: `build/codeanalysis/pmd/pmd.xml`.
+
+### SpotBugs (Java bytecode — opt-in)
+
+Enable: `-Pgrails.codeanalysis.enabled.spotbugs=true`
+
+Runs at `Effort.MAX` / `Confidence.HIGH`. Only high-confidence bugs are reported.
+
+### Spotless (Java auto-formatting — opt-in)
+
+Enable: `-Pgrails.codestyle.enabled.spotless=true`
+
+Uses Palantir Java Format. Can auto-fix by running:
+```bash
+./gradlew spotlessApply
+```
+
+---
+
+## Configuration Properties
+
+All properties can be set in `gradle.properties` or passed as `-P` flags:
+
+### `grails-code-style` plugin (Checkstyle + CodeNarc)
+
+| Property | Default | Description |
+|----------|---------|-------------|
+| `grails.codestyle.enabled.checkstyle` | `true` | Enable Checkstyle |
+| `grails.codestyle.enabled.codenarc` | `true` | Enable CodeNarc |
+| `grails.codestyle.enabled.spotless` | `false` | Enable Spotless |
+| `grails.codestyle.enabled.tests` | `false` | Also check test source sets |
+| `grails.codestyle.ignoreFailures` | `false` | Collect reports without failing build |
+| `grails.codestyle.codenarc.fix` | `false` | Run `codenarcFix` before CodeNarc tasks |
+| `grails.codestyle.dir.checkstyle` | (auto) | Custom path to Checkstyle config dir |
+| `grails.codestyle.dir.codenarc` | (auto) | Custom path to CodeNarc config dir |
+| `skipCodeStyle` | unset | If present, all style tasks are skipped |
+
+### `grails-code-analysis` plugin (PMD + SpotBugs)
+
+| Property | Default | Description |
+|----------|---------|-------------|
+| `grails.codeanalysis.enabled.pmd` | `false` | Enable PMD |
+| `grails.codeanalysis.enabled.spotbugs` | `false` | Enable SpotBugs |
+| `grails.codeanalysis.enabled.tests` | `false` | Also analyse test source sets |
+| `grails.codeanalysis.ignoreFailures` | `false` | Collect reports without failing build |
+| `grails.codeanalysis.dir.pmd` | (auto) | Custom path to PMD config dir |
+| `skipCodeStyle` | unset | If present, all analysis tasks are also skipped |
+
+---
+
+## Fixing Violations Workflow
+
+1. Run `./gradlew aggregateViolations -Pgrails.codestyle.ignoreFailures=true -Pgrails.codeanalysis.ignoreFailures=true`
+2. Open `build/reports/violations/CODENARC_VIOLATIONS.md` and `build/reports/violations/CHECKSTYLE_VIOLATIONS.md` to see all issues by module
+3. For CodeNarc, run `./gradlew codenarcFix` to auto-fix what it can
+4. Fix remaining violations manually using the table above
+5. Re-run `./gradlew aggregateViolations` and confirm files contain `No violations found! 🎉`
+6. The reports are inside `build/` and do not need to be deleted before committing
+
+---
+
+## Reports Directory Structure
+
+All XML reports are consolidated at:
+```
+build/reports/codestyle/        ← XML inputs for style aggregation
+├── checkstyle/
+│   ├── grails-core-checkstyleMain.xml
+│   ├── grails-web-mvc-checkstyleMain.xml
+│   └── ...
+├── codenarc/
+│   ├── grails-core-codenarcMain.xml
+│   └── ...
+├── pmd/       (if enabled)
+└── spotbugs/  (if enabled)
+
+build/reports/violations/       ← Markdown summaries written by aggregateViolations
+├── CODENARC_VIOLATIONS.md
+├── CHECKSTYLE_VIOLATIONS.md
+├── PMD_VIOLATIONS.md
+├── SPOTBUGS_VIOLATIONS.md
+└── JACOCO_COVERAGE.md          ← written by aggregateJacocoCoverage
+```
+
+The module name is derived from the filename: everything before the last `-` (e.g. `grails-core-checkstyleMain.xml` → module `grails-core`).
diff --git a/.github/workflows/codeanalysis.yml b/.github/workflows/codeanalysis.yml
new file mode 100644
index 0000000000..f9d08fe5f2
--- /dev/null
+++ b/.github/workflows/codeanalysis.yml
@@ -0,0 +1,95 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "Code Analysis"
+on:
+  push:
+    branches:
+      - '[0-9]+.[0-9]+.x'
+      - '8.0.x-hibernate7.*'
+  pull_request:
+  workflow_dispatch:
+# queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+jobs:
+  check_core_projects:
+    name: "Core Projects"
+    runs-on: ubuntu-24.04
+    steps:
+      - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it
+        run: curl -s https://api.ipify.org
+      - name: "📥 Checkout repository"
+        uses: actions/checkout@v6
+      - name: "☕️ Setup JDK"
+        uses: actions/setup-java@v4
+        with:
+          distribution: liberica
+          java-version: 21
+      - name: "🐘 Setup Gradle"
+        uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
+        with:
+          develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+      - name: "🔎 Check Core Projects"
+        run: ./gradlew aggregateAnalysisViolations --continue
+      - name: "📤 Upload Reports"
+        if: always()
+        uses: actions/upload-artifact@v7.0.1
+        with:
+          name: core-reports
+          path: build/reports/violations/
+      - name: "📋 Publish Code Analysis Report in Job Summary"
+        if: always()
+        run: |
+          echo "## 🔎 Code Analysis Report - Core Projects" >> $GITHUB_STEP_SUMMARY
+          for report in PMD_VIOLATIONS.md SPOTBUGS_VIOLATIONS.md; do
+            file="build/reports/violations/$report"
+            [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true
+          done
+  check_gradle_plugin_projects:
+    name: "Gradle Plugin Projects"
+    runs-on: ubuntu-24.04
+    steps:
+      - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it
+        run: curl -s https://api.ipify.org
+      - name: "📥 Checkout repository"
+        uses: actions/checkout@v6
+      - name: "☕️ Setup JDK"
+        uses: actions/setup-java@v4
+        with:
+          distribution: liberica
+          java-version: 21
+      - name: "🐘 Setup Gradle"
+        uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
+        with:
+          develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
+      - name: "🔎 Check Gradle Plugin Projects"
+        working-directory: grails-gradle
+        run: ./gradlew aggregateAnalysisViolations --continue
+      - name: "📤 Upload Reports"
+        if: always()
+        uses: actions/upload-artifact@v7.0.1
+        with:
+          name: gradle-plugin-reports
+          path: grails-gradle/build/reports/violations/
+      - name: "📋 Publish Code Analysis Report in Job Summary"
+        if: always()
+        run: |
+          echo "## 🔎 Code Analysis Report - Gradle Plugin Projects" >> $GITHUB_STEP_SUMMARY
+          for report in PMD_VIOLATIONS.md SPOTBUGS_VIOLATIONS.md; do
+            file="grails-gradle/build/reports/violations/$report"
+            [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true
+          done
diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml
index 4a3663fea1..cb4abe4ec8 100644
--- a/.github/workflows/codestyle.yml
+++ b/.github/workflows/codestyle.yml
@@ -18,6 +18,7 @@ on:
   push:
     branches:
       - '[0-9]+.[0-9]+.x'
+      - '8.0.x-hibernate7.*'
   pull_request:
   workflow_dispatch:
 # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits
@@ -43,27 +44,23 @@ jobs:
         with:
           develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
       - name: "🔎 Check Core Projects"
-        run: ./gradlew codeStyle
-      - name: "📤 Upload Failure Reports"
+        run: ./gradlew aggregateStyleViolations --continue
+      - name: "📤 Upload Reports"
         if: always()
         uses: actions/upload-artifact@v7.0.1
         with:
           name: core-reports
-          path: build/reports/codestyle/
+          path: build/reports/violations/
       - name: "📋 Publish Code Style Report in Job Summary"
         if: always()
         run: |
           echo "## 🔎 Code Style Report - Core Projects" >> $GITHUB_STEP_SUMMARY
-          for file in build/reports/codestyle/checkstyle/*.xml build/reports/codestyle/codenarc/*.xml; do
-            [ -f "$file" ] || continue
-            if grep -q "<error " "$file" || grep -q "<Violation " "$file"; then
-              echo "### ❌ $(basename $file .xml)" >> $GITHUB_STEP_SUMMARY
-              grep "<error " "$file" | awk -F'"' '{print "- **Line " $2 "**: " $8}' >> $GITHUB_STEP_SUMMARY
-              grep "<Violation " "$file" | awk -F"'" '{print "- **Line " $6 "**: " $2}' >> $GITHUB_STEP_SUMMARY
-            fi
+          for report in CODENARC_VIOLATIONS.md CHECKSTYLE_VIOLATIONS.md; do
+            file="build/reports/violations/$report"
+            [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true
           done
-  check_gradle_plugin_projects:
-    name: "Gradle Plugin Projects"
+  check_forge_projects:
+    name: "Forge Projects"
     runs-on: ubuntu-24.04
     steps:
       - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it
@@ -79,29 +76,25 @@ jobs:
         uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
         with:
           develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
-      - name: "🔎 Check Gradle Plugin Projects"
-        working-directory: grails-gradle
-        run: ./gradlew codeStyle
-      - name: "📤 Upload Failure Reports"
+      - name: "🔎 Check Forge Projects"
+        working-directory: grails-forge
+        run: ./gradlew aggregateStyleViolations --continue
+      - name: "📤 Upload Reports"
         if: always()
         uses: actions/upload-artifact@v7.0.1
         with:
-          name: gradle-plugin-reports
-          path: grails-gradle/build/reports/codestyle/
+          name: forge-reports
+          path: grails-forge/build/reports/violations/
       - name: "📋 Publish Code Style Report in Job Summary"
         if: always()
         run: |
-          echo "## 🔎 Code Style Report - Gradle Plugin Projects" >> $GITHUB_STEP_SUMMARY
-          for file in grails-gradle/build/reports/codestyle/checkstyle/*.xml grails-gradle/build/reports/codestyle/codenarc/*.xml; do
-            [ -f "$file" ] || continue
-            if grep -q "<error " "$file" || grep -q "<Violation " "$file"; then
-              echo "### ❌ $(basename $file .xml)" >> $GITHUB_STEP_SUMMARY
-              grep "<error " "$file" | awk -F'"' '{print "- **Line " $2 "**: " $8}' >> $GITHUB_STEP_SUMMARY
-              grep "<Violation " "$file" | awk -F"'" '{print "- **Line " $6 "**: " $2}' >> $GITHUB_STEP_SUMMARY
-            fi
+          echo "## 🔎 Code Style Report - Forge Projects" >> $GITHUB_STEP_SUMMARY
+          for report in CODENARC_VIOLATIONS.md CHECKSTYLE_VIOLATIONS.md; do
+            file="grails-forge/build/reports/violations/$report"
+            [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true
           done
-  check_grails_forge_projects:
-    name: "Forge Projects"
+  check_gradle_plugin_projects:
+    name: "Gradle Plugin Projects"
     runs-on: ubuntu-24.04
     steps:
       - name: "🌐 Output Agent IP" # in the event RAO blocks this agent, this can be used to debug it
@@ -117,24 +110,20 @@ jobs:
         uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
         with:
           develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
-      - name: "🔎 Check Forge Projects"
-        working-directory: grails-forge
-        run: ./gradlew codeStyle
-      - name: "📤 Upload Failure Reports"
+      - name: "🔎 Check Gradle Plugin Projects"
+        working-directory: grails-gradle
+        run: ./gradlew aggregateStyleViolations --continue
+      - name: "📤 Upload Reports"
         if: always()
         uses: actions/upload-artifact@v7.0.1
         with:
-          name: forge-reports
-          path: grails-forge/build/reports/codestyle/
+          name: gradle-plugin-reports
+          path: grails-gradle/build/reports/violations/
       - name: "📋 Publish Code Style Report in Job Summary"
         if: always()
         run: |
-          echo "## 🔎 Code Style Report - Forge Projects" >> $GITHUB_STEP_SUMMARY
-          for file in grails-forge/build/reports/codestyle/checkstyle/*.xml grails-forge/build/reports/codestyle/codenarc/*.xml; do
-            [ -f "$file" ] || continue
-            if grep -q "<error " "$file" || grep -q "<Violation " "$file"; then
-              echo "### ❌ $(basename $file .xml)" >> $GITHUB_STEP_SUMMARY
-              grep "<error " "$file" | awk -F'"' '{print "- **Line " $2 "**: " $8}' >> $GITHUB_STEP_SUMMARY
-              grep "<Violation " "$file" | awk -F"'" '{print "- **Line " $6 "**: " $2}' >> $GITHUB_STEP_SUMMARY
-            fi
-          done
\ No newline at end of file
+          echo "## 🔎 Code Style Report - Gradle Plugin Projects" >> $GITHUB_STEP_SUMMARY
+          for report in CODENARC_VIOLATIONS.md CHECKSTYLE_VIOLATIONS.md; do
+            file="grails-gradle/build/reports/violations/$report"
+            [ -f "$file" ] && cat "$file" >> $GITHUB_STEP_SUMMARY || true
+          done
diff --git a/build-logic/plugins/build.gradle b/build-logic/plugins/build.gradle
index ef5b010fbd..b196ee2431 100644
--- a/build-logic/plugins/build.gradle
+++ b/build-logic/plugins/build.gradle
@@ -38,6 +38,15 @@ dependencies {
     implementation "${gradleBomDependencies['grails-publish-plugin']}"
     implementation "org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:${gradleProperties.gradleChecksumPluginVersion}"
     implementation "org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:${gradleProperties.gradleCycloneDxPluginVersion}"
+    implementation "com.github.spotbugs.snom:spotbugs-gradle-plugin:${gradleProperties.spotbugsPluginVersion}"
+
+    testImplementation "org.spockframework:spock-core:${gradleBomDependencyVersions['gradle-spock.version']}"
+    testImplementation gradleTestKit()
+    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+tasks.named('test') {
+    useJUnitPlatform()
 }
 
 gradlePlugin {
@@ -62,6 +71,14 @@ gradlePlugin {
             id = 'org.apache.grails.gradle.grails-code-style'
             implementationClass = 'org.apache.grails.buildsrc.GrailsCodeStylePlugin'
         }
+        register('grailsCodeAnalysis') {
+            id = 'org.apache.grails.gradle.grails-code-analysis'
+            implementationClass = 'org.apache.grails.buildsrc.GrailsCodeAnalysisPlugin'
+        }
+        register('grailsViolationAggregation') {
+            id = 'org.apache.grails.gradle.grails-violation-aggregation'
+            implementationClass = 'org.apache.grails.buildsrc.GrailsViolationAggregationPlugin'
+        }
         register('groovydocEnhancer') {
             id = 'org.apache.grails.buildsrc.groovydoc-enhancer'
             implementationClass = 'org.apache.grails.buildsrc.GroovydocEnhancerPlugin'
@@ -79,4 +96,4 @@ gradlePlugin {
             implementationClass = 'org.apache.grails.buildsrc.GrailsDependencyValidatorPlugin'
         }
     }
-}
\ No newline at end of file
+}
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy
index 55ac4e38ab..f556d634e2 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GradleUtils.groovy
@@ -22,6 +22,7 @@ package org.apache.grails.buildsrc
 import groovy.transform.CompileStatic
 import org.gradle.api.Project
 import org.gradle.api.file.Directory
+import org.gradle.api.provider.Provider
 
 @CompileStatic
 class GradleUtils {
@@ -37,6 +38,12 @@ class GradleUtils {
         asfFile.exists() ? currentDirectory : findAsfRootDir(currentDirectory.dir('../'))
     }
 
+    static Provider<Boolean> booleanProvider(Project project, String name, boolean defaultValue = false) {
+        project.providers.gradleProperty(name)
+                .map { Boolean.parseBoolean(it as String) }
+                .orElse(defaultValue)
+    }
+
     static <T> T lookupProperty(Project project, String name, T defaultValue = null) {
         T v = lookupPropertyByType(project, name, defaultValue?.class) as T
         return v == null ? defaultValue : v
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisExtension.groovy
similarity index 61%
copy from build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy
copy to build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisExtension.groovy
index 8d338d3482..f7ba0448fe 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisExtension.groovy
@@ -27,36 +27,27 @@ import org.gradle.api.file.DirectoryProperty
 import org.gradle.api.model.ObjectFactory
 
 @CompileStatic
-class GrailsCodeStyleExtension {
+class GrailsCodeAnalysisExtension {
 
     /**
-     * Defaults to project.buildDir/checkstyle.
-     * Default checkstyle files will be written here and used from this location.
+     * Defaults to project.rootProject.buildDir/codestyle/pmd.
+     * Directory for PMD configuration files (e.g. pmd.xml).
      */
-    final DirectoryProperty checkstyleDirectory
+    final DirectoryProperty pmdDirectory
 
     /**
-     * Defaults to project.buildDir/codenarc.
-     * Default codenarc files will be written here and used from this location.
-     */
-    final DirectoryProperty codenarcDirectory
-
-    /**
-     * Defaults to rootProject.buildDir/reports/codestyle.
-     * All Checkstyle and Codenarc reports will be written here.
+     * Defaults to rootProject.buildDir/reports/codeanalysis.
+     * PMD and SpotBugs XML reports will be written here.
      */
     final DirectoryProperty reportsDirectory
 
     @Inject
-    GrailsCodeStyleExtension(ObjectFactory objects, Project project) {
-        checkstyleDirectory = objects.directoryProperty().convention(
-                project.rootProject.layout.buildDirectory.dir('checkstyle')
-        )
-        codenarcDirectory = objects.directoryProperty().convention(
-                project.rootProject.layout.buildDirectory.dir('codenarc')
+    GrailsCodeAnalysisExtension(ObjectFactory objects, Project project) {
+        pmdDirectory = objects.directoryProperty().convention(
+                project.rootProject.layout.buildDirectory.dir('codeanalysis/pmd')
         )
         reportsDirectory = objects.directoryProperty().convention(
-                project.rootProject.layout.buildDirectory.dir('reports/codestyle')
+                project.rootProject.layout.buildDirectory.dir('reports/codeanalysis')
         )
     }
 }
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy
new file mode 100644
index 0000000000..a4d1ee17d1
--- /dev/null
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeAnalysisPlugin.groovy
@@ -0,0 +1,183 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.buildsrc
+
+import java.nio.file.Files
+import java.nio.file.Path
+
+import groovy.transform.CompileStatic
+
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.quality.Pmd
+import org.gradle.api.plugins.quality.PmdExtension
+import org.gradle.api.plugins.quality.PmdPlugin
+import org.gradle.api.provider.Provider
+
+import com.github.spotbugs.snom.Confidence
+import com.github.spotbugs.snom.Effort
+import com.github.spotbugs.snom.SpotBugsExtension
+import com.github.spotbugs.snom.SpotBugsPlugin
+import com.github.spotbugs.snom.SpotBugsReport
+import com.github.spotbugs.snom.SpotBugsTask
+
+/**
+ * Convention plugin for Grails byte code analysis (PMD and SpotBugs).
+ * Both tools are opt-in; enable via Gradle properties.
+ */
+@CompileStatic
+class GrailsCodeAnalysisPlugin implements Plugin<Project> {
+
+    static String PMD_DIR_PROPERTY = 'grails.codeanalysis.dir.pmd'
+    static String PMD_ENABLED_PROPERTY = 'grails.codeanalysis.enabled.pmd'
+    static String PMD_CONFIG_FILE_NAME = 'pmd.xml'
+
+    static String SPOTBUGS_ENABLED_PROPERTY = 'grails.codeanalysis.enabled.spotbugs'
+
+    static String IGNORE_FAILURES_PROPERTY = 'grails.codeanalysis.ignoreFailures'
+    static String TEST_ANALYSIS_PROPERTY = 'grails.codeanalysis.enabled.tests'
+
+    static String BASE_RESOURCE_PATH = '/META-INF/org.apache.grails.buildsrc.grails-code-analysis'
+
+    @Override
+    void apply(Project project) {
+        initExtension(project)
+        configurePmd(project)
+        configureSpotbugs(project)
+
+        // withType returns a live empty collection when the tool is not enabled,
+        // so these dependsOn calls are safe regardless of whether PMD/SpotBugs are active
+        project.tasks.register('codeAnalysis') { task ->
+            task.group = 'verification'
+            task.description = 'Runs code analysis checks (PMD, SpotBugs)'
+            task.dependsOn(project.tasks.withType(Pmd))
+            task.dependsOn(project.tasks.withType(SpotBugsTask))
+        }
+    }
+
+    private static void initExtension(Project project) {
+        def gca = project.extensions.create('grailsCodeAnalysis', GrailsCodeAnalysisExtension)
+
+        gca.pmdDirectory.set(project.provider {
+            def directory = project.hasProperty(PMD_DIR_PROPERTY) ?
+                    project.rootProject.layout.projectDirectory.dir(project.property(PMD_DIR_PROPERTY) as String) :
+                    project.rootProject.layout.buildDirectory.get().dir('codeanalysis').dir('pmd')
+
+            def toCreate = directory.asFile.toPath()
+            Files.createDirectories(toCreate)
+
+            createOrLoad(
+                    toCreate.resolve(PMD_CONFIG_FILE_NAME),
+                    "${BASE_RESOURCE_PATH}/pmd/${PMD_CONFIG_FILE_NAME}",
+                    project
+            )
+
+            directory
+        })
+    }
+
+    private static void createOrLoad(Path expectedPath, String defaultResource, Project project) {
+        boolean defaultPath = expectedPath.startsWith(project.rootProject.buildDir.toPath())
+        if (!Files.exists(expectedPath) || expectedPath.size() == 0 || defaultPath) {
+            def defaultValue = GrailsCodeAnalysisPlugin.getResourceAsStream(defaultResource)
+            if (!defaultValue) {
+                throw new IllegalStateException("Could not locate default configuration file: ${defaultResource}")
+            }
+            project.logger.info('Replacing code analysis configuration')
+            expectedPath.text = defaultValue.text
+        }
+    }
+
+    static void configurePmd(Project project) {
+        Provider<Boolean> pmdEnabled = GradleUtils.booleanProvider(project, PMD_ENABLED_PROPERTY)
+        if (!pmdEnabled.get()) {
+            return
+        }
+
+        project.pluginManager.apply(PmdPlugin)
+
+        Provider<Boolean> ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY)
+        Provider<Boolean> testStylingEnabled = GradleUtils.booleanProvider(project, TEST_ANALYSIS_PROPERTY)
+
+        project.extensions.configure(PmdExtension) {
+            it.ruleSetFiles = project.files(project.extensions.getByType(GrailsCodeAnalysisExtension).pmdDirectory.file(PMD_CONFIG_FILE_NAME))
+            it.ruleSets = []
+            it.ignoreFailures = ignoreFailures.get()
+            it.consoleOutput = true
+            it.toolVersion = project.findProperty('pmdVersion')
+        }
+
+        project.tasks.withType(Pmd).configureEach { Pmd task ->
+            task.group = 'verification'
+            task.onlyIf { !project.hasProperty('skipCodeStyle') }
+            task.ignoreFailures = ignoreFailures.get()
+
+            if (task.name.contains('Test') || task.name.contains('test')) {
+                task.enabled = testStylingEnabled.get()
+            }
+
+            task.reports.xml.required.set(true)
+            task.reports.xml.outputLocation.set(
+                    project.extensions.getByType(GrailsCodeAnalysisExtension)
+                            .reportsDirectory.get()
+                            .dir('pmd')
+                            .file("${project.name}-${task.name}.xml")
+            )
+        }
+    }
+
+    static void configureSpotbugs(Project project) {
+        Provider<Boolean> spotbugsEnabled = GradleUtils.booleanProvider(project, SPOTBUGS_ENABLED_PROPERTY)
+        if (!spotbugsEnabled.get()) {
+            return
+        }
+
+        project.pluginManager.apply(SpotBugsPlugin)
+
+        Provider<Boolean> ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY)
+        Provider<Boolean> testStylingEnabled = GradleUtils.booleanProvider(project, TEST_ANALYSIS_PROPERTY)
+
+        project.extensions.configure(SpotBugsExtension) {
+            it.effort.set(Effort.valueOf('MAX'))
+            it.reportLevel.set(Confidence.valueOf('HIGH'))
+            it.ignoreFailures.set(ignoreFailures)
+        }
+
+        project.tasks.withType(SpotBugsTask).configureEach { SpotBugsTask task ->
+            task.group = 'verification'
+            NamedDomainObjectContainer<SpotBugsReport> spotBugsReports = task.reports
+            SpotBugsReport htmlReport = spotBugsReports.maybeCreate('html')
+            htmlReport.required.set(true)
+            SpotBugsReport xmlReport = spotBugsReports.maybeCreate('xml')
+            xmlReport.required.set(true)
+            xmlReport.outputLocation.set(
+                project.extensions.getByType(GrailsCodeAnalysisExtension)
+                        .reportsDirectory.get()
+                        .dir('spotbugs')
+                        .file("${project.name}-${task.name}.xml")
+            )
+            task.onlyIf { !project.hasProperty('skipCodeStyle') }
+
+            if (task.name.contains('Test') || task.name.contains('test')) {
+                task.enabled = testStylingEnabled.get()
+            }
+        }
+    }
+}
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy
index 8d338d3482..6fb04b0ae4 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStyleExtension.groovy
@@ -25,18 +25,19 @@ import groovy.transform.CompileStatic
 import org.gradle.api.Project
 import org.gradle.api.file.DirectoryProperty
 import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
 
 @CompileStatic
 class GrailsCodeStyleExtension {
 
     /**
-     * Defaults to project.buildDir/checkstyle.
+     * Defaults to project.rootProject.buildDir/codestyle/checkstyle.
      * Default checkstyle files will be written here and used from this location.
      */
     final DirectoryProperty checkstyleDirectory
 
     /**
-     * Defaults to project.buildDir/codenarc.
+     * Defaults to project.rootProject.buildDir/codestyle/codenarc.
      * Default codenarc files will be written here and used from this location.
      */
     final DirectoryProperty codenarcDirectory
@@ -50,10 +51,10 @@ class GrailsCodeStyleExtension {
     @Inject
     GrailsCodeStyleExtension(ObjectFactory objects, Project project) {
         checkstyleDirectory = objects.directoryProperty().convention(
-                project.rootProject.layout.buildDirectory.dir('checkstyle')
+                project.rootProject.layout.buildDirectory.dir('codestyle/checkstyle')
         )
         codenarcDirectory = objects.directoryProperty().convention(
-                project.rootProject.layout.buildDirectory.dir('codenarc')
+                project.rootProject.layout.buildDirectory.dir('codestyle/codenarc')
         )
         reportsDirectory = objects.directoryProperty().convention(
                 project.rootProject.layout.buildDirectory.dir('reports/codestyle')
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy
index 67974e3b88..d8ba2c881e 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsCodeStylePlugin.groovy
@@ -21,44 +21,52 @@ package org.apache.grails.buildsrc
 import java.nio.file.Files
 import java.nio.file.Path
 
+import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 
 import org.gradle.api.Plugin
 import org.gradle.api.Project
-import org.gradle.api.file.Directory
 import org.gradle.api.plugins.quality.Checkstyle
 import org.gradle.api.plugins.quality.CheckstyleExtension
 import org.gradle.api.plugins.quality.CheckstylePlugin
 import org.gradle.api.plugins.quality.CodeNarc
 import org.gradle.api.plugins.quality.CodeNarcExtension
 import org.gradle.api.plugins.quality.CodeNarcPlugin
+import org.gradle.api.provider.Provider
 
+/**
+ * Convention plugin for Grails code style enforcement (Checkstyle and CodeNarc).
+ */
 @CompileStatic
 class GrailsCodeStylePlugin implements Plugin<Project> {
 
     static String CHECKSTYLE_DIR_PROPERTY = 'grails.codestyle.dir.checkstyle'
+    static String CHECKSTYLE_ENABLED_PROPERTY = 'grails.codestyle.enabled.checkstyle'
     static String CHECKSTYLE_CONFIG_FILE_NAME = 'checkstyle.xml'
     static String CHECKSTYLE_SUPPRESSION_CONFIG_FILE_NAME = 'checkstyle-suppressions.xml'
 
     static String CODENARC_DIR_PROPERTY = 'grails.codestyle.dir.codenarc'
+    static String CODENARC_ENABLED_PROPERTY = 'grails.codestyle.enabled.codenarc'
     static String CODENARC_CONFIG_FILE_NAME = 'codenarc.groovy'
 
-    static String BASE_RESOURCE_PATH = '/META-INF/org.apache.grails.buildsrc.codestyle'
+    static String CODENARC_FIX_PROPERTY = 'grails.codestyle.codenarc.fix'
+
+    static String IGNORE_FAILURES_PROPERTY = 'grails.codestyle.ignoreFailures'
+
+    static String TEST_STYLING_PROPERTY = 'grails.codestyle.enabled.tests'
+
+    static String BASE_RESOURCE_PATH = '/META-INF/org.apache.grails.buildsrc.grails-code-style'
 
     @Override
     void apply(Project project) {
         initExtension(project)
         configureCodeStyle(project)
-        doNotApplyStylingToTests(project)
     }
 
     private static void initExtension(Project project) {
         def gce = project.extensions.create('grailsCodeStyle', GrailsCodeStyleExtension)
 
-        // Unfortunately, the codenarc plugin is still using a non-lazy property.
-        // Rather than rewrite the plugin to use afterEvaluate,
-        // this plugin uses properties to override the configuration location by default
-
+        // We are trying to avoid afterEvaluate usage here, so use properties for enabling / disabling
 
         gce.checkstyleDirectory.set(project.provider {
             def directory = project.hasProperty(CHECKSTYLE_DIR_PROPERTY) ?
@@ -70,11 +78,13 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
 
             createOrLoad(
                     toCreate.resolve(CHECKSTYLE_CONFIG_FILE_NAME),
-                    "${BASE_RESOURCE_PATH}/checkstyle/${CHECKSTYLE_CONFIG_FILE_NAME}"
+                    "${BASE_RESOURCE_PATH}/checkstyle/${CHECKSTYLE_CONFIG_FILE_NAME}",
+                    project
             )
             createOrLoad(
                     toCreate.resolve(CHECKSTYLE_SUPPRESSION_CONFIG_FILE_NAME),
-                    "${BASE_RESOURCE_PATH}/checkstyle/${CHECKSTYLE_SUPPRESSION_CONFIG_FILE_NAME}"
+                    "${BASE_RESOURCE_PATH}/checkstyle/${CHECKSTYLE_SUPPRESSION_CONFIG_FILE_NAME}",
+                    project
             )
 
             directory
@@ -90,40 +100,27 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
 
             createOrLoad(
                     toCreate.resolve(CODENARC_CONFIG_FILE_NAME),
-                    "${BASE_RESOURCE_PATH}/codenarc/${CODENARC_CONFIG_FILE_NAME}"
+                    "${BASE_RESOURCE_PATH}/codenarc/${CODENARC_CONFIG_FILE_NAME}",
+                    project
             )
 
             directory
         })
     }
 
-    private static void createOrLoad(Path expectedPath, String defaultResource) {
-        if (!Files.exists(expectedPath) || expectedPath.size() == 0) {
+    private static void createOrLoad(Path expectedPath, String defaultResource, Project project) {
+        boolean defaultPath = expectedPath.startsWith(project.rootProject.buildDir.toPath())
+        if (!Files.exists(expectedPath) || expectedPath.size() == 0 || defaultPath) {
             def defaultValue = GrailsCodeStylePlugin.getResourceAsStream(defaultResource)
             if (!defaultValue) {
                 throw new IllegalStateException("Could not locate default configuration file: ${defaultResource}")
             }
+            // TODO: This really need to use gradle caching instead
+            project.logger.info("Replacing code style configuration")
             expectedPath.text = defaultValue.text
         }
     }
 
-    private static void doNotApplyStylingToTests(Project project) {
-        project.tasks.named('checkstyleTest') {
-            it.enabled = false // Do not check test sources at this time
-        }
-
-        project.afterEvaluate {
-            // Do not check test sources at this time
-            ['codenarcIntegrationTest', 'codenarcTest'].each { testTaskName ->
-                if (project.tasks.names.contains(testTaskName)) {
-                    project.tasks.named(testTaskName) {
-                        it.enabled = false
-                    }
-                }
-            }
-        }
-    }
-
     private static void configureCodeStyle(Project project) {
         configureCheckstyle(project)
         configureCodenarc(project)
@@ -131,31 +128,37 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
         project.tasks.register('codeStyle') {
             it.group = 'verification'
             it.description = 'Runs code style checks'
-            it.dependsOn(project.tasks.withType(Checkstyle))
             it.dependsOn(project.tasks.withType(CodeNarc))
+            it.dependsOn(project.tasks.withType(Checkstyle))
         }
     }
 
     static void configureCheckstyle(Project project) {
         project.pluginManager.apply(CheckstylePlugin)
 
+        Provider<Boolean> ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY)
+        Provider<Boolean> testStylingEnabled = GradleUtils.booleanProvider(project, TEST_STYLING_PROPERTY)
+
         project.extensions.configure(CheckstyleExtension) {
             // Explicit `it` is required in extension configuration
             it.getConfigDirectory().set(project.extensions.getByType(GrailsCodeStyleExtension).checkstyleDirectory)
             it.maxWarnings = 0
             it.showViolations = true
-            it.ignoreFailures = false
+            it.ignoreFailures = ignoreFailures.get()
             it.toolVersion = project.findProperty('checkstyleVersion')
         }
 
         project.tasks.withType(Checkstyle).configureEach { Checkstyle task ->
             task.group = 'verification'
             task.onlyIf { !project.hasProperty('skipCodeStyle') }
+            task.ignoreFailures = ignoreFailures.get()
+
+            if (task.name.contains('Test') || task.name.contains('test')) {
+                task.enabled = testStylingEnabled.get()
+            }
 
             // Redirect XML report output to a single directory to consolidate
-            // reports across all subprojects into one known location.
-            // Include the task name to avoid overlapping outputs when a project has
-            // multiple source sets (e.g. grails-cache has ast + main).
+            // reports across all subprojects into one known location
             task.reports.xml.outputLocation.set(
                     project.extensions.getByType(GrailsCodeStyleExtension)
                             .reportsDirectory.get()
@@ -168,20 +171,34 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
     static void configureCodenarc(Project project) {
         project.pluginManager.apply(CodeNarcPlugin)
 
+        registerCodenarcFixTask(project)
+
+        Provider<Boolean> ignoreFailures = GradleUtils.booleanProvider(project, IGNORE_FAILURES_PROPERTY)
+        Provider<Boolean> testStylingEnabled = GradleUtils.booleanProvider(project, TEST_STYLING_PROPERTY)
+        Provider<Boolean> codenarcFix = GradleUtils.booleanProvider(project, CODENARC_FIX_PROPERTY)
+
         project.extensions.configure(CodeNarcExtension) {
             it.configFile = project.extensions.getByType(GrailsCodeStyleExtension)
-                    .codenarcDirectory.get().file(CODENARC_CONFIG_FILE_NAME).asFile
+                    .codenarcDirectory.file(CODENARC_CONFIG_FILE_NAME).get().asFile
+            it.ignoreFailures = ignoreFailures.get()
             it.toolVersion = project.findProperty('codenarcVersion')
         }
 
         project.tasks.withType(CodeNarc).configureEach { CodeNarc task ->
             task.group = 'verification'
             task.onlyIf { !project.hasProperty('skipCodeStyle') }
+            task.ignoreFailures = ignoreFailures.get()
+
+            if (codenarcFix.get()) {
+                task.dependsOn('codenarcFix')
+            }
+
+            if (task.name.contains('Test') || task.name.contains('test')) {
+                task.enabled = testStylingEnabled.get()
+            }
 
             // Redirect XML report output to a single directory to consolidate
-            // reports across all subprojects into one known location.
-            // Include the task name to avoid overlapping outputs when a project has
-            // multiple source sets.
+            // reports across all subprojects into one known location
             task.reports.xml.required.set(true)
             task.reports.xml.outputLocation.set(
                     project.extensions.getByType(GrailsCodeStyleExtension)
@@ -191,4 +208,52 @@ class GrailsCodeStylePlugin implements Plugin<Project> {
             )
         }
     }
+
+    @CompileDynamic
+    private static void registerCodenarcFixTask(Project project) {
+        project.tasks.register('codenarcFix') {
+            it.group = 'verification'
+            it.description = 'Automatically fixes some CodeNarc violations'
+            it.doLast {
+                project.fileTree(project.projectDir) {
+                    it.include 'src/**/*.groovy'
+                    it.include 'grails-app/**/*.groovy'
+                    it.include 'scripts/**/*.groovy'
+                    it.exclude '**/build/**'
+                }.each { file ->
+                    String content = file.text
+                    String original = content
+
+                    // 1. ClassStartsWithBlankLine
+                    content = content.replaceAll(/(class\s+[^{]+\{\n)([ \t]*[^ \s\n\/])/, '$1\n$2')
+
+                    // 2. SpaceAroundMapEntryColon
+                    content = content.replaceAll(/([\[,]\s*(?:[\w\-.]+|'[^']+'|"[^"]+")):([^\s\/])/, '$1: $2')
+                    content = content.replaceAll(/(\(\s*(?:[\w\-.]+|'[^']+'|"[^"]+")):([^\s\/])/, '$1: $2')
+
+                    // 3. UnnecessaryGString
+                    content = content.replaceAll(/(?<!\\)(?<!")"([^"$\n\\]*)"(?!")/) { all, inner ->
+                        if (!inner.contains("'")) {
+                            return "'$inner'"
+                        }
+                        return all
+                    }
+
+                    // 4. UnnecessarySemicolon
+                    content = content.replaceAll(/(?m);[ \t]*$/, '')
+
+                    // 5. SpaceBeforeOpeningBrace
+                    content = content.replaceAll(/(?<!\\)([\)\]\}\w])\{/, '$1 {')
+
+                    // 6. ConsecutiveBlankLines
+                    content = content.replaceAll(/\n{3,}/, '\n\n')
+
+                    if (content != original) {
+                        file.text = content
+                        project.logger.lifecycle("Fixed CodeNarc violations in ${file.path}")
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy
new file mode 100644
index 0000000000..d8e69897a8
--- /dev/null
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy
@@ -0,0 +1,467 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.buildsrc
+
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.Directory
+import org.gradle.api.file.FileCollection
+import org.gradle.api.logging.Logger
+import org.gradle.api.logging.Logging
+import org.gradle.api.plugins.AppliedPlugin
+import org.gradle.api.plugins.quality.Checkstyle
+import org.gradle.api.plugins.quality.CodeNarc
+import org.gradle.api.plugins.quality.Pmd
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.testing.jacoco.tasks.JacocoReport
+
+import com.github.spotbugs.snom.SpotBugsTask
+import groovy.xml.XmlSlurper
+
+/**
+ * Root-only convention plugin that aggregates code-style violation XML reports and JaCoCo coverage
+ * CSV reports into human-readable Markdown files under build/reports/violations/.
+ *
+ * Apply this plugin to the root project only. Subprojects should apply
+ * grails-code-style and grails-jacoco individually.
+ *
+ * Tasks registered:
+ *   aggregateStyleViolations    — CodeNarc + Checkstyle only
+ *   aggregateAnalysisViolations — PMD + SpotBugs only (requires opt-in properties)
+ *   aggregateViolations         — depends on both of the above
+ *   aggregateJacocoCoverage     — JaCoCo CSV → Markdown
+ */
+@CompileStatic
+class GrailsViolationAggregationPlugin implements Plugin<Project> {
+
+    private static final Logger LOGGER = Logging.getLogger(GrailsViolationAggregationPlugin)
+
+    @Override
+    void apply(Project project) {
+        if (project != project.rootProject) {
+            throw new GradleException(
+                'GrailsViolationAggregationPlugin must be applied to the root project only. ' +
+                'Apply grails-code-style and grails-jacoco to subprojects instead.'
+            )
+        }
+
+        Provider<Directory> violationsDir = project.layout.buildDirectory.dir('reports/violations')
+        Provider<Directory> styleXmlDir = project.layout.buildDirectory.dir('reports/codestyle')
+        Provider<Directory> analysisXmlDir = project.layout.buildDirectory.dir('reports/codeanalysis')
+
+        TaskProvider<Task> styleTask = registerStyleAggregation(project, styleXmlDir, violationsDir)
+        TaskProvider<Task> analysisTask = registerAnalysisAggregation(project, analysisXmlDir, violationsDir)
+        registerJacocoAggregation(project, violationsDir)
+
+        project.tasks.register('aggregateViolations') { Task task ->
+            task.group = 'verification'
+            task.description = 'Aggregates all violation reports (style + analysis) into build/reports/violations/'
+            task.dependsOn(styleTask, analysisTask)
+        }
+    }
+
+    private static TaskProvider<Task> registerStyleAggregation(Project root, Provider<Directory> styleXmlDir, Provider<Directory> violationsDir) {
+        // Wire property flags as Providers — values are resolved at task execution time, not at apply() time,
+        // and Providers are configuration-cache safe to capture in task actions
+        Provider<Boolean> checkStyleTests = GradleUtils.booleanProvider(root, GrailsCodeStylePlugin.TEST_STYLING_PROPERTY)
+        Provider<Boolean> codenarcEnabled = GradleUtils.booleanProvider(root, GrailsCodeStylePlugin.CODENARC_ENABLED_PROPERTY, true)
+        Provider<Boolean> checkstyleEnabled = GradleUtils.booleanProvider(root, GrailsCodeStylePlugin.CHECKSTYLE_ENABLED_PROPERTY, true)
+
+        TaskProvider<Task> aggregateTask = root.tasks.register('aggregateStyleViolations') { Task task ->
+            task.group = 'verification'
+            task.description = 'Aggregates CodeNarc and Checkstyle violation reports into build/reports/violations/'
+            task.outputs.file(root.file('build/reports/violations/CODENARC_VIOLATIONS.md'))
+            task.outputs.file(root.file('build/reports/violations/CHECKSTYLE_VIOLATIONS.md'))
+            task.doLast {
+                parseStyleViolations(styleXmlDir.get(), violationsDir.get(),
+                    checkStyleTests.get(), codenarcEnabled.get(), checkstyleEnabled.get())
+            }
+        }
+        root.subprojects { Project sub ->
+            sub.pluginManager.withPlugin('codenarc') { AppliedPlugin p ->
+                aggregateTask.configure { Task task ->
+                    task.dependsOn(sub.tasks.withType(CodeNarc))
+                }
+            }
+            sub.pluginManager.withPlugin('checkstyle') { AppliedPlugin p ->
+                aggregateTask.configure { Task task ->
+                    task.dependsOn(sub.tasks.withType(Checkstyle))
+                }
+            }
+        }
+        aggregateTask
+    }
+
+    private static TaskProvider<Task> registerAnalysisAggregation(Project root, Provider<Directory> analysisXmlDir, Provider<Directory> violationsDir) {
+        Provider<Boolean> checkAnalysisTests = GradleUtils.booleanProvider(root, GrailsCodeAnalysisPlugin.TEST_ANALYSIS_PROPERTY)
+        Provider<Boolean> pmdEnabled = GradleUtils.booleanProvider(root, GrailsCodeAnalysisPlugin.PMD_ENABLED_PROPERTY)
+        Provider<Boolean> spotbugsEnabled = GradleUtils.booleanProvider(root, GrailsCodeAnalysisPlugin.SPOTBUGS_ENABLED_PROPERTY)
+
+        TaskProvider<Task> aggregateTask = root.tasks.register('aggregateAnalysisViolations') { Task task ->
+            task.group = 'verification'
+            task.description = 'Aggregates PMD and SpotBugs violation reports into build/reports/violations/'
+            task.outputs.file(root.file('build/reports/violations/PMD_VIOLATIONS.md'))
+            task.outputs.file(root.file('build/reports/violations/SPOTBUGS_VIOLATIONS.md'))
+            task.doLast {
+                parseAnalysisViolations(analysisXmlDir.get(), violationsDir.get(),
+                    checkAnalysisTests.get(), pmdEnabled.get(), spotbugsEnabled.get())
+            }
+        }
+        root.subprojects { Project sub ->
+            sub.pluginManager.withPlugin('pmd') { AppliedPlugin p ->
+                aggregateTask.configure { Task task ->
+                    task.dependsOn(sub.tasks.withType(Pmd))
+                }
+            }
+            sub.pluginManager.withPlugin('com.github.spotbugs') { AppliedPlugin p ->
+                aggregateTask.configure { Task task ->
+                    task.dependsOn(sub.tasks.withType(SpotBugsTask))
+                }
+            }
+        }
+        aggregateTask
+    }
+
+    private static void registerJacocoAggregation(Project root, Provider<Directory> violationsDir) {
+        // Collect all potential CSV paths at configuration time — Project must not be referenced from task actions
+        FileCollection jacocoCsvFiles = root.files(
+            root.allprojects.collect { Project p -> p.file('build/reports/jacoco/test/jacocoTestReport.csv') }
+        )
+
+        TaskProvider<Task> aggregateTask = root.tasks.register('aggregateJacocoCoverage') { Task task ->
+            task.group = 'verification'
+            task.description = 'Aggregates JaCoCo coverage reports from all subprojects into build/reports/violations/'
+            task.inputs.files(jacocoCsvFiles).optional(true)
+            task.outputs.file(root.file('build/reports/violations/JACOCO_COVERAGE.md'))
+            task.doLast {
+                parseJacocoCoverage(jacocoCsvFiles, violationsDir.get())
+            }
+        }
+        root.subprojects { Project sub ->
+            sub.pluginManager.withPlugin('jacoco') { AppliedPlugin p ->
+                aggregateTask.configure { Task task ->
+                    task.dependsOn(sub.tasks.withType(JacocoReport))
+                }
+            }
+        }
+    }
+
+    @CompileDynamic
+    private static void parseStyleViolations(Directory styleXmlDir, Directory violationsDir,
+            boolean checkStyleTests, boolean codenarcEnabled, boolean checkstyleEnabled) {
+        def slurper = new XmlSlurper()
+        slurper.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false)
+        slurper.setFeature('http://xml.org/sax/features/namespaces', false)
+
+        def getModule = { String fileName ->
+            def lastDash = fileName.lastIndexOf('-')
+            lastDash != -1 ? fileName.substring(0, lastDash) : fileName
+        }
+
+        def isTestFile = { String fileName ->
+            fileName.toLowerCase().contains('test') || fileName.toLowerCase().contains('integrationtest')
+        }
+
+        def shouldSkipClass = { boolean includeTests, String className, String filePath = null ->
+            if (includeTests) {
+                return false
+            }
+            if (filePath && (filePath.contains('src/test/') || filePath.contains('src/integrationTest/'))) {
+                return true
+            }
+            !filePath && (className.contains('Spec') || className.contains('Test') || className.contains('Tests'))
+        }
+
+        def writeReport = { String fileName, List violations, String title ->
+            def outDir = violationsDir.asFile
+            outDir.mkdirs()
+            def reportFile = new File(outDir, fileName)
+            def out = new StringBuilder()
+            out.append("# ${title}\n")
+            out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))}\n\n")
+
+            if (violations.isEmpty()) {
+                out.append('No violations found! 🎉\n')
+            } else {
+                def uniqueViolations = violations.unique().sort { v -> "${v.module}:${v.className}:${v.line}" }
+                def groupedByModule = uniqueViolations.groupBy { it.module }.sort()
+                groupedByModule.each { module, modViolations ->
+                    out.append("## Module: ${module}\n")
+                    out.append('| Class | Tool | Violation | Line | Message |\n')
+                    out.append('| :--- | :--- | :--- | :--- | :--- |\n')
+                    modViolations.each { v ->
+                        out.append("| ${v.className} | ${v.tool} | ${v.type} | ${v.line} | ${v.message.replaceAll(/\|/, '\\|')} |\n")
+                    }
+                    out.append('\n')
+                }
+            }
+            reportFile.text = out.toString()
+            LOGGER.lifecycle("Aggregated report generated: ${reportFile.absolutePath}")
+        }
+
+        // CodeNarc
+        def codenarcViolations = []
+        def codenarcDir = styleXmlDir.dir('codenarc').asFile
+        if (codenarcDir.exists() && codenarcEnabled) {
+            codenarcDir.eachFileMatch(~/.*\.xml/) { file ->
+                if (file.size() == 0 || (!checkStyleTests && isTestFile(file.name))) {
+                    return
+                }
+                def module = getModule(file.name)
+                def xml = slurper.parse(file)
+                xml.Package.each { pkg ->
+                    pkg.File.each { f ->
+                        def pkgName = pkg.@name.text()
+                        def fileName = f.@name.text()
+                        def className = pkgName ? "${pkgName}.${fileName}" : fileName
+                        className = className.replace('.groovy', '').replace('.java', '')
+                        if (shouldSkipClass(checkStyleTests, className, f.@name.text())) {
+                            return
+                        }
+                        f.Violation.each { v ->
+                            codenarcViolations << [
+                                    module   : module,
+                                    className: className,
+                                    tool     : 'CodeNarc',
+                                    type     : v.@ruleName.text(),
+                                    line     : v.@lineNumber.text(),
+                                    message  : v.Message.text().trim()
+                            ]
+                        }
+                    }
+                }
+            }
+        }
+        writeReport('CODENARC_VIOLATIONS.md', codenarcViolations, 'CodeNarc Violations Summary')
+
+        // Checkstyle
+        def checkstyleViolations = []
+        def checkstyleDir = styleXmlDir.dir('checkstyle').asFile
+        if (checkstyleDir.exists() && checkstyleEnabled) {
+            checkstyleDir.eachFileMatch(~/.*\.xml/) { file ->
+                if (file.size() == 0 || (!checkStyleTests && isTestFile(file.name))) {
+                    return
+                }
+                def module = getModule(file.name)
+                def xml = slurper.parse(file)
+                xml.file.each { f ->
+                    def filePath = f.@name.text()
+                    def className = filePath.contains('src/main/groovy/') ? filePath.split('src/main/groovy/')[1] :
+                                    filePath.contains('src/main/java/')   ? filePath.split('src/main/java/')[1] :
+                                    filePath.contains('src/test/groovy/') ? filePath.split('src/test/groovy/')[1] :
+                                    filePath.contains('src/test/java/')   ? filePath.split('src/test/java/')[1] :
+                                    filePath.split('/').last()
+                    className = className.replace('.groovy', '').replace('.java', '').replace('/', '.')
+                    if (shouldSkipClass(checkStyleTests, className)) {
+                        return
+                    }
+                    f.error.each { e ->
+                        checkstyleViolations << [
+                                module   : module,
+                                className: className,
+                                tool     : 'Checkstyle',
+                                type     : e.@source.text().split(/\./).last(),
+                                line     : e.@line.text(),
+                                message  : e.@message.text().trim()
+                        ]
+                    }
+                }
+            }
+        }
+        writeReport('CHECKSTYLE_VIOLATIONS.md', checkstyleViolations, 'Checkstyle Violations Summary')
+    }
+
+    @CompileDynamic
+    private static void parseAnalysisViolations(Directory analysisXmlDir, Directory violationsDir,
+            boolean checkAnalysisTests, boolean pmdEnabled, boolean spotbugsEnabled) {
+        def slurper = new XmlSlurper()
+        slurper.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false)
+        slurper.setFeature('http://xml.org/sax/features/namespaces', false)
+
+        def getModule = { String fileName ->
+            def lastDash = fileName.lastIndexOf('-')
+            lastDash != -1 ? fileName.substring(0, lastDash) : fileName
+        }
+
+        def isTestFile = { String fileName ->
+            fileName.toLowerCase().contains('test') || fileName.toLowerCase().contains('integrationtest')
+        }
+
+        def shouldSkipClass = { boolean includeTests, String className ->
+            if (includeTests) {
+                return false
+            }
+            className.contains('Spec') || className.contains('Test') || className.contains('Tests')
+        }
+
+        def writeReport = { String fileName, List violations, String title ->
+            def outDir = violationsDir.asFile
+            outDir.mkdirs()
+            def reportFile = new File(outDir, fileName)
+            def out = new StringBuilder()
+            out.append("# ${title}\n")
+            out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))}\n\n")
+
+            if (violations.isEmpty()) {
+                out.append('No violations found! 🎉\n')
+            } else {
+                def uniqueViolations = violations.unique().sort { v -> "${v.module}:${v.className}:${v.line}" }
+                def groupedByModule = uniqueViolations.groupBy { it.module }.sort()
+                groupedByModule.each { module, modViolations ->
+                    out.append("## Module: ${module}\n")
+                    out.append('| Class | Tool | Violation | Line | Message |\n')
+                    out.append('| :--- | :--- | :--- | :--- | :--- |\n')
+                    modViolations.each { v ->
+                        out.append("| ${v.className} | ${v.tool} | ${v.type} | ${v.line} | ${v.message.replaceAll(/\|/, '\\|')} |\n")
+                    }
+                    out.append('\n')
+                }
+            }
+            reportFile.text = out.toString()
+            LOGGER.lifecycle("Aggregated report generated: ${reportFile.absolutePath}")
+        }
+
+        // PMD
+        def pmdViolations = []
+        def pmdDir = analysisXmlDir.dir('pmd').asFile
+        if (pmdDir.exists() && pmdEnabled) {
+            pmdDir.eachFileMatch(~/.*\.xml/) { file ->
+                if (file.size() == 0 || (!checkAnalysisTests && isTestFile(file.name))) {
+                    return
+                }
+                def module = getModule(file.name)
+                def xml = slurper.parse(file)
+                xml.file.each { f ->
+                    f.violation.each { v ->
+                        def className = "${v.@package}.${v.@class}"
+                        if (shouldSkipClass(checkAnalysisTests, className)) {
+                            return
+                        }
+                        pmdViolations << [
+                                module   : module,
+                                className: className,
+                                tool     : 'PMD',
+                                type     : v.@rule.text(),
+                                line     : v.@beginline.text(),
+                                message  : v.text().trim()
+                        ]
+                    }
+                }
+            }
+        }
+        writeReport('PMD_VIOLATIONS.md', pmdViolations, 'PMD Violations Summary')
+
+        // SpotBugs
+        def spotbugsViolations = []
+        def spotbugsDir = analysisXmlDir.dir('spotbugs').asFile
+        if (spotbugsDir.exists() && spotbugsEnabled) {
+            spotbugsDir.eachFileMatch(~/.*\.xml/) { file ->
+                if (file.size() == 0 || (!checkAnalysisTests && isTestFile(file.name))) {
+                    return
+                }
+                def module = getModule(file.name)
+                def xml = slurper.parse(file)
+                xml.BugInstance.each { b ->
+                    def className = b.Class.@classname.text()
+                    if (shouldSkipClass(checkAnalysisTests, className)) {
+                        return
+                    }
+                    spotbugsViolations << [
+                            module   : module,
+                            className: className,
+                            tool     : 'SpotBugs',
+                            type     : b.@type.text(),
+                            line     : b.SourceLine.@start.text(),
+                            message  : b.LongMessage.text().trim()
+                    ]
+                }
+            }
+        }
+        writeReport('SPOTBUGS_VIOLATIONS.md', spotbugsViolations, 'SpotBugs Violations Summary')
+    }
+
+    @CompileDynamic
+    private static void parseJacocoCoverage(FileCollection csvFiles, Directory violationsDir) {
+        def jacocoCoverage = []
+        csvFiles.each { File csvReport ->
+            if (csvReport.exists()) {
+                LOGGER.debug("Processing JaCoCo report: ${csvReport.absolutePath}")
+                csvReport.splitEachLine(',') { fields ->
+                    if (fields.size() < 5 || fields[0] == 'GROUP') {
+                        return
+                    }
+                    def module = fields[0]
+                    def pkg = fields[1]
+                    def clazz = fields[2]
+                    def missedStr = fields[3]
+                    def coveredStr = fields[4]
+
+                    if (missedStr.isNumber() && coveredStr.isNumber()) {
+                        def m = missedStr.toInteger()
+                        def c = coveredStr.toInteger()
+                        def total = m + c
+                        def percent = total > 0 ? (c * 100 / total).round(2) : 100.0
+
+                        jacocoCoverage << [
+                                module   : module,
+                                className: "${pkg}.${clazz}",
+                                percent  : percent
+                        ]
+                    }
+                }
+            }
+        }
+
+        if (jacocoCoverage.isEmpty()) {
+            LOGGER.info('No JaCoCo coverage reports found to aggregate')
+            return
+        }
+
+        jacocoCoverage.removeIf { it.className.startsWith('org.grails.orm.hibernate.support.hibernate7.') }
+
+        def outDir = violationsDir.asFile
+        outDir.mkdirs()
+        def reportFile = new File(outDir, 'JACOCO_COVERAGE.md')
+        def out = new StringBuilder()
+        out.append('# JaCoCo Coverage Report\n')
+        out.append("Generated on: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))}\n\n")
+
+        def groupedByModule = jacocoCoverage.groupBy { it.module }.sort()
+        groupedByModule.each { module, coverageList ->
+            out.append("## Module: ${module}\n")
+            out.append('| Class | % Instructions Covered |\n')
+            out.append('| :--- | :--- |\n')
+            coverageList.sort { it.percent }.each { c ->
+                out.append("| ${c.className} | ${c.percent}% |\n")
+            }
+            out.append('\n')
+        }
+        reportFile.text = out.toString()
+        LOGGER.lifecycle("Aggregated JaCoCo report generated: ${reportFile.absolutePath}")
+    }
+}
diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-analysis/pmd/pmd.xml
similarity index 63%
copy from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml
copy to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-analysis/pmd/pmd.xml
index 58ee65cc40..cf494a7125 100644
--- a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml
+++ b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-analysis/pmd/pmd.xml
@@ -17,6 +17,15 @@
     specific language governing permissions and limitations
     under the License.
 -->
-<!DOCTYPE suppressions PUBLIC "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" "https://checkstyle.org/dtds/suppressions_1_2.dtd">
-<suppressions>
-</suppressions>
+<ruleset name="Grails PMD Rules"
+        xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
+
+    <description>PMD ruleset for the Grails codebase</description>
+
+    <rule ref="category/java/bestpractices.xml"/>
+    <rule ref="category/java/errorprone.xml"/>
+    <rule ref="category/java/security.xml"/>
+
+</ruleset>
diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle-suppressions.xml
similarity index 100%
rename from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle-suppressions.xml
rename to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle-suppressions.xml
diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle.xml b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle.xml
similarity index 100%
rename from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/checkstyle/checkstyle.xml
rename to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/checkstyle/checkstyle.xml
diff --git a/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/codenarc/codenarc.groovy b/build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/codenarc/codenarc.groovy
similarity index 100%
rename from build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.codestyle/codenarc/codenarc.groovy
rename to build-logic/plugins/src/main/resources/META-INF/org.apache.grails.buildsrc.grails-code-style/codenarc/codenarc.groovy
diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy
new file mode 100644
index 0000000000..1d26a0eced
--- /dev/null
+++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsCodeStylePluginSpec.groovy
@@ -0,0 +1,135 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.buildsrc
+
+import org.gradle.testkit.runner.GradleRunner
+import spock.lang.Specification
+import spock.lang.TempDir
+import java.nio.file.Path
+
+class GrailsCodeStylePluginSpec extends Specification {
+    @TempDir
+    Path testProjectDir
+    
+    File buildFile
+    File groovyFile
+
+    def setup() {
+        buildFile = testProjectDir.resolve('build.gradle').toFile()
+        buildFile << """
+            plugins {
+                id 'groovy'
+                id 'org.apache.grails.gradle.grails-code-style'
+            }
+            
+            // Minimal configuration for the plugin
+            repositories {
+                mavenCentral()
+            }
+        """
+        
+        testProjectDir.resolve('src/main/groovy').toFile().mkdirs()
+        groovyFile = testProjectDir.resolve('src/main/groovy/Test.groovy').toFile()
+    }
+
+    def "test codenarcFix task fixes violations"() {
+        given: "a file with violations"
+        groovyFile.text = """package org.test
+
+class Test{
+    def map = [key:"value"]
+    def str = "unnecessary gstring"
+    def semi = "semicolon";
+    def lines = 1
+
+
+    def other = 2
+}
+"""
+        when: "running codenarcFix"
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('codenarcFix', '--stacktrace')
+                .withPluginClasspath()
+                .build()
+
+        then: "task finished successfully"
+        result.task(':codenarcFix').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS
+
+        and: "violations are fixed"
+        def fixedContent = groovyFile.text
+        fixedContent.contains('class Test {') // SpaceBeforeOpeningBrace
+        fixedContent.contains('class Test {\n\n    def map') 
+        fixedContent.contains("[key: 'value']") // SpaceAroundMapEntryColon and UnnecessaryGString
+        fixedContent.contains("'unnecessary gstring'") // UnnecessaryGString
+        fixedContent.contains("def semi = 'semicolon'") // UnnecessarySemicolon
+        !fixedContent.contains(";")
+        fixedContent.count('\n\n') == 3 // ConsecutiveBlankLines
+    }
+
+    def "test codenarcFix task does not break strings with single quotes"() {
+        given: "a file with double quoted strings containing single quotes"
+        groovyFile.text = """package org.test
+
+class Test {
+    def s1 = "it's a test"
+    def s2 = "format 'yyyy-MM-dd'"
+    def s3 = "contains \\"double\\" and 'single' quotes"
+}
+"""
+        when: "running codenarcFix"
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('codenarcFix', '--stacktrace')
+                .withPluginClasspath()
+                .build()
+
+        then: "task finished successfully"
+        result.task(':codenarcFix').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS
+
+        and: "strings with single quotes are NOT changed to single quotes (which would break them)"
+        def content = groovyFile.text
+        content.contains('def s1 = "it\'s a test"')
+        content.contains('def s2 = "format \'yyyy-MM-dd\'"')
+        // s3 has escaped double quotes, so it should also remain double quoted
+        content.contains('def s3 = "contains \\"double\\" and \'single\' quotes"')
+    }
+
+    def "test codenarcFix task does not break escaped double quotes in double quotes"() {
+        given: "a file with escaped double quotes"
+        groovyFile.text = """package org.test
+
+class Test {
+    def s = "\\"\\\$it\\""
+}
+"""
+        when: "running codenarcFix"
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('codenarcFix', '--stacktrace')
+                .withPluginClasspath()
+                .build()
+
+        then: "task finished successfully"
+        result.task(':codenarcFix').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS
+
+        and: "escaped quotes are NOT broken"
+        groovyFile.text.contains('"\\"\\$it\\""')
+    }
+}
diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPluginSpec.groovy
new file mode 100644
index 0000000000..be51ff7508
--- /dev/null
+++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPluginSpec.groovy
@@ -0,0 +1,177 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.grails.buildsrc
+
+import org.gradle.testkit.runner.GradleRunner
+import org.gradle.testkit.runner.TaskOutcome
+import spock.lang.Specification
+import spock.lang.TempDir
+
+import java.nio.file.Path
+
+class GrailsViolationAggregationPluginSpec extends Specification {
+
+    @TempDir
+    Path testProjectDir
+
+    def "plugin must be applied to root project only"() {
+        given: "a subproject-only build with the aggregation plugin"
+        testProjectDir.resolve('settings.gradle').toFile().text = "include 'sub'"
+        testProjectDir.resolve('build.gradle').toFile().text = ''
+        def sub = testProjectDir.resolve('sub')
+        sub.toFile().mkdirs()
+        sub.resolve('build.gradle').toFile().text = """
+            plugins {
+                id 'org.apache.grails.gradle.grails-violation-aggregation'
+            }
+        """
+
+        when: "configuring the project"
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('tasks')
+                .withPluginClasspath()
+                .buildAndFail()
+
+        then: "an error is thrown"
+        result.output.contains('must be applied to the root project only')
+    }
+
+    def "all aggregation tasks are registered on root"() {
+        given: "root project with aggregation plugin"
+        testProjectDir.resolve('settings.gradle').toFile().text = ''
+        testProjectDir.resolve('build.gradle').toFile().text = """
+            plugins {
+                id 'org.apache.grails.gradle.grails-violation-aggregation'
+            }
+        """
+
+        when: "listing verification tasks"
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('tasks', '--group=verification')
+                .withPluginClasspath()
+                .build()
+
+        then:
+        result.output.contains('aggregateStyleViolations')
+        result.output.contains('aggregateAnalysisViolations')
+        result.output.contains('aggregateViolations')
+        result.output.contains('aggregateJacocoCoverage')
+    }
+
+    def "aggregateStyleViolations writes CodeNarc and Checkstyle reports to build/reports/violations/"() {
+        given: "a root project with a subproject that has codestyle XML reports"
+        testProjectDir.resolve('settings.gradle').toFile().text = "include 'app-module'"
+        testProjectDir.resolve('build.gradle').toFile().text = """
+            plugins {
+                id 'org.apache.grails.gradle.grails-violation-aggregation'
+            }
+        """
+        def moduleDir = testProjectDir.resolve('app-module')
+        moduleDir.toFile().mkdirs()
+        moduleDir.resolve('build.gradle').toFile().text = """
+            plugins {
+                id 'groovy'
+                id 'org.apache.grails.gradle.grails-code-style'
+            }
+            repositories { mavenCentral() }
+            dependencies {
+                implementation localGroovy()
+            }
+        """
+        def srcFile = moduleDir.resolve('src/main/groovy/com/example/AppClass.groovy').toFile()
+        srcFile.parentFile.mkdirs()
+        srcFile.text = 'package com.example\nclass AppClass {}'
+
+        // Pre-populate XML reports in the standard consolidated location (build/reports/codestyle/)
+        def checkstyleDir = testProjectDir.resolve('build/reports/codestyle/checkstyle').toFile()
+        checkstyleDir.mkdirs()
+        new File(checkstyleDir, 'app-module-checkstyleMain.xml').text = """<?xml version="1.0" encoding="UTF-8"?>
+<checkstyle version="10.0">
+<file name="${srcFile.absolutePath}">
+<error line="2" column="1" severity="error" message="Missing a Javadoc comment." source="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck"/>
+</file>
+</checkstyle>
+"""
+        def codenarcDir = testProjectDir.resolve('build/reports/codestyle/codenarc').toFile()
+        codenarcDir.mkdirs()
+        new File(codenarcDir, 'app-module-codenarcMain.xml').text = """<?xml version="1.0" encoding="UTF-8"?>
+<CodeNarc version="3.1.0">
+<Package name="com.example">
+<File name="AppClass.groovy">
+<Violation ruleName="EmptyClass" priority="2" lineNumber="1">
+<Message>The class is empty</Message>
+</Violation>
+</File>
+</Package>
+</CodeNarc>
+"""
+
+        when: "running aggregateStyleViolations skipping the actual style tasks"
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('aggregateStyleViolations', '-x', 'checkstyleMain', '-x', 'codenarcMain', '--stacktrace')
+                .withPluginClasspath()
+                .build()
+
+        then: "task succeeds"
+        result.task(':aggregateStyleViolations').outcome == TaskOutcome.SUCCESS
+
+        and: "reports land in build/reports/violations/ — NOT in the repo root"
+        def violationsDir = testProjectDir.resolve('build/reports/violations').toFile()
+        new File(violationsDir, 'CHECKSTYLE_VIOLATIONS.md').exists()
+        new File(violationsDir, 'CODENARC_VIOLATIONS.md').exists()
+        !testProjectDir.resolve('CHECKSTYLE_VIOLATIONS.md').toFile().exists()
+        !testProjectDir.resolve('CODENARC_VIOLATIONS.md').toFile().exists()
+
+        and: "checkstyle report contains the violation"
+        def checkstyleMd = new File(violationsDir, 'CHECKSTYLE_VIOLATIONS.md').text
+        checkstyleMd.contains('## Module: app-module')
+        checkstyleMd.contains('JavadocPackageCheck')
+
+        and: "codenarc report contains the violation"
+        def codenarcMd = new File(violationsDir, 'CODENARC_VIOLATIONS.md').text
+        codenarcMd.contains('## Module: app-module')
+        codenarcMd.contains('EmptyClass')
+    }
+
+    def "aggregateJacocoCoverage handles no csv reports gracefully"() {
+        given: "root project with no subproject csv reports"
+        testProjectDir.resolve('settings.gradle').toFile().text = ''
+        testProjectDir.resolve('build.gradle').toFile().text = """
+            plugins {
+                id 'org.apache.grails.gradle.grails-violation-aggregation'
+            }
+        """
+
+        when:
+        def result = GradleRunner.create()
+                .withProjectDir(testProjectDir.toFile())
+                .withArguments('aggregateJacocoCoverage', '--stacktrace')
+                .withPluginClasspath()
+                .build()
+
+        then: "task succeeds without error"
+        result.task(':aggregateJacocoCoverage').outcome == TaskOutcome.SUCCESS
+
+        and: "no report file is created"
+        !testProjectDir.resolve('build/reports/violations/JACOCO_COVERAGE.md').toFile().exists()
+    }
+}
diff --git a/build.gradle b/build.gradle
index bb330e2487..87b54c25f2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,6 +15,10 @@
  *  limitations under the License.
  */
 
+plugins {
+    id 'org.apache.grails.gradle.grails-violation-aggregation'
+}
+
 import java.time.Instant
 import java.time.LocalDate
 import java.time.ZoneOffset
diff --git a/gradle.properties b/gradle.properties
index 547692b327..75cd9d7d1c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -70,6 +70,8 @@ jbossTransactionApiVersion=2.0.0.Final
 # build dependencies for code quality checks
 checkstyleVersion=11.0.0
 codenarcVersion=3.6.0-groovy-4.0
+pmdVersion=6.55.0
+spotbugsPluginVersion=6.4.8
 
 # This prevents the Grails Gradle Plugin from unnecessarily excluding slf4j-simple in the generated POMs
 # https://github.com/apache/grails-gradle-plugin/issues/222
diff --git a/grails-async/core/build.gradle b/grails-async/core/build.gradle
index cee4816f2e..b2f5f45bc1 100644
--- a/grails-async/core/build.gradle
+++ b/grails-async/core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-async/gpars/build.gradle b/grails-async/gpars/build.gradle
index 8c8d001b26..e30f7d2e28 100644
--- a/grails-async/gpars/build.gradle
+++ b/grails-async/gpars/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-async/plugin/build.gradle b/grails-async/plugin/build.gradle
index c6d1f34085..73984e20e0 100644
--- a/grails-async/plugin/build.gradle
+++ b/grails-async/plugin/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-async/rxjava/build.gradle b/grails-async/rxjava/build.gradle
index 0bc5e0a8c7..9494becc59 100644
--- a/grails-async/rxjava/build.gradle
+++ b/grails-async/rxjava/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-async/rxjava2/build.gradle b/grails-async/rxjava2/build.gradle
index 1e3d564320..aa269ef1b8 100644
--- a/grails-async/rxjava2/build.gradle
+++ b/grails-async/rxjava2/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-async/rxjava3/build.gradle b/grails-async/rxjava3/build.gradle
index d5c36cb6f4..25e643124b 100644
--- a/grails-async/rxjava3/build.gradle
+++ b/grails-async/rxjava3/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-bootstrap/build.gradle b/grails-bootstrap/build.gradle
index 8326c9da51..22b423f149 100644
--- a/grails-bootstrap/build.gradle
+++ b/grails-bootstrap/build.gradle
@@ -29,6 +29,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-cache/build.gradle b/grails-cache/build.gradle
index 9d607d16ca..45c83921c4 100644
--- a/grails-cache/build.gradle
+++ b/grails-cache/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-codecs-core/build.gradle b/grails-codecs-core/build.gradle
index a6e2562f86..8b952e765f 100644
--- a/grails-codecs-core/build.gradle
+++ b/grails-codecs-core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-codecs/build.gradle b/grails-codecs/build.gradle
index 0e612cbb19..47fbaab85c 100644
--- a/grails-codecs/build.gradle
+++ b/grails-codecs/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-common/build.gradle b/grails-common/build.gradle
index 8957177a9a..f6ca3c40f1 100644
--- a/grails-common/build.gradle
+++ b/grails-common/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-console/build.gradle b/grails-console/build.gradle
index d3ec60a3f1..21b341638d 100644
--- a/grails-console/build.gradle
+++ b/grails-console/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-controllers/build.gradle b/grails-controllers/build.gradle
index dc68dacf8f..962430281b 100644
--- a/grails-controllers/build.gradle
+++ b/grails-controllers/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-converters/build.gradle b/grails-converters/build.gradle
index 0bb79ca376..22e3e605e1 100644
--- a/grails-converters/build.gradle
+++ b/grails-converters/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-core/build.gradle b/grails-core/build.gradle
index 7e486bfaae..bbea195d0d 100644
--- a/grails-core/build.gradle
+++ b/grails-core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-hibernate5/boot-plugin/build.gradle b/grails-data-hibernate5/boot-plugin/build.gradle
index 2c6c0aa627..6dfcb4389d 100644
--- a/grails-data-hibernate5/boot-plugin/build.gradle
+++ b/grails-data-hibernate5/boot-plugin/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-hibernate5/core/build.gradle b/grails-data-hibernate5/core/build.gradle
index 1f77e3657d..4838afb4af 100644
--- a/grails-data-hibernate5/core/build.gradle
+++ b/grails-data-hibernate5/core/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-hibernate5/dbmigration/build.gradle b/grails-data-hibernate5/dbmigration/build.gradle
index 3a74dd0429..342dbcfb17 100644
--- a/grails-data-hibernate5/dbmigration/build.gradle
+++ b/grails-data-hibernate5/dbmigration/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-hibernate5/grails-plugin/build.gradle b/grails-data-hibernate5/grails-plugin/build.gradle
index 22fd52fe58..dd0a1a13de 100644
--- a/grails-data-hibernate5/grails-plugin/build.gradle
+++ b/grails-data-hibernate5/grails-plugin/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-hibernate5/spring-orm/build.gradle b/grails-data-hibernate5/spring-orm/build.gradle
index 3713dfb9cc..6e65fbdc63 100644
--- a/grails-data-hibernate5/spring-orm/build.gradle
+++ b/grails-data-hibernate5/spring-orm/build.gradle
@@ -28,6 +28,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-mongodb/boot-plugin/build.gradle b/grails-data-mongodb/boot-plugin/build.gradle
index 572e90fa09..31a76648f2 100644
--- a/grails-data-mongodb/boot-plugin/build.gradle
+++ b/grails-data-mongodb/boot-plugin/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-mongodb/bson/build.gradle b/grails-data-mongodb/bson/build.gradle
index 1ac2663ee7..6c1e0243bb 100644
--- a/grails-data-mongodb/bson/build.gradle
+++ b/grails-data-mongodb/bson/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-mongodb/core/build.gradle b/grails-data-mongodb/core/build.gradle
index f080478090..94c1e6d97e 100644
--- a/grails-data-mongodb/core/build.gradle
+++ b/grails-data-mongodb/core/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-mongodb/ext/build.gradle b/grails-data-mongodb/ext/build.gradle
index f8271315be..982b44960d 100644
--- a/grails-data-mongodb/ext/build.gradle
+++ b/grails-data-mongodb/ext/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-mongodb/grails-plugin/build.gradle b/grails-data-mongodb/grails-plugin/build.gradle
index 1ace1bc70d..c22e6dca21 100644
--- a/grails-data-mongodb/grails-plugin/build.gradle
+++ b/grails-data-mongodb/grails-plugin/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-mongodb/gson-templates/build.gradle b/grails-data-mongodb/gson-templates/build.gradle
index 445f6f3676..da0d0758fa 100644
--- a/grails-data-mongodb/gson-templates/build.gradle
+++ b/grails-data-mongodb/gson-templates/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-data-simple/build.gradle b/grails-data-simple/build.gradle
index 22567f865c..1290a63ad5 100644
--- a/grails-data-simple/build.gradle
+++ b/grails-data-simple/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-databinding-core/build.gradle b/grails-databinding-core/build.gradle
index cf2bf90b51..cee4c8051b 100644
--- a/grails-databinding-core/build.gradle
+++ b/grails-databinding-core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-databinding/build.gradle b/grails-databinding/build.gradle
index 86b4fe0f04..363c001285 100644
--- a/grails-databinding/build.gradle
+++ b/grails-databinding/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datamapping-async/build.gradle b/grails-datamapping-async/build.gradle
index a273194893..00c2afdff4 100644
--- a/grails-datamapping-async/build.gradle
+++ b/grails-datamapping-async/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datamapping-core/build.gradle b/grails-datamapping-core/build.gradle
index 9eb9a7412c..700db6df88 100644
--- a/grails-datamapping-core/build.gradle
+++ b/grails-datamapping-core/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datamapping-rx/build.gradle b/grails-datamapping-rx/build.gradle
index 66157ce459..e0d5c9d6ea 100644
--- a/grails-datamapping-rx/build.gradle
+++ b/grails-datamapping-rx/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datamapping-support/build.gradle b/grails-datamapping-support/build.gradle
index 95656e2b87..78a342a5c8 100644
--- a/grails-datamapping-support/build.gradle
+++ b/grails-datamapping-support/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datamapping-tck/build.gradle b/grails-datamapping-tck/build.gradle
index 15e7de9509..bc42a8baa4 100644
--- a/grails-datamapping-tck/build.gradle
+++ b/grails-datamapping-tck/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datamapping-validation/build.gradle b/grails-datamapping-validation/build.gradle
index 0748bcffd9..accbf04ac2 100644
--- a/grails-datamapping-validation/build.gradle
+++ b/grails-datamapping-validation/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datasource/build.gradle b/grails-datasource/build.gradle
index a6ed818743..c6cf03293e 100644
--- a/grails-datasource/build.gradle
+++ b/grails-datasource/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datastore-async/build.gradle b/grails-datastore-async/build.gradle
index 68f9ed6685..edb0d968a2 100644
--- a/grails-datastore-async/build.gradle
+++ b/grails-datastore-async/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datastore-core/build.gradle b/grails-datastore-core/build.gradle
index 8750516d37..85da8ab7a1 100644
--- a/grails-datastore-core/build.gradle
+++ b/grails-datastore-core/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-datastore-web/build.gradle b/grails-datastore-web/build.gradle
index 138497f193..7589e7531e 100644
--- a/grails-datastore-web/build.gradle
+++ b/grails-datastore-web/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-domain-class/build.gradle b/grails-domain-class/build.gradle
index a34f2607f9..9510314243 100644
--- a/grails-domain-class/build.gradle
+++ b/grails-domain-class/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-encoder/build.gradle b/grails-encoder/build.gradle
index a1cac6930e..692cf98566 100644
--- a/grails-encoder/build.gradle
+++ b/grails-encoder/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/core/build.gradle b/grails-events/core/build.gradle
index 0c68efd560..fed99b5dd2 100644
--- a/grails-events/core/build.gradle
+++ b/grails-events/core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/gpars/build.gradle b/grails-events/gpars/build.gradle
index ad365eceee..3e80b36094 100644
--- a/grails-events/gpars/build.gradle
+++ b/grails-events/gpars/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/plugin/build.gradle b/grails-events/plugin/build.gradle
index d2cc1e2445..5069cad90f 100644
--- a/grails-events/plugin/build.gradle
+++ b/grails-events/plugin/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/rxjava/build.gradle b/grails-events/rxjava/build.gradle
index 312477fefa..d53e072abc 100644
--- a/grails-events/rxjava/build.gradle
+++ b/grails-events/rxjava/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/rxjava2/build.gradle b/grails-events/rxjava2/build.gradle
index 5a107dd924..43791ba91f 100644
--- a/grails-events/rxjava2/build.gradle
+++ b/grails-events/rxjava2/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/rxjava3/build.gradle b/grails-events/rxjava3/build.gradle
index efca93c18d..2daa711d67 100644
--- a/grails-events/rxjava3/build.gradle
+++ b/grails-events/rxjava3/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/spring/build.gradle b/grails-events/spring/build.gradle
index c568f92623..caefaf3ef6 100644
--- a/grails-events/spring/build.gradle
+++ b/grails-events/spring/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-events/transforms/build.gradle b/grails-events/transforms/build.gradle
index 778980c095..b8797a4699 100644
--- a/grails-events/transforms/build.gradle
+++ b/grails-events/transforms/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-fields/build.gradle b/grails-fields/build.gradle
index 1ed72931b9..1b30c8c623 100644
--- a/grails-fields/build.gradle
+++ b/grails-fields/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-forge/build.gradle b/grails-forge/build.gradle
index d262f97fab..a7ac18ca66 100644
--- a/grails-forge/build.gradle
+++ b/grails-forge/build.gradle
@@ -25,6 +25,7 @@ import java.time.format.DateTimeFormatter
 plugins {
     id 'org.apache.grails.buildsrc.properties'
     id 'org.apache.grails.buildsrc.dependency-validator'
+    id 'org.apache.grails.gradle.grails-violation-aggregation'
 }
 
 ext {
diff --git a/grails-forge/gradle/code-style-config.gradle b/grails-forge/gradle/code-style-config.gradle
index c8d7bbaf83..b3a1e36a60 100644
--- a/grails-forge/gradle/code-style-config.gradle
+++ b/grails-forge/gradle/code-style-config.gradle
@@ -25,6 +25,7 @@ apply plugin: 'codenarc'
 apply plugin: 'com.diffplug.spotless'
 apply plugin: 'io.spring.nohttp' // enforce https everywhere
 apply plugin: 'org.apache.grails.gradle.grails-code-style'
+apply plugin: 'org.apache.grails.gradle.grails-code-analysis'
 
 extensions.configure(SpotlessExtension) {
     // Explicit `it` is required in extension configuration block
diff --git a/grails-geb/build.gradle b/grails-geb/build.gradle
index 160bdf8e95..db3b5b47ea 100644
--- a/grails-geb/build.gradle
+++ b/grails-geb/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gradle/common/build.gradle b/grails-gradle/common/build.gradle
index 7d05cb8211..9ec66022ff 100644
--- a/grails-gradle/common/build.gradle
+++ b/grails-gradle/common/build.gradle
@@ -24,6 +24,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gradle/model/build.gradle b/grails-gradle/model/build.gradle
index 23f4e81e02..2cd0762d4d 100644
--- a/grails-gradle/model/build.gradle
+++ b/grails-gradle/model/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gradle/plugins/build.gradle b/grails-gradle/plugins/build.gradle
index 405b5d4462..78bca0abee 100644
--- a/grails-gradle/plugins/build.gradle
+++ b/grails-gradle/plugins/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gradle/tasks/build.gradle b/grails-gradle/tasks/build.gradle
index a994c5f868..5970381fa2 100644
--- a/grails-gradle/tasks/build.gradle
+++ b/grails-gradle/tasks/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/core/build.gradle b/grails-gsp/core/build.gradle
index e55a044be7..a032e6f974 100644
--- a/grails-gsp/core/build.gradle
+++ b/grails-gsp/core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-layout/build.gradle b/grails-gsp/grails-layout/build.gradle
index 32eb8c13c3..59e02446ec 100644
--- a/grails-gsp/grails-layout/build.gradle
+++ b/grails-gsp/grails-layout/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-sitemesh3/build.gradle b/grails-gsp/grails-sitemesh3/build.gradle
index 676039b035..494e1dd0c9 100644
--- a/grails-gsp/grails-sitemesh3/build.gradle
+++ b/grails-gsp/grails-sitemesh3/build.gradle
@@ -26,6 +26,7 @@ plugins {
     // TODO: id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-taglib/build.gradle b/grails-gsp/grails-taglib/build.gradle
index 55c91cfe45..6ddf4cdd5c 100644
--- a/grails-gsp/grails-taglib/build.gradle
+++ b/grails-gsp/grails-taglib/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-web-gsp-taglib/build.gradle b/grails-gsp/grails-web-gsp-taglib/build.gradle
index 869592160c..e29c66a4ae 100644
--- a/grails-gsp/grails-web-gsp-taglib/build.gradle
+++ b/grails-gsp/grails-web-gsp-taglib/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-web-gsp/build.gradle b/grails-gsp/grails-web-gsp/build.gradle
index f9df4c2edd..b5c19fef86 100644
--- a/grails-gsp/grails-web-gsp/build.gradle
+++ b/grails-gsp/grails-web-gsp/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-web-jsp/build.gradle b/grails-gsp/grails-web-jsp/build.gradle
index 7776095819..92a20f8bcd 100644
--- a/grails-gsp/grails-web-jsp/build.gradle
+++ b/grails-gsp/grails-web-jsp/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/grails-web-taglib/build.gradle b/grails-gsp/grails-web-taglib/build.gradle
index a69196b114..f6c5fed7d1 100644
--- a/grails-gsp/grails-web-taglib/build.gradle
+++ b/grails-gsp/grails-web-taglib/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-gsp/plugin/build.gradle b/grails-gsp/plugin/build.gradle
index 767a130825..82e924f5a1 100644
--- a/grails-gsp/plugin/build.gradle
+++ b/grails-gsp/plugin/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-i18n/build.gradle b/grails-i18n/build.gradle
index ad7785dbfc..c08aa68d99 100644
--- a/grails-i18n/build.gradle
+++ b/grails-i18n/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-interceptors/build.gradle b/grails-interceptors/build.gradle
index 7d18d14b91..a54d1af354 100644
--- a/grails-interceptors/build.gradle
+++ b/grails-interceptors/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-logging/build.gradle b/grails-logging/build.gradle
index 9ba07de826..40ca7b5ae2 100644
--- a/grails-logging/build.gradle
+++ b/grails-logging/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-mimetypes/build.gradle b/grails-mimetypes/build.gradle
index 0391a33364..20cd89b716 100644
--- a/grails-mimetypes/build.gradle
+++ b/grails-mimetypes/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-rest-transforms/build.gradle b/grails-rest-transforms/build.gradle
index 3cbfdba682..ad80cb4057 100644
--- a/grails-rest-transforms/build.gradle
+++ b/grails-rest-transforms/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-scaffolding/build.gradle b/grails-scaffolding/build.gradle
index 49020837a1..93dfa5f2a4 100644
--- a/grails-scaffolding/build.gradle
+++ b/grails-scaffolding/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-services/build.gradle b/grails-services/build.gradle
index eda69a8eaf..c13a91ef24 100644
--- a/grails-services/build.gradle
+++ b/grails-services/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-shell-cli/build.gradle b/grails-shell-cli/build.gradle
index 1843aa8825..b63e2a865c 100644
--- a/grails-shell-cli/build.gradle
+++ b/grails-shell-cli/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-spring/build.gradle b/grails-spring/build.gradle
index 574091c26e..45048ac9c3 100644
--- a/grails-spring/build.gradle
+++ b/grails-spring/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-test-core/build.gradle b/grails-test-core/build.gradle
index 82af7708ce..f23de34dc1 100644
--- a/grails-test-core/build.gradle
+++ b/grails-test-core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-test-suite-base/build.gradle b/grails-test-suite-base/build.gradle
index 453cb33570..6e5b9a320d 100644
--- a/grails-test-suite-base/build.gradle
+++ b/grails-test-suite-base/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.dependency-validator'
     id 'org.apache.grails.buildsrc.compile'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-core/build.gradle b/grails-testing-support-core/build.gradle
index 11d60439dd..223b2a9ba9 100644
--- a/grails-testing-support-core/build.gradle
+++ b/grails-testing-support-core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-datamapping/build.gradle b/grails-testing-support-datamapping/build.gradle
index 09f4203521..715fbe3f98 100755
--- a/grails-testing-support-datamapping/build.gradle
+++ b/grails-testing-support-datamapping/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-dbcleanup-core/build.gradle b/grails-testing-support-dbcleanup-core/build.gradle
index 7368f6dee1..70f9682077 100644
--- a/grails-testing-support-dbcleanup-core/build.gradle
+++ b/grails-testing-support-dbcleanup-core/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 
diff --git a/grails-testing-support-dbcleanup-h2/build.gradle b/grails-testing-support-dbcleanup-h2/build.gradle
index c12e761a30..2bc1cc0adf 100644
--- a/grails-testing-support-dbcleanup-h2/build.gradle
+++ b/grails-testing-support-dbcleanup-h2/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 
diff --git a/grails-testing-support-dbcleanup-postgresql/build.gradle b/grails-testing-support-dbcleanup-postgresql/build.gradle
index 3644ad1461..6ff298cbc1 100644
--- a/grails-testing-support-dbcleanup-postgresql/build.gradle
+++ b/grails-testing-support-dbcleanup-postgresql/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-http-client/build.gradle b/grails-testing-support-http-client/build.gradle
index ddb22c9656..7d8119d07d 100644
--- a/grails-testing-support-http-client/build.gradle
+++ b/grails-testing-support-http-client/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-mongodb/build.gradle b/grails-testing-support-mongodb/build.gradle
index be2333a450..cad012a5cb 100644
--- a/grails-testing-support-mongodb/build.gradle
+++ b/grails-testing-support-mongodb/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-views-gson/build.gradle b/grails-testing-support-views-gson/build.gradle
index 7bfcc80cfe..c847e79679 100644
--- a/grails-testing-support-views-gson/build.gradle
+++ b/grails-testing-support-views-gson/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-testing-support-web/build.gradle b/grails-testing-support-web/build.gradle
index 385b7bca5d..6b362c056d 100755
--- a/grails-testing-support-web/build.gradle
+++ b/grails-testing-support-web/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-url-mappings/build.gradle b/grails-url-mappings/build.gradle
index 720b4ba50d..9e548d1212 100644
--- a/grails-url-mappings/build.gradle
+++ b/grails-url-mappings/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-validation/build.gradle b/grails-validation/build.gradle
index 22cf4c820a..38b907bf6d 100644
--- a/grails-validation/build.gradle
+++ b/grails-validation/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-views-core/build.gradle b/grails-views-core/build.gradle
index 3da0370d06..d30a0d560d 100644
--- a/grails-views-core/build.gradle
+++ b/grails-views-core/build.gradle
@@ -26,6 +26,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-views-gson/build.gradle b/grails-views-gson/build.gradle
index 3848ebd373..de8b38ed5f 100644
--- a/grails-views-gson/build.gradle
+++ b/grails-views-gson/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-views-markup/build.gradle b/grails-views-markup/build.gradle
index 6d6bc3da1b..48de6f7f0c 100644
--- a/grails-views-markup/build.gradle
+++ b/grails-views-markup/build.gradle
@@ -25,6 +25,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-web-boot/build.gradle b/grails-web-boot/build.gradle
index 755fe01739..3dbbd9fde6 100644
--- a/grails-web-boot/build.gradle
+++ b/grails-web-boot/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-web-common/build.gradle b/grails-web-common/build.gradle
index 1053fba613..ff03053790 100644
--- a/grails-web-common/build.gradle
+++ b/grails-web-common/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-web-core/build.gradle b/grails-web-core/build.gradle
index 2b60e56ea9..cd9b52c658 100644
--- a/grails-web-core/build.gradle
+++ b/grails-web-core/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-web-databinding/build.gradle b/grails-web-databinding/build.gradle
index 3fc3cd3578..92b7aecc41 100644
--- a/grails-web-databinding/build.gradle
+++ b/grails-web-databinding/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-web-mvc/build.gradle b/grails-web-mvc/build.gradle
index ac72a8621b..d16e9f41f3 100644
--- a/grails-web-mvc/build.gradle
+++ b/grails-web-mvc/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-web-url-mappings/build.gradle b/grails-web-url-mappings/build.gradle
index adcd1f6579..06c6b87870 100644
--- a/grails-web-url-mappings/build.gradle
+++ b/grails-web-url-mappings/build.gradle
@@ -27,6 +27,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion
diff --git a/grails-wrapper/build.gradle b/grails-wrapper/build.gradle
index d51388ab06..71a80b3ee4 100644
--- a/grails-wrapper/build.gradle
+++ b/grails-wrapper/build.gradle
@@ -24,6 +24,7 @@ plugins {
     id 'org.apache.grails.buildsrc.publish'
     id 'org.apache.grails.buildsrc.sbom'
     id 'org.apache.grails.gradle.grails-code-style'
+    id 'org.apache.grails.gradle.grails-code-analysis'
 }
 
 version = projectVersion