jamesfredley commented on code in PR #16025:
URL: https://github.com/apache/grails-core/pull/16025#discussion_r3677414855


##########
build-logic/plugins/build.gradle:
##########
@@ -40,6 +40,7 @@ dependencies {
     implementation 
"org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:${gradleProperties.gradleCycloneDxPluginVersion}"
     implementation 
"com.github.spotbugs.snom:spotbugs-gradle-plugin:${gradleProperties.spotbugsPluginVersion}"
     implementation 
"org.sonatype.gradle.plugins:scan-gradle-plugin:${gradleProperties.sonatypeScanPluginVersion}"
+    implementation 'org.yaml:snakeyaml:2.4'

Review Comment:
   Fixed. `snakeyamlVersion=2.6` now lives in the build-tooling block of the 
root `gradle.properties` alongside `pmdVersion`, `checkstyleVersion`, and 
friends, and `build-logic/plugins/build.gradle` consumes it as 
`"org.yaml:snakeyaml:${gradleProperties.snakeyamlVersion}"`, matching how every 
other build-tooling dependency in that file is declared. That also brings it up 
to the 2.6 you identified instead of 2.4.



##########
gradle.properties:
##########
@@ -72,6 +72,9 @@ pmdVersion=7.25.0
 spotbugsPluginVersion=6.4.8
 sonatypeScanPluginVersion=3.1.6
 
+# PMD is enforced only for projects with a clean baseline. Add project paths 
after clearing debt.
+grails.code-analysis.enabled.pmd.projects=:grails-data-graphql-core,:grails-data-mongodb-spring-data,:grails-datasource,:grails-testing-support-core

Review Comment:
   Agreed, and removed. The hardcoded list is gone from `gradle.properties` 
entirely.
   
   PMD and SpotBugs are now opted into per project, in the module that owns the 
decision:
   
   ```groovy
   grailsCodeAnalysis {
       // PMD baseline is clean for this module; keep it blocking.
       pmdEnabled = true
   }
   ```
   
   `GrailsCodeAnalysisExtension` gained lazy `pmdEnabled` / `spotbugsEnabled` 
properties, and `GrailsCodeAnalysisPlugin` defers `configurePmd` / 
`configureSpotbugs` to `afterEvaluate` so a module's own `build.gradle` can set 
them. The four clean modules carry the flag themselves.
   
   The root aggregator no longer needs a list either: it derives the enabled 
set from `pluginManager.withPlugin('pmd')` / 
`withPlugin('com.github.spotbugs')` across `allprojects`, so adding a module is 
a one-line change in that module and nothing central has to be edited. The 
`-Pgrails.code-analysis.enabled.pmd` and `-P...pmd.projects=:a,:b` properties 
are kept purely as overrides for baseline and CI runs, and the unknown-path 
validation now applies only to paths passed through that override.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy:
##########
@@ -84,82 +86,217 @@ class GrailsViolationAggregationPlugin implements 
Plugin<Project> {
         }
 
         def violationsDir = 
project.layout.buildDirectory.dir('reports/violations')
-        def styleXmlDir = 
project.layout.buildDirectory.dir('reports/code-style')
-        def analysisXmlDir = 
project.layout.buildDirectory.dir('reports/code-analysis')
-
-        def styleTask = registerStyleAggregation(project, styleXmlDir, 
violationsDir)
-        def analysisTask = registerAnalysisAggregation(project, 
analysisXmlDir, violationsDir)
+        TaskProvider<RepositoryConventionsTask> repositoryConventionsTask = 
project.file(CANONICAL_ROOT_MARKER).isDirectory() ?
+                registerRepositoryConventions(project, violationsDir) : null
+        def styleTask = registerStyleAggregation(project, violationsDir)
+        def analysisTask = registerAnalysisAggregation(project, 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)
+            if (repositoryConventionsTask) {
+                task.dependsOn(repositoryConventionsTask)
+            }
+        }
+    }
+
+    private static TaskProvider<RepositoryConventionsTask> 
registerRepositoryConventions(Project root, Provider<Directory> violationsDir) {
+        root.tasks.register('validateRepositoryConventions', 
RepositoryConventionsTask) { RepositoryConventionsTask task ->
+            task.group = 'verification'
+            task.description = 'Validates repository conventions and writes 
build/reports/violations/REPOSITORY_CONVENTIONS.md'
+            task.repositoryDirectory.set(root.layout.projectDirectory)
+            task.conventionSources.from(
+                    root.file('AGENTS.md'),
+                    root.fileTree('.agents/skills') { include '*/SKILL.md' },
+                    root.fileTree('.github/workflows') { include '**/*.yml', 
'**/*.yaml' },
+                    root.fileTree('.') {
+                        include '**/action.yml', '**/action.yaml'
+                        exclude '**/build/**', '**/generated/**', 
'**/.gradle/**', '**/.git/**', '**/.hg/**', '**/.svn/**'
+                    },
+                    root.fileTree('.') {
+                        include '**/messages*.properties'
+                        exclude '**/build/**', '**/generated/**'
+                    }
+            )
+            task.reportFile.set(violationsDir.map { 
it.file('REPOSITORY_CONVENTIONS.md') })
+            task.outputs.upToDateWhen { false }
+            task.dependsOn(root.tasks.matching { Task candidate -> 
candidate.name == 'rat' })
         }
     }
 
-    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
+    private static TaskProvider<Task> registerStyleAggregation(Project root, 
Provider<Directory> violationsDir) {
+        Directory rootDirectory = root.layout.projectDirectory
         def checkStyleTests = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.TEST_STYLING_PROPERTY)
+        def ignoreFailures = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.IGNORE_FAILURES_PROPERTY)
         def codenarcEnabled = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.CODENARC_ENABLED_PROPERTY, true)
         def checkstyleEnabled = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.CHECKSTYLE_ENABLED_PROPERTY, true)
+        def codenarcMarkers = root.files()
+        def checkstyleMarkers = root.files()
+        def codenarcReports = root.files()
+        def checkstyleReports = root.files()
+        def codenarcMarkdown = 
root.layout.buildDirectory.file('reports/violations/CODENARC_VIOLATIONS.md')
+        def checkstyleMarkdown = 
root.layout.buildDirectory.file('reports/violations/CHECKSTYLE_VIOLATIONS.md')
+        def cleanupTask = root.tasks.register('cleanAggregateStyleReports') {
+            it.doLast {
+                deleteReports(codenarcMarkers.files, codenarcReports.files)
+                deleteReports(checkstyleMarkers.files, checkstyleReports.files)
+            }
+        }
 
-        def aggregateTask = root.tasks.register('aggregateStyleViolations') {
+        def writerTask = root.tasks.register('writeStyleViolations') {
             it.group = 'verification'
-            it.description = 'Aggregates CodeNarc and Checkstyle violation 
reports into build/reports/violations/'
-            
it.outputs.file(root.file('build/reports/violations/CODENARC_VIOLATIONS.md'))
-            
it.outputs.file(root.file('build/reports/violations/CHECKSTYLE_VIOLATIONS.md'))
+            it.description = 'Writes CodeNarc and Checkstyle violation reports 
into build/reports/violations/'
+            it.inputs.files(codenarcMarkers).optional()
+            it.inputs.files(checkstyleMarkers).optional()
+            it.inputs.property('ignoreFailures', ignoreFailures)
+            it.outputs.file(codenarcMarkdown)
+            it.outputs.file(checkstyleMarkdown)
+            it.outputs.upToDateWhen { false }
+            it.doFirst {
+                codenarcMarkdown.get().asFile.delete()
+                checkstyleMarkdown.get().asFile.delete()
+            }
             it.doLast {
-                parseStyleViolations(styleXmlDir.get(), violationsDir.get(),
-                    checkStyleTests.get(), codenarcEnabled.get(), 
checkstyleEnabled.get())
+                parseStyleViolations(codenarcMarkers.files, 
checkstyleMarkers.files, rootDirectory, violationsDir.get(),
+                    checkStyleTests.get(), codenarcEnabled.get(),
+                    checkstyleEnabled.get(), ignoreFailures.get())
             }
         }
-        root.subprojects { Project sub ->
-            sub.pluginManager.withPlugin('codenarc') {
-                aggregateTask.configure {
-                    it.dependsOn(sub.tasks.withType(CodeNarc))
-                }
+        def aggregateTask = root.tasks.register('aggregateStyleViolations') {
+            it.group = 'verification'
+            it.description = 'Aggregates CodeNarc and Checkstyle violations 
into build/reports/violations/'
+            it.dependsOn(writerTask)
+        }
+        if (root.tasks.names.contains('validateRepositoryConventions')) {
+            aggregateTask.configure { 
it.dependsOn(root.tasks.named('validateRepositoryConventions')) }
+        }
+        def finalizeTask = root.tasks.register('finalizeStyleViolations') {
+            it.group = 'verification'
+            it.dependsOn(writerTask)
+        }
+        root.allprojects { Project sub ->
+            sub.tasks.withType(CodeNarc).all { CodeNarc codeNarcTask ->

Review Comment:
   Fixed. All four blocks use `configureEach`, and the aggregate, writer, and 
cleanup relationships are now collection-based:
   
   ```groovy
   def pmdTasks = sub.tasks.withType(Pmd)
   aggregateTask.configure { it.dependsOn(pmdTasks); it.dependsOn(cleanupTask) }
   writerTask.configure { it.mustRunAfter(pmdTasks) }
   pmdTasks.configureEach { Pmd pmdTask -> ... }
   ```
   
   Nothing is realized at configuration time any more. Thanks for the note 
about never-realized markers, that is what made the collection form safe to 
adopt without extra bookkeeping.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy:
##########
@@ -84,82 +86,217 @@ class GrailsViolationAggregationPlugin implements 
Plugin<Project> {
         }
 
         def violationsDir = 
project.layout.buildDirectory.dir('reports/violations')
-        def styleXmlDir = 
project.layout.buildDirectory.dir('reports/code-style')
-        def analysisXmlDir = 
project.layout.buildDirectory.dir('reports/code-analysis')
-
-        def styleTask = registerStyleAggregation(project, styleXmlDir, 
violationsDir)
-        def analysisTask = registerAnalysisAggregation(project, 
analysisXmlDir, violationsDir)
+        TaskProvider<RepositoryConventionsTask> repositoryConventionsTask = 
project.file(CANONICAL_ROOT_MARKER).isDirectory() ?
+                registerRepositoryConventions(project, violationsDir) : null
+        def styleTask = registerStyleAggregation(project, violationsDir)
+        def analysisTask = registerAnalysisAggregation(project, 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)
+            if (repositoryConventionsTask) {
+                task.dependsOn(repositoryConventionsTask)
+            }
+        }
+    }
+
+    private static TaskProvider<RepositoryConventionsTask> 
registerRepositoryConventions(Project root, Provider<Directory> violationsDir) {
+        root.tasks.register('validateRepositoryConventions', 
RepositoryConventionsTask) { RepositoryConventionsTask task ->
+            task.group = 'verification'
+            task.description = 'Validates repository conventions and writes 
build/reports/violations/REPOSITORY_CONVENTIONS.md'
+            task.repositoryDirectory.set(root.layout.projectDirectory)
+            task.conventionSources.from(
+                    root.file('AGENTS.md'),
+                    root.fileTree('.agents/skills') { include '*/SKILL.md' },
+                    root.fileTree('.github/workflows') { include '**/*.yml', 
'**/*.yaml' },
+                    root.fileTree('.') {
+                        include '**/action.yml', '**/action.yaml'
+                        exclude '**/build/**', '**/generated/**', 
'**/.gradle/**', '**/.git/**', '**/.hg/**', '**/.svn/**'
+                    },
+                    root.fileTree('.') {
+                        include '**/messages*.properties'
+                        exclude '**/build/**', '**/generated/**'
+                    }
+            )
+            task.reportFile.set(violationsDir.map { 
it.file('REPOSITORY_CONVENTIONS.md') })
+            task.outputs.upToDateWhen { false }
+            task.dependsOn(root.tasks.matching { Task candidate -> 
candidate.name == 'rat' })
         }
     }
 
-    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
+    private static TaskProvider<Task> registerStyleAggregation(Project root, 
Provider<Directory> violationsDir) {
+        Directory rootDirectory = root.layout.projectDirectory
         def checkStyleTests = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.TEST_STYLING_PROPERTY)
+        def ignoreFailures = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.IGNORE_FAILURES_PROPERTY)
         def codenarcEnabled = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.CODENARC_ENABLED_PROPERTY, true)
         def checkstyleEnabled = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.CHECKSTYLE_ENABLED_PROPERTY, true)
+        def codenarcMarkers = root.files()
+        def checkstyleMarkers = root.files()
+        def codenarcReports = root.files()
+        def checkstyleReports = root.files()
+        def codenarcMarkdown = 
root.layout.buildDirectory.file('reports/violations/CODENARC_VIOLATIONS.md')
+        def checkstyleMarkdown = 
root.layout.buildDirectory.file('reports/violations/CHECKSTYLE_VIOLATIONS.md')
+        def cleanupTask = root.tasks.register('cleanAggregateStyleReports') {

Review Comment:
   Fixed. Quality tasks no longer `dependsOn` the cleanup task, so a 
single-module `build` no longer wipes the other 60+ modules and no longer 
defeats each task's own up-to-date check. The cleanup task is now attached to 
the aggregate lane only: the aggregate task depends on it and quality tasks 
merely `mustRunAfter` it, which keeps stale markers from renamed or removed 
tasks purged when the aggregate lane actually runs.
   
   You were also right that the report deletion was redundant. 
`GradleUtils.configureReportMarker` already deletes each task's own report in 
`doFirst`, so the cleanup task now only removes markers, and the 
`codenarcReports` / `checkstyleReports` / `pmdReports` / `spotbugsReports` 
collections that existed solely to feed that deletion are gone.
   
   One follow-on worth flagging, since it is the other half of this thread: 
with cleanup no longer a dependency of every analyzer task, a direct 
single-analyzer run would have left earlier runs' markers in place and folded 
them into the regenerated report. That is fixed in the thread on the finalizer 
above, by removing the analyzer finalizers entirely so only the aggregate lane 
writes the Markdown. Cleanup stays scoped to that lane, which is where it 
belongs.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy:
##########
@@ -84,82 +86,217 @@ class GrailsViolationAggregationPlugin implements 
Plugin<Project> {
         }
 
         def violationsDir = 
project.layout.buildDirectory.dir('reports/violations')
-        def styleXmlDir = 
project.layout.buildDirectory.dir('reports/code-style')
-        def analysisXmlDir = 
project.layout.buildDirectory.dir('reports/code-analysis')
-
-        def styleTask = registerStyleAggregation(project, styleXmlDir, 
violationsDir)
-        def analysisTask = registerAnalysisAggregation(project, 
analysisXmlDir, violationsDir)
+        TaskProvider<RepositoryConventionsTask> repositoryConventionsTask = 
project.file(CANONICAL_ROOT_MARKER).isDirectory() ?
+                registerRepositoryConventions(project, violationsDir) : null
+        def styleTask = registerStyleAggregation(project, violationsDir)
+        def analysisTask = registerAnalysisAggregation(project, 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)
+            if (repositoryConventionsTask) {
+                task.dependsOn(repositoryConventionsTask)
+            }
+        }
+    }
+
+    private static TaskProvider<RepositoryConventionsTask> 
registerRepositoryConventions(Project root, Provider<Directory> violationsDir) {
+        root.tasks.register('validateRepositoryConventions', 
RepositoryConventionsTask) { RepositoryConventionsTask task ->
+            task.group = 'verification'
+            task.description = 'Validates repository conventions and writes 
build/reports/violations/REPOSITORY_CONVENTIONS.md'
+            task.repositoryDirectory.set(root.layout.projectDirectory)
+            task.conventionSources.from(
+                    root.file('AGENTS.md'),
+                    root.fileTree('.agents/skills') { include '*/SKILL.md' },
+                    root.fileTree('.github/workflows') { include '**/*.yml', 
'**/*.yaml' },
+                    root.fileTree('.') {
+                        include '**/action.yml', '**/action.yaml'
+                        exclude '**/build/**', '**/generated/**', 
'**/.gradle/**', '**/.git/**', '**/.hg/**', '**/.svn/**'
+                    },
+                    root.fileTree('.') {
+                        include '**/messages*.properties'
+                        exclude '**/build/**', '**/generated/**'
+                    }
+            )
+            task.reportFile.set(violationsDir.map { 
it.file('REPOSITORY_CONVENTIONS.md') })
+            task.outputs.upToDateWhen { false }
+            task.dependsOn(root.tasks.matching { Task candidate -> 
candidate.name == 'rat' })
         }
     }
 
-    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
+    private static TaskProvider<Task> registerStyleAggregation(Project root, 
Provider<Directory> violationsDir) {
+        Directory rootDirectory = root.layout.projectDirectory
         def checkStyleTests = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.TEST_STYLING_PROPERTY)
+        def ignoreFailures = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.IGNORE_FAILURES_PROPERTY)
         def codenarcEnabled = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.CODENARC_ENABLED_PROPERTY, true)
         def checkstyleEnabled = GradleUtils.booleanProvider(root, 
GrailsCodeStylePlugin.CHECKSTYLE_ENABLED_PROPERTY, true)
+        def codenarcMarkers = root.files()
+        def checkstyleMarkers = root.files()
+        def codenarcReports = root.files()
+        def checkstyleReports = root.files()
+        def codenarcMarkdown = 
root.layout.buildDirectory.file('reports/violations/CODENARC_VIOLATIONS.md')
+        def checkstyleMarkdown = 
root.layout.buildDirectory.file('reports/violations/CHECKSTYLE_VIOLATIONS.md')
+        def cleanupTask = root.tasks.register('cleanAggregateStyleReports') {
+            it.doLast {
+                deleteReports(codenarcMarkers.files, codenarcReports.files)
+                deleteReports(checkstyleMarkers.files, checkstyleReports.files)
+            }
+        }
 
-        def aggregateTask = root.tasks.register('aggregateStyleViolations') {
+        def writerTask = root.tasks.register('writeStyleViolations') {
             it.group = 'verification'
-            it.description = 'Aggregates CodeNarc and Checkstyle violation 
reports into build/reports/violations/'
-            
it.outputs.file(root.file('build/reports/violations/CODENARC_VIOLATIONS.md'))
-            
it.outputs.file(root.file('build/reports/violations/CHECKSTYLE_VIOLATIONS.md'))
+            it.description = 'Writes CodeNarc and Checkstyle violation reports 
into build/reports/violations/'
+            it.inputs.files(codenarcMarkers).optional()
+            it.inputs.files(checkstyleMarkers).optional()
+            it.inputs.property('ignoreFailures', ignoreFailures)
+            it.outputs.file(codenarcMarkdown)
+            it.outputs.file(checkstyleMarkdown)
+            it.outputs.upToDateWhen { false }
+            it.doFirst {
+                codenarcMarkdown.get().asFile.delete()
+                checkstyleMarkdown.get().asFile.delete()
+            }
             it.doLast {
-                parseStyleViolations(styleXmlDir.get(), violationsDir.get(),
-                    checkStyleTests.get(), codenarcEnabled.get(), 
checkstyleEnabled.get())
+                parseStyleViolations(codenarcMarkers.files, 
checkstyleMarkers.files, rootDirectory, violationsDir.get(),
+                    checkStyleTests.get(), codenarcEnabled.get(),
+                    checkstyleEnabled.get(), ignoreFailures.get())
             }
         }
-        root.subprojects { Project sub ->
-            sub.pluginManager.withPlugin('codenarc') {
-                aggregateTask.configure {
-                    it.dependsOn(sub.tasks.withType(CodeNarc))
-                }
+        def aggregateTask = root.tasks.register('aggregateStyleViolations') {
+            it.group = 'verification'
+            it.description = 'Aggregates CodeNarc and Checkstyle violations 
into build/reports/violations/'
+            it.dependsOn(writerTask)
+        }
+        if (root.tasks.names.contains('validateRepositoryConventions')) {
+            aggregateTask.configure { 
it.dependsOn(root.tasks.named('validateRepositoryConventions')) }
+        }
+        def finalizeTask = root.tasks.register('finalizeStyleViolations') {
+            it.group = 'verification'
+            it.dependsOn(writerTask)
+        }
+        root.allprojects { Project sub ->
+            sub.tasks.withType(CodeNarc).all { CodeNarc codeNarcTask ->
+                codenarcMarkers.from(GradleUtils.reportMarker(sub, 'codenarc', 
codeNarcTask.name))
+                def reportLocation = codeNarcTask.reports.xml.outputLocation
+                codenarcReports.from { reportLocation.get().asFile }
+                codeNarcTask.dependsOn(cleanupTask)
+                codeNarcTask.finalizedBy(finalizeTask)

Review Comment:
   Fixed, and I ended up taking both of the options you offered, because a 
review pass over the final diff showed the first one alone was not enough.
   
   First, the reports now record their scope. Every aggregate report carries a 
header line naming the modules that actually contributed data:
   
   ```
   # CodeNarc Violations Summary
   Generated on: ...
   
   Modules analyzed: :grails-core
   ```
   
   `none` is emitted when nothing contributed.
   
   Second, and this is the part I had missed: the header alone did not fix it. 
Because the reports are assembled from marker files left on disk, a module 
analyzed in an EARLIER run still contributed its stale markers to the 
regenerated report, so `./gradlew :grails-core:check` could produce a report 
listing modules it never ran, built from previous-run data. So I also took your 
"only rewrite in the aggregate lane" option: `finalizeStyleViolations` and 
`finalizeAnalysisViolations` are gone along with all four analyzer 
`finalizedBy` registrations. A direct analyzer task now produces only its own 
XML report and marker, and the aggregate Markdown is written solely by 
`aggregateStyleViolations`, `aggregateAnalysisViolations`, and 
`aggregateViolations`, which purge markers first.
   
   There is a regression test that builds an authoritative aggregate report, 
runs a single direct analyzer, and asserts the aggregate Markdown is 
byte-for-byte unchanged.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy:
##########
@@ -84,82 +86,217 @@ class GrailsViolationAggregationPlugin implements 
Plugin<Project> {
         }
 
         def violationsDir = 
project.layout.buildDirectory.dir('reports/violations')
-        def styleXmlDir = 
project.layout.buildDirectory.dir('reports/code-style')
-        def analysisXmlDir = 
project.layout.buildDirectory.dir('reports/code-analysis')
-
-        def styleTask = registerStyleAggregation(project, styleXmlDir, 
violationsDir)
-        def analysisTask = registerAnalysisAggregation(project, 
analysisXmlDir, violationsDir)
+        TaskProvider<RepositoryConventionsTask> repositoryConventionsTask = 
project.file(CANONICAL_ROOT_MARKER).isDirectory() ?
+                registerRepositoryConventions(project, violationsDir) : null
+        def styleTask = registerStyleAggregation(project, violationsDir)
+        def analysisTask = registerAnalysisAggregation(project, 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)
+            if (repositoryConventionsTask) {
+                task.dependsOn(repositoryConventionsTask)
+            }
+        }
+    }
+
+    private static TaskProvider<RepositoryConventionsTask> 
registerRepositoryConventions(Project root, Provider<Directory> violationsDir) {
+        root.tasks.register('validateRepositoryConventions', 
RepositoryConventionsTask) { RepositoryConventionsTask task ->
+            task.group = 'verification'
+            task.description = 'Validates repository conventions and writes 
build/reports/violations/REPOSITORY_CONVENTIONS.md'
+            task.repositoryDirectory.set(root.layout.projectDirectory)
+            task.conventionSources.from(
+                    root.file('AGENTS.md'),
+                    root.fileTree('.agents/skills') { include '*/SKILL.md' },
+                    root.fileTree('.github/workflows') { include '**/*.yml', 
'**/*.yaml' },
+                    root.fileTree('.') {
+                        include '**/action.yml', '**/action.yaml'
+                        exclude '**/build/**', '**/generated/**', 
'**/.gradle/**', '**/.git/**', '**/.hg/**', '**/.svn/**'
+                    },
+                    root.fileTree('.') {
+                        include '**/messages*.properties'
+                        exclude '**/build/**', '**/generated/**'
+                    }
+            )
+            task.reportFile.set(violationsDir.map { 
it.file('REPOSITORY_CONVENTIONS.md') })
+            task.outputs.upToDateWhen { false }
+            task.dependsOn(root.tasks.matching { Task candidate -> 
candidate.name == 'rat' })

Review Comment:
   Both fixed. The lookup is now `root.tasks.named { String name -> name == 
'rat' }`, which filters by name without realizing anything, and the 
relationship is `mustRunAfter` rather than `dependsOn` since the task reads 
nothing from rat's output.
   
   You are right that the hard dependency was silently no-oping when rat is not 
registered, which is exactly the independent-build case. RAT is still part of 
the developer-facing gate: `aggregateViolations` pulls in both the conventions 
task and `rat` directly, so the ordering holds where it matters and standalone 
runs are no longer taxed.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy:
##########
@@ -84,82 +86,217 @@ class GrailsViolationAggregationPlugin implements 
Plugin<Project> {
         }
 
         def violationsDir = 
project.layout.buildDirectory.dir('reports/violations')
-        def styleXmlDir = 
project.layout.buildDirectory.dir('reports/code-style')
-        def analysisXmlDir = 
project.layout.buildDirectory.dir('reports/code-analysis')
-
-        def styleTask = registerStyleAggregation(project, styleXmlDir, 
violationsDir)
-        def analysisTask = registerAnalysisAggregation(project, 
analysisXmlDir, violationsDir)
+        TaskProvider<RepositoryConventionsTask> repositoryConventionsTask = 
project.file(CANONICAL_ROOT_MARKER).isDirectory() ?
+                registerRepositoryConventions(project, violationsDir) : null
+        def styleTask = registerStyleAggregation(project, violationsDir)
+        def analysisTask = registerAnalysisAggregation(project, 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)
+            if (repositoryConventionsTask) {
+                task.dependsOn(repositoryConventionsTask)
+            }
+        }
+    }
+
+    private static TaskProvider<RepositoryConventionsTask> 
registerRepositoryConventions(Project root, Provider<Directory> violationsDir) {
+        root.tasks.register('validateRepositoryConventions', 
RepositoryConventionsTask) { RepositoryConventionsTask task ->
+            task.group = 'verification'
+            task.description = 'Validates repository conventions and writes 
build/reports/violations/REPOSITORY_CONVENTIONS.md'
+            task.repositoryDirectory.set(root.layout.projectDirectory)
+            task.conventionSources.from(
+                    root.file('AGENTS.md'),
+                    root.fileTree('.agents/skills') { include '*/SKILL.md' },
+                    root.fileTree('.github/workflows') { include '**/*.yml', 
'**/*.yaml' },
+                    root.fileTree('.') {
+                        include '**/action.yml', '**/action.yaml'
+                        exclude '**/build/**', '**/generated/**', 
'**/.gradle/**', '**/.git/**', '**/.hg/**', '**/.svn/**'
+                    },
+                    root.fileTree('.') {

Review Comment:
   Fixed. Both trees now carry the same exclusion set, including 
`**/.gradle/**`, so daemon-mutated files can no longer perturb the input 
fingerprint of an `@InputFiles` property.
   
   While in there I also took your other suggestion and widened the include 
from `**/messages*.properties` to `**/grails-app/i18n/**/*.properties`, so the 
`spring-security-core.properties` family is covered too.



##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsViolationAggregationPlugin.groovy:
##########
@@ -243,9 +382,11 @@ class GrailsViolationAggregationPlugin implements 
Plugin<Project> {
     }
 
     @CompileDynamic
-    private static void parseStyleViolations(Directory styleXmlDir, Directory 
violationsDir,
-            boolean checkStyleTests, boolean codenarcEnabled, boolean 
checkstyleEnabled) {
+    private static void parseStyleViolations(Set<File> codenarcMarkers, 
Set<File> checkstyleMarkers, Directory rootDirectory,
+            Directory violationsDir, boolean checkStyleTests,
+            boolean codenarcEnabled, boolean checkstyleEnabled, boolean 
ignoreFailures) {

Review Comment:
   Removed, option (b). CodeNarc and Checkstyle tasks already honor their own 
`ignoreFailures` task property and fail on their own findings, so mirroring the 
gate in the aggregator would have duplicated it rather than added anything. The 
parameter, the `inputs.property('ignoreFailures', ...)` declaration on 
`writeStyleViolations`, and the unused argument are all gone, so there is no 
longer an input that cannot influence the output. The unconditional 
missing-report failure is unchanged, and `parseAnalysisViolations` keeps its 
own gate.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to