Repository: gradle/gradle
Pull request #33515: Add promo message at the end of the build if CC state isn't given
Author: login: mlopatkin
Created: 2025-05-16T08:35:52Z | Merged: 2025-05-23T14:05:05Z

The message is shown if the user doesn't explicitly enable or disable CC.
Enabling IP counts as explicitly enabling CC, but disabling IP by itself doesn't suppress the nudging.

The exact message is TBD.


Diff:
diff --git a/platforms/core-configuration/configuration-cache-base/src/main/kotlin/org/gradle/internal/cc/base/problems/IgnoringProblemsListener.kt b/platforms/core-configuration/configuration-cache-base/src/main/kotlin/org/gradle/internal/cc/base/problems/IgnoringProblemsListener.kt
new file mode 100644
index 0000000000000..137d0c98ea835
--- /dev/null
+++ b/platforms/core-configuration/configuration-cache-base/src/main/kotlin/org/gradle/internal/cc/base/problems/IgnoringProblemsListener.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.cc.base.problems
+
+import org.gradle.internal.configuration.problems.ProblemsListener
+import org.gradle.internal.configuration.problems.PropertyProblem
+import org.gradle.internal.configuration.problems.PropertyTrace
+import org.gradle.internal.configuration.problems.StructuredMessageBuilder
+import org.gradle.internal.service.scopes.Scope
+import org.gradle.internal.service.scopes.ServiceScope
+
+/**
+ * A stub implementation of ProblemsListener.
+ */
+@ServiceScope(Scope.BuildTree::class)
+object IgnoringProblemsListener : ProblemsListener {
+    override fun onProblem(problem: PropertyProblem) = Unit
+
+    override fun onError(trace: PropertyTrace, error: Exception, message: StructuredMessageBuilder) = Unit
+
+    override fun forIncompatibleTask(trace: PropertyTrace, reason: String): ProblemsListener = this
+}
diff --git a/platforms/core-configuration/configuration-cache/build.gradle.kts b/platforms/core-configuration/configuration-cache/build.gradle.kts
index 3f0a03f0f0e3c..f24a2b509e1d3 100644
--- a/platforms/core-configuration/configuration-cache/build.gradle.kts
+++ b/platforms/core-configuration/configuration-cache/build.gradle.kts
@@ -27,6 +27,7 @@ dependencies {
     api(projects.dependencyManagement)
     api(projects.fileTemp)
     api(projects.graphSerialization)
+    api(projects.logging)
     api(projects.loggingApi)
     api(projects.messaging)
     api(projects.native)
@@ -61,7 +62,6 @@ dependencies {
     implementation(projects.hashing)
     implementation(projects.inputTracking)
     implementation(projects.instrumentationAgentServices)
-    implementation(projects.logging)
     implementation(projects.modelCore)
     implementation(projects.persistentCache)
     implementation(projects.problemsApi)
diff --git a/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/ConfigurationCacheTestKitIntegrationTest.groovy b/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/ConfigurationCacheTestKitIntegrationTest.groovy
index 6e417648dcfe7..b4af08d668cb7 100644
--- a/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/ConfigurationCacheTestKitIntegrationTest.groovy
+++ b/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/ConfigurationCacheTestKitIntegrationTest.groovy
@@ -27,6 +27,8 @@ import org.gradle.util.internal.TextUtil
 import spock.lang.Issue
 import spock.lang.TempDir
 
+import static org.gradle.integtests.fixtures.logging.ConfigurationCacheOutputNormalizer.PROMO_PREFIX
+
 @Requires(UnitTestPreconditions.NotWindows)
 class ConfigurationCacheTestKitIntegrationTest extends AbstractConfigurationCacheIntegrationTest {
 
@@ -73,11 +75,13 @@ class ConfigurationCacheTestKitIntegrationTest extends AbstractConfigurationCach
         }
         runner.forwardOutput()
         runner.withProjectDir(testDirectory)
+        runner.withPluginClasspath([new File("some-dir")])
         result = runner.build()
         output = result.output
 
         then:
         !output.contains("configuration cache")
+        !output.contains(PROMO_PREFIX)
     }
 
     @Issue("https://github.com/gradle/gradle/issues/27956")
diff --git a/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/promo/ConfigurationCachePromoIntegrationTest.groovy b/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/promo/ConfigurationCachePromoIntegrationTest.groovy
new file mode 100644
index 0000000000000..c343114607d6c
--- /dev/null
+++ b/platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/promo/ConfigurationCachePromoIntegrationTest.groovy
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.cc.impl.promo
+
+import org.gradle.initialization.StartParameterBuildOptions.ConfigurationCacheOption
+import org.gradle.initialization.StartParameterBuildOptions.IsolatedProjectsOption
+import org.gradle.internal.cc.impl.AbstractConfigurationCacheIntegrationTest
+
+import static org.gradle.integtests.fixtures.logging.ConfigurationCacheOutputNormalizer.PROMO_PREFIX
+
+class ConfigurationCachePromoIntegrationTest extends AbstractConfigurationCacheIntegrationTest {
+    def "shows promo message when build succeeds without giving explicit CC state"() {
+        given:
+        buildFile """
+            tasks.register("greet") { doLast { println("Hello") } }
+        """
+
+        when:
+        run("greet")
+
+        then:
+        postBuildOutputContains(PROMO_PREFIX)
+    }
+
+    def "shows promo message when build fails without giving explicit CC state"() {
+        given:
+        buildFile """
+            tasks.register("fail") { doLast { throw new UnsupportedOperationException("I must fail") } }
+        """
+
+        when:
+        fails("fail")
+
+        then:
+        // TODO(mlopatkin) post-build output scraping is broken for failed builds
+        outputContains(PROMO_PREFIX)
+    }
+
+    def "shows promo message when running with isolated projects disabled in command-line"() {
+        given:
+        buildFile """
+            tasks.register("greet") { doLast { println("Hello") } }
+        """
+
+        when:
+        run("greet", "-D${IsolatedProjectsOption.PROPERTY_NAME}=false")
+
+        then:
+        postBuildOutputContains(PROMO_PREFIX)
+    }
+
+    def "shows promo message when running with isolated projects disabled in properties files"() {
+        given:
+        buildFile """
+            tasks.register("greet") { doLast { println("Hello") } }
+        """
+
+        propertiesFile.text = "${IsolatedProjectsOption.PROPERTY_NAME}=false"
+
+        when:
+        run("greet")
+
+        then:
+        postBuildOutputContains(PROMO_PREFIX)
+    }
+
+    def "shows no promo message when #ccSwitch is given in command-line"() {
+        given:
+        buildFile """
+            tasks.register("greet") { doLast { println("Hello") } }
+        """
+
+        when:
+        run("greet", ccSwitch)
+
+        then:
+        postBuildOutputDoesNotContain(PROMO_PREFIX)
+
+        where:
+        ccSwitch << [
+            "--${ConfigurationCacheOption.LONG_OPTION}",
+            "--no-${ConfigurationCacheOption.LONG_OPTION}",
+            "-D${ConfigurationCacheOption.DEPRECATED_PROPERTY_NAME}=false",
+            "-D${ConfigurationCacheOption.DEPRECATED_PROPERTY_NAME}=true",
+            "-D${ConfigurationCacheOption.PROPERTY_NAME}=false",
+            "-D${ConfigurationCacheOption.PROPERTY_NAME}=true",
+            "-D${IsolatedProjectsOption.PROPERTY_NAME}=true",
+        ]
+    }
+
+    def "shows no promo message when #ccStateLine is given in properties file"() {
+        given:
+        buildFile """
+            tasks.register("greet") { doLast { println("Hello") } }
+        """
+
+        propertiesFile.text = ccStateLine
+
+        when:
+        run("greet")
+
+        then:
+        postBuildOutputDoesNotContain(PROMO_PREFIX)
+
+        where:
+        ccStateLine << [
+            "${ConfigurationCacheOption.PROPERTY_NAME}=false",
+            "${ConfigurationCacheOption.PROPERTY_NAME}=true",
+            "${IsolatedProjectsOption.PROPERTY_NAME}=true",
+        ]
+    }
+
+    def "shows no promo message if configuration is not cc compatible"() {
+        given:
+        buildFile """
+            gradle.taskGraph.beforeTask {
+                println("I break CC contract")
+            }
+
+            tasks.register("greet") { doLast { println("Hello") } }
+        """
+
+        executer.noDeprecationChecks()
+
+        when:
+        run("greet")
+
+        then:
+        postBuildOutputDoesNotContain(PROMO_PREFIX)
+    }
+
+    def "shows no promo message if execution is not cc compatible"() {
+        given:
+        buildFile """
+            tasks.register("greet") {
+                doLast { task ->
+                    println("Hello from " + task.project.name)
+                }
+            }
+        """
+
+        executer.noDeprecationChecks()
+
+        when:
+        run("greet")
+
+        then:
+        postBuildOutputDoesNotContain(PROMO_PREFIX)
+    }
+
+    def "shows promo message if configuration-only calls are used correctly"() {
+        given:
+        buildFile """
+            tasks.register("greet") { task ->
+                def myName = task.project.name
+                doLast {
+                    println("Hello from " + myName)
+                }
+            }
+        """
+
+        when:
+        run("greet")
+
+        then:
+        postBuildOutputContains(PROMO_PREFIX)
+    }
+}
diff --git a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
index c46a13e67ceae..a6f8698516107 100644
--- a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
+++ b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
@@ -39,6 +39,7 @@ import org.gradle.internal.buildtree.DefaultBuildTreeModelSideEffectExecutor
 import org.gradle.internal.buildtree.DefaultBuildTreeWorkGraphPreparer
 import org.gradle.internal.buildtree.RunTasksRequirements
 import org.gradle.internal.cc.base.logger
+import org.gradle.internal.cc.base.problems.IgnoringProblemsListener
 import org.gradle.internal.cc.base.services.ConfigurationCacheEnvironmentChangeTracker
 import org.gradle.internal.cc.impl.fingerprint.ConfigurationCacheFingerprintController
 import org.gradle.internal.cc.impl.initialization.ConfigurationCacheInjectedClasspathInstrumentationStrategy
@@ -49,6 +50,7 @@ import org.gradle.internal.cc.impl.initialization.InstrumentedExecutionAccessLis
 import org.gradle.internal.cc.impl.initialization.VintageInjectedClasspathInstrumentationStrategy
 import org.gradle.internal.cc.impl.models.DefaultToolingModelParameterCarrierFactory
 import org.gradle.internal.cc.impl.problems.ConfigurationCacheProblems
+import org.gradle.internal.cc.impl.promo.ConfigurationCachePromoHandler
 import org.gradle.internal.cc.impl.services.ConfigurationCacheBuildTreeModelSideEffectExecutor
 import org.gradle.internal.cc.impl.services.DefaultBuildModelParameters
 import org.gradle.internal.cc.impl.services.DefaultDeferredRootBuildGradle
@@ -238,14 +240,24 @@ class DefaultBuildTreeModelControllerServices : BuildTreeModelControllerServices
         // This was originally only for the configuration cache, but now used for configuration cache and problems reporting
         registration.add(ProblemFactory::class.java, DefaultProblemFactory::class.java)
 
+        registration.add(ConfigurationCacheProblemsListener::class.java, DefaultConfigurationCacheProblemsListener::class.java)
+        // Set up CC problem reporting pipeline and promo, based on the build configuration
+        when {
+            // Collect and report problems. Don't suggest enabling CC if it is on, even if implicitly (e.g. enabled by isolated projects).
+            // Most likely, the user who tries IP is already aware of CC and nudging will be just noise.
+            modelParameters.isConfigurationCache -> registration.add(ConfigurationCacheProblems::class.java)
+            // Allow nudging to enable CC if it is off and there is no explicit decision.
+            !requirements.startParameter.configurationCache.isExplicit -> registration.add(ConfigurationCachePromoHandler::class.java)
+            // Do not nudge if CC is explicitly disabled.
+            else -> registration.add(IgnoringProblemsListener::class.java, IgnoringProblemsListener)
+        }
+
         if (modelParameters.isConfigurationCache) {
             registration.add(BuildTreeLifecycleControllerFactory::class.java, ConfigurationCacheBuildTreeLifecycleControllerFactory::class.java)
             registration.add(ConfigurationCacheStartParameter::class.java)
             registration.add(ConfigurationCacheClassLoaderScopeRegistryListener::class.java)
             registration.add(InjectedClasspathInstrumentationStrategy::class.java, ConfigurationCacheInjectedClasspathInstrumentationStrategy::class.java)
             registration.add(ConfigurationCacheEnvironmentChangeTracker::class.java)
-            registration.add(ConfigurationCacheProblemsListener::class.java, DefaultConfigurationCacheProblemsListener::class.java)
-            registration.add(ConfigurationCacheProblems::class.java)
             registration.add(BuildTreeConfigurationCache::class.java, DefaultConfigurationCache::class.java)
             registration.add(InstrumentedExecutionAccessListenerRegistry::class.java)
             registration.add(ConfigurationCacheFingerprintController::class.java)
diff --git a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/AbstractInjectedClasspathInstrumentationStrategy.kt b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/AbstractInjectedClasspathInstrumentationStrategy.kt
index 66f6dfa74707c..3cdb6b72b6665 100644
--- a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/AbstractInjectedClasspathInstrumentationStrategy.kt
+++ b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/AbstractInjectedClasspathInstrumentationStrategy.kt
@@ -16,23 +16,41 @@
 
 package org.gradle.internal.cc.impl.initialization
 
+import org.gradle.internal.configuration.problems.DocumentationSection
+import org.gradle.internal.configuration.problems.ProblemsListener
+import org.gradle.internal.configuration.problems.PropertyProblem
+import org.gradle.internal.configuration.problems.PropertyTrace
+import org.gradle.internal.configuration.problems.StructuredMessage
 import org.gradle.internal.instrumentation.agent.AgentUtils
 import org.gradle.plugin.use.resolve.service.internal.InjectedClasspathInstrumentationStrategy
 import org.gradle.plugin.use.resolve.service.internal.InjectedClasspathInstrumentationStrategy.TransformMode
 import java.lang.management.ManagementFactory
 
 
-abstract class AbstractInjectedClasspathInstrumentationStrategy : InjectedClasspathInstrumentationStrategy {
+abstract class AbstractInjectedClasspathInstrumentationStrategy(
+    private val problems: ProblemsListener
+) : InjectedClasspathInstrumentationStrategy {
     override fun getTransform(): TransformMode {
         val isThirdPartyAgentPresent = ManagementFactory.getRuntimeMXBean().inputArguments.find { AgentUtils.isThirdPartyJavaAgentSwitch(it) } != null
         return if (isThirdPartyAgentPresent) {
+            reportThirdPartyAgentPresent()
             // Currently, the build logic instrumentation can interfere with Java agents, such as Jacoco
-            // So, disable or fail or whatever based on which execution modes are enabled
+            // So, ignore, disable or fail or whatever based on which execution modes are enabled
             whenThirdPartyAgentPresent()
         } else {
             TransformMode.BUILD_LOGIC
         }
     }
 
+    private fun reportThirdPartyAgentPresent() {
+        problems.onProblem(
+            PropertyProblem(
+                PropertyTrace.Gradle,
+                StructuredMessage.build { text("support for using a Java agent with TestKit builds is not yet implemented with the configuration cache.") },
+                documentationSection = DocumentationSection.NotYetImplementedTestKitJavaAgent
+            )
+        )
+    }
+
     abstract fun whenThirdPartyAgentPresent(): TransformMode
 }
diff --git a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/ConfigurationCacheInjectedClasspathInstrumentationStrategy.kt b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/ConfigurationCacheInjectedClasspathInstrumentationStrategy.kt
index 5931658c0b4cd..5717110e7376d 100644
--- a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/ConfigurationCacheInjectedClasspathInstrumentationStrategy.kt
+++ b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/ConfigurationCacheInjectedClasspathInstrumentationStrategy.kt
@@ -16,26 +16,14 @@
 
 package org.gradle.internal.cc.impl.initialization
 
-import org.gradle.internal.configuration.problems.DocumentationSection
 import org.gradle.internal.configuration.problems.ProblemsListener
-import org.gradle.internal.configuration.problems.PropertyProblem
-import org.gradle.internal.configuration.problems.PropertyTrace
-import org.gradle.internal.configuration.problems.StructuredMessage
 import org.gradle.plugin.use.resolve.service.internal.InjectedClasspathInstrumentationStrategy.TransformMode
 
-
 class ConfigurationCacheInjectedClasspathInstrumentationStrategy(
-    private val problems: ProblemsListener
-) : AbstractInjectedClasspathInstrumentationStrategy() {
+    problems: ProblemsListener
+) : AbstractInjectedClasspathInstrumentationStrategy(problems) {
     override fun whenThirdPartyAgentPresent(): TransformMode {
-        // Report a problem and instrument anyway
-        problems.onProblem(
-            PropertyProblem(
-                PropertyTrace.Gradle,
-                StructuredMessage.build { text("support for using a Java agent with TestKit builds is not yet implemented with the configuration cache.") },
-                documentationSection = DocumentationSection.NotYetImplementedTestKitJavaAgent
-            )
-        )
+        // Instrument anyway, the emitted problem will cause the build to fail.
         return TransformMode.BUILD_LOGIC
     }
 }
diff --git a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/VintageInjectedClasspathInstrumentationStrategy.kt b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/VintageInjectedClasspathInstrumentationStrategy.kt
index 826b661df15d0..f14b68f55ad0e 100644
--- a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/VintageInjectedClasspathInstrumentationStrategy.kt
+++ b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/initialization/VintageInjectedClasspathInstrumentationStrategy.kt
@@ -16,10 +16,13 @@
 
 package org.gradle.internal.cc.impl.initialization
 
+import org.gradle.internal.configuration.problems.ProblemsListener
 import org.gradle.plugin.use.resolve.service.internal.InjectedClasspathInstrumentationStrategy.TransformMode
 
 
-class VintageInjectedClasspathInstrumentationStrategy : AbstractInjectedClasspathInstrumentationStrategy() {
+class VintageInjectedClasspathInstrumentationStrategy(
+    problems: ProblemsListener
+) : AbstractInjectedClasspathInstrumentationStrategy(problems) {
     override fun whenThirdPartyAgentPresent(): TransformMode {
         // For now, disable the instrumentation
         return TransformMode.NONE
diff --git a/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/promo/ConfigurationCachePromoHandler.kt b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/promo/ConfigurationCachePromoHandler.kt
new file mode 100644
index 0000000000000..f8cf030b59655
--- /dev/null
+++ b/platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/promo/ConfigurationCachePromoHandler.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.internal.cc.impl.promo
+
+import org.gradle.api.internal.DocumentationRegistry
+import org.gradle.api.logging.Logging
+import org.gradle.initialization.RootBuildLifecycleListener
+import org.gradle.internal.configuration.problems.ProblemsListener
+import org.gradle.internal.configuration.problems.PropertyProblem
+import org.gradle.internal.configuration.problems.PropertyTrace
+import org.gradle.internal.configuration.problems.StructuredMessageBuilder
+import org.gradle.internal.service.scopes.Scope
+import org.gradle.internal.service.scopes.ServiceScope
+
+/**
+ * This class handles configuration cache promo message at the end of the build.
+ *
+ * Instances of this class are thread-safe.
+ */
+@ServiceScope(Scope.BuildTree::class)
+class ConfigurationCachePromoHandler(
+    private val documentationRegistry: DocumentationRegistry
+) : RootBuildLifecycleListener, ProblemsListener {
+    @Volatile
+    private var hasProblems = false
+
+    override fun afterStart() = Unit
+
+    override fun beforeComplete() {
+        if (hasProblems) {
+            return
+        }
+
+        val docUrl = documentationRegistry.getDocumentationFor("configuration_cache", "config_cache:usage")
+        // TODO(mlopatkin): finalize the message
+        Logging.getLogger(ConfigurationCachePromoHandler::class.java).lifecycle("Consider enabling configuration cache to speed up this build: $docUrl")
+    }
+
+    override fun onProblem(problem: PropertyProblem) {
+        hasProblems = true
+    }
+
+    override fun onError(trace: PropertyTrace, error: Exception, message: StructuredMessageBuilder) {
+        hasProblems = true
+    }
+
+    override fun forIncompatibleTask(trace: PropertyTrace, reason: String): ProblemsListener = this
+}
diff --git a/platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt b/platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt
index 7863ca8c75732..afbf467ddbfcb 100644
--- a/platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt
+++ b/platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt
@@ -16,11 +16,7 @@
 
 package org.gradle.internal.configuration.problems
 
-import org.gradle.internal.service.scopes.EventScope
-import org.gradle.internal.service.scopes.Scope
 
-
-@EventScope(Scope.BuildTree::class)
 interface ProblemsListener {
 
     fun onProblem(problem: PropertyProblem)
diff --git a/testing/internal-integ-testing/src/main/groovy/org/gradle/integtests/fixtures/logging/ConfigurationCacheOutputNormalizer.groovy b/testing/internal-integ-testing/src/main/groovy/org/gradle/integtests/fixtures/logging/ConfigurationCacheOutputNormalizer.groovy
index ca74cabb5898a..d934dca244925 100644
--- a/testing/internal-integ-testing/src/main/groovy/org/gradle/integtests/fixtures/logging/ConfigurationCacheOutputNormalizer.groovy
+++ b/testing/internal-integ-testing/src/main/groovy/org/gradle/integtests/fixtures/logging/ConfigurationCacheOutputNormalizer.groovy
@@ -20,11 +20,16 @@ import org.gradle.exemplar.executor.ExecutionMetadata
 import org.gradle.exemplar.test.normalizer.OutputNormalizer
 
 class ConfigurationCacheOutputNormalizer implements OutputNormalizer {
+    public static final String PROMO_PREFIX = "Consider enabling configuration cache to speed up this build: "
+
     @Override
     String normalize(String output, ExecutionMetadata executionMetadata) {
         return output.replaceAll(
             "configuration-cache/.*/configuration-cache-report",
             "configuration-cache/<hash>/configuration-cache-report"
+        ).replaceAll(
+            "(?m)^${PROMO_PREFIX}.*\$\n?", // Remove CC Promo Line from the output. Note the multiline flag to match line boundaries within a multiline string.
+            ""
         )
     }
 }


Reviews:
user: login: 6hundreds
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T09:40:58Z
user: login: 6hundreds
body: LGTM 👍 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-16T09:45:12Z
user: login: mlopatkin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T18:11:02Z
user: login: abstratt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T14:11:28Z
user: login: abstratt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T14:15:47Z
user: login: abstratt
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T14:21:31Z
user: login: mlopatkin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T16:28:58Z
user: login: mlopatkin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T12:08:28Z
user: login: alllex
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-06-04T13:48:56Z

Review comments:
diff_hunk: @@ -265,6 +268,19 @@ class DefaultBuildTreeModelControllerServices : BuildTreeModelControllerServices
         } else {
             registration.addProvider(VintageModelProvider())
         }
+
+        if (allowNudgingToEnableConfigurationCache(requirements, modelParameters)) {
+            // Don't suggest enabling CC if it is on, even if implicitly (e.g. enabled by isolated projects).
+            // Most likely, the user who tries IP is already aware of CC and nudging will be just noise.
+            registration.add(ConfigurationCachePromoHandler::class.java)
+        } else if (!modelParameters.isConfigurationCache) {
path: platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
user: login: 6hundreds
body: 🤔 Will any `ProblemListener` instance be added in the DI graph in case of mismatch this `else if` condition?

`DefaultConfigurationCacheProblemsListener` requires `ProblemListener` service be injected. 
- For CC we have `ConfigurationCacheProblems `
- For nudging we have `ConfigurationCachePromoHandler `
- For noCC we have `IgnoringProblemsListener`

Having written this, I see exhaustiveness in `CC + noCC` branches. Initially I thought it would be a little more safe to have just `else` in this line to have some `ProblemListener` implementation injected anyways

created_at: 2025-05-16T09:40:58Z
author_association: MEMBER
diff_hunk: @@ -265,6 +268,19 @@ class DefaultBuildTreeModelControllerServices : BuildTreeModelControllerServices
         } else {
             registration.addProvider(VintageModelProvider())
         }
+
+        if (allowNudgingToEnableConfigurationCache(requirements, modelParameters)) {
+            // Don't suggest enabling CC if it is on, even if implicitly (e.g. enabled by isolated projects).
+            // Most likely, the user who tries IP is already aware of CC and nudging will be just noise.
+            registration.add(ConfigurationCachePromoHandler::class.java)
+        } else if (!modelParameters.isConfigurationCache) {
path: platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
user: login: mlopatkin
body: > Having written this, I see exhaustiveness in `CC + noCC` branches. Initially I thought it would be a little more safe to have just `else` in this line to have some `ProblemListener` implementation injected anyways

Thanks, I've adjusted the registration code to select one of three in a more explicit fashion.
created_at: 2025-05-16T18:11:02Z
author_association: MEMBER
diff_hunk: @@ -73,11 +75,13 @@ class ConfigurationCacheTestKitIntegrationTest extends AbstractConfigurationCach
         }
         runner.forwardOutput()
         runner.withProjectDir(testDirectory)
+        runner.withPluginClasspath([new File("some-dir")])
         result = runner.build()
         output = result.output
 
         then:
         !output.contains("configuration cache")
+        !output.contains(PROMO_PREFIX)
path: platforms/core-configuration/configuration-cache/src/integTest/groovy/org/gradle/internal/cc/impl/ConfigurationCacheTestKitIntegrationTest.groovy
user: login: abstratt
body: 💭 

Maybe add a note here explaining why it is not expected 

```
// No CC promo when build has CC problems
```
created_at: 2025-05-22T14:11:28Z
author_association: MEMBER
diff_hunk: @@ -238,14 +240,24 @@ class DefaultBuildTreeModelControllerServices : BuildTreeModelControllerServices
         // This was originally only for the configuration cache, but now used for configuration cache and problems reporting
         registration.add(ProblemFactory::class.java, DefaultProblemFactory::class.java)
 
+        registration.add(ConfigurationCacheProblemsListener::class.java, DefaultConfigurationCacheProblemsListener::class.java)
+        // Set up CC problem reporting pipeline, based on the build configuration
+        when {
+            modelParameters.isConfigurationCache -> registration.add(ConfigurationCacheProblems::class.java)
+            allowNudgingToEnableConfigurationCache(requirements, modelParameters) -> {
+                // Don't suggest enabling CC if it is on, even if implicitly (e.g. enabled by isolated projects).
path: platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
user: login: abstratt
body: 💅 

nitpicking, but do we need to block here? This helpful comment could go before the alternative (between lines 246 and 247), or between the arrow and `registration.add(ConfigurationCachePromoHandler::class.java)`? 
created_at: 2025-05-22T14:15:46Z
author_association: MEMBER
diff_hunk: @@ -16,11 +16,7 @@
 
 package org.gradle.internal.configuration.problems
 
-import org.gradle.internal.service.scopes.EventScope
-import org.gradle.internal.service.scopes.Scope
 
-
-@EventScope(Scope.BuildTree::class)
path: platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt
user: login: abstratt
body: ❓ 

For my education, does this mean we no longer annotate the service interfaces, only the implementations? I thought it was useful to have the interfaces annotated (unless implementations varied in scope)
created_at: 2025-05-22T14:21:31Z
author_association: MEMBER
diff_hunk: @@ -16,11 +16,7 @@
 
 package org.gradle.internal.configuration.problems
 
-import org.gradle.internal.service.scopes.EventScope
-import org.gradle.internal.service.scopes.Scope
 
-
-@EventScope(Scope.BuildTree::class)
path: platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt
user: login: mlopatkin
body: > ❓
> 
> For my education, does this mean we no longer annotate the service interfaces, only the implementations? I thought it was useful to have the interfaces annotated (unless implementations varied in scope)

I decided to let existing checks to guide me. Our interface-to-implementation bindings are not mandatory, it seems, and this particular interface serves two purposes: it is a service used by CC machinery, but can be also used as part of serialization and can even have some non-DI-owned implementation. I've tried to untie these two, but stopped to keep this PR focused.
created_at: 2025-05-22T16:28:58Z
author_association: MEMBER
diff_hunk: @@ -238,14 +240,24 @@ class DefaultBuildTreeModelControllerServices : BuildTreeModelControllerServices
         // This was originally only for the configuration cache, but now used for configuration cache and problems reporting
         registration.add(ProblemFactory::class.java, DefaultProblemFactory::class.java)
 
+        registration.add(ConfigurationCacheProblemsListener::class.java, DefaultConfigurationCacheProblemsListener::class.java)
+        // Set up CC problem reporting pipeline, based on the build configuration
+        when {
+            modelParameters.isConfigurationCache -> registration.add(ConfigurationCacheProblems::class.java)
+            allowNudgingToEnableConfigurationCache(requirements, modelParameters) -> {
+                // Don't suggest enabling CC if it is on, even if implicitly (e.g. enabled by isolated projects).
path: platforms/core-configuration/configuration-cache/src/main/kotlin/org/gradle/internal/cc/impl/DefaultBuildTreeModelControllerServices.kt
user: login: mlopatkin
body: > 💅
> 
> nitpicking, but do we need to block here? This helpful comment could go before the alternative (between lines 246 and 247), or between the arrow and `registration.add(ConfigurationCachePromoHandler::class.java)`?

Looking closely into this, I realized that the comment doesn't make much sense in its current place. I've changed the structure a bit.
created_at: 2025-05-23T12:08:28Z
author_association: MEMBER
diff_hunk: @@ -16,11 +16,7 @@
 
 package org.gradle.internal.configuration.problems
 
-import org.gradle.internal.service.scopes.EventScope
-import org.gradle.internal.service.scopes.Scope
 
-
-@EventScope(Scope.BuildTree::class)
path: platforms/core-configuration/configuration-problems-base/src/main/kotlin/org/gradle/internal/configuration/problems/ProblemsListener.kt
user: login: alllex
body: > Our interface-to-implementation bindings are not mandatory

It's not mandatory, because there are many places where legacy bindings were not converted yet. But in general the goal is to annotate interfaces as services, and not implementation.

Note, however, that what is removed is **not a `@ServiceScope`** annotation, but `@EventScope`. I don't recall from the top of my head what the latter does exactly. But it's a concern of the `ListenerManager` and not the general service scope handling.

Given that this PR **replaces** `@EventScope` with `@ServiceScope` on implementations, I'm a bit concerned we didn't see any test failures 🤔 
created_at: 2025-06-04T13:48:56Z
author_association: MEMBER

PR comments:
user: login: mlopatkin
created_at: 2025-05-16T18:11:11Z
body: @bot-gradle test this
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-05-16T18:11:15Z
body: <!-- {"replyTargetCommentId":2887383812,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_Stage_PullRequestFeedback_Trigger","buildId":"98289787","headRef":"8a4f7db358158378d7b581e4d57496bbd806bf5b"}]} -->
I've triggered the following builds:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/build/98289787)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=8a4f7db358158378d7b581e4d57496bbd806bf5b,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-05-16T19:07:35Z
body: The following builds have failed:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_Stage_PullRequestFeedback_Trigger/98289787)
  - [Build Scan](https://ge.gradle.org/scans?search.buildOutcome=failure&amp;search.tags=CI&amp;search.names=gitCommitId&amp;search.values=8a4f7db358158378d7b581e4d57496bbd806bf5b&amp;)

author_association: COLLABORATOR
user: login: mlopatkin
created_at: 2025-05-16T19:47:29Z
body: @bot-gradle test this
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-05-16T19:47:33Z
body: <!-- {"replyTargetCommentId":2887551911,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_Stage_PullRequestFeedback_Trigger","buildId":"98290742","headRef":"0290658dddb6260cfe61e5dc68a6ded44f3707a5"}]} -->
I've triggered the following builds:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/build/98290742)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=0290658dddb6260cfe61e5dc68a6ded44f3707a5,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-05-16T20:33:15Z
body: The following builds have passed:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_Stage_PullRequestFeedback_Trigger/98290742)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=0290658dddb6260cfe61e5dc68a6ded44f3707a5,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: mlopatkin
created_at: 2025-05-19T11:47:36Z
body: @bot-gradle test APT
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-05-19T11:47:42Z
body: <!-- {"replyTargetCommentId":2890713886,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_AllPerformanceTests_Trigger","buildId":"98322639","headRef":"0290658dddb6260cfe61e5dc68a6ded44f3707a5"}]} -->
I've triggered the following builds:
- [All Performance Tests (Trigger)](https://builds.gradle.org/build/98322639)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=0290658dddb6260cfe61e5dc68a6ded44f3707a5,ReadyforRelease&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-05-19T12:53:05Z
body: The following builds have passed:
- [All Performance Tests (Trigger)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_AllPerformanceTests_Trigger/98322639)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=0290658dddb6260cfe61e5dc68a6ded44f3707a5,ReadyforRelease&amp;)

author_association: COLLABORATOR
user: login: mlopatkin
created_at: 2025-05-23T12:09:17Z
body: @bot-gradle test this
author_association: MEMBER
user: login: bot-gradle
created_at: 2025-05-23T12:09:24Z
body: <!-- {"replyTargetCommentId":2904216280,"triggeredTeamCityBuilds":[{"buildConfigurationId":"Gradle_Master_Check_Stage_PullRequestFeedback_Trigger","buildId":"98485694","headRef":"3f86ff1304779352a8b8a34d06cb65123e3c4dc4"}]} -->
I've triggered the following builds:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/build/98485694)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=3f86ff1304779352a8b8a34d06cb65123e3c4dc4,PullRequestFeedback&amp;)

author_association: COLLABORATOR
user: login: bot-gradle
created_at: 2025-05-23T13:09:57Z
body: The following builds have passed:
- [Pull Request Feedback (Trigger) (Check)](https://builds.gradle.org/buildConfiguration/Gradle_Master_Check_Stage_PullRequestFeedback_Trigger/98485694)
  - [Build Scan](https://ge.gradle.org/scans?search.tags=CI&amp;search.names=gitCommitId,PartOf&amp;search.values=3f86ff1304779352a8b8a34d06cb65123e3c4dc4,PullRequestFeedback&amp;)

author_association: COLLABORATOR