When trying to upgrade one of our more complex gra...
# community-support
n
When trying to upgrade one of our more complex gradle projects to gradle 9.+ (from 8.14.3), I'm seeing the following error coming from the
io.qameta.allure
plugin:
Copy code
org.gradle.api.internal.tasks.TaskDependencyResolveException: Could not determine the dependencies of task ':selenium-tests:check'.	
... SNIP ...
Caused by: org.gradle.api.internal.tasks.DefaultTaskContainer$TaskCreationException: Could not create task ':selenium-tests:test'.
... SNIP ...
Caused by: org.gradle.api.InvalidUserCodeException: Cannot mutate the artifacts of configuration ':selenium-tests:allureRawResultElements' after the configuration was consumed as a variant. After a configuration has been observed, it should not be modified.
at org.gradle.api.internal.artifacts.configurations.DefaultConfiguration.validateMutation(DefaultConfiguration.java:1229)
at org.gradle.api.internal.artifacts.configurations.DefaultConfiguration.lambda$validateMutationType$0(DefaultConfiguration.java:319)
at org.gradle.internal.ImmutableActionSet$SingletonSet.execute(ImmutableActionSet.java:226)
at org.gradle.api.internal.DefaultDomainObjectCollection.assertCanMutate(DefaultDomainObjectCollection.java:453)
at org.gradle.api.internal.DefaultDomainObjectCollection.add(DefaultDomainObjectCollection.java:273)
at org.gradle.api.internal.DelegatingDomainObjectSet.add(DelegatingDomainObjectSet.java:115)
at org.gradle.api.internal.artifacts.configurations.DefaultConfigurationPublications.artifact(DefaultConfigurationPublications.java:137)
at io.qameta.allure.gradle.adapter.AllureAdapterExtension.exposeArtifact(AllureAdapterExtension.kt:167)
at io.qameta.allure.gradle.adapter.AllureAdapterExtension.access$exposeArtifact(AllureAdapterExtension.kt:31)
at io.qameta.allure.gradle.adapter.AllureAdapterExtension$gatherResultsFrom$1.execute(AllureAdapterExtension.kt:104)
at io.qameta.allure.gradle.adapter.AllureAdapterExtension$gatherResultsFrom$1.execute(AllureAdapterExtension.kt:102)
It seems to be a negative interaction with multiple other plugins. So far discovered the issue popping up with
jacoco-report-aggregation
and
com.autonomousapps.build-health
. It goes away when I make sure not to have the failing module be affected by those 2 plugins. Unfortunately, it doesn't happen in a trivial setup, so I'm struggling to figure out the root cause. For reference, the code that triggers the error in the allure plugin: https://github.com/allure-framework/allure-gradle/blob/main/allure-adapter-plugin/[…]otlin/io/qameta/allure/gradle/adapter/AllureAdapterExtension.kt Is there an easy way to figure out what's causing a configuration to be consumed as a variant? And is the allure plugin actually doing something wrong here or should I assume it's our configuration that's wrong?
1
v
Is there an easy way to figure out what's causing a configuration to be consumed as a variant?
Well, it usually means some consumer has it in a configuration that was resolved. I guess the "easiest" way would be to set a breakpoint to the modification of the flag that marks the configuration as immutable which then later causes the failure when the configuration is tried to be modified. Most often the actual problem is, that something somewhere prematurely resolves a configuration during configuration time which should be avoided as much as possible to prevent problems like this.
And is the allure plugin actually doing something wrong here or should I assume it's our configuration that's wrong?
Hard to say from a quick look. It indeed does questionable things like in
gatherResultsFrom(task: Task)
doing
project.tasks.named(task.name)
instead of directly using the
Task
already available, or similar in
gatherResultsFrom(tasks: TaskCollection<out Task>)
, and there is also a usage of
afterEvaluate
that of course has the typical problem but might not be relevant in this concrete case. The code that complains is triggered by a call in the extension, so theoretically it could also be the problem that you or whoever calls that method too late like at execution phase, but I don't think that is the case from the message details, but you could also place a breakpoint there to see where it is coming from, or read in the stacktrace from where you cut it off. But as said, usually the problem is that somewhere a configuration is resolved at configuration time.
n
Most often the actual problem is, that something somewhere prematurely resolves a configuration during configuration time which should be avoided as much as possible to prevent problems like this.
That's kind of what I was assuming was happening, but I just can't seem to figure out how/where.
I guess the "easiest" way would be to set a breakpoint to the modification of the flag that marks the configuration as immutable which then later causes the failure when the configuration is tried to be modified.
Great suggestion, thank you.
It seems like mutating artifacts is just not allowed, regardless: https://github.com/gradle/gradle/blame/master/platforms/software/dependency-manage[…]api/internal/artifacts/configurations/DefaultConfiguration.java The entire logic of configuration mutation tracking was rewritten in this PR: https://github.com/gradle/gradle/pull/32903 Due to the complexity of that PR, it's not really clear to me if the behavior has changed regarding the mutation of artifacts 🤔
I had IntelliJ print a message when it hit that linked line
name + " - non-dependency state is never mutable: " + type
And I got:
allureRawResultElements - non-dependency state is never mutable: artifacts
. But that means it's not in fact related to early resolution of the configuration, right? So I'm a bit confused about how those other plugins would trigger this to explode 🤷
v
It seems like mutating artifacts is just not allowed, regardless:
If there is an observation reason, meaning it took part in depenedency resolution. If there is no observation reason and thus was not observed at all yet, the first
if
already returns
false
immediately.
That method is checking whether mutation is allowed, you need to break on setting the observation reason
Or you again are just looking at the symptom, not the problem
n
Ah, of course, I was wrongly looking at
dependenciesObserved
🤦
Ok, so I see that the
aggregateCodeCoverageReportResults
configuration is the one consuming the
allureRawResultElements
when configuring the
testCodeCoverageReport
task. But that seems correct to me, as that's the whole point of that configuration. Clearly stated by its description: > The configuration exposes Allure raw results (simple-result.json, executor.json) for reporting So the question remains if there's a change in behavior from gradle wrt the mutation of artifacts on a "consumed" configuration 🤔
v
Well, yes, of course. 🙂
In 8.14.3 you get a deprecation warning that you ignored.
Not in 9.0.0 it turned into a hard error like in most cases with Gradle deprecations
The deprecation warning is
Mutating a configuration after it has been resolved, consumed as a variant, or used for generating published metadata. This behavior has been deprecated. This will fail with an error in Gradle 9.0. The artifacts of configuration 'selenium testsallureRawResultElements' were mutated after the configuration was consumed as a variant. After a configuration has been observed, it should not be modified. Consult the upgrading guide for further information: https://docs.gradle.org/8.14.3/userguide/upgrading_version_8.html#mutate_configuration_after_locking
But still, the problem is purely caused by resolving
aggregateCodeCoverageReportResults
prematurely (if that one is the culprit)
This is the whole situation stripped down to an MCVE:
Copy code
val foo by sourceSets.registering
java {
    registerFeature("foo") {
        usingSourceSet(foo.get())
    }
}
dependencies {
    compileOnly(project(":")) {
        capabilities {
            requireFeature("foo")
        }
    }
}
configurations.compileClasspath.get().resolve()
val fooApiElements by configurations.existing {
    outgoing.artifact(buildFile)
}
The
resolve()
is the culprit. This will trigger deprecation warning in 8.14.3 and fail with 9+
n
I see, indeed, I found the deprecation warning in 8.14.3 and it points to the `io.qameta.allure-adapter`:
Copy code
at io.qameta.allure.gradle.adapter.AllureAdapterExtension.exposeArtifact(AllureAdapterExtension.kt:167)
So it's definitely the plugin that's in violation here. I'll see if I can find a ticket for that and otherwise open one. Thanks for walking me through this. Much appreciated as always 👍
v
Nah, you are still looking at the symptom, not the problem
That call triggers the symptom, the question is which call made the configuration immutable
n
Alright, in that case I'm back to "how do I find the cause of the symptom"? I can see when the configuration gets marked as observed, but I can't find the trigger for it, nor can I easily see if this happens during the configuration phase (if that's even relevant, or am I potentially dealing with an ordering issue during the task execution phase?). For reference, when debugging I have IntelliJ print a stacktrace and log a message when the configuration gets marked with
Configuration#preventUsageMutation()
To clarify, I went back to gradle
8.14.3
to see if I can fix the deprecation before upgrading, so this stacktrace is from that version: https://github.com/gradle/gradle/blob/v8.14.3/platforms/software/dependency-manage[…]api/internal/artifacts/configurations/DefaultConfiguration.java
v
Hm, I don't really know that part of the code, but from a skim over the stack frames, it seems that this is happening when building the task graph, which would indeed hint at someone calling the extension method at execution time. What's the full stacktrace of when that extension function is called, causing the error / deprecation warning?
n
This is the full stacktrace that triggers the warning.
For extra context: I'm triggering the warning by calling
./gradlew :testCodeCoverageReport --dry-run
, so it's indeed sufficient to simply build the task graph to trigger the error.
v
Interesting, I'd say what happens is, that a task is configured sooo late, that it is too late to modify the artifacts. That extension code is using
tasks.configureEach
to then inside add the task providers as
builtBy
tasks. It might for example be the
clean
task, this reproduces your issue standalone:
Copy code
plugins {
    base
}
val foo = configurations.consumable("foo") {
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named("foo"))
    }
}
tasks.clean {
    foo.configure { outgoing.artifact(buildFile) }
}
val bar = configurations.dependencyScope("bar")
val baz = configurations.resolvable("baz") {
    extendsFrom(bar.get())
    attributes {
        attribute(Category.CATEGORY_ATTRIBUTE, objects.named("foo"))
    }
}
dependencies {
    bar(project)
}
val bam by tasks.registering {
    inputs.files(baz)
    doLast {
        baz.get().files.forEach { println(it) }
    }
}
With any other task added by the
java-library
plugin I tried it works without problem. So I'm not sure whether this is actually a Gradle bug, or the Allure extension doing bad things. In my contrived example it should have been
Copy code
foo.configure { outgoing.artifact(tasks.clean.map { buildFile }) }
instead of
Copy code
tasks.clean {
    foo.configure { outgoing.artifact(buildFile) }
}
👀 1
n
I got it down to the bare minimum MCVE (I think). Create a new gradle project. settings.gradle.kts
Copy code
rootProject.name = "test-allure"

pluginManagement {
    repositories {
        gradlePluginPortal()
    }
}
build.gradle.kts
Copy code
plugins {
    java
    `jacoco-report-aggregation`
    id("io.qameta.allure") version "3.0.0"
}

repositories {
    mavenCentral()
}
Then run
./gradlew :testCodeCoverageReport --dry-run
and it outputs
Copy code
Calculating task graph as no cached configuration is available for tasks: :testCodeCoverageReport

FAILURE: Build failed with an exception.

* What went wrong:
Could not determine the dependencies of task ':testCodeCoverageReport'.
> Could not resolve all dependencies for configuration ':aggregateCodeCoverageReportResults'.
   > Could not create task ':test'.
      > Cannot mutate the artifacts of configuration ':allureRawResultElements' after the configuration was consumed as a variant. After a configuration has been observed, it should not be modified.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to generate a Build Scan (Powered by Develocity).
> Get more help at <https://help.gradle.org>.

BUILD FAILED in 699ms
Configuration cache entry stored.
Ok, if you downgrade the allure plugin to
2.12.0
, the issue goes away, so I'm going to assume it's a bug on their end.
v
Yeah, it is similar to what I showed. It happens when configuring the
test
task. And the time it gets configured is too late to add artifacts it seems. And you can work-around it by breaking task-configuration avoidance using
tasks.test.get()
in your build script.
n
That does indeed work 🎉 So by forcing early realization of the task, it avoids others consuming the configuration first. That would explain why this change would cause it: https://github.com/allure-framework/allure-gradle/pull/125/files#diff-53c49873e630c9f00ea220d4fcb00986813315a201e075aff2411e8fa6b780b0R100-R106 I assume the
afterEvaluate
from the previous version of the plugin does the same trick as manually realizing the task.
Would there be a different way to achieve this, instead of reverting back to
afterEvaluate
? Or would this be a correct use-case for it?
v
Not quite, or well similar. In the new version the
exposeArtifact
call that registers the artifact is done in a
tasks.configureEach
so done when the task (
test
in the MCVE case) is configured, which with working task-configuration avoidance is too latest under certain situations like when the aggregation plugin is applied. The eager realization ensures this is done earlier as the configuration is done earlier. In the old version the artifact registration is done with the evil
afterEvaluate
and thus is done before the task graph calculation is done and thus before that variant-resolution was done. So in a sense, yes, both move the artifact adding to an earlier point in time where artifact adding still works as the configuration was not yet involved in resolution.
I'm pretty sure there is a better way than using
afterEvaluate
, there almost always is unless you need to cooperate with other bad code that is using
afterEvaluate
too.
But how to properly do it I cannot say out of the box without in-depth analyzing the allure plugin code.
n
Yep, that all starts making more sense to me now. Thanks for all the insights 🙏 I'll open a ticket and an accompanying PR to revert the change. The latter not necessarily to get it merged, but as a starting point for discussion.
v
From a quick skim and experiment, I'd say that should just do
builtBy(theTaskCollection)
insted of doing
theTaskCollection.configureEach { task -> ... builtBy(tasks.named(task.name)) ... }
So something like this might work as expected, but I did not test:
Copy code
diff --git a/allure-adapter-plugin/src/main/kotlin/io/qameta/allure/gradle/adapter/AllureAdapterExtension.kt b/allure-adapter-plugin/src/main/kotlin/io/qameta/allure/gradle/adapter/AllureAdapterExtension.kt
index 6c43f4f..4f796bf 100644
--- a/allure-adapter-plugin/src/main/kotlin/io/qameta/allure/gradle/adapter/AllureAdapterExtension.kt
+++ b/allure-adapter-plugin/src/main/kotlin/io/qameta/allure/gradle/adapter/AllureAdapterExtension.kt
@@ -100,10 +100,9 @@ open class AllureAdapterExtension @Inject constructor(
     fun gatherResultsFrom(tasks: TaskCollection<out Task>) {
         project.apply<AllureAdapterBasePlugin>()
         tasks.configureEach {
-            // Expose outgoing artifact and configure per-task without relying on afterEvaluate
-            exposeArtifact(project.tasks.named(name))
             internalGatherResultsFrom(this)
         }
+        exposeArtifact(tasks)
     }

     fun gatherResultsFrom(task: TaskProvider<out Task>) {
@@ -160,12 +159,12 @@ open class AllureAdapterExtension @Inject constructor(
         }
     }

-    private fun exposeArtifact(task: TaskProvider<*>) {
+    private fun exposeArtifact(taskOrTasks: Any) {
         // Expose the gathered raw results
         val allureResults =
             project.configurations[AllureAdapterBasePlugin.ALLURE_RAW_RESULT_ELEMENTS_CONFIGURATION_NAME]
         allureResults.outgoing.artifact(allureResultsDir) {
-            builtBy(task)
+            builtBy(taskOrTasks)
         }
     }
n
that seems to fix it indeed, amazing 👍
👌 1