From 2d05edf6eafab1b947581254e65608c9ba846684 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 23 Feb 2026 18:17:25 +0100 Subject: [PATCH 01/17] build: refactoring and renaming --- build-logic/build.gradle | 25 ++++--- build-logic/settings.gradle | 4 +- .../src/main/groovy/config.app-debug.gradle | 12 +++ ....style.gradle => config.code-style.gradle} | 15 ++-- .../src/main/groovy/config.compile.gradle | 65 ++++++++++++++++ ...dle => config.coverage-aggregation.gradle} | 59 ++++++++------- .../src/main/groovy/config.docs.gradle | 74 +++++++++++++++++++ ...ample.gradle => config.example-app.gradle} | 6 +- ...ets.gradle => config.grails-assets.gradle} | 14 ++-- ...gin.gradle => config.grails-plugin.gradle} | 8 +- ...lish.gradle => config.publish-root.gradle} | 0 .../src/main/groovy/config.publish.gradle | 13 ++++ ...g.testing.gradle => config.testing.gradle} | 42 ++++++----- ...grails.plugins.servertiming.compile.gradle | 49 ------------ ...rg.grails.plugins.servertiming.docs.gradle | 52 ------------- ...lugins.servertiming.project-publish.gradle | 28 ------- ...org.grails.plugins.servertiming.run.gradle | 10 --- build.gradle | 6 +- coverage/build.gradle | 2 +- docs/build.gradle | 41 +++++----- examples/app1/build.gradle | 52 +++++++------ examples/app2/build.gradle | 51 +++++++------ plugin/build.gradle | 38 +++++++--- 23 files changed, 368 insertions(+), 298 deletions(-) create mode 100644 build-logic/src/main/groovy/config.app-debug.gradle rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.style.gradle => config.code-style.gradle} (66%) create mode 100644 build-logic/src/main/groovy/config.compile.gradle rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.coverage-aggregation.gradle => config.coverage-aggregation.gradle} (52%) create mode 100644 build-logic/src/main/groovy/config.docs.gradle rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.example.gradle => config.example-app.gradle} (51%) rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.assets.gradle => config.grails-assets.gradle} (58%) rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.plugin.gradle => config.grails-plugin.gradle} (55%) rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.root-publish.gradle => config.publish-root.gradle} (100%) create mode 100644 build-logic/src/main/groovy/config.publish.gradle rename build-logic/src/main/groovy/{org.grails.plugins.servertiming.testing.gradle => config.testing.gradle} (75%) delete mode 100644 build-logic/src/main/groovy/org.grails.plugins.servertiming.compile.gradle delete mode 100644 build-logic/src/main/groovy/org.grails.plugins.servertiming.docs.gradle delete mode 100644 build-logic/src/main/groovy/org.grails.plugins.servertiming.project-publish.gradle delete mode 100644 build-logic/src/main/groovy/org.grails.plugins.servertiming.run.gradle diff --git a/build-logic/build.gradle b/build-logic/build.gradle index b227d94..ee4464f 100644 --- a/build-logic/build.gradle +++ b/build-logic/build.gradle @@ -2,23 +2,28 @@ plugins { id 'groovy-gradle-plugin' } -file('../gradle.properties').withInputStream { - Properties props = new Properties() - props.load(it) - project.ext.gradleProperties = props +file('../gradle.properties').withInputStream { is -> + extensions.extraProperties.set( + 'gradleProperties', + new Properties().tap { load(is) } + ) } -allprojects { - for (String key : gradleProperties.stringPropertyNames()) { - ext.set(key, gradleProperties.getProperty(key)) +allprojects {project -> + gradleProperties.stringPropertyNames().each { key -> + project.extensions.extraProperties.set( + key, + gradleProperties.getProperty(key) + ) } } dependencies { implementation platform("org.apache.grails:grails-bom:${gradleProperties.grailsVersion}") - implementation 'org.apache.grails:grails-gradle-plugins' - implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" - implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:${gradleProperties.asciidoctorVersion}" implementation 'cloud.wondrify:asset-pipeline-gradle' + implementation "com.adarshr:gradle-test-logger-plugin:${gradleProperties.testLoggerVersion}" + implementation 'org.apache.grails:grails-gradle-plugins' implementation 'org.apache.grails.gradle:grails-publish' + implementation "org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:${gradleProperties.asciidoctorVersion}" } + diff --git a/build-logic/settings.gradle b/build-logic/settings.gradle index 9b949e0..d712464 100644 --- a/build-logic/settings.gradle +++ b/build-logic/settings.gradle @@ -1,6 +1,6 @@ import org.gradle.api.initialization.resolve.RepositoriesMode -rootProject.name = "build-logic" +rootProject.name = 'build-logic' dependencyResolutionManagement { repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS @@ -10,4 +10,4 @@ dependencyResolutionManagement { } maven { url = 'https://repo.grails.org/grails/restricted' } } -} \ No newline at end of file +} diff --git a/build-logic/src/main/groovy/config.app-debug.gradle b/build-logic/src/main/groovy/config.app-debug.gradle new file mode 100644 index 0000000..8363425 --- /dev/null +++ b/build-logic/src/main/groovy/config.app-debug.gradle @@ -0,0 +1,12 @@ +pluginManager.withPlugin('org.springframework.boot') { + tasks.named('bootRun', JavaExec) { + doFirst { + if (project.hasProperty('debugWait')) { + jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005') + } + if (project.hasProperty('debug')) { + jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005') + } + } + } +} diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.style.gradle b/build-logic/src/main/groovy/config.code-style.gradle similarity index 66% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.style.gradle rename to build-logic/src/main/groovy/config.code-style.gradle index 1e1359c..91179da 100644 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.style.gradle +++ b/build-logic/src/main/groovy/config.code-style.gradle @@ -1,15 +1,12 @@ -import org.gradle.api.plugins.quality.CheckstyleExtension -import org.gradle.api.plugins.quality.CodeNarcExtension - plugins { id 'checkstyle' id 'codenarc' } // Resolved relative to the root project directory, which is the parent of build-logic/. -def codeStyleConfigDir = rootProject.file('build-logic/config') -def checkstyleConfigDir = new File(codeStyleConfigDir, 'checkstyle') -def codenarcConfigDir = new File(codeStyleConfigDir, 'codenarc') +def codeStyleConfigDir = rootProject.layout.settingsDirectory.dir('build-logic/config') +def checkstyleConfigDir = codeStyleConfigDir.dir('checkstyle') +def codenarcConfigDir = codeStyleConfigDir.dir('codenarc') extensions.configure(CheckstyleExtension) { it.toolVersion = checkstyleVersion @@ -26,7 +23,7 @@ tasks.withType(Checkstyle).configureEach { extensions.configure(CodeNarcExtension) { it.toolVersion = codenarcVersion - it.configFile = new File(codenarcConfigDir, 'codenarc.groovy') + it.configFile = codenarcConfigDir.file('codenarc.groovy').getAsFile() it.maxPriority1Violations = 0 it.maxPriority2Violations = 0 it.maxPriority3Violations = 0 @@ -40,6 +37,6 @@ tasks.withType(CodeNarc).configureEach { tasks.register('codeStyle') { group = 'verification' description = 'Runs all code style checks (Checkstyle + CodeNarc).' - dependsOn tasks.withType(Checkstyle) - dependsOn tasks.withType(CodeNarc) + dependsOn(tasks.withType(Checkstyle)) + dependsOn(tasks.withType(CodeNarc)) } diff --git a/build-logic/src/main/groovy/config.compile.gradle b/build-logic/src/main/groovy/config.compile.gradle new file mode 100644 index 0000000..457667b --- /dev/null +++ b/build-logic/src/main/groovy/config.compile.gradle @@ -0,0 +1,65 @@ +import java.nio.charset.StandardCharsets + +plugins { + id 'groovy' +} + +tasks.withType(JavaCompile).configureEach { + options.with { + compilerArgs.add('-parameters') + encoding = StandardCharsets.UTF_8.name() + fork = true + incremental = true + release.set(resolveSdkmanJavaMajor(project)) + } + options.forkOptions.with { + jvmArgs.add('-Xmx1g') + memoryMaximumSize = '1g' + } +} + +tasks.withType(GroovyCompile).configureEach { + options.with { + compilerArgs.add('-parameters') + encoding = StandardCharsets.UTF_8.name() + fork = true + incremental = true + } + groovyOptions.with { + encoding = StandardCharsets.UTF_8.name() + optimizationOptions.indy = false + parameters = true + } + groovyOptions.forkOptions.with { + memoryMaximumSize = '1g' + jvmArgs.add('-Xmx1g') + } +} + +private static Provider resolveSdkmanJavaMajor(Project project) { + project.providers.provider { + def sdkmanrc = project.rootProject.file('.sdkmanrc') + if (!sdkmanrc.exists()) { + throw new GradleException('Missing .sdkmanrc in root project') + } + + def props = new Properties() + sdkmanrc.withInputStream { props.load(it) } + + def raw = props.getProperty('java')?.trim() + if (!raw) { + throw new GradleException('Missing java version in root project .sdkmanrc') + } + + def major = raw.tokenize('.').first() + if (!(major ==~ /\d+/)) { + throw new GradleException( + "Invalid java version '$raw' in root project .sdkmanrc (major '$major' is not an integer)" + ) + } + + return major.toInteger() + + } as Provider +} + diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.coverage-aggregation.gradle b/build-logic/src/main/groovy/config.coverage-aggregation.gradle similarity index 52% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.coverage-aggregation.gradle rename to build-logic/src/main/groovy/config.coverage-aggregation.gradle index 277a4f7..b0165a2 100644 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.coverage-aggregation.gradle +++ b/build-logic/src/main/groovy/config.coverage-aggregation.gradle @@ -1,5 +1,3 @@ -import org.gradle.testing.jacoco.plugins.JacocoPluginExtension - plugins { id 'base' id 'jacoco' @@ -10,32 +8,39 @@ extensions.configure(JacocoPluginExtension) { } // Configuration for declaring which projects contribute coverage data. -configurations { - coverageDataProjects { - canBeConsumed = false - canBeResolved = true - } +def coverageDataProjects = configurations.register('coverageDataProjects') { + canBeConsumed = false + canBeResolved = true } // Lazily collect source directories and class files from all coverageDataProjects dependencies. -def covProjectList = configurations.named('coverageDataProjects').map { config -> - config.dependencies.withType(ProjectDependency).collect { project.project(it.path) } +def covProjectList = coverageDataProjects.map { + it.dependencies.withType(ProjectDependency).collect { + project.project(it.path) + } } -def allSourceDirs = covProjectList.map { projects -> - projects.findAll { it.plugins.hasPlugin('java') } - .collectMany { it.sourceSets.main.allSource.sourceDirectories.files } +def allSourceDirs = covProjectList.map { + it.findAll { it.plugins.hasPlugin('java') } + .collectMany { + it.extensions.getByType(SourceSetContainer).named('main').get() + .allSource.sourceDirectories.files + } } -def allClassDirs = covProjectList.map { projects -> - projects.findAll { it.plugins.hasPlugin('java') } - .collectMany { it.sourceSets.main.output.files } +def allClassDirs = covProjectList.map { + it.findAll { it.plugins.hasPlugin('java') } + .collectMany { + it.extensions.getByType(SourceSetContainer).named('main').get() + .output.files + } } -def allExecFiles = covProjectList.map { projects -> - projects.collectMany { prj -> - prj.layout.buildDirectory.dir('jacoco').get().asFile - .listFiles({ File f -> f.name.endsWith('.exec') } as FileFilter)?.toList() ?: [] +def allExecFiles = covProjectList.map { + it.collectMany { + it.fileTree(it.layout.buildDirectory.dir('jacoco')) { + include('**/*.exec') + }.files } } @@ -44,12 +49,12 @@ def allExecFiles = covProjectList.map { projects -> // Task dependencies on all Test tasks (test, integrationTest, etc.) in the declared // projects are derived automatically — no hard-coded project paths needed. tasks.register('jacocoAggregatedReport', JacocoReport) { - group = 'verification' description = 'Generates aggregated JaCoCo coverage report across all subprojects.' + group = 'verification' + classDirectories.from(allClassDirs) executionData.from(allExecFiles) sourceDirectories.from(allSourceDirs) - classDirectories.from(allClassDirs) reports { xml.required = true @@ -61,19 +66,19 @@ tasks.register('jacocoAggregatedReport', JacocoReport) { // After evaluation, wire dependsOn for every Test task in every coverage project. // This ensures all .exec files exist before the aggregated report collects them. afterEvaluate { - def projects = configurations.coverageDataProjects.dependencies + def projects = coverageDataProjects.get().dependencies .withType(ProjectDependency) .collect { project.project(it.path) } - tasks.named('jacocoAggregatedReport') { - projects.each { prj -> - prj.tasks.withType(Test).each { testTask -> - dependsOn testTask + tasks.named('jacocoAggregatedReport') {reportTask -> + projects.each { + it.tasks.withType(Test).configureEach { testTask -> + reportTask.dependsOn(testTask) } } } } tasks.named('check') { - dependsOn tasks.named('jacocoAggregatedReport') + dependsOn('jacocoAggregatedReport') } diff --git a/build-logic/src/main/groovy/config.docs.gradle b/build-logic/src/main/groovy/config.docs.gradle new file mode 100644 index 0000000..6a47c8f --- /dev/null +++ b/build-logic/src/main/groovy/config.docs.gradle @@ -0,0 +1,74 @@ +def docProject = provider { + project(":${project.name - 'root'}docs") +} +def pluginProject = provider { + project(":${project.name - '-root'}") +} + +tasks.register('cleanDocs', Delete) { + description = 'Deletes the documentation output' + group = 'documentation' + + delete(rootProject.layout.projectDirectory.dir('build/docs')) +} + +tasks.register('aggregateGroovyApiDoc', Groovydoc) { + description = 'Generates Groovy API Documentation for the plugin project under build/docs/gapi' + group = 'documentation' + + def upstream = pluginProject.flatMap { + it.tasks.named('groovydoc', Groovydoc) + } as Provider + + dependsOn(tasks.named('cleanDocs')) + dependsOn(upstream) + + access = GroovydocAccess.PROTECTED + includeAuthor = false + includeMainForScripts = true + processScripts = true + exclude('**/Application.groovy') + + + source = { upstream.get().source } + destinationDir = rootProject.layout.buildDirectory.dir('docs/gapi').get().asFile + classpath = files({ upstream.get().classpath }) + groovyClasspath = files({ upstream.get().groovyClasspath }) +} + +tasks.register('docs') { + description = 'Generates the documentation' + group = 'documentation' + + dependsOn( + 'aggregateGroovyApiDoc', + docProject.get().tasks.named('asciidoctor') + ) + finalizedBy( + 'copyAsciiDoctorDocs', + 'ghPagesRootIndexPage' + ) +} + +tasks.register('copyAsciiDoctorDocs', Copy) { + group = 'documentation' + + from(docProject.flatMap { it.layout.buildDirectory }) + into(rootProject.layout.buildDirectory) + include('docs/**') + includeEmptyDirs = false + + dependsOn('docs') +} + +tasks.register('ghPagesRootIndexPage', Copy) { + description = 'Provides a root index page for historical versions that are currently managed manually' + group = 'documentation' + + from(docProject.map { it.layout.projectDirectory.file('src/docs/index.tmpl') }) + into(rootProject.layout.buildDirectory.dir('docs')) + rename('index.tmpl', 'ghpages.html') + + dependsOn('docs') +} + diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.example.gradle b/build-logic/src/main/groovy/config.example-app.gradle similarity index 51% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.example.gradle rename to build-logic/src/main/groovy/config.example-app.gradle index 97ceefb..7d38a79 100644 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.example.gradle +++ b/build-logic/src/main/groovy/config.example-app.gradle @@ -1,6 +1,6 @@ plugins { + id 'config.grails-assets' + id 'config.app-debug' id 'org.apache.grails.gradle.grails-web' id 'org.apache.grails.gradle.grails-gsp' - id 'org.grails.plugins.servertiming.assets' - id 'org.grails.plugins.servertiming.run' -} \ No newline at end of file +} diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.assets.gradle b/build-logic/src/main/groovy/config.grails-assets.gradle similarity index 58% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.assets.gradle rename to build-logic/src/main/groovy/config.grails-assets.gradle index e447d30..57b9381 100644 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.assets.gradle +++ b/build-logic/src/main/groovy/config.grails-assets.gradle @@ -1,20 +1,22 @@ +import asset.pipeline.gradle.AssetPipelineExtension + plugins { id 'cloud.wondrify.asset-pipeline' } dependencies { - assetDevelopmentRuntime 'org.webjars.npm:bootstrap' - assetDevelopmentRuntime 'org.webjars.npm:bootstrap-icons' - assetDevelopmentRuntime 'org.webjars.npm:jquery' + add('assetDevelopmentRuntime', 'org.webjars.npm:bootstrap') + add('assetDevelopmentRuntime', 'org.webjars.npm:bootstrap-icons') + add('assetDevelopmentRuntime', 'org.webjars.npm:jquery') } -assets { - excludes = [ +extensions.configure(AssetPipelineExtension) { + it.excludes = [ 'webjars/jquery/**', 'webjars/bootstrap/**', 'webjars/bootstrap-icons/**' ] - includes = [ + it.includes = [ 'webjars/jquery/*/dist/jquery.js', 'webjars/bootstrap/*/dist/js/bootstrap.bundle.js', 'webjars/bootstrap/*/dist/css/bootstrap.css', diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.plugin.gradle b/build-logic/src/main/groovy/config.grails-plugin.gradle similarity index 55% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.plugin.gradle rename to build-logic/src/main/groovy/config.grails-plugin.gradle index 08f7a9f..dee24f9 100644 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.plugin.gradle +++ b/build-logic/src/main/groovy/config.grails-plugin.gradle @@ -1,8 +1,10 @@ +import org.grails.gradle.plugin.core.GrailsExtension + plugins { id 'org.apache.grails.gradle.grails-plugin' } -grails { +extensions.configure(GrailsExtension) { // Plugins should avoid the spring dependency management plugin due to how it prefers certain libraries - springDependencyManagement = false -} \ No newline at end of file + it.springDependencyManagement = false +} diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.root-publish.gradle b/build-logic/src/main/groovy/config.publish-root.gradle similarity index 100% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.root-publish.gradle rename to build-logic/src/main/groovy/config.publish-root.gradle diff --git a/build-logic/src/main/groovy/config.publish.gradle b/build-logic/src/main/groovy/config.publish.gradle new file mode 100644 index 0000000..5ccbf1a --- /dev/null +++ b/build-logic/src/main/groovy/config.publish.gradle @@ -0,0 +1,13 @@ +plugins { + id 'org.apache.grails.gradle.grails-publish' +} + +// Useful when testing a release version locally and not wanting to setup signing +pluginManager.withPlugin('signing') { + if (System.getenv('DISABLE_BUILD_SIGNING')) { + logger.lifecycle('Signing is disabled for this build per configuration.') + tasks.withType(Sign).configureEach { + enabled = false + } + } +} diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.testing.gradle b/build-logic/src/main/groovy/config.testing.gradle similarity index 75% rename from build-logic/src/main/groovy/org.grails.plugins.servertiming.testing.gradle rename to build-logic/src/main/groovy/config.testing.gradle index 47304a3..3c4b648 100644 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.testing.gradle +++ b/build-logic/src/main/groovy/config.testing.gradle @@ -1,4 +1,4 @@ -import org.gradle.testing.jacoco.plugins.JacocoPluginExtension +import com.adarshr.gradle.testlogger.TestLoggerExtension plugins { id 'com.adarshr.test-logger' @@ -11,14 +11,14 @@ def isWindows = System.getProperty('os.name')?.toLowerCase()?.contains('windows' // This configures the 'pretty' test logging // mocha-parallel uses Unicode symbols that require special config on Windows; // standard-parallel is a safe fallback there. -testlogger { - theme isCi ? 'plain-parallel' : (isWindows ? 'standard-parallel' : 'mocha-parallel') - showExceptions true - showStandardStreams false - showSummary true - showPassed true - showSkipped true - showFailed true +extensions.configure(TestLoggerExtension) { + it.theme = isCi ? 'plain-parallel' : (isWindows ? 'standard-parallel' : 'mocha-parallel') + it.showExceptions = true + it.showStandardStreams = false + it.showSummary = true + it.showPassed = true + it.showSkipped = true + it.showFailed = true } extensions.configure(JacocoPluginExtension) { @@ -32,7 +32,7 @@ tasks.withType(Test).configureEach { useJUnitPlatform() - maxHeapSize = "1g" // set to match the groovy compile task to ensure the worker daemons are reused + maxHeapSize = '1g' // set to match the groovy compile task to ensure the worker daemons are reused reports { junitXml.required = false @@ -52,41 +52,47 @@ tasks.withType(Test).configureEach { // The jacoco plugin automatically creates 'jacocoTestReport' for the 'test' task. // Configure it to produce XML (for CI tools) and HTML reports. tasks.named('jacocoTestReport', JacocoReport) { - dependsOn tasks.named('test') - reports { xml.required = true html.required = true csv.required = false } + + dependsOn(tasks.named('test')) } // Ensure coverage report runs after tests tasks.named('test') { - finalizedBy tasks.named('jacocoTestReport') + finalizedBy(tasks.named('jacocoTestReport')) } // When an integrationTest task is registered (e.g., via the Grails web plugin in example apps), // register a JaCoCo report task for it. Unlike the 'test' task, the JaCoCo plugin does not // auto-create report tasks for custom Test tasks. -afterEvaluate { +afterEvaluate { proj -> + def integrationTestTasks = tasks.withType(Test).matching { it.name == 'integrationTest' } if (!integrationTestTasks.isEmpty()) { + def integrationTest = integrationTestTasks.first() def execFile = integrationTest.extensions.getByType(JacocoTaskExtension).destinationFile + def reportTask = tasks.register('jacocoIntegrationTestReport', JacocoReport) { - group = 'verification' description = 'Generates code coverage report for the integrationTest task.' - dependsOn integrationTest + group = 'verification' + executionData.from(execFile) - sourceSets(project.sourceSets.main) + sourceSets(proj.extensions.getByType(SourceSetContainer).named('main').get()) reports { xml.required = true html.required = true csv.required = false } + + dependsOn(integrationTest) } - integrationTest.finalizedBy reportTask + + integrationTest.finalizedBy(reportTask) } } diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.compile.gradle b/build-logic/src/main/groovy/org.grails.plugins.servertiming.compile.gradle deleted file mode 100644 index 9e3c725..0000000 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.compile.gradle +++ /dev/null @@ -1,49 +0,0 @@ -import java.nio.charset.StandardCharsets - -plugins { - id 'groovy' -} -tasks.withType(JavaCompile).configureEach { - options.with { - encoding = StandardCharsets.UTF_8.name() - incremental = true - fork = true - compilerArgs += ['-parameters'] - release = project.provider { - def sdkmanrc = project.rootProject.file(".sdkmanrc") - if (!sdkmanrc.exists()) { - throw new GradleException("Missing .sdkmanrc in root project") - } - - Properties props = new Properties() - sdkmanrc.withInputStream { - props.load(it) - } - - props.getProperty('java').split('[.]')[0].toInteger() - } - } - options.forkOptions.with { - memoryMaximumSize = "1g" - jvmArgs = ['-Xmx1g'] - } -} - -tasks.withType(GroovyCompile).configureEach { - options.with { - encoding = StandardCharsets.UTF_8.name() - incremental = true - fork = true - compilerArgs += ['-parameters'] - } - groovyOptions.with { - encoding = StandardCharsets.UTF_8.name() - fork = true - optimizationOptions.indy = false - parameters = true - } - groovyOptions.forkOptions.with { - memoryMaximumSize = "1g" - jvmArgs = ['-Xmx1g'] - } -} diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.docs.gradle b/build-logic/src/main/groovy/org.grails.plugins.servertiming.docs.gradle deleted file mode 100644 index 76eaf52..0000000 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.docs.gradle +++ /dev/null @@ -1,52 +0,0 @@ -Provider docProject = project.provider { - project(":${project.name - 'root' + 'docs'}" as String) -} -Provider pluginProject = project.provider { - project(":${project.name - '-root'}" as String) -} - -tasks.register('cleanDocs', Delete) { - delete rootProject.layout.projectDirectory.dir('build/docs') -} - -tasks.register('aggregateGroovyApiDoc', Groovydoc) { - def groovyDocProjects = [pluginProject.get()] - dependsOn(tasks.named('cleanDocs'), pluginProject.get().tasks.named('groovydoc')) - - description = 'Generates Groovy API Documentation for all plugin projects under rootDir/gapi' - - group = JavaBasePlugin.DOCUMENTATION_GROUP - access = GroovydocAccess.PROTECTED - includeAuthor = false - includeMainForScripts = true - processScripts = true - source = groovyDocProjects.groovydoc.source - destinationDir = rootProject.layout.buildDirectory.dir('docs/gapi').get().asFile - classpath = files(groovyDocProjects.groovydoc.classpath) - groovyClasspath = files(groovyDocProjects.groovydoc.groovyClasspath) - exclude '**/Application.groovy' -} - -tasks.register('docs') { - group = JavaBasePlugin.DOCUMENTATION_GROUP - dependsOn('aggregateGroovyApiDoc', docProject.get().tasks.named('asciidoctor')) - finalizedBy 'copyAsciiDoctorDocs', 'ghPagesRootIndexPage' -} - -tasks.register('copyAsciiDoctorDocs', Copy) { - group = JavaBasePlugin.DOCUMENTATION_GROUP - dependsOn('docs') - from docProject.get().layout.buildDirectory - includes = ['docs/**'] - into rootProject.layout.buildDirectory - includeEmptyDirs = false -} - -// provides a root index page for historical versions that are currently managed manually -tasks.register('ghPagesRootIndexPage', Copy) { - group = 'documentation' - dependsOn('docs') - from docProject.get().layout.projectDirectory.file('src/docs/index.tmpl') - into rootProject.layout.buildDirectory.dir('docs') - rename 'index.tmpl', 'ghpages.html' -} \ No newline at end of file diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.project-publish.gradle b/build-logic/src/main/groovy/org.grails.plugins.servertiming.project-publish.gradle deleted file mode 100644 index 5e4a7ef..0000000 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.project-publish.gradle +++ /dev/null @@ -1,28 +0,0 @@ -import org.apache.grails.gradle.publish.GrailsPublishExtension - -plugins { - id 'org.apache.grails.gradle.grails-publish' -} - -extensions.configure(GrailsPublishExtension) { - it.artifactId = project.name - it.githubSlug = 'grails-plugins/grails-server-timing' - it.license.name = 'Apache-2.0' - it.title = 'Grails Server Timing Plugin' - it.desc = 'A Grails Plugin that populates the ServerTiming header for monitoring performance metrics' - it.organization { - it.name = 'Grails Plugins' - it.url = 'https://github.com/grails-plugins' - } - it.developers = [jdaugherty: 'James Daugherty'] -} - -// Useful when testing a release version locally and not wanting to setup signing -project.pluginManager.withPlugin('signing') { - if (System.getenv('DISABLE_BUILD_SIGNING')) { - project.logger.lifecycle('Signing is disabled for this build per configuration.') - project.tasks.withType(Sign).configureEach { - enabled = false - } - } -} \ No newline at end of file diff --git a/build-logic/src/main/groovy/org.grails.plugins.servertiming.run.gradle b/build-logic/src/main/groovy/org.grails.plugins.servertiming.run.gradle deleted file mode 100644 index 66719b6..0000000 --- a/build-logic/src/main/groovy/org.grails.plugins.servertiming.run.gradle +++ /dev/null @@ -1,10 +0,0 @@ -tasks.named('bootRun', JavaExec) { - doFirst { - if (project.hasProperty("debugWait")) { - jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005") - } - if (project.hasProperty("debug")) { - jvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005") - } - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2595572..f4e064b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id "idea" - id 'org.grails.plugins.servertiming.docs' - id 'org.grails.plugins.servertiming.root-publish' + id 'idea' + id 'config.docs' + id 'config.publish-root' } // Intentionally left blank - use composition instead diff --git a/coverage/build.gradle b/coverage/build.gradle index 8c75d83..0425bdc 100644 --- a/coverage/build.gradle +++ b/coverage/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.grails.plugins.servertiming.coverage-aggregation' + id 'config.coverage-aggregation' } dependencies { diff --git a/docs/build.gradle b/docs/build.gradle index e14902f..6027ac9 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -7,38 +7,39 @@ plugins { version = projectVersion group = 'org.grails.plugins' -String getGrailsDocumentationVersion(String version) { - if (version.endsWith('-SNAPSHOT')) { - return 'snapshot' - } - - return version -} - def asciidoctorAttributes = [ 'source-highlighter': 'coderay', toc : 'left', toclevels : '2', 'toc-title' : 'Table of Contents', icons : 'font', - id : (project.name - '-docs') + ':' + project.version, + id : "${project.name - '-docs'}:${project.version}", idprefix : '', idseparator : '-', version : project.version, - projectUrl : "https://github.com/grails-plugins/grails-server-timing", + projectUrl : 'https://github.com/grails-plugins/grails-server-timing', sourcedir : "${rootProject.allprojects.find { it.name == 'grails-server-timing' }.projectDir}/src/main/groovy", - grailsDocBase : "https://grails.apache.org/docs/${getGrailsDocumentationVersion(project.grailsVersion)}" + grailsDocBase : "https://grails.apache.org/docs/${resolveGrailsDocsDirName(project.grailsVersion)}" ] -tasks.named('asciidoctor', AsciidoctorTask) { AsciidoctorTask it -> - it.jvm { - jvmArgs("--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED") +tasks.named('asciidoctor', AsciidoctorTask) { + + outputDir = project.layout.buildDirectory.dir('docs') + sourceDir = project.layout.projectDirectory.dir('src/docs') + + attributes(asciidoctorAttributes) + baseDirFollowsSourceDir() + options(doctype: 'book') + sources { include 'index.adoc' } + + jvm { + jvmArgs( + '--add-opens', 'java.base/java.io=ALL-UNNAMED', + '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED' + ) } +} - it.baseDirFollowsSourceDir() - it.sourceDir project.file('src/docs') - it.sources { include 'index.adoc' } - it.outputDir = project.layout.buildDirectory.dir('docs') - it.options doctype: 'book' - it.attributes asciidoctorAttributes +static String resolveGrailsDocsDirName(String version) { + version.endsWith('-SNAPSHOT') ? 'snapshot' : version } diff --git a/examples/app1/build.gradle b/examples/app1/build.gradle index efe5e92..7330508 100644 --- a/examples/app1/build.gradle +++ b/examples/app1/build.gradle @@ -1,44 +1,50 @@ plugins { - id 'org.grails.plugins.servertiming.compile' - id 'org.grails.plugins.servertiming.testing' - id 'org.grails.plugins.servertiming.example' - id 'org.grails.plugins.servertiming.style' + id 'config.code-style' + id 'config.compile' + id 'config.example-app' + id 'config.testing' } version = projectVersion group = 'app1' dependencies { + + profile 'org.apache.grails.profiles:web' console 'org.apache.grails:grails-console' + implementation platform("org.apache.grails:grails-bom:$grailsVersion") - implementation 'org.springframework.boot:spring-boot-starter-logging' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-autoconfigure' - implementation 'org.springframework.boot:spring-boot-starter' + + implementation project(':grails-server-timing') + implementation 'org.apache.grails:grails-core' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-tomcat' - implementation 'org.apache.grails:grails-web-boot' + implementation 'org.apache.grails:grails-data-hibernate5' + implementation 'org.apache.grails:grails-databinding' + implementation 'org.apache.grails:grails-gsp' + implementation 'org.apache.grails:grails-interceptors' + implementation 'org.apache.grails:grails-layout' implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-rest-transforms' - implementation 'org.apache.grails:grails-databinding' + implementation 'org.apache.grails:grails-scaffolding' implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' - implementation 'org.apache.grails:grails-layout' - implementation 'org.apache.grails:grails-interceptors' - implementation 'org.apache.grails:grails-scaffolding' - implementation 'org.apache.grails:grails-data-hibernate5' - implementation 'org.apache.grails:grails-gsp' - integrationTestImplementation testFixtures('org.apache.grails:grails-geb') - profile 'org.apache.grails.profiles:web' - runtimeOnly 'org.fusesource.jansi:jansi' + implementation 'org.apache.grails:grails-web-boot' + implementation 'org.springframework.boot:spring-boot-autoconfigure' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.springframework.boot:spring-boot-starter-tomcat' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + runtimeOnly 'cloud.wondrify:asset-pipeline-grails' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.zaxxer:HikariCP' - runtimeOnly 'cloud.wondrify:asset-pipeline-grails' + runtimeOnly 'org.fusesource.jansi:jansi' + + integrationTestImplementation testFixtures('org.apache.grails:grails-geb') testImplementation 'org.apache.grails:grails-testing-support-datamapping' - testImplementation 'org.spockframework:spock-core' testImplementation 'org.apache.grails:grails-testing-support-web' + testImplementation 'org.spockframework:spock-core' - implementation project(':grails-server-timing') } diff --git a/examples/app2/build.gradle b/examples/app2/build.gradle index 5d5db40..f67e57d 100644 --- a/examples/app2/build.gradle +++ b/examples/app2/build.gradle @@ -1,44 +1,49 @@ plugins { - id 'org.grails.plugins.servertiming.compile' - id 'org.grails.plugins.servertiming.testing' - id 'org.grails.plugins.servertiming.example' - id 'org.grails.plugins.servertiming.style' + id 'config.code-style' + id 'config.compile' + id 'config.example-app' + id 'config.testing' } version = projectVersion group = 'app2' dependencies { + + profile 'org.apache.grails.profiles:web' console 'org.apache.grails:grails-console' + implementation platform("org.apache.grails:grails-bom:$grailsVersion") - implementation 'org.springframework.boot:spring-boot-starter-logging' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-autoconfigure' - implementation 'org.springframework.boot:spring-boot-starter' + + implementation project(':grails-server-timing') + implementation 'org.apache.grails:grails-core' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-tomcat' - implementation 'org.apache.grails:grails-web-boot' + implementation 'org.apache.grails:grails-data-hibernate5' + implementation 'org.apache.grails:grails-databinding' + implementation 'org.apache.grails:grails-gsp' + implementation 'org.apache.grails:grails-interceptors' + implementation 'org.apache.grails:grails-layout' implementation 'org.apache.grails:grails-logging' implementation 'org.apache.grails:grails-rest-transforms' - implementation 'org.apache.grails:grails-databinding' + implementation 'org.apache.grails:grails-scaffolding' implementation 'org.apache.grails:grails-services' implementation 'org.apache.grails:grails-url-mappings' - implementation 'org.apache.grails:grails-layout' - implementation 'org.apache.grails:grails-interceptors' - implementation 'org.apache.grails:grails-scaffolding' - implementation 'org.apache.grails:grails-data-hibernate5' - implementation 'org.apache.grails:grails-gsp' - integrationTestImplementation testFixtures('org.apache.grails:grails-geb') - profile 'org.apache.grails.profiles:web' - runtimeOnly 'org.fusesource.jansi:jansi' + implementation 'org.apache.grails:grails-web-boot' + implementation 'org.springframework.boot:spring-boot-autoconfigure' + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-logging' + implementation 'org.springframework.boot:spring-boot-starter-tomcat' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + runtimeOnly 'cloud.wondrify:asset-pipeline-grails' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.zaxxer:HikariCP' - runtimeOnly 'cloud.wondrify:asset-pipeline-grails' + runtimeOnly 'org.fusesource.jansi:jansi' testImplementation 'org.apache.grails:grails-testing-support-datamapping' - testImplementation 'org.spockframework:spock-core' testImplementation 'org.apache.grails:grails-testing-support-web' + testImplementation 'org.spockframework:spock-core' - implementation project(':grails-server-timing') + integrationTestImplementation testFixtures('org.apache.grails:grails-geb') } diff --git a/plugin/build.gradle b/plugin/build.gradle index 079310d..4afa31c 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,22 +1,38 @@ +import org.apache.grails.gradle.publish.GrailsPublishExtension + plugins { - id 'org.grails.plugins.servertiming.compile' - id 'org.grails.plugins.servertiming.testing' - id 'org.grails.plugins.servertiming.plugin' - id 'org.grails.plugins.servertiming.project-publish' - id 'org.grails.plugins.servertiming.style' + id 'config.code-style' + id 'config.compile' + id 'config.grails-plugin' + id 'config.publish' + id 'config.testing' } version = projectVersion -group = "org.grails.plugins" +group = 'org.grails.plugins' dependencies { + + profile 'org.apache.grails.profiles:web-plugin' + console 'org.apache.grails:grails-console' + compileOnly platform("org.apache.grails:grails-bom:$grailsVersion") compileOnly 'org.apache.grails:grails-dependencies-starter-web' - console "org.apache.grails:grails-console" - profile "org.apache.grails.profiles:web-plugin" - testImplementation platform("org.apache.grails:grails-bom:$grailsVersion") - testImplementation "org.apache.grails:grails-dependencies-starter-web" - testImplementation "org.apache.grails:grails-dependencies-test" + testImplementation 'org.apache.grails:grails-dependencies-starter-web' + testImplementation 'org.apache.grails:grails-dependencies-test' +} + +extensions.configure(GrailsPublishExtension) { + it.artifactId = project.name + it.githubSlug = 'grails-plugins/grails-server-timing' + it.license.name = 'Apache-2.0' + it.title = 'Grails Server Timing Plugin' + it.desc = 'A Grails Plugin that populates the Server-Timing http header for monitoring performance metrics' + it.organization { + it.name = 'Grails Plugins' + it.url = 'https://github.com/grails-plugins' + } + it.developers = [jdaugherty: 'James Daugherty'] } From 42f134cdc0f4c4bde979678acc8be5ecd519fa84 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 23 Feb 2026 18:19:53 +0100 Subject: [PATCH 02/17] chore: whitespace --- build-logic/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/build.gradle b/build-logic/build.gradle index ee4464f..746607d 100644 --- a/build-logic/build.gradle +++ b/build-logic/build.gradle @@ -9,7 +9,7 @@ file('../gradle.properties').withInputStream { is -> ) } -allprojects {project -> +allprojects { project -> gradleProperties.stringPropertyNames().each { key -> project.extensions.extraProperties.set( key, From 00c2812bbee34a87f35464c09e607df15da28928 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 08:12:37 +0100 Subject: [PATCH 03/17] build: rename app-debug to app-run --- .../groovy/{config.app-debug.gradle => config.app-run.gradle} | 0 build-logic/src/main/groovy/config.example-app.gradle | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename build-logic/src/main/groovy/{config.app-debug.gradle => config.app-run.gradle} (100%) diff --git a/build-logic/src/main/groovy/config.app-debug.gradle b/build-logic/src/main/groovy/config.app-run.gradle similarity index 100% rename from build-logic/src/main/groovy/config.app-debug.gradle rename to build-logic/src/main/groovy/config.app-run.gradle diff --git a/build-logic/src/main/groovy/config.example-app.gradle b/build-logic/src/main/groovy/config.example-app.gradle index 7d38a79..da58c2a 100644 --- a/build-logic/src/main/groovy/config.example-app.gradle +++ b/build-logic/src/main/groovy/config.example-app.gradle @@ -1,6 +1,6 @@ plugins { + id 'config.app-run' id 'config.grails-assets' - id 'config.app-debug' id 'org.apache.grails.gradle.grails-web' id 'org.apache.grails.gradle.grails-gsp' } From 843c7499f0f7c7401e5e51d382a381781cf72cdc Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 09:00:20 +0100 Subject: [PATCH 04/17] build: refactor code coverage --- ...-aggregation.gradle => config.code-coverage.gradle} | 10 ++++++---- coverage/build.gradle | 6 +++--- gradle.properties | 5 ++++- 3 files changed, 13 insertions(+), 8 deletions(-) rename build-logic/src/main/groovy/{config.coverage-aggregation.gradle => config.code-coverage.gradle} (94%) diff --git a/build-logic/src/main/groovy/config.coverage-aggregation.gradle b/build-logic/src/main/groovy/config.code-coverage.gradle similarity index 94% rename from build-logic/src/main/groovy/config.coverage-aggregation.gradle rename to build-logic/src/main/groovy/config.code-coverage.gradle index b0165a2..5306ff7 100644 --- a/build-logic/src/main/groovy/config.coverage-aggregation.gradle +++ b/build-logic/src/main/groovy/config.code-coverage.gradle @@ -1,10 +1,9 @@ plugins { - id 'base' id 'jacoco' } extensions.configure(JacocoPluginExtension) { - it.toolVersion = '0.8.12' + it.toolVersion = jacocoVersion } // Configuration for declaring which projects contribute coverage data. @@ -79,6 +78,9 @@ afterEvaluate { } } -tasks.named('check') { - dependsOn('jacocoAggregatedReport') +pluginManager.withPlugin('base') { + tasks.named('check') { + dependsOn('jacocoAggregatedReport') + } } + diff --git a/coverage/build.gradle b/coverage/build.gradle index 0425bdc..41545f6 100644 --- a/coverage/build.gradle +++ b/coverage/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'config.coverage-aggregation' + id 'config.code-coverage' } dependencies { @@ -7,7 +7,7 @@ dependencies { coverageDataProjects project(':grails-server-timing') // Auto-discover all example apps under examples/ - rootDir.toPath().resolve('examples').toFile().list()?.each { example -> - coverageDataProjects project(":$example") + rootDir.toPath().resolve('examples').toFile().list()?.each { exampleApp -> + coverageDataProjects project(":$exampleApp") } } diff --git a/gradle.properties b/gradle.properties index 7681403..b118218 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,12 @@ projectVersion=0.0.1-SNAPSHOT grailsVersion=7.0.7 + +# Build dependencies +asciidoctorVersion=4.0.5 checkstyleVersion=10.21.4 codenarcVersion=3.6.0 +jacocoVersion=0.8.12 testLoggerVersion=4.0.0 -asciidoctorVersion=4.0.5 org.gradle.caching=true org.gradle.daemon=true From eca597f175f377fb0101f0ddfbbff206b6281969 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 10:29:44 +0100 Subject: [PATCH 05/17] build: extract code coverage from testing --- .../config.code-coverage-aggregate.gradle | 86 ++++++++++++++++ .../main/groovy/config.code-coverage.gradle | 97 +++++++------------ .../src/main/groovy/config.example-app.gradle | 4 + .../src/main/groovy/config.testing.gradle | 53 ---------- code-coverage/build.gradle | 14 +++ coverage/build.gradle | 13 --- examples/app1/build.gradle | 3 - examples/app2/build.gradle | 3 - plugin/build.gradle | 1 + settings.gradle | 20 ++-- 10 files changed, 150 insertions(+), 144 deletions(-) create mode 100644 build-logic/src/main/groovy/config.code-coverage-aggregate.gradle create mode 100644 code-coverage/build.gradle delete mode 100644 coverage/build.gradle diff --git a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle new file mode 100644 index 0000000..5306ff7 --- /dev/null +++ b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle @@ -0,0 +1,86 @@ +plugins { + id 'jacoco' +} + +extensions.configure(JacocoPluginExtension) { + it.toolVersion = jacocoVersion +} + +// Configuration for declaring which projects contribute coverage data. +def coverageDataProjects = configurations.register('coverageDataProjects') { + canBeConsumed = false + canBeResolved = true +} + +// Lazily collect source directories and class files from all coverageDataProjects dependencies. +def covProjectList = coverageDataProjects.map { + it.dependencies.withType(ProjectDependency).collect { + project.project(it.path) + } +} + +def allSourceDirs = covProjectList.map { + it.findAll { it.plugins.hasPlugin('java') } + .collectMany { + it.extensions.getByType(SourceSetContainer).named('main').get() + .allSource.sourceDirectories.files + } +} + +def allClassDirs = covProjectList.map { + it.findAll { it.plugins.hasPlugin('java') } + .collectMany { + it.extensions.getByType(SourceSetContainer).named('main').get() + .output.files + } +} + +def allExecFiles = covProjectList.map { + it.collectMany { + it.fileTree(it.layout.buildDirectory.dir('jacoco')) { + include('**/*.exec') + }.files + } +} + +// Register the aggregated coverage report task. +// This merges JaCoCo execution data from all coverageDataProjects into a single report. +// Task dependencies on all Test tasks (test, integrationTest, etc.) in the declared +// projects are derived automatically — no hard-coded project paths needed. +tasks.register('jacocoAggregatedReport', JacocoReport) { + description = 'Generates aggregated JaCoCo coverage report across all subprojects.' + group = 'verification' + + classDirectories.from(allClassDirs) + executionData.from(allExecFiles) + sourceDirectories.from(allSourceDirs) + + reports { + xml.required = true + html.required = true + csv.required = false + } +} + +// After evaluation, wire dependsOn for every Test task in every coverage project. +// This ensures all .exec files exist before the aggregated report collects them. +afterEvaluate { + def projects = coverageDataProjects.get().dependencies + .withType(ProjectDependency) + .collect { project.project(it.path) } + + tasks.named('jacocoAggregatedReport') {reportTask -> + projects.each { + it.tasks.withType(Test).configureEach { testTask -> + reportTask.dependsOn(testTask) + } + } + } +} + +pluginManager.withPlugin('base') { + tasks.named('check') { + dependsOn('jacocoAggregatedReport') + } +} + diff --git a/build-logic/src/main/groovy/config.code-coverage.gradle b/build-logic/src/main/groovy/config.code-coverage.gradle index 5306ff7..e5e0aab 100644 --- a/build-logic/src/main/groovy/config.code-coverage.gradle +++ b/build-logic/src/main/groovy/config.code-coverage.gradle @@ -6,81 +6,52 @@ extensions.configure(JacocoPluginExtension) { it.toolVersion = jacocoVersion } -// Configuration for declaring which projects contribute coverage data. -def coverageDataProjects = configurations.register('coverageDataProjects') { - canBeConsumed = false - canBeResolved = true -} +pluginManager.withPlugin('groovy') { + // The jacoco plugin automatically creates 'jacocoTestReport' for the 'test' task. + // Configure it to produce XML (for CI tools) and HTML reports. + tasks.named('jacocoTestReport', JacocoReport) { + reports { + xml.required = true + html.required = true + csv.required = false + } -// Lazily collect source directories and class files from all coverageDataProjects dependencies. -def covProjectList = coverageDataProjects.map { - it.dependencies.withType(ProjectDependency).collect { - project.project(it.path) + dependsOn(tasks.named('test')) } -} - -def allSourceDirs = covProjectList.map { - it.findAll { it.plugins.hasPlugin('java') } - .collectMany { - it.extensions.getByType(SourceSetContainer).named('main').get() - .allSource.sourceDirectories.files - } -} - -def allClassDirs = covProjectList.map { - it.findAll { it.plugins.hasPlugin('java') } - .collectMany { - it.extensions.getByType(SourceSetContainer).named('main').get() - .output.files - } -} -def allExecFiles = covProjectList.map { - it.collectMany { - it.fileTree(it.layout.buildDirectory.dir('jacoco')) { - include('**/*.exec') - }.files + // Ensure coverage report runs after tests + tasks.named('test') { + finalizedBy(tasks.named('jacocoTestReport')) } } -// Register the aggregated coverage report task. -// This merges JaCoCo execution data from all coverageDataProjects into a single report. -// Task dependencies on all Test tasks (test, integrationTest, etc.) in the declared -// projects are derived automatically — no hard-coded project paths needed. -tasks.register('jacocoAggregatedReport', JacocoReport) { - description = 'Generates aggregated JaCoCo coverage report across all subprojects.' - group = 'verification' +// When an integrationTest task is registered (e.g., via the Grails web plugin in example apps), +// register a JaCoCo report task for it. Unlike the 'test' task, the JaCoCo plugin does not +// auto-create report tasks for custom Test tasks. +afterEvaluate { proj -> - classDirectories.from(allClassDirs) - executionData.from(allExecFiles) - sourceDirectories.from(allSourceDirs) + def integrationTestTasks = tasks.withType(Test).matching { it.name == 'integrationTest' } + if (!integrationTestTasks.isEmpty()) { - reports { - xml.required = true - html.required = true - csv.required = false - } -} + def integrationTest = integrationTestTasks.first() + def execFile = integrationTest.extensions.getByType(JacocoTaskExtension).destinationFile + + def reportTask = tasks.register('jacocoIntegrationTestReport', JacocoReport) { + description = 'Generates code coverage report for the integrationTest task.' + group = 'verification' -// After evaluation, wire dependsOn for every Test task in every coverage project. -// This ensures all .exec files exist before the aggregated report collects them. -afterEvaluate { - def projects = coverageDataProjects.get().dependencies - .withType(ProjectDependency) - .collect { project.project(it.path) } + executionData.from(execFile) + sourceSets(proj.extensions.getByType(SourceSetContainer).named('main').get()) - tasks.named('jacocoAggregatedReport') {reportTask -> - projects.each { - it.tasks.withType(Test).configureEach { testTask -> - reportTask.dependsOn(testTask) + reports { + xml.required = true + html.required = true + csv.required = false } + + dependsOn(integrationTest) } - } -} -pluginManager.withPlugin('base') { - tasks.named('check') { - dependsOn('jacocoAggregatedReport') + integrationTest.finalizedBy(reportTask) } } - diff --git a/build-logic/src/main/groovy/config.example-app.gradle b/build-logic/src/main/groovy/config.example-app.gradle index da58c2a..ab33f6e 100644 --- a/build-logic/src/main/groovy/config.example-app.gradle +++ b/build-logic/src/main/groovy/config.example-app.gradle @@ -1,6 +1,10 @@ plugins { id 'config.app-run' + id 'config.code-coverage' + id 'config.code-style' + id 'config.compile' id 'config.grails-assets' + id 'config.testing' id 'org.apache.grails.gradle.grails-web' id 'org.apache.grails.gradle.grails-gsp' } diff --git a/build-logic/src/main/groovy/config.testing.gradle b/build-logic/src/main/groovy/config.testing.gradle index 3c4b648..667d35b 100644 --- a/build-logic/src/main/groovy/config.testing.gradle +++ b/build-logic/src/main/groovy/config.testing.gradle @@ -2,7 +2,6 @@ import com.adarshr.gradle.testlogger.TestLoggerExtension plugins { id 'com.adarshr.test-logger' - id 'jacoco' } def isCi = System.getenv('CI') != null @@ -21,10 +20,6 @@ extensions.configure(TestLoggerExtension) { it.showFailed = true } -extensions.configure(JacocoPluginExtension) { - it.toolVersion = '0.8.12' -} - tasks.withType(Test).configureEach { onlyIf { !project.hasProperty('skipTests') @@ -48,51 +43,3 @@ tasks.withType(Test).configureEach { showCauses = true } } - -// The jacoco plugin automatically creates 'jacocoTestReport' for the 'test' task. -// Configure it to produce XML (for CI tools) and HTML reports. -tasks.named('jacocoTestReport', JacocoReport) { - reports { - xml.required = true - html.required = true - csv.required = false - } - - dependsOn(tasks.named('test')) -} - -// Ensure coverage report runs after tests -tasks.named('test') { - finalizedBy(tasks.named('jacocoTestReport')) -} - -// When an integrationTest task is registered (e.g., via the Grails web plugin in example apps), -// register a JaCoCo report task for it. Unlike the 'test' task, the JaCoCo plugin does not -// auto-create report tasks for custom Test tasks. -afterEvaluate { proj -> - - def integrationTestTasks = tasks.withType(Test).matching { it.name == 'integrationTest' } - if (!integrationTestTasks.isEmpty()) { - - def integrationTest = integrationTestTasks.first() - def execFile = integrationTest.extensions.getByType(JacocoTaskExtension).destinationFile - - def reportTask = tasks.register('jacocoIntegrationTestReport', JacocoReport) { - description = 'Generates code coverage report for the integrationTest task.' - group = 'verification' - - executionData.from(execFile) - sourceSets(proj.extensions.getByType(SourceSetContainer).named('main').get()) - - reports { - xml.required = true - html.required = true - csv.required = false - } - - dependsOn(integrationTest) - } - - integrationTest.finalizedBy(reportTask) - } -} diff --git a/code-coverage/build.gradle b/code-coverage/build.gradle new file mode 100644 index 0000000..f8adea9 --- /dev/null +++ b/code-coverage/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'config.code-coverage-aggregate' +} + +dependencies { + // The plugin project (always included) + coverageDataProjects project(':grails-server-timing') + + // Auto-discover all example apps under examples/ + rootDir.toPath().resolve('examples').toFile() + .listFiles({ it.directory } as FileFilter) + .each { coverageDataProjects project(":$it.name") + } +} diff --git a/coverage/build.gradle b/coverage/build.gradle deleted file mode 100644 index 41545f6..0000000 --- a/coverage/build.gradle +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id 'config.code-coverage' -} - -dependencies { - // The plugin project (always included) - coverageDataProjects project(':grails-server-timing') - - // Auto-discover all example apps under examples/ - rootDir.toPath().resolve('examples').toFile().list()?.each { exampleApp -> - coverageDataProjects project(":$exampleApp") - } -} diff --git a/examples/app1/build.gradle b/examples/app1/build.gradle index 7330508..dc6e54c 100644 --- a/examples/app1/build.gradle +++ b/examples/app1/build.gradle @@ -1,8 +1,5 @@ plugins { - id 'config.code-style' - id 'config.compile' id 'config.example-app' - id 'config.testing' } version = projectVersion diff --git a/examples/app2/build.gradle b/examples/app2/build.gradle index f67e57d..661d543 100644 --- a/examples/app2/build.gradle +++ b/examples/app2/build.gradle @@ -1,8 +1,5 @@ plugins { - id 'config.code-style' - id 'config.compile' id 'config.example-app' - id 'config.testing' } version = projectVersion diff --git a/plugin/build.gradle b/plugin/build.gradle index 4afa31c..6da6ea7 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,6 +1,7 @@ import org.apache.grails.gradle.publish.GrailsPublishExtension plugins { + id 'config.code-coverage' id 'config.code-style' id 'config.compile' id 'config.grails-plugin' diff --git a/settings.gradle b/settings.gradle index 87c6a01..aa039a9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,8 +22,11 @@ def isLocal = !isCI def isReproducibleBuild = System.getenv('SOURCE_DATE_EPOCH') != null if (isReproducibleBuild) { gradle.settingsEvaluated { - logger.warn('*************** Remote Build Cache Disabled due to Reproducible Build ********************') - logger.warn("Build date will be set to (SOURCE_DATE_EPOCH=${System.getenv("SOURCE_DATE_EPOCH")})") + logger.warn( + '***** Remote Build Cache Disabled due to Reproducible Build *****\n' + + 'Build date will be set to (SOURCE_DATE_EPOCH={})', + System.getenv('SOURCE_DATE_EPOCH') + ) } } @@ -33,16 +36,15 @@ buildCache { rootProject.name = 'grails-server-timing-root' -include 'plugin' +include('plugin') project(':plugin').name = 'grails-server-timing' -include 'docs' +include('docs') project(':docs').name = 'grails-server-timing-docs' -include 'coverage' +include('code-coverage') -def examples = file('examples').list() -examples.each { example -> - include example - project(":$example").projectDir = file("examples/$example") +file('examples').listFiles({ it.directory } as FileFilter).each { + include(it.name) + project(":$it.name").projectDir = file("examples/$it.name") } dependencyResolutionManagement { From 64a9bf275368e5a439022eda78c349c6508b56cb Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 10:54:49 +0100 Subject: [PATCH 06/17] ci: update workflows --- .github/workflows/ci.yml | 20 ++++++------ .../{coverage.yml => code-coverage.yml} | 14 ++++----- .github/workflows/code-style.yml | 6 ++-- .github/workflows/release.yml | 31 ++++++++++--------- 4 files changed, 35 insertions(+), 36 deletions(-) rename .github/workflows/{coverage.yml => code-coverage.yml} (89%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 957189e..cd6476c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: "Output Agent IP" # in the event your agent has network issues, you can use this to debug run: curl -s https://api.ipify.org - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Export .sdkmanrc properties" uses: apache/grails-github-actions/export-gradle-properties@asf with: @@ -28,14 +28,14 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} - name: 'Ensure Common Build Date' # to ensure a reproducible build run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "🔨 Build project without tests" if: ${{ contains(github.event.head_commit.message, '[skip tests]') }} run: > @@ -53,15 +53,16 @@ jobs: --rerun-tasks -PskipCodeStyle publish: - # only run the publish task on this repo instead of forks + # only run the publishing task on this repo (not on forks) if: github.repository_owner == 'grails-plugins' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') - needs: [ build ] + needs: build + name: "Publish Snapshot" runs-on: ubuntu-24.04 steps: - name: "Output Agent IP" # in the event your agent has network issues, you can use this to debug run: curl -s https://api.ipify.org - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Export .sdkmanrc properties" uses: apache/grails-github-actions/export-gradle-properties@asf with: @@ -70,30 +71,27 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} - name: 'Ensure Common Build Date' # to ensure a reproducible build run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "📤 Publish Gradle Snapshot Artifacts" env: GRAILS_PUBLISH_RELEASE: 'false' MAVEN_PUBLISH_URL: 'https://central.sonatype.com/repository/maven-snapshots/' MAVEN_PUBLISH_USERNAME: ${{ secrets.NEXUS_PUBLISH_USERNAME }} MAVEN_PUBLISH_PASSWORD: ${{ secrets.NEXUS_PUBLISH_PASSWORD }} - working-directory: './plugin' run: > ../gradlew publish --no-build-cache --rerun-tasks - name: "📜 Generate Documentation" - if: success() run: ./gradlew docs - name: "🚀 Publish to Github Pages" - if: success() uses: apache/grails-github-actions/deploy-github-pages@asf env: GRADLE_PUBLISH_RELEASE: 'false' diff --git a/.github/workflows/coverage.yml b/.github/workflows/code-coverage.yml similarity index 89% rename from .github/workflows/coverage.yml rename to .github/workflows/code-coverage.yml index c72adc3..4f459d9 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -1,4 +1,4 @@ -name: "Coverage" +name: "Code Coverage" on: push: branches: @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Export .sdkmanrc properties" uses: apache/grails-github-actions/export-gradle-properties@asf with: @@ -24,24 +24,24 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: liberica java-version: ${{ env.SDKMANRC_java }} - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "🔨 Build and run tests" run: > ./gradlew build --continue --stacktrace -PskipCodeStyle - - name: "📊 Post coverage summary" + - name: "📊 Post code coverage summary" if: always() run: | - REPORT="coverage/build/reports/jacoco/jacocoAggregatedReport/jacocoAggregatedReport.xml" + REPORT="code-coverage/build/reports/jacoco/jacocoAggregatedReport/jacocoAggregatedReport.xml" if [ ! -f "$REPORT" ]; then - echo "::warning::Coverage report not found at $REPORT" + echo "::warning::Code Coverage report not found at $REPORT" exit 0 fi diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index bc649de..7fdf71f 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: "Export .sdkmanrc properties" uses: apache/grails-github-actions/export-gradle-properties@asf with: @@ -24,12 +24,12 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: liberica java-version: ${{ env.SDKMANRC_java }} - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "🎨 Run code style checks" run: > ./gradlew codeStyle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0274660..47aa465 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,9 @@ on: types: [ published ] permissions: { } env: - # to prevent throttling of the github api, include the github token in an environment variable since the build will check for it + # To prevent throttling of the GitHub api, + # include the GitHub token in an environment variable + # since the build will check for it GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GRAILS_PUBLISH_RELEASE: 'true' JAVA_DISTRIBUTION: 'liberica' @@ -19,7 +21,7 @@ jobs: name: "Stage Jar Files" permissions: packages: read # pre-release workflow - contents: write # to create release + contents: write # to create a release issues: write # to modify milestones runs-on: ubuntu-24.04 steps: @@ -28,7 +30,7 @@ jobs: - 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@v4 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ env.TAG }} @@ -49,12 +51,12 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "⚙️ Run pre-release" uses: apache/grails-github-actions/pre-release@asf env: @@ -71,7 +73,6 @@ jobs: ./gradlew -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg publishMavenPublicationToSonatypeRepository - publishPluginMavenPublicationToSonatypeRepository closeSonatypeStagingRepository - name: "Generate Build Date file" run: echo "$SOURCE_DATE_EPOCH" >> build/BUILD_DATE.txt @@ -92,7 +93,7 @@ jobs: - name: "📝 Establish release version" run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ env.TAG }} @@ -104,12 +105,12 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "📤 Release staging repository" env: NEXUS_PUBLISH_USERNAME: ${{ secrets.MAVEN_USERNAME }} @@ -131,7 +132,7 @@ jobs: - name: "📝 Establish release version" run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ env.TAG }} @@ -143,12 +144,12 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "🔨 Build Documentation" run: ./gradlew docs - name: "🚀 Publish to Github Pages" @@ -171,7 +172,7 @@ jobs: - name: "📝 Establish release version" run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" - name: "📥 Checkout repository" - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ env.TAG }} @@ -183,11 +184,11 @@ jobs: - name: "Determine Java Version" run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - name: "☕️ Setup JDK" - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v4 + uses: gradle/actions/setup-gradle@v5 - name: "⚙️ Run post-release" uses: apache/grails-github-actions/post-release@asf From 358cd0665e25a9bdbfc0853742de23103dd91454 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 11:38:39 +0100 Subject: [PATCH 07/17] build: fix code coverage aggregation --- .../src/main/groovy/config.code-coverage-aggregate.gradle | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle index 5306ff7..595e563 100644 --- a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle +++ b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle @@ -1,4 +1,5 @@ plugins { + id 'base' id 'jacoco' } @@ -78,9 +79,6 @@ afterEvaluate { } } -pluginManager.withPlugin('base') { - tasks.named('check') { - dependsOn('jacocoAggregatedReport') - } +tasks.named('check') { + dependsOn('jacocoAggregatedReport') } - From fd5e95fb7982d9787a86c8ab913ddd9bfbef00be Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 12:45:56 +0100 Subject: [PATCH 08/17] ci: setup conditional build scans --- .github/workflows/ci.yml | 12 ++++++++++++ .github/workflows/code-coverage.yml | 6 ++++++ .github/workflows/code-style.yml | 6 ++++++ .github/workflows/release.yml | 18 ++++++++++++++++++ gradle.properties | 5 +++++ 5 files changed, 47 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd6476c..d6d903f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,14 @@ jobs: java-version: ${{ env.SDKMANRC_java }} - name: 'Ensure Common Build Date' # to ensure a reproducible build run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "🔨 Build project without tests" if: ${{ contains(github.event.head_commit.message, '[skip tests]') }} run: > @@ -77,8 +83,14 @@ jobs: java-version: ${{ env.SDKMANRC_java }} - name: 'Ensure Common Build Date' # to ensure a reproducible build run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "📤 Publish Gradle Snapshot Artifacts" env: GRAILS_PUBLISH_RELEASE: 'false' diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 4f459d9..eebb2f1 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -28,8 +28,14 @@ jobs: with: distribution: liberica java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "🔨 Build and run tests" run: > ./gradlew build diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index 7fdf71f..db6872e 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -28,8 +28,14 @@ jobs: with: distribution: liberica java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "🎨 Run code style checks" run: > ./gradlew codeStyle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47aa465..a008a0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,8 +55,14 @@ jobs: with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "⚙️ Run pre-release" uses: apache/grails-github-actions/pre-release@asf env: @@ -109,8 +115,14 @@ jobs: with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "📤 Release staging repository" env: NEXUS_PUBLISH_USERNAME: ${{ secrets.MAVEN_USERNAME }} @@ -148,8 +160,14 @@ jobs: with: distribution: ${{ env.JAVA_DISTRIBUTION }} java-version: ${{ env.SDKMANRC_java }} + - name: "Export gradle.properties properties" + uses: apache/grails-github-actions/export-gradle-properties@asf - name: "🐘 Setup Gradle" uses: gradle/actions/setup-gradle@v5 + with: + build-scan-publish: ${{ env.ciBuildScanPublish }} + build-scan-terms-of-use-url: ${{ env.ciBuildScanTermsOfUseUrl }} + build-scan-terms-of-use-agree: ${{ env.ciBuildScanTermsOfUseAgree }} - name: "🔨 Build Documentation" run: ./gradlew docs - name: "🚀 Publish to Github Pages" diff --git a/gradle.properties b/gradle.properties index b118218..e62c934 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,11 @@ codenarcVersion=3.6.0 jacocoVersion=0.8.12 testLoggerVersion=4.0.0 +# Enable and set agree=yes to publish build scans from GitHub workflows +ciBuildScanPublish=true +ciBuildScanTermsOfUseUrl=https://gradle.com/terms-of-service +ciBuildScanTermsOfUseAgree=yes + org.gradle.caching=true org.gradle.daemon=true org.gradle.parallel=true From 9bfc3f13eb9d5963f967a3b0f3f19a08a2156005 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 13:47:15 +0100 Subject: [PATCH 09/17] ci: clean up post-release --- .github/workflows/release.yml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a008a0a..d04f043 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -187,26 +187,5 @@ jobs: issues: write # required for milestone closing pull-requests: write # to create the PR that will increment the version steps: - - name: "📝 Establish release version" - run: echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" - - name: "📥 Checkout repository" - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ env.TAG }} - - name: "Export .sdkmanrc properties" - uses: apache/grails-github-actions/export-gradle-properties@asf - with: - file: ".sdkmanrc" - prefix: "SDKMANRC_" - - name: "Determine Java Version" - run: echo "SDKMANRC_java=${{ env.SDKMANRC_java }}" | sed 's/-.*//' >> $GITHUB_ENV - - name: "☕️ Setup JDK" - uses: actions/setup-java@v5 - with: - distribution: ${{ env.JAVA_DISTRIBUTION }} - java-version: ${{ env.SDKMANRC_java }} - - name: "🐘 Setup Gradle" - uses: gradle/actions/setup-gradle@v5 - name: "⚙️ Run post-release" uses: apache/grails-github-actions/post-release@asf From acffb2621427747e83290613ed646bc0f2f4548f Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 15:03:44 +0100 Subject: [PATCH 10/17] docs: update readme and other md files --- .skills/example-apps.md | 34 ++++++++-------- .skills/gradle-best-practices.md | 65 ++++++++++++++++--------------- .skills/plugin-project.md | 56 ++++++++++++--------------- .skills/repository-structure.md | 58 ++++++++++++++-------------- AGENTS.md | 66 ++++++++++++++++---------------- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 8 ++-- README.md | 35 +++++++++-------- 8 files changed, 161 insertions(+), 163 deletions(-) diff --git a/.skills/example-apps.md b/.skills/example-apps.md index 2a92dbb..b8cd07a 100644 --- a/.skills/example-apps.md +++ b/.skills/example-apps.md @@ -47,14 +47,14 @@ The `examples/` directory can contain more than one app. Different apps can test All apps under `examples/` are auto-discovered by `settings.gradle`: ```groovy -def examples = file('examples').list() +def examples = file('examples').listFiles({ it.directory } as FileFilter) examples.each { example -> - include example - project(":$example").projectDir = file("examples/$example") + include example.name + project(":$example.name").projectDir = file("examples/$example.name") } ``` -New apps are also automatically included in coverage aggregation -- `coverage/build.gradle` discovers all example apps +New apps are also automatically included in coverage aggregation -- `code-coverage/build.gradle` discovers all example apps under `examples/` at configuration time, so no manual registration is needed. ## Project Structure @@ -90,9 +90,7 @@ Example apps apply convention plugins and declare their own dependencies: ```groovy plugins { - id 'org.grails.plugins.servertiming.compile' - id 'org.grails.plugins.servertiming.testing' - id 'org.grails.plugins.servertiming.example' + id 'config.example-app' } version = projectVersion @@ -119,7 +117,7 @@ dependencies { Key patterns: -- Apply `compile`, `testing`, and `example` convention plugins +- Apply `example-app` convention plugin - Depend on the plugin via `project(':grails-server-timing')` - NEVER apply `project-publish` -- example apps are not published - NEVER apply `plugin` -- example apps are applications, not plugins @@ -159,7 +157,7 @@ class ServerTimingIntegrationSpec extends Specification { ### What to test in integration tests - HTTP headers are present and correctly formatted -- Timing values are within expected ranges (e.g., slow action >= 200ms) +- Timing values are within expected ranges (e.g., slow action >= 200 ms) - Different response types (GSP views, JSON, plain text) all include headers - Static assets include `other`/`total` metrics but not `action`/`view` - Header format matches the W3C Server Timing specification @@ -169,10 +167,10 @@ class ServerTimingIntegrationSpec extends Specification { ### Integration test patterns 1. **Use `RestTemplate` or similar HTTP client** -- test real HTTP round-trips -2. **Verify headers, not internals** -- assert on `Server-Timing` header values, not internal class state +2. **Verify headers, not internals** – assert on `Server-Timing` header values, not internal class state 3. **Use timing thresholds, not exact values** -- assert `>= 200ms`, never `== 203ms` -4. **Test edge cases** -- static assets, JSON responses, redirects, errors -5. **Extract helper methods** -- centralize header parsing (e.g., `extractDuration()`) +4. **Test edge cases** – static assets, JSON responses, redirects, errors +5. **Extract helper methods** – centralize header parsing (e.g., `extractDuration()`) ### Test organization @@ -185,12 +183,12 @@ class ServerTimingIntegrationSpec extends Specification { Example apps should include purpose-built controllers and views that exercise the plugin's features: -- **Fast actions** -- verify baseline header presence -- **Slow actions** (with `Thread.sleep()`) -- verify timing accuracy -- **Variable delay actions** -- parameterized timing tests -- **Slow views** (GSP with embedded sleep) -- verify view timing separation -- **JSON/text responses** -- verify non-GSP response types -- **Multiple operations** -- verify timing accumulation +- **Fast actions** – verify baseline header presence +- **Slow actions** (with `Thread.sleep()`) – verify timing accuracy +- **Variable delay actions** – parameterized timing tests +- **Slow views** (GSP with embedded sleep) – verify view timing separation +- **JSON/text responses** – verify non-GSP response types +- **Multiple operations** – verify timing accumulation These are test fixtures that live in the example app, NOT in the plugin project. diff --git a/.skills/gradle-best-practices.md b/.skills/gradle-best-practices.md index 392bd10..bd3fd93 100644 --- a/.skills/gradle-best-practices.md +++ b/.skills/gradle-best-practices.md @@ -3,7 +3,7 @@ ## Purpose This skill covers Gradle best practices for this project, including convention plugins, extension configuration, -lazy APIs, and build structure. Convention plugins eliminate duplication across subprojects by centralizing shared +lazy APIs, and build structure. Convention plugins remove duplication across subprojects by centralizing shared build logic. They live in the `build-logic/` composite build and are applied by ID in each subproject's `build.gradle`. ## Core Rules @@ -11,7 +11,7 @@ build logic. They live in the `build-logic/` composite build and are applied by ### NEVER configure subprojects from the root build.gradle The root `build.gradle` must NEVER use `subprojects {}`, `allprojects {}`, or `configure(subprojects.matching {...}) {}` -to apply plugins or configure subproject behavior. This is an anti-pattern that causes ordering issues, breaks project +to apply plugins or configure subproject behavior. This is an antipattern that causes ordering issues, breaks project isolation, and makes builds harder to reason about. ```groovy @@ -34,7 +34,7 @@ allprojects { Instead, create a convention plugin in `build-logic/` and apply it in each subproject that needs it: ```groovy -// GOOD - build-logic/src/main/groovy/org.grails.plugins.servertiming.compile.gradle +// GOOD - build-logic/src/main/groovy/config.compile.gradle plugins { id 'groovy' } @@ -44,7 +44,7 @@ plugins { ```groovy // GOOD - plugin/build.gradle plugins { - id 'org.grails.plugins.servertiming.compile' + id 'config.compile' } ``` @@ -69,12 +69,12 @@ pluginManagement { Convention plugin files follow the pattern: ``` -build-logic/src/main/groovy/org.grails.plugins.servertiming..gradle +build-logic/src/main/groovy/config..gradle ``` The plugin ID matches the filename (minus the `.gradle` extension). For example: -- `org.grails.plugins.servertiming.compile.gradle` -> plugin ID `org.grails.plugins.servertiming.compile` +- `config.compile.gradle` -> plugin ID `config.compile` ### Declare external plugin dependencies in build-logic/build.gradle @@ -102,15 +102,19 @@ The `build-logic/build.gradle` reads the root `gradle.properties` and exposes th convention plugins can reference them (e.g., `grailsVersion`): ```groovy -file('../gradle.properties').withInputStream { - Properties props = new Properties() - props.load(it) - project.ext.gradleProperties = props -} - -allprojects { - for (String key : gradleProperties.stringPropertyNames()) { - ext.set(key, gradleProperties.getProperty(key)) +file('../gradle.properties').withInputStream { is -> + extensions.extraProperties.set( + 'gradleProperties', + new Properties().tap { load(is) } + ) +} + +allprojects { project -> + gradleProperties.stringPropertyNames().each { key -> + project.extensions.extraProperties.set( + key, + gradleProperties.getProperty(key) + ) } } ``` @@ -201,26 +205,27 @@ Convention plugins should compose by applying other convention plugins rather th plugins { id 'org.apache.grails.gradle.grails-web' id 'org.apache.grails.gradle.grails-gsp' - id 'org.grails.plugins.servertiming.assets' - id 'org.grails.plugins.servertiming.run' + id 'config.grails-assets' + id 'config.app-run' } ``` ## Existing Convention Plugins -| Plugin | Purpose | -|-------------------------------|-------------------------------------------------------------------------------------------------------| -| `compile.gradle` | Java/Groovy compilation: UTF-8, incremental, forked JVM, `-parameters`, Java release from `.sdkmanrc` | -| `testing.gradle` | Test framework: Spock, JUnit Platform, test-logger (mocha-parallel locally, plain-parallel in CI) | -| `plugin.gradle` | Applies `grails-plugin` profile, disables Spring dependency management | -| `example.gradle` | Applies grails-web, grails-gsp, assets, and run plugins for example apps | -| `project-publish.gradle` | Maven publishing metadata (artifact ID, license, developers, GitHub slug) | -| `root-publish.gradle` | Nexus publishing workaround (root-level only) | -| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor + GitHub Pages index) | -| `assets.gradle` | Asset pipeline with Bootstrap/jQuery/Bootstrap-Icons WebJars | -| `run.gradle` | Debug/debugWait JVM flags for `bootRun` | -| `coverage-aggregation.gradle` | JaCoCo coverage aggregation across subprojects (XML + HTML reports) | -| `style.gradle` | Checkstyle + CodeNarc code style checking; configs in `build-logic/config/` | +| Plugin | Purpose | +|----------------------------------|--------------------------------------------------------------------------------------| +| `app-run.gradle` | Debug flags for `bootRun` | +| `code-coverage.gradle` | JaCoCo coverage for project (XML + HTML reports) | +| `code-coverage-aggregate.gradle` | JaCoCo coverage aggregation across subprojects (XML + HTML reports) | +| `code-style.gradle` | Checkstyle + CodeNarc code style checking (configs in `build-logic/config/`) | +| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | +| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | +| `example-app.gradle` | Example app config (grails-web, GSP, assets) | +| `grails-assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | +| `grails-plugin.gradle` | Grails plugin application | +| `publish.gradle` | Per-project Maven publishing metadata | +| `publish-root.gradle` | Root-level Nexus publishing workaround | +| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | ## When to Create a New Convention Plugin diff --git a/.skills/plugin-project.md b/.skills/plugin-project.md index f5f276f..19453d4 100644 --- a/.skills/plugin-project.md +++ b/.skills/plugin-project.md @@ -27,7 +27,7 @@ The plugin project must NOT contain: Keeping integration/functional tests out of the plugin project ensures: -1. The plugin artifact is clean -- no test dependencies or test code leaks into the published JAR +1. The plugin artifact is clean – no test dependencies or test code leaks into the published JAR 2. Tests that require a running Grails application exercise the plugin as a real consumer would 3. The plugin's API surface is validated from the outside, not the inside 4. Different example apps can test different configurations of the plugin @@ -40,23 +40,26 @@ plugin/ ├── grails-app/ │ ├── conf/ │ │ ├── application.yml # Plugin-specific config defaults -│ │ └── logback-spring.xml # Logging config │ ├── controllers/ # Interceptors, controller-scoped artifacts │ │ └── org/grails/plugins/servertiming/ │ │ └── ServerTimingInterceptor.groovy -│ └── init/ # Plugin application class -│ └── org/grails/plugins/servertiming/ -│ └── Application.groovy └── src/ ├── main/groovy/ # Core plugin classes │ └── org/grails/plugins/servertiming/ - │ ├── GrailsServerTimingGrailsPlugin.groovy + │ ├── ServerTimingAutoConfiguration.groovy │ ├── ServerTimingFilter.groovy + │ ├── ServerTimingGrailsPlugin.groovy │ ├── ServerTimingResponseWrapper.groovy - │ ├── ServerTimingUtils.groovy + │ ├── config/ + │ │ ├── EnabledCondition.groovy + │ │ └── ServerTimingConfig.groovy │ └── core/ │ ├── Metric.groovy │ └── TimingMetric.groovy + ├── main/resources/ + │ ├── META-INF/spring + │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports + │ └── spring-configuration-metadata.json └── test/groovy/ # Unit tests ONLY └── org/grails/plugins/servertiming/ ├── MetricSpec.groovy @@ -69,25 +72,26 @@ The plugin's `build.gradle` should be minimal -- apply convention plugins and de ```groovy plugins { - id 'org.grails.plugins.servertiming.compile' - id 'org.grails.plugins.servertiming.testing' - id 'org.grails.plugins.servertiming.plugin' - id 'org.grails.plugins.servertiming.project-publish' + id 'config.compile' + id 'config.testing' + id 'config.grails-plugin' + id 'config.publish' } version = projectVersion -group = "org.grails.plugins" +group = 'org.grails.plugins' dependencies { + + profile 'org.apache.grails.profiles:web-plugin' + console 'org.apache.grails:grails-console' + compileOnly platform("org.apache.grails:grails-bom:$grailsVersion") compileOnly 'org.apache.grails:grails-dependencies-starter-web' - console "org.apache.grails:grails-console" - profile "org.apache.grails.profiles:web-plugin" - testImplementation platform("org.apache.grails:grails-bom:$grailsVersion") - testImplementation "org.apache.grails:grails-dependencies-starter-web" - testImplementation "org.apache.grails:grails-dependencies-test" + testImplementation 'org.apache.grails:grails-dependencies-starter-web' + testImplementation 'org.apache.grails:grails-dependencies-test' } ``` @@ -95,8 +99,8 @@ Key patterns: - Use `compileOnly` for framework dependencies the consuming application will provide - Use `testImplementation` for test-only dependencies -- Apply `project-publish` to configure Maven publishing metadata -- NEVER add custom task configuration here - move it to a convention plugin +- Apply `config.publish` to configure Maven publishing metadata +- NEVER add custom task configuration here – move it to a convention plugin ## Unit Test Guidelines @@ -127,18 +131,8 @@ Unit tests in the plugin project test individual classes in isolation: ## Plugin Descriptor -The `GrailsServerTimingGrailsPlugin` class extends `grails.plugins.Plugin` and registers Spring beans. It uses -`ServerTimingUtils` to check whether the plugin is enabled before registering the filter: - -```groovy -Closure doWithSpring() { - { -> - if (ServerTimingUtils.instance.isEnabled(grailsApplication)) { - // register filter beans - } - } -} -``` +The `ServerTimingGrailsPlugin` class extends `grails.plugins.Plugin` and exposes important +information about the plugin to the Grails framework. ## Dependency Scoping diff --git a/.skills/repository-structure.md b/.skills/repository-structure.md index 6078e60..060060d 100644 --- a/.skills/repository-structure.md +++ b/.skills/repository-structure.md @@ -13,6 +13,8 @@ grails-server-timing/ ├── .github/ # CI/CD workflows and GitHub config │ ├── workflows/ │ │ ├── ci.yml # Build, test, publish snapshots +│ │ ├── code-coverage.yml # Create a code coverage report +│ │ ├── code-style.yml # Check code style │ │ ├── release.yml # Multi-stage release pipeline │ │ └── release-notes.yml # Automated release draft notes │ ├── release-drafter.yml # Release drafter categories/labels @@ -26,24 +28,24 @@ grails-server-timing/ │ │ ├── checkstyle/ # Checkstyle XML configs │ │ └── codenarc/ # CodeNarc ruleset │ └── src/main/groovy/ # Convention plugin files (*.gradle) -│ ├── ...compile.gradle -│ ├── ...testing.gradle -│ ├── ...plugin.gradle -│ ├── ...example.gradle -│ ├── ...project-publish.gradle -│ ├── ...root-publish.gradle -│ ├── ...docs.gradle -│ ├── ...assets.gradle -│ ├── ...run.gradle -│ ├── ...coverage-aggregation.gradle -│ └── ...style.gradle +│ ├── config.app-run.gradle +│ ├── config.code-coverage.gradle +│ ├── config.code-coverage-aggregate.gradle +│ ├── config.code-style.gradle +│ ├── config.compile.gradle +│ ├── config.docs.gradle +│ ├── config.example-app.gradle +│ ├── config.grails-assets.gradle +│ ├── config.grails-plugin.gradle +│ ├── config.publish.gradle +│ ├── config.publish-root.gradle +│ └── config.testing.gradle │ ├── plugin/ # The Grails plugin artifact │ ├── build.gradle # Convention plugins + dependencies only │ ├── grails-app/ │ │ ├── conf/ # Plugin config (application.yml, logback) -│ │ ├── controllers/ # Interceptors and controller artifacts -│ │ └── init/ # Plugin Application class +│ │ └── controllers/ # Interceptors and controller artifacts │ └── src/ │ ├── main/groovy/ # Plugin source code │ └── test/groovy/ # Unit tests ONLY @@ -72,7 +74,7 @@ grails-server-timing/ │ └── src/ │ └── integration-test/ # Integration & functional tests │ -├── coverage/ # JaCoCo coverage aggregation +├── code-coverage/ # JaCoCo coverage aggregation │ └── build.gradle # Declares which projects contribute coverage data │ ├── docs/ # Asciidoctor documentation @@ -100,9 +102,9 @@ flows through convention plugins. ```groovy // Root build.gradle -- this is all that should be here plugins { - id "idea" - id 'org.grails.plugins.servertiming.docs' - id 'org.grails.plugins.servertiming.root-publish' + id 'idea' + id 'config.docs' + id 'config.root-publish' } ``` @@ -128,13 +130,13 @@ All tests requiring a running Grails application live in example apps under `exa Convention plugins in `build-logic/` eliminate all duplication: -- Compilation settings: `compile.gradle` -- Test configuration: `testing.gradle` -- Plugin setup: `plugin.gradle` -- Example app setup: `example.gradle` -- Publishing: `project-publish.gradle` -- Coverage aggregation: `coverage-aggregation.gradle` -- Code style checking: `style.gradle` +- Compilation settings: `config.compile.gradle` +- Test configuration: `config.testing.gradle` +- Plugin setup: `config.grails-plugin.gradle` +- Example app setup: `config.example-app.gradle` +- Publishing: `config.publish.gradle` +- Coverage aggregation: `config.coverage-aggregate.gradle` +- Code style checking: `config.code-style.gradle` ### 5. Centralized dependency resolution @@ -158,9 +160,7 @@ These are available in all subprojects as project properties (`projectVersion`, 2. Add a `build.gradle` applying the convention plugins: ```groovy plugins { - id 'org.grails.plugins.servertiming.compile' - id 'org.grails.plugins.servertiming.testing' - id 'org.grails.plugins.servertiming.example' + id 'config.example-app' } ``` 3. Add standard Grails app structure under `grails-app/` @@ -170,7 +170,7 @@ These are available in all subprojects as project properties (`projectVersion`, ## Adding a New Convention Plugin -1. Create a new file: `build-logic/src/main/groovy/org.grails.plugins.servertiming..gradle` +1. Create a new file: `build-logic/src/main/groovy/config..gradle` 2. If the plugin applies third-party plugins, add their dependencies to `build-logic/build.gradle` 3. Apply the new plugin ID in the relevant subproject(s) 4. Keep the plugin focused on a single concern @@ -188,7 +188,7 @@ These are available in all subprojects as project properties (`projectVersion`, ./gradlew :app1:integrationTest # Aggregated coverage report (unit + integration) -./gradlew :coverage:jacocoAggregatedReport +./gradlew jacocoAggregatedReport # Run an example app ./gradlew :app1:bootRun diff --git a/AGENTS.md b/AGENTS.md index 9f8b90b..5c13cd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,12 +16,12 @@ rendering time, and total request time, surfacing them in browser DevTools. Detailed best practices are documented in `.skills/`: -| Skill File | Purpose | -|--------------------------------------------------------------------------------|-------------------------------------------------------| -| [`.skills/repository-structure.md`](.skills/repository-structure.md) | Canonical directory layout and architectural rules | -| [`.skills/gradle-best-practices.md`](.skills/gradle-best-practices.md) | Gradle best practices, convention plugins, and idioms | -| [`.skills/plugin-project.md`](.skills/plugin-project.md) | Plugin project scope: source code + unit tests only | -| [`.skills/example-apps.md`](.skills/example-apps.md) | Example app patterns: integration & functional tests | +| Skill File | Purpose | +|------------------------------------------------------------------------|-------------------------------------------------------| +| [`.skills/repository-structure.md`](.skills/repository-structure.md) | Canonical directory layout and architectural rules | +| [`.skills/gradle-best-practices.md`](.skills/gradle-best-practices.md) | Gradle best practices, convention plugins, and idioms | +| [`.skills/plugin-project.md`](.skills/plugin-project.md) | Plugin project scope: source code + unit tests only | +| [`.skills/example-apps.md`](.skills/example-apps.md) | Example app patterns: integration & functional tests | **Read these skill files before making structural changes to the repository.** @@ -48,7 +48,7 @@ grails-server-timing/ │ └── src/test/ # Unit tests ONLY ├── examples/app1/ # Example Grails app │ └── src/integration-test/ # Integration & functional tests -├── coverage/ # JaCoCo coverage aggregation +├── code-coverage/ # JaCoCo coverage aggregation ├── docs/ # Asciidoctor documentation ├── build-logic/ # Gradle convention plugins (composite build) │ └── config/ # Code style configs (checkstyle, codenarc) @@ -71,7 +71,7 @@ grails-server-timing/ ./gradlew :app1:integrationTest # Aggregated coverage report (unit + integration) -./gradlew :coverage:jacocoAggregatedReport +./gradlew jacocoAggregatedReport # Skip tests ./gradlew build -PskipTests @@ -116,15 +116,14 @@ The plugin intercepts HTTP requests via a servlet filter and Grails interceptor: ### Core Classes (plugin/src/main/groovy/org/grails/plugins/servertiming/) -| Class | Purpose | -|----------------------------------|--------------------------------------------------------------------| -| `GrailsServerTimingGrailsPlugin` | Plugin descriptor; registers the filter bean when enabled | -| `ServerTimingFilter` | Servlet filter; creates `TimingMetric` per request, wraps response | -| `ServerTimingResponseWrapper` | Response wrapper; injects `Server-Timing` header on commit | -| `ServerTimingInterceptor` | Grails interceptor; tracks action and view timing | -| `ServerTimingUtils` | Reads plugin configuration; auto-enables in DEV/TEST environments | -| `core/Metric` | Single timing metric model with RFC 7230 name validation | -| `core/TimingMetric` | Collection of metrics; generates header value | +| Class | Purpose | +|-------------------------------|--------------------------------------------------------------------| +| `ServerTimingGrailsPlugin` | Plugin descriptor; registers the filter bean when enabled | +| `ServerTimingFilter` | Servlet filter; creates `TimingMetric` per request, wraps response | +| `ServerTimingResponseWrapper` | Response wrapper; injects `Server-Timing` header on commit | +| `ServerTimingInterceptor` | Grails interceptor; tracks action and view timing | +| `core/Metric` | Single timing metric model with RFC 7230 name validation | +| `core/TimingMetric` | Collection of metrics; generates header value | ## Configuration @@ -157,19 +156,20 @@ Tests use the **Spock Framework** and run on JUnit Platform. Convention plugins in `build-logic/src/main/groovy/` standardize build configuration: -| Plugin | Purpose | -|-------------------------------|--------------------------------------------------------------------------------------| -| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | -| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | -| `plugin.gradle` | Grails plugin application | -| `example.gradle` | Example app config (grails-web, GSP, assets) | -| `project-publish.gradle` | Per-project Maven publishing metadata | -| `root-publish.gradle` | Root-level Nexus publishing workaround | -| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | -| `assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | -| `run.gradle` | Debug flags for `bootRun` | -| `coverage-aggregation.gradle` | JaCoCo coverage aggregation across subprojects (XML + HTML reports) | -| `style.gradle` | Checkstyle + CodeNarc code style checking (configs in `build-logic/config/`) | +| Plugin | Purpose | +|----------------------------------|--------------------------------------------------------------------------------------| +| `app-run.gradle` | Debug flags for `bootRun` | +| `code-coverage.gradle` | JaCoCo coverage for project (XML + HTML reports) | +| `code-coverage-aggregate.gradle` | JaCoCo coverage aggregation across subprojects (XML + HTML reports) | +| `code-style.gradle` | Checkstyle + CodeNarc code style checking (configs in `build-logic/config/`) | +| `compile.gradle` | Java/Groovy compilation settings (UTF-8, incremental, Java release from `.sdkmanrc`) | +| `docs.gradle` | Documentation aggregation (Groovydoc + Asciidoctor) | +| `example-app.gradle` | Example app config (grails-web, GSP, assets) | +| `grails-assets.gradle` | Asset pipeline with Bootstrap/jQuery WebJars | +| `grails-plugin.gradle` | Grails plugin application | +| `publish.gradle` | Per-project Maven publishing metadata | +| `publish-root.gradle` | Root-level Nexus publishing workaround | +| `testing.gradle` | Test framework config (Spock, JUnit Platform, test-logger) | ## CI/CD @@ -186,9 +186,9 @@ Convention plugins in `build-logic/src/main/groovy/` standardize build configura - Groovy source files use standard Grails conventions (domain classes, controllers, interceptors, services in `grails-app/`, other classes in `src/main/groovy/`). -- **Use `def` for local variables** where the type is inferred from the right-hand side (e.g., constructor calls, casts, - factory methods). Explicit types should only be used for local variables when the type cannot be inferred or when - needed for `@CompileStatic` compilation. This applies to both production code and tests. +- **Use `def` for local variables** where the type is inferred from the right-hand side (e.g., constructor calls, + method calls, casts, factory methods). Explicit types should only be used for local variables when the type cannot + be inferred or when needed for `@CompileStatic` compilation. This applies to both production code and tests. - Metric names must conform to RFC 7230 token rules (alphanumeric plus `!#$%&'*+-.^_`|~`). - Description strings follow HTTP quoted-string escaping rules. - The plugin uses `System.nanoTime()` for timing precision. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 7e3dc45..4231310 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -33,7 +33,7 @@ We strive to: - Sharing private communications without consent - Personal insults - Unwelcome sexual attention - - Repeated harassment -- if someone asks you to stop, then stop + - Repeated harassment – if someone asks you to stop, then stop - Advocating for or encouraging any of the above behavior 6. **Be concise.** Respect others' time. Write clearly so conversations stay productive. When a long explanation is diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfb0289..d49c336 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ sdk env install grails-server-timing/ ├── plugin/ # The publishable Grails plugin (source + unit tests ONLY) ├── examples/app1/ # Example app with integration tests -├── coverage/ # JaCoCo coverage aggregation +├── code-coverage/ # JaCoCo coverage aggregation ├── build-logic/ # Gradle convention plugins (shared build configuration) ├── docs/ # Asciidoctor documentation └── .skills/ # AI agent best-practice docs @@ -45,7 +45,7 @@ grails-server-timing/ Key architectural rules: -- **Plugin module** contains only plugin source code and unit tests -- no integration tests, no example controllers. +- **Plugin module** contains only plugin source code and unit tests – no integration tests, no example controllers. - **Example apps** under `examples/` host all integration and functional tests. They depend on the plugin as a real consumer would. - **Convention plugins** in `build-logic/` deduplicate build configuration. Never use `subprojects {}`, @@ -82,14 +82,14 @@ The project uses JaCoCo to aggregate coverage data from both plugin unit tests a ```bash # Generate the aggregated coverage report -./gradlew :coverage:jacocoAggregatedReport +./gradlew jacocoAggregatedReport ``` Reports are generated at: | Report | Location | |---------------------------------|----------------------------------------------------------------------------------| -| Aggregated (unit + integration) | `coverage/build/reports/jacoco/jacocoAggregatedReport/html/index.html` | +| Aggregated (unit + integration) | `code-coverage/build/reports/jacoco/jacocoAggregatedReport/html/index.html` | | Plugin unit tests | `plugin/build/reports/jacoco/test/html/index.html` | | App1 integration tests | `examples/app1/build/reports/jacoco/jacocoIntegrationTestReport/html/index.html` | diff --git a/README.md b/README.md index 7bff92b..a587372 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -# Grails Server Timing Plugin +# 🧩 Grails Server Timing Plugin -[![CI](https://github.com/grails-plugins/grails-server-timing/actions/workflows/ci.yml/badge.svg)](https://github.com/grails-plugins/grails-server-timing/actions/workflows/ci.yml) -[![Coverage](https://github.com/grails-plugins/grails-server-timing/actions/workflows/coverage.yml/badge.svg)](https://github.com/grails-plugins/grails-server-timing/actions/workflows/coverage.yml) [![Maven Central](https://img.shields.io/maven-central/v/org.grails.plugins/grails-server-timing)](https://central.sonatype.com/artifact/org.grails.plugins/grails-server-timing) [![License](https://img.shields.io/github/license/grails-plugins/grails-server-timing)](https://www.apache.org/licenses/LICENSE-2.0) +[![CI](https://github.com/grails-plugins/grails-server-timing/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/grails-plugins/grails-server-timing/actions/workflows/ci.yml) +[![Coverage](https://github.com/grails-plugins/grails-server-timing/actions/workflows/coverage.yml/badge.svg?event=push)](https://github.com/grails-plugins/grails-server-timing/actions/workflows/coverage.yml) -A Grails plugin that injects [Server Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing) HTTP headers into -responses, implementing the [W3C Server Timing specification](https://w3c.github.io/server-timing/). It automatically -tracks controller action time, view rendering time, and total request time -- surfacing them directly in your browser's -DevTools. +A Grails plugin that injects [Server Timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing) +HTTP headers into responses, implementing the [W3C Server Timing specification](https://w3c.github.io/server-timing/). +It automatically tracks controller action time, view rendering time, and total request time – surfacing them directly in +your browser's DevTools. + +## Documentation + +Full documentation is available at the [project documentation site](https://grails-plugins.github.io/grails-server-timing/). +This includes architecture details, the W3C specification, security considerations, and browser DevTools usage guides. ## Quick Start @@ -16,14 +21,15 @@ Add the dependency to your `build.gradle`: ```groovy dependencies { - implementation 'org.grails.plugins:grails-server-timing:0.0.1-SNAPSHOT' + implementation 'org.grails.plugins:grails-server-timing:' } ``` That's it. The plugin is **automatically enabled** in `development` and `test` environments. No additional configuration is required. -> **Note:** The plugin is disabled by default in production to prevent exposing timing data that could +> [!NOTE] +> The plugin is disabled by default in production to prevent exposing timing data that could > facilitate [timing attacks](https://w3c.github.io/server-timing/#security-considerations). ### Using Snapshot Builds @@ -52,11 +58,12 @@ Then reference the snapshot version in your `build.gradle`: ```groovy dependencies { - implementation 'org.grails.plugins:grails-server-timing:0.0.1-SNAPSHOT' + implementation 'org.grails.plugins:grails-server-timing:-SNAPSHOT' } ``` -> **Note:** Snapshot versions are unstable and may change without notice. They are intended for testing +> [!NOTE] +> Snapshot versions are unstable and may change without notice. They are intended for testing > upcoming changes before a release. ## How It Works @@ -120,12 +127,6 @@ environments: |----------------|--------|------|--------| | 0.x | 7.0.x | 17+ | 4.0.x | -## Documentation - -Full documentation is available at -the [project documentation site](https://grails-plugins.github.io/grails-server-timing/). This includes architecture -details, the W3C specification, security considerations, and browser DevTools usage guides. - ## Building from Source Prerequisites: [SDKMAN!](https://sdkman.io/) From d087969a4b969ba8e399bf5eba9935a243a4c880 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 15:04:10 +0100 Subject: [PATCH 11/17] build: add `org.junit.jupiter:junit-jupiter-api` for Gradle 9 --- build-logic/src/main/groovy/config.testing.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build-logic/src/main/groovy/config.testing.gradle b/build-logic/src/main/groovy/config.testing.gradle index 667d35b..fc1af94 100644 --- a/build-logic/src/main/groovy/config.testing.gradle +++ b/build-logic/src/main/groovy/config.testing.gradle @@ -43,3 +43,10 @@ tasks.withType(Test).configureEach { showCauses = true } } + +pluginManager.withPlugin('groovy') { + project.dependencies.add( + 'testRuntimeOnly', + 'org.junit.jupiter:junit-jupiter-api' + ) +} From 0787a2712dd7dc18e8443453a53d957a8dbc4948 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 15:04:28 +0100 Subject: [PATCH 12/17] fix: remove redundant plugin Application class --- .../plugins/servertiming/Application.groovy | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 plugin/grails-app/init/org/grails/plugins/servertiming/Application.groovy diff --git a/plugin/grails-app/init/org/grails/plugins/servertiming/Application.groovy b/plugin/grails-app/init/org/grails/plugins/servertiming/Application.groovy deleted file mode 100644 index b8f0dff..0000000 --- a/plugin/grails-app/init/org/grails/plugins/servertiming/Application.groovy +++ /dev/null @@ -1,15 +0,0 @@ -package org.grails.plugins.servertiming - -import grails.boot.GrailsApp -import grails.boot.config.GrailsAutoConfiguration -import grails.plugins.metadata.PluginSource -import groovy.transform.CompileStatic - -@PluginSource -@CompileStatic -class Application extends GrailsAutoConfiguration { - - static void main(String[] args) { - GrailsApp.run(Application, args) - } -} From 5154fe7be905439042e6f1ee54f13b42d4815cd7 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 15:12:33 +0100 Subject: [PATCH 13/17] docs: update titles in readme --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a587372..3e59fc7 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ HTTP headers into responses, implementing the [W3C Server Timing specification]( It automatically tracks controller action time, view rendering time, and total request time – surfacing them directly in your browser's DevTools. -## Documentation +## 📖 Documentation Full documentation is available at the [project documentation site](https://grails-plugins.github.io/grails-server-timing/). This includes architecture details, the W3C specification, security considerations, and browser DevTools usage guides. -## Quick Start +## 🚀 Quick Start Add the dependency to your `build.gradle`: @@ -66,7 +66,7 @@ dependencies { > Snapshot versions are unstable and may change without notice. They are intended for testing > upcoming changes before a release. -## How It Works +## ❔ How It Works The plugin intercepts HTTP requests using a servlet filter and a Grails interceptor: @@ -87,7 +87,7 @@ Server-Timing: total;dur=156.3;desc="Total", action;dur=45.2;desc="Action", view | Controller with render (JSON, text) | `total`, `action` | | Static assets / other resources | `total`, `other` | -## Viewing in Browser DevTools +## 🌐 Viewing in Browser DevTools Open DevTools (F12), go to the **Network** tab, click a request, and select the **Timing** tab. Metrics appear under "Server Timing": @@ -96,7 +96,7 @@ appear under "Server Timing": - **Firefox** 61+ - **Safari** 16.4+ -## Configuration +## ⚙️ Configuration Configure in `application.yml` under `grails.plugins.servertiming`: @@ -121,13 +121,13 @@ environments: enabled: false ``` -## Compatibility +## 🤝 Compatibility | Plugin Version | Grails | Java | Groovy | |----------------|--------|------|--------| | 0.x | 7.0.x | 17+ | 4.0.x | -## Building from Source +## 🔨 Building from Source Prerequisites: [SDKMAN!](https://sdkman.io/) @@ -138,10 +138,10 @@ sdk env install # Install Java 17, Gradle 8.14, Groovy 4.0 See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development setup. -## Contributing +## 💡 Contributing Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request. -## License +## 📜 License This project is licensed under the [Apache License 2.0](LICENSE). From 18b05651c15006a382285b13fbbf24f9df16876a Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 15:54:44 +0100 Subject: [PATCH 14/17] build: update code coverage aggregation --- .../groovy/config.code-coverage-aggregate.gradle | 13 ++++++++++++- code-coverage/build.gradle | 11 ----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle index 595e563..d91766b 100644 --- a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle +++ b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle @@ -10,9 +10,20 @@ extensions.configure(JacocoPluginExtension) { // Configuration for declaring which projects contribute coverage data. def coverageDataProjects = configurations.register('coverageDataProjects') { canBeConsumed = false - canBeResolved = true + canBeResolved = false } +def aggregateProject = project +rootProject.subprojects { sub -> + sub.pluginManager.withPlugin('config.code-coverage') { + aggregateProject.dependencies.add( + 'coverageDataProjects', + project(sub.path) + ) + } +} + + // Lazily collect source directories and class files from all coverageDataProjects dependencies. def covProjectList = coverageDataProjects.map { it.dependencies.withType(ProjectDependency).collect { diff --git a/code-coverage/build.gradle b/code-coverage/build.gradle index f8adea9..4b062a8 100644 --- a/code-coverage/build.gradle +++ b/code-coverage/build.gradle @@ -1,14 +1,3 @@ plugins { id 'config.code-coverage-aggregate' } - -dependencies { - // The plugin project (always included) - coverageDataProjects project(':grails-server-timing') - - // Auto-discover all example apps under examples/ - rootDir.toPath().resolve('examples').toFile() - .listFiles({ it.directory } as FileFilter) - .each { coverageDataProjects project(":$it.name") - } -} From 6dafe3112a3199840092af9607e79e5aafb30f15 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 16:35:36 +0100 Subject: [PATCH 15/17] build: update code coverage aggregation --- .../config.code-coverage-aggregate.gradle | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle index d91766b..2f74414 100644 --- a/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle +++ b/build-logic/src/main/groovy/config.code-coverage-aggregate.gradle @@ -7,8 +7,7 @@ extensions.configure(JacocoPluginExtension) { it.toolVersion = jacocoVersion } -// Configuration for declaring which projects contribute coverage data. -def coverageDataProjects = configurations.register('coverageDataProjects') { +def aggregateConfiguration = configurations.register('aggregateConfiguration') { canBeConsumed = false canBeResolved = false } @@ -17,54 +16,37 @@ def aggregateProject = project rootProject.subprojects { sub -> sub.pluginManager.withPlugin('config.code-coverage') { aggregateProject.dependencies.add( - 'coverageDataProjects', - project(sub.path) + 'aggregateConfiguration', + aggregateProject.dependencies.project(path: sub.path) ) } } - -// Lazily collect source directories and class files from all coverageDataProjects dependencies. -def covProjectList = coverageDataProjects.map { - it.dependencies.withType(ProjectDependency).collect { - project.project(it.path) - } +def coverageProjects = aggregateConfiguration.map { + it.dependencies.withType(ProjectDependency).collect { project.project(it.path) } } -def allSourceDirs = covProjectList.map { - it.findAll { it.plugins.hasPlugin('java') } - .collectMany { - it.extensions.getByType(SourceSetContainer).named('main').get() +def allSourceDirs = coverageProjects.map { + it.findAll { it.plugins.hasPlugin(JavaPlugin) } + .collectMany { p -> + p.extensions.getByType(SourceSetContainer).named('main').get() .allSource.sourceDirectories.files } } -def allClassDirs = covProjectList.map { - it.findAll { it.plugins.hasPlugin('java') } - .collectMany { - it.extensions.getByType(SourceSetContainer).named('main').get() +def allClassDirs = coverageProjects.map { + it.findAll { it.plugins.hasPlugin(JavaPlugin) } + .collectMany { p -> + p.extensions.getByType(SourceSetContainer).named('main').get() .output.files } } -def allExecFiles = covProjectList.map { - it.collectMany { - it.fileTree(it.layout.buildDirectory.dir('jacoco')) { - include('**/*.exec') - }.files - } -} - -// Register the aggregated coverage report task. -// This merges JaCoCo execution data from all coverageDataProjects into a single report. -// Task dependencies on all Test tasks (test, integrationTest, etc.) in the declared -// projects are derived automatically — no hard-coded project paths needed. -tasks.register('jacocoAggregatedReport', JacocoReport) { +def jacocoAggregatedReport = tasks.register('jacocoAggregatedReport', JacocoReport) { description = 'Generates aggregated JaCoCo coverage report across all subprojects.' group = 'verification' classDirectories.from(allClassDirs) - executionData.from(allExecFiles) sourceDirectories.from(allSourceDirs) reports { @@ -74,22 +56,14 @@ tasks.register('jacocoAggregatedReport', JacocoReport) { } } -// After evaluation, wire dependsOn for every Test task in every coverage project. -// This ensures all .exec files exist before the aggregated report collects them. -afterEvaluate { - def projects = coverageDataProjects.get().dependencies - .withType(ProjectDependency) - .collect { project.project(it.path) } - - tasks.named('jacocoAggregatedReport') {reportTask -> - projects.each { - it.tasks.withType(Test).configureEach { testTask -> - reportTask.dependsOn(testTask) - } +coverageProjects.get().each { + it.tasks.withType(Test).configureEach { test -> + jacocoAggregatedReport.configure { JacocoReport report -> + report.executionData(test) } } } tasks.named('check') { - dependsOn('jacocoAggregatedReport') + dependsOn(jacocoAggregatedReport) } From 597bbbeb33dde79676dcd07375ec640c90749cdb Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 16:46:39 +0100 Subject: [PATCH 16/17] build: try and generalize publishing --- .../src/main/groovy/config.publish-root.gradle | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/build-logic/src/main/groovy/config.publish-root.gradle b/build-logic/src/main/groovy/config.publish-root.gradle index 65febe8..18fcdd9 100644 --- a/build-logic/src/main/groovy/config.publish-root.gradle +++ b/build-logic/src/main/groovy/config.publish-root.gradle @@ -4,13 +4,9 @@ version = projectVersion group = 'this.will.be.overridden' -def publishedProjects = [ - (project.name - '-root') -] - -subprojects { - if (name in publishedProjects) { +subprojects { sub -> + sub.pluginManager.withPlugin('config.publish') { // This has to be applied here in the root project due to the nexus plugin requirements - apply plugin: 'org.apache.grails.gradle.grails-publish' + sub.apply(plugin: 'org.apache.grails.gradle.grails-publish') } -} \ No newline at end of file +} From fc3f24381b61062ce25eda6e3cae2389362945be Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 24 Feb 2026 16:49:53 +0100 Subject: [PATCH 17/17] chore: add matrei as developer --- plugin/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/build.gradle b/plugin/build.gradle index 6da6ea7..1dbda13 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -35,5 +35,8 @@ extensions.configure(GrailsPublishExtension) { it.name = 'Grails Plugins' it.url = 'https://github.com/grails-plugins' } - it.developers = [jdaugherty: 'James Daugherty'] + it.developers = [ + jdaugherty: 'James Daugherty', + matrei: 'Mattias Reichel', + ] }