Niels Doucet
10/03/2025, 10:51 AMio.qameta.allure plugin:
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?Vampire
10/03/2025, 12:35 PMIs 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.
Vampire
10/03/2025, 12:43 PMAnd 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.Niels Doucet
10/03/2025, 1:07 PMMost 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.
Niels Doucet
10/03/2025, 1:15 PMNiels Doucet
10/03/2025, 1:18 PMname + " - 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 🤷Vampire
10/03/2025, 1:21 PMIt 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.Vampire
10/03/2025, 1:23 PMVampire
10/03/2025, 1:23 PMNiels Doucet
10/03/2025, 1:27 PMdependenciesObserved 🤦Niels Doucet
10/03/2025, 1:51 PMaggregateCodeCoverageReportResults 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 🤔Vampire
10/03/2025, 11:37 PMVampire
10/03/2025, 11:37 PMVampire
10/03/2025, 11:37 PMVampire
10/03/2025, 11:39 PMMutating 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
Vampire
10/03/2025, 11:40 PMaggregateCodeCoverageReportResults prematurely (if that one is the culprit)Vampire
10/03/2025, 11:43 PMval 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+Niels Doucet
10/04/2025, 8:45 PMat 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 👍Vampire
10/05/2025, 5:34 AMVampire
10/05/2025, 5:35 AMNiels Doucet
10/06/2025, 10:04 AMConfiguration#preventUsageMutation()Niels Doucet
10/06/2025, 10:07 AM8.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.javaVampire
10/06/2025, 11:16 AMNiels Doucet
10/06/2025, 12:07 PMNiels Doucet
10/06/2025, 12:09 PM./gradlew :testCodeCoverageReport --dry-run, so it's indeed sufficient to simply build the task graph to trigger the error.Vampire
10/06/2025, 10:36 PMtasks.configureEach to then inside add the task providers as builtBy tasks.
It might for example be the clean task, this reproduces your issue standalone:
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
foo.configure { outgoing.artifact(tasks.clean.map { buildFile }) }
instead of
tasks.clean {
foo.configure { outgoing.artifact(buildFile) }
}Niels Doucet
10/07/2025, 8:37 AMrootProject.name = "test-allure"
pluginManagement {
repositories {
gradlePluginPortal()
}
}
build.gradle.kts
plugins {
java
`jacoco-report-aggregation`
id("io.qameta.allure") version "3.0.0"
}
repositories {
mavenCentral()
}
Then run ./gradlew :testCodeCoverageReport --dry-run and it outputs
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.Niels Doucet
10/07/2025, 9:14 AM2.12.0, the issue goes away, so I'm going to assume it's a bug on their end.Vampire
10/07/2025, 10:11 AMtest 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.Niels Doucet
10/07/2025, 10:21 AMafterEvaluate from the previous version of the plugin does the same trick as manually realizing the task.Niels Doucet
10/07/2025, 10:23 AMafterEvaluate? Or would this be a correct use-case for it?Vampire
10/07/2025, 10:26 AMexposeArtifact 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.Vampire
10/07/2025, 10:27 AMafterEvaluate, there almost always is unless you need to cooperate with other bad code that is using afterEvaluate too.Vampire
10/07/2025, 10:28 AMNiels Doucet
10/07/2025, 10:34 AMVampire
10/07/2025, 10:34 AMbuiltBy(theTaskCollection) insted of doing theTaskCollection.configureEach { task -> ... builtBy(tasks.named(task.name)) ... }Vampire
10/07/2025, 10:36 AMdiff --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)
}
}Niels Doucet
10/07/2025, 10:37 AM