Repository: apollographql/apollo-kotlin
Pull request #4340 (2022-08-12 15:11:05)

Enable Gradle's build cache
Enable Gradle's build cache. 

I manually disabled it on the CI for the push and tag workflows, to minimize the risk of non-reproducible builds there.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *apollo-android-docs* processing.


|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 49148b2c54e05cd166cc4f63a6d32a647a689268 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/apollo-android-docs/deploys/62f66d8b76de7300080b2a7a |

Patch:
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index b458346f0e..8521d1b56b 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -23,13 +23,13 @@ jobs:
           ulimit -c unlimited
           # Workaround an issue where kotlinNpmInstall outputs
           # 'Resolving NPM dependencies using yarn' returns 137
-          ./gradlew compileKotlinJsIr compileKotlinJsLegacy
+          ./gradlew --no-build-cache compileKotlinJsIr compileKotlinJsLegacy
           ./gradlew --stop
-          ./gradlew ciBuild
+          ./gradlew --no-build-cache ciBuild
           ./gradlew --stop
-          ./gradlew -p tests ciBuild
-          ./gradlew dokkaHtmlMultiModule
-          ./gradlew ciPublishSnapshot
+          ./gradlew --no-build-cache -p tests ciBuild
+          ./gradlew --no-build-cache dokkaHtmlMultiModule
+          ./gradlew --no-build-cache ciPublishSnapshot
         env:
           SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
           SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml
index aabac55de9..745542ff92 100644
--- a/.github/workflows/tag.yml
+++ b/.github/workflows/tag.yml
@@ -18,7 +18,7 @@ jobs:
       - uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee #v2.1.4
       - name: Build with Gradle
         run: |
-          ./gradlew ciPublishRelease -Pgradle.publish.key="${{ secrets.GRADLE_PUBLISH_KEY }}" -Pgradle.publish.secret="${{ secrets.GRADLE_PUBLISH_SECRET }}"
+          ./gradlew --no-build-cache ciPublishRelease -Pgradle.publish.key="${{ secrets.GRADLE_PUBLISH_KEY }}" -Pgradle.publish.secret="${{ secrets.GRADLE_PUBLISH_SECRET }}"
         env:
           SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
           SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index b38a02db3a..497634e6ef 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -21,6 +21,9 @@
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="50" />
       <option name="ALLOW_TRAILING_COMMA" value="true" />
     </JetCodeStyleSettings>
+    <Properties>
+      <option name="KEEP_BLANK_LINES" value="true" />
+    </Properties>
     <XML>
       <option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
     </XML>
diff --git a/apollo-adapters/build.gradle.kts b/apollo-adapters/build.gradle.kts
index f9b655ad34..3d9d8323f8 100644
--- a/apollo-adapters/build.gradle.kts
+++ b/apollo-adapters/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.adapter")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -19,15 +25,3 @@ kotlin {
     }
   }
 }
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.adapter")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-annotations/build.gradle.kts b/apollo-annotations/build.gradle.kts
index 5f2b1f9982..fe0d908394 100644
--- a/apollo-annotations/build.gradle.kts
+++ b/apollo-annotations/build.gradle.kts
@@ -1,8 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults()
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.annotations")
+  mpp {}
+}
 
 kotlin {
   sourceSets {
@@ -18,18 +22,6 @@ kotlin {
         api(libs.kotlin.stdlib.js)
       }
     }
-
   }
 }
 
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.annotations")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-api/build.gradle.kts b/apollo-api/build.gradle.kts
index 66f80bf4ba..88259eb008 100644
--- a/apollo-api/build.gradle.kts
+++ b/apollo-api/build.gradle.kts
@@ -1,8 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults()
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.api")
+  mpp {}
+}
 
 kotlin {
   sourceSets {
@@ -16,14 +20,3 @@ kotlin {
   }
 }
 
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.api")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-ast/build.gradle.kts b/apollo-ast/build.gradle.kts
index 33672a5376..c6b66c3b0a 100644
--- a/apollo-ast/build.gradle.kts
+++ b/apollo-ast/build.gradle.kts
@@ -3,9 +3,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 plugins {
   antlr
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
   id("com.google.devtools.ksp")
 }
 
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.ast")
+}
+
 dependencies {
   antlr(libs.antlr)
   implementation(libs.antlr.runtime)
@@ -31,13 +36,4 @@ tasks.withType(KotlinCompile::class.java) {
   // This used to work and fails now. Strangely enough, it fails on both `dev-3.x` and `main` as of writing while both these branches have
   // compiled successfully before...
   dependsOn("generateGrammarSource")
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
-
-val jar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.ast")
-  }
 }
diff --git a/apollo-cli/build.gradle.kts b/apollo-cli/build.gradle.kts
index 606d75a113..7acb6f9ffe 100644
--- a/apollo-cli/build.gradle.kts
+++ b/apollo-cli/build.gradle.kts
@@ -1,7 +1,6 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
   id("application")
 }
 
@@ -12,12 +11,6 @@ dependencies {
   implementation(libs.clikt)
 }
 
-tasks.withType(KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
-
 application {
   mainClass.set("com.apollographql.apollo3.cli.MainKt")
 }
diff --git a/apollo-compiler/build.gradle.kts b/apollo-compiler/build.gradle.kts
index 0fd17206d2..519ca9d222 100644
--- a/apollo-compiler/build.gradle.kts
+++ b/apollo-compiler/build.gradle.kts
@@ -2,9 +2,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
   id("com.google.devtools.ksp")
 }
 
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.compiler")
+}
+
 dependencies {
   implementation(projects.apolloAst)
   implementation(projects.apolloApi) {
@@ -63,12 +68,6 @@ tasks.withType(KotlinCompile::class.java) {
   dependsOn(pluginVersionTaskProvider)
 }
 
-tasks.withType(KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
-
 // since test/graphql is not an input to Test tasks, they're not run with the changes made in there.
 tasks.withType<Test>().configureEach {
   inputs.dir("src/test/graphql")
@@ -77,9 +76,3 @@ tasks.withType<Test>().configureEach {
   inputs.dir("src/test/usedtypes")
   inputs.dir("src/test/validation")
 }
-
-val jar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.compiler")
-  }
-}
diff --git a/apollo-gradle-plugin-external/build.gradle.kts b/apollo-gradle-plugin-external/build.gradle.kts
index 6dd6f9562a..63922acb3a 100644
--- a/apollo-gradle-plugin-external/build.gradle.kts
+++ b/apollo-gradle-plugin-external/build.gradle.kts
@@ -1,9 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
   id("java-gradle-plugin")
   id("com.gradleup.gr8") // Only used for removeGradleApiFromApi()
 }
 
+apolloLibrary {
+  treatWarningsAsErrors(false)
+}
+
 dependencies {
   compileOnly(libs.gradle.api.min)
   compileOnly(libs.kotlin.plugin.min)
diff --git a/apollo-gradle-plugin/build.gradle.kts b/apollo-gradle-plugin/build.gradle.kts
index b6b49a4369..f72342a276 100644
--- a/apollo-gradle-plugin/build.gradle.kts
+++ b/apollo-gradle-plugin/build.gradle.kts
@@ -1,10 +1,15 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
   id("java-gradle-plugin")
   id("com.gradle.plugin-publish")
   id("com.gradleup.gr8")
 }
 
+apolloLibrary {
+  treatWarningsAsErrors(false)
+}
+
 // Configuration for extra jar to pass to R8 to give it more context about what can be relocated
 configurations.create("gr8Classpath")
 // Configuration dependencies that will be shadowed
diff --git a/apollo-http-cache/build.gradle.kts b/apollo-http-cache/build.gradle.kts
index 88dc7248b0..c86df1f40f 100644
--- a/apollo-http-cache/build.gradle.kts
+++ b/apollo-http-cache/build.gradle.kts
@@ -1,5 +1,10 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
+}
+
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.http")
 }
 
 dependencies {
@@ -13,15 +18,3 @@ dependencies {
   testImplementation(libs.kotlin.test.junit)
   testImplementation(libs.truth)
 }
-
-val jar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.http")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-idling-resource/build.gradle.kts b/apollo-idling-resource/build.gradle.kts
index 49877383e6..d18718bc10 100644
--- a/apollo-idling-resource/build.gradle.kts
+++ b/apollo-idling-resource/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
   id("com.android.library")
   id("org.jetbrains.kotlin.android")
+  id("apollo.library")
 }
 
 dependencies {
@@ -16,9 +17,3 @@ android {
     targetSdk = libs.versions.android.sdkversion.target.get().toInt()
   }
 }
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-mockserver/build.gradle.kts b/apollo-mockserver/build.gradle.kts
index 4a2148fe18..d87507714e 100644
--- a/apollo-mockserver/build.gradle.kts
+++ b/apollo-mockserver/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.mockserver")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -36,14 +42,3 @@ kotlin {
   }
 }
 
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.mockserver")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-mpp-utils/build.gradle.kts b/apollo-mpp-utils/build.gradle.kts
index 841f4b605a..87359ff410 100644
--- a/apollo-mpp-utils/build.gradle.kts
+++ b/apollo-mpp-utils/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.mpp")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -14,14 +20,3 @@ kotlin {
   }
 }
 
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.mpp")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-normalized-cache-api-incubating/build.gradle.kts b/apollo-normalized-cache-api-incubating/build.gradle.kts
index 02244a0d27..10b0570a4e 100644
--- a/apollo-normalized-cache-api-incubating/build.gradle.kts
+++ b/apollo-normalized-cache-api-incubating/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.normalized.api")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -16,15 +22,3 @@ kotlin {
     }
   }
 }
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.normalized.api")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-normalized-cache-api/build.gradle.kts b/apollo-normalized-cache-api/build.gradle.kts
index 02244a0d27..10b0570a4e 100644
--- a/apollo-normalized-cache-api/build.gradle.kts
+++ b/apollo-normalized-cache-api/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.normalized.api")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -16,15 +22,3 @@ kotlin {
     }
   }
 }
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.normalized.api")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-normalized-cache-incubating/build.gradle.kts b/apollo-normalized-cache-incubating/build.gradle.kts
index 09bc9e2aa2..fce1e9dbc1 100644
--- a/apollo-normalized-cache-incubating/build.gradle.kts
+++ b/apollo-normalized-cache-incubating/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.normalized")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -18,15 +24,3 @@ kotlin {
     }
   }
 }
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.normalized")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-normalized-cache-sqlite-incubating/build.gradle.kts b/apollo-normalized-cache-sqlite-incubating/build.gradle.kts
index 13d08f6fef..db30687eef 100644
--- a/apollo-normalized-cache-sqlite-incubating/build.gradle.kts
+++ b/apollo-normalized-cache-sqlite-incubating/build.gradle.kts
@@ -1,7 +1,20 @@
+plugins {
+  id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
+}
+
 apply(plugin = "com.android.library")
-apply(plugin = "org.jetbrains.kotlin.multiplatform")
 apply(plugin = "com.squareup.sqldelight")
 
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.normalized.sql")
+  mpp {
+    withLinux.set(false)
+    // https://github.com/cashapp/sqldelight/pull/1486
+    withJs.set(false)
+  }
+}
+
 configure<com.squareup.sqldelight.gradle.SqlDelightExtension> {
   database("JsonDatabase") {
     packageName = "com.apollographql.apollo3.cache.normalized.sql.internal.json"
@@ -20,13 +33,10 @@ configure<com.squareup.sqldelight.gradle.SqlDelightExtension> {
   }
 }
 
-// https://github.com/cashapp/sqldelight/pull/1486
-configureMppDefaults(withJs = false, withLinux = false)
-
-configure<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension> {
-    android {
-      publishAllLibraryVariants()
-    }
+kotlin {
+  android {
+    publishAllLibraryVariants()
+  }
 
   sourceSets {
     val commonMain by getting {
@@ -56,19 +66,19 @@ configure<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension> {
       }
     }
 
-      val androidMain by getting {
-        dependsOn(commonMain)
-        dependencies {
-          api(libs.androidx.sqlite)
-          implementation(libs.sqldelight.android)
-          implementation(libs.androidx.sqlite.framework)
-          implementation(libs.androidx.startup.runtime)
-        }
+    val androidMain by getting {
+      dependsOn(commonMain)
+      dependencies {
+        api(libs.androidx.sqlite)
+        implementation(libs.sqldelight.android)
+        implementation(libs.androidx.sqlite.framework)
+        implementation(libs.androidx.startup.runtime)
+      }
+    }
+    val androidTest by getting {
+      dependencies {
+        implementation(libs.kotlin.test.junit)
       }
-      val androidTest by getting {
-        dependencies {
-          implementation(libs.kotlin.test.junit)
-        }
     }
     val commonTest by getting {
       dependencies {
@@ -78,50 +88,37 @@ configure<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension> {
   }
 }
 
-  configure<com.android.build.gradle.LibraryExtension> {
-    compileSdk = libs.versions.android.sdkversion.compile.get().toInt()
+configure<com.android.build.gradle.LibraryExtension> {
+  compileSdk = libs.versions.android.sdkversion.compile.get().toInt()
 
-    defaultConfig {
-      minSdk = libs.versions.android.sdkversion.min.get().toInt()
-      targetSdk = libs.versions.android.sdkversion.target.get().toInt()
-    }
+  defaultConfig {
+    minSdk = libs.versions.android.sdkversion.min.get().toInt()
+    targetSdk = libs.versions.android.sdkversion.target.get().toInt()
   }
+}
 
 
-  tasks.named("lint") {
+tasks.named("lint") {
+  /**
+   * lint warns with:
+   *
+   * ```
+   * Could not load custom lint check jar file /Users/mbonnin/.gradle/caches/transforms-3/a58c406cc84b74815c738fa583c867e0/transformed/startup-runtime-1.1.1/jars/lint.jar
+   * java.lang.NoClassDefFoundError: com/android/tools/lint/client/api/Vendor
+   * ```
+   *
+   * In general, there is so little Android code here, it's not really worth running lint
+   */
+  enabled = false
+}
+
+tasks.configureEach {
+  if (name.endsWith("UnitTest")) {
     /**
-     * lint warns with:
-     *
-     * ```
-     * Could not load custom lint check jar file /Users/mbonnin/.gradle/caches/transforms-3/a58c406cc84b74815c738fa583c867e0/transformed/startup-runtime-1.1.1/jars/lint.jar
-     * java.lang.NoClassDefFoundError: com/android/tools/lint/client/api/Vendor
-     * ```
-     *
-     * In general, there is so little Android code here, it's not really worth running lint
+     * Because there is no App Startup in Android unit tests, the Android tests
+     * fail at runtime so ignore them
+     * We could make the Android unit tests use the Jdbc driver if we really wanted to
      */
     enabled = false
   }
-
-  tasks.configureEach {
-    if (name.endsWith("UnitTest")) {
-      /**
-       * Because there is no App Startup in Android unit tests, the Android tests
-       * fail at runtime so ignore them
-       * We could make the Android unit tests use the Jdbc driver if we really wanted to
-       */
-      enabled = false
-    }
-  }
-
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.normalized.sql")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
 }
diff --git a/apollo-normalized-cache-sqlite/build.gradle.kts b/apollo-normalized-cache-sqlite/build.gradle.kts
index a81a7a67e5..19138453fe 100644
--- a/apollo-normalized-cache-sqlite/build.gradle.kts
+++ b/apollo-normalized-cache-sqlite/build.gradle.kts
@@ -1,7 +1,20 @@
+plugins {
+  id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
+}
+
 apply(plugin = "com.android.library")
-apply(plugin = "org.jetbrains.kotlin.multiplatform")
 apply(plugin = "com.squareup.sqldelight")
 
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.normalized.sql")
+  mpp {
+    withLinux.set(false)
+    // https://github.com/cashapp/sqldelight/pull/1486
+    withJs.set(false)
+  }
+}
+
 configure<com.squareup.sqldelight.gradle.SqlDelightExtension> {
   database("JsonDatabase") {
     packageName = "com.apollographql.apollo3.cache.normalized.sql.internal.json"
@@ -10,10 +23,7 @@ configure<com.squareup.sqldelight.gradle.SqlDelightExtension> {
   }
 }
 
-// https://github.com/cashapp/sqldelight/pull/1486
-configureMppDefaults(withJs = false, withLinux = false)
-
-configure<org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension> {
+kotlin {
   android {
     publishAllLibraryVariants()
   }
@@ -104,14 +114,3 @@ tasks.configureEach {
   }
 }
 
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.normalized.sql")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-normalized-cache/build.gradle.kts b/apollo-normalized-cache/build.gradle.kts
index e0bba2624d..324b9eb561 100644
--- a/apollo-normalized-cache/build.gradle.kts
+++ b/apollo-normalized-cache/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.cache.normalized")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -18,15 +24,3 @@ kotlin {
     }
   }
 }
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.cache.normalized")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-runtime/build.gradle.kts b/apollo-runtime/build.gradle.kts
index d0ebb4c2b2..3ad2b4e885 100644
--- a/apollo-runtime/build.gradle.kts
+++ b/apollo-runtime/build.gradle.kts
@@ -1,8 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.runtime")
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
@@ -59,15 +65,3 @@ tasks.register("iOSSimTest") {
     }
   }
 }
-
-val jvmJar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.runtime")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-rx2-support/build.gradle.kts b/apollo-rx2-support/build.gradle.kts
index 32de497943..a8ed129a15 100644
--- a/apollo-rx2-support/build.gradle.kts
+++ b/apollo-rx2-support/build.gradle.kts
@@ -1,5 +1,10 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
+}
+
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.rx2")
 }
 
 dependencies {
@@ -10,15 +15,3 @@ dependencies {
   api(projects.apolloRuntime)
   api(projects.apolloNormalizedCache)
 }
-
-val jar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.rx2")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-rx3-support/build.gradle.kts b/apollo-rx3-support/build.gradle.kts
index e55e9ac6eb..1c77e32651 100644
--- a/apollo-rx3-support/build.gradle.kts
+++ b/apollo-rx3-support/build.gradle.kts
@@ -3,6 +3,11 @@
  */
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
+}
+
+apolloLibrary {
+  javaModuleName("com.apollographql.apollo3.rx3")
 }
 
 dependencies {
@@ -13,15 +18,3 @@ dependencies {
   api(projects.apolloRuntime)
   api(projects.apolloNormalizedCache)
 }
-
-val jar by tasks.getting(Jar::class) {
-  manifest {
-    attributes("Automatic-Module-Name" to "com.apollographql.apollo3.rx3")
-  }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/apollo-tooling/build.gradle.kts b/apollo-tooling/build.gradle.kts
index b009b48723..54d3a705bf 100644
--- a/apollo-tooling/build.gradle.kts
+++ b/apollo-tooling/build.gradle.kts
@@ -1,7 +1,6 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.library")
 }
 
 dependencies {
@@ -16,9 +15,3 @@ dependencies {
   testImplementation(libs.junit)
   testImplementation(libs.truth)
 }
-
-tasks.withType(KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
-  }
-}
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index 631eafc93d..2040164b0b 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -1,11 +1,14 @@
+import org.jetbrains.kotlin.samWithReceiver.gradle.SamWithReceiverExtension
+import org.jetbrains.kotlin.samWithReceiver.gradle.SamWithReceiverGradleSubplugin
+
 plugins {
   `embedded-kotlin`
+  id("java-gradle-plugin")
 }
 
-repositories {
-  mavenCentral()
-  google()
-  gradlePluginPortal()
+plugins.apply(SamWithReceiverGradleSubplugin::class.java)
+extensions.configure(SamWithReceiverExtension::class.java) {
+  annotations(HasImplicitReceiver::class.qualifiedName!!)
 }
 
 group = "com.apollographql.apollo3"
@@ -14,7 +17,7 @@ dependencies {
   compileOnly(libs.gradle.api)
 
   implementation(libs.okhttp)
-  implementation(libs.moshi)
+
   implementation(libs.dokka.plugin)
   implementation(libs.dokka.base)
 
@@ -51,8 +54,16 @@ java {
   toolchain.languageVersion.set(JavaLanguageVersion.of(8))
 }
 
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
-  kotlinOptions {
-    allWarningsAsErrors = true
+gradlePlugin {
+  plugins {
+    register("apollo.library") {
+      id = "apollo.library"
+      implementationClass = "com.apollographql.apollo3.buildlogic.plugin.LibraryConventionPlugin"
+    }
+
+    register("apollo.test") {
+      id = "apollo.test"
+      implementationClass = "com.apollographql.apollo3.buildlogic.plugin.TestConventionPlugin"
+    }
   }
 }
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index 5054e84dbf..b331eabc77 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -1,13 +1,17 @@
 rootProject.name = "build-logic"
 
-pluginManagement {
+dependencyResolutionManagement {
   repositories {
     mavenCentral()
-    gradlePluginPortal()
+    google()
+    gradlePluginPortal() {
+      content {
+        includeModule("me.champeau.gradle", "japicmp-gradle-plugin")
+        includeModule("me.tylerbwong.gradle", "metalava-gradle")
+      }
+    }
   }
-}
 
-dependencyResolutionManagement {
   versionCatalogs {
     create("libs") {
       from(files("../gradle/libs.versions.toml"))
diff --git a/build-logic/src/main/kotlin/CompilerOptions.kt b/build-logic/src/main/kotlin/CompilerOptions.kt
index a9c735c6d4..cf246c0346 100644
--- a/build-logic/src/main/kotlin/CompilerOptions.kt
+++ b/build-logic/src/main/kotlin/CompilerOptions.kt
@@ -5,49 +5,72 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
 import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
-fun Project.configureJavaAndKotlinCompilers() {
-  // Because this is called from subproject {}, it might actually be called before the java/kotlin plugins are applied
-  // XXX: replace with plugins.withId("") {}
-  afterEvaluate {
-    // For Kotlin JVM projects
-    tasks.withType(KotlinCompile::class.java) {
-      it.kotlinOptions {
-        freeCompilerArgs = freeCompilerArgs + listOf(
-            "-opt-in=kotlin.RequiresOptIn",
-            "-opt-in=com.apollographql.apollo3.annotations.ApolloExperimental",
-            "-opt-in=com.apollographql.apollo3.annotations.ApolloInternal"
-        )
-        if (getKotlinPluginVersion() == "1.6.10") {
-          // This is a workaround for https://youtrack.jetbrains.com/issue/KT-47000 (fixed in Kotlin 1.6.20)
-          // Since we don't use @JvmDefault anywhere, the option has no effect, but suppresses the bogus compiler error
-          // See also https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-default/
-          // See also https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/
-          freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=compatibility"
-        }
-        apiVersion = "1.5"
-        languageVersion = "1.5"
+fun Project.configureJavaAndKotlinCompilers(treatWarningsAsErrors: Boolean = false) {
+  // For Kotlin JVM projects
+  tasks.withType(KotlinCompile::class.java) {
+    kotlinOptions {
+      freeCompilerArgs = freeCompilerArgs + listOf(
+          "-opt-in=kotlin.RequiresOptIn",
+          "-opt-in=com.apollographql.apollo3.annotations.ApolloExperimental",
+          "-opt-in=com.apollographql.apollo3.annotations.ApolloInternal"
+      )
+      if (getKotlinPluginVersion() == "1.6.10") {
+        // This is a workaround for https://youtrack.jetbrains.com/issue/KT-47000 (fixed in Kotlin 1.6.20)
+        // Since we don't use @JvmDefault anywhere, the option has no effect, but suppresses the bogus compiler error
+        // See also https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-default/
+        // See also https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/
+        freeCompilerArgs = freeCompilerArgs + "-Xjvm-default=compatibility"
       }
+      apiVersion = "1.5"
+      languageVersion = "1.5"
+      jvmTarget = "1.8"
     }
+  }
 
-    // For Kotlin Multiplatform projects
-    (project.extensions.findByName("kotlin")
-        as? org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension)?.run {
-      sourceSets.all {
-        it.languageSettings.optIn("kotlin.RequiresOptIn")
-        it.languageSettings.optIn("com.apollographql.apollo3.annotations.ApolloExperimental")
-        it.languageSettings.optIn("com.apollographql.apollo3.annotations.ApolloInternal")
-      }
+  // For Kotlin Multiplatform projects
+  (project.extensions.findByName("kotlin")
+      as? org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension)?.run {
+    sourceSets.all {
+      languageSettings.optIn("kotlin.RequiresOptIn")
+      languageSettings.optIn("com.apollographql.apollo3.annotations.ApolloExperimental")
+      languageSettings.optIn("com.apollographql.apollo3.annotations.ApolloInternal")
     }
+  }
+
+  @Suppress("UnstableApiUsage")
+  project.extensions.getByType(JavaPluginExtension::class.java).apply {
+    // Compile and run tests with Java11
+    toolchain.languageVersion.set(JavaLanguageVersion.of(11))
+  }
+  @Suppress("UnstableApiUsage")
+  project.tasks.withType(JavaCompile::class.java).configureEach {
+    // Ensure "org.gradle.jvm.version" is set to "8" in Gradle metadata of jvm-only modules.
+    options.release.set(8)
+  }
 
-    @Suppress("UnstableApiUsage")
-    project.extensions.getByType(JavaPluginExtension::class.java).apply {
-      // Compile and run tests with Java11
-      toolchain.languageVersion.set(JavaLanguageVersion.of(11))
+  if (treatWarningsAsErrors) treatWarningsAsErrors()
+}
+
+private fun Project.treatWarningsAsErrors() {
+  tasks.withType(KotlinCompile::class.java) {
+    kotlinOptions {
+      allWarningsAsErrors = true
     }
-    @Suppress("UnstableApiUsage")
-    project.tasks.withType(JavaCompile::class.java).configureEach {
-      // Ensure "org.gradle.jvm.version" is set to "8" in Gradle metadata of jvm-only modules.
-      it.options.release.set(8)
+  }
+}
+
+// Workaround for https://youtrack.jetbrains.com/issue/KT-51970
+fun Project.workaroundForIssueKT51970() {
+  afterEvaluate {
+    afterEvaluate {
+      tasks.configureEach {
+        if (
+            name.startsWith("compile")
+            && name.endsWith("KotlinMetadata")
+        ) {
+          enabled = false
+        }
+      }
     }
   }
 }
diff --git a/build-logic/src/main/kotlin/GitStatus.kt b/build-logic/src/main/kotlin/GitStatus.kt
index 6ad949c439..30fc7f44cb 100644
--- a/build-logic/src/main/kotlin/GitStatus.kt
+++ b/build-logic/src/main/kotlin/GitStatus.kt
@@ -1,4 +1,3 @@
-
 fun checkGitStatus() {
   val modifiedFiles = runCommand("git", "status", "--porcelain")
   if (modifiedFiles.isNotEmpty()) {
diff --git a/build-logic/src/main/kotlin/JapiCmp.kt b/build-logic/src/main/kotlin/JapiCmp.kt
index 8f42128ddd..79e9eb5ae6 100644
--- a/build-logic/src/main/kotlin/JapiCmp.kt
+++ b/build-logic/src/main/kotlin/JapiCmp.kt
@@ -1,4 +1,3 @@
-import com.squareup.moshi.Moshi
 import okhttp3.OkHttpClient
 import okhttp3.Request
 import org.gradle.api.DefaultTask
@@ -43,8 +42,8 @@ object JapiCmp {
         else -> project.name
       }
 
-      it.artifact = artifact
-      it.output.set(File(buildDir, "japicmp/cache/$artifact"))
+      this.artifact = artifact
+      output.set(File(buildDir, "japicmp/cache/$artifact"))
     }
 
     // TODO: Make this lazy
@@ -56,26 +55,26 @@ object JapiCmp {
       val jarTask = tasks.findByName(jarTaskName) as? org.gradle.jvm.tasks.Jar
       if (jarTask != null) {
         tasks.register("generateJapicmp", me.champeau.gradle.japicmp.JapicmpTask::class.java) {
-          it.dependsOn(downloadBaselineJarTaskProvider)
-          it.oldClasspath = files(downloadBaselineJarTaskProvider.get().output.asFile.get())
-          it.newClasspath = files(jarTask.archiveFile)
-          it.ignoreMissingClasses = true
-          it.packageExcludes = listOf("*.internal*")
-          it.onlyModified = true
-          it.failOnSourceIncompatibility = false
-          it.htmlOutputFile = file("$buildDir/reports/japicmp.html")
-          it.txtOutputFile = file("$buildDir/reports/japicmp.txt")
+          dependsOn(downloadBaselineJarTaskProvider)
+          oldClasspath = files(downloadBaselineJarTaskProvider.get().output.asFile.get())
+          newClasspath = files(jarTask.archiveFile)
+          ignoreMissingClasses = true
+          packageExcludes = listOf("*.internal*")
+          onlyModified = true
+          failOnSourceIncompatibility = false
+          htmlOutputFile = file("$buildDir/reports/japicmp.html")
+          txtOutputFile = file("$buildDir/reports/japicmp.txt")
         }
         tasks.register("checkJapicmp", me.champeau.gradle.japicmp.JapicmpTask::class.java) {
-          it.dependsOn(downloadBaselineJarTaskProvider)
-          it.oldClasspath = files(downloadBaselineJarTaskProvider.get().output.asFile.get())
-          it.newClasspath = files(jarTask.archiveFile)
-          it.ignoreMissingClasses = true
-          it.packageExcludes = listOf("*.internal*")
-          it.onlyModified = true
-          it.failOnSourceIncompatibility = true
-          it.htmlOutputFile = file("$buildDir/reports/japicmp.html")
-          it.txtOutputFile = file("$buildDir/reports/japicmp.txt")
+          dependsOn(downloadBaselineJarTaskProvider)
+          oldClasspath = files(downloadBaselineJarTaskProvider.get().output.asFile.get())
+          newClasspath = files(jarTask.archiveFile)
+          ignoreMissingClasses = true
+          packageExcludes = listOf("*.internal*")
+          onlyModified = true
+          failOnSourceIncompatibility = true
+          htmlOutputFile = file("$buildDir/reports/japicmp.html")
+          txtOutputFile = file("$buildDir/reports/japicmp.txt")
         }
       }
     }
@@ -96,4 +95,4 @@ object JapiCmp {
               .replace(Regex(".*<latest>(.*)<latest>.*"), "$1")
         }
   }
-}
\ No newline at end of file
+}
diff --git a/build-logic/src/main/kotlin/Mpp.kt b/build-logic/src/main/kotlin/Mpp.kt
index 024b757840..9bd6d4a281 100644
--- a/build-logic/src/main/kotlin/Mpp.kt
+++ b/build-logic/src/main/kotlin/Mpp.kt
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
 import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
 
-fun Project.configureMppDefaults(withJs: Boolean = true, withLinux: Boolean = true) {
+fun Project.configureMppDefaults(withJs: Boolean, withLinux: Boolean) {
   val kotlinExtension = extensions.findByName("kotlin") as? KotlinMultiplatformExtension
   check(kotlinExtension != null) {
     "No multiplatform extension found"
@@ -31,7 +31,7 @@ fun Project.configureMppDefaults(withJs: Boolean = true, withLinux: Boolean = tr
       linuxX64("linux")
     }
 
-    configureAppleTargets(
+    configureAppleTargets(setOf(
         "macosX64",
         "macosArm64",
         "iosArm64",
@@ -43,9 +43,9 @@ fun Project.configureMppDefaults(withJs: Boolean = true, withLinux: Boolean = tr
         "tvosArm64",
         "tvosX64",
         "tvosSimulatorArm64",
-    )
+    ))
 
-    addTestDependencies(withJs)
+    addTestDependencies()
 
     enableNewMemoryManager()
   }
@@ -69,7 +69,7 @@ fun Project.okioNodeJs(): String {
   return "com.squareup.okio:okio-nodefilesystem:$okioVersion"
 }
 
-fun KotlinMultiplatformExtension.configureAppleTargets(vararg presetNames: String) {
+private fun KotlinMultiplatformExtension.configureAppleTargets(presetNames: Set<String>) {
   if (System.getProperty("idea.sync.active") != null) {
     // Early return. Inside intelliJ, only configure one target
     // Try to guess the dev machine to make sure the tests are running smoothly
@@ -99,10 +99,15 @@ fun KotlinMultiplatformExtension.configureAppleTargets(vararg presetNames: Strin
 }
 
 /**
- * Same as [configureMppDefaults] but without iOS or Linux targets.
+ * Same as [configureMppDefaults] but without Linux targets. Apple targets can be configured.
  * Tests only run on the JVM, JS and MacOS
  */
-fun Project.configureMppTestsDefaults(withJs: Boolean = true) {
+fun Project.configureMppTestsDefaults(
+    withJs: Boolean,
+    withJvm: Boolean,
+    newMemoryManager: Boolean,
+    appleTargets: Set<String>,
+) {
   val kotlinExtension = extensions.findByName("kotlin") as? KotlinMultiplatformExtension
   check(kotlinExtension != null) {
     "No multiplatform extension found"
@@ -111,7 +116,8 @@ fun Project.configureMppTestsDefaults(withJs: Boolean = true) {
     /**
      * configure targets
      */
-    jvm()
+    if (withJvm) jvm()
+
     if (withJs) {
       js(IR) {
         nodejs {
@@ -125,40 +131,27 @@ fun Project.configureMppTestsDefaults(withJs: Boolean = true) {
       }
     }
 
-    configureAppleTargets("macosX64", "macosArm64")
+    configureAppleTargets(appleTargets)
 
-    addTestDependencies(withJs)
+    addTestDependencies()
 
-    enableNewMemoryManager()
+    if (newMemoryManager) enableNewMemoryManager()
   }
 }
 
-fun KotlinMultiplatformExtension.addTestDependencies(withJs: Boolean) {
+private fun KotlinMultiplatformExtension.addTestDependencies() {
   sourceSets.getByName("commonTest") {
-    it.dependencies {
-      implementation(kotlin("test-common"))
-      implementation(kotlin("test-annotations-common"))
-    }
-  }
-  if (withJs) {
-    sourceSets.getByName("jsTest") {
-      it.dependencies {
-        implementation(kotlin("test-js"))
-      }
-    }
-  }
-  sourceSets.getByName("jvmTest") {
-    it.dependencies {
-      implementation(kotlin("test-junit"))
+    dependencies {
+      implementation(kotlin("test"))
     }
   }
 }
 
 // See https://github.com/JetBrains/kotlin/blob/master/kotlin-native/NEW_MM.md
-fun KotlinMultiplatformExtension.enableNewMemoryManager() {
-  targets.withType(KotlinNativeTarget::class.java) { target ->
-    target.binaries.all { binary ->
-      binary.binaryOptions["memoryModel"] = "experimental"
+private fun KotlinMultiplatformExtension.enableNewMemoryManager() {
+  targets.withType(KotlinNativeTarget::class.java) {
+    binaries.all {
+      binaryOptions["memoryModel"] = "experimental"
     }
   }
 }
diff --git a/build-logic/src/main/kotlin/Node.kt b/build-logic/src/main/kotlin/Node.kt
index fadefd9467..3f2489aa90 100644
--- a/build-logic/src/main/kotlin/Node.kt
+++ b/build-logic/src/main/kotlin/Node.kt
@@ -7,16 +7,14 @@ import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension
 
 fun Project.configureNode() {
   tasks.withType(org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask::class.java).configureEach {
-    it.apply {
-      args.addAll(
-          listOf(
-              "--network-concurrency",
-              "1",
-              "--mutex",
-              "network"
-          )
-      )
-    }
+    args.addAll(
+        listOf(
+            "--network-concurrency",
+            "1",
+            "--mutex",
+            "network"
+        )
+    )
   }
 
   // See https://youtrack.jetbrains.com/issue/KT-47215
@@ -32,4 +30,4 @@ fun Project.configureNode() {
   rootProject.plugins.withType(NodeJsRootPlugin::class.java) {
     project.extensions.getByType(NodeJsRootExtension::class.java).nodeVersion = "16.10.0"
   }
-}
\ No newline at end of file
+}
diff --git a/build-logic/src/main/kotlin/Publishing.kt b/build-logic/src/main/kotlin/Publishing.kt
index de19c02211..3d3f30ad76 100644
--- a/build-logic/src/main/kotlin/Publishing.kt
+++ b/build-logic/src/main/kotlin/Publishing.kt
@@ -10,15 +10,16 @@ import org.gradle.api.tasks.TaskProvider
 import org.gradle.api.tasks.bundling.Jar
 import org.gradle.plugins.signing.Sign
 import org.gradle.plugins.signing.SigningExtension
+import org.jetbrains.dokka.gradle.AbstractDokkaTask
 import org.jetbrains.dokka.gradle.DokkaTask
 import org.jetbrains.dokka.gradle.DokkaTaskPartial
 
 fun Project.configurePublishing() {
   apply {
-    it.plugin("signing")
+    plugin("signing")
   }
   apply {
-    it.plugin("maven-publish")
+    plugin("maven-publish")
   }
 
   pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
@@ -27,24 +28,28 @@ fun Project.configurePublishing() {
   pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
     configureDokka()
   }
-  // Not sure if we still need that afterEvaluate
-  afterEvaluate {
-    configurePublishingDelayed()
-  }
+  configurePublishingInternal()
 }
 
 fun Project.configureDokka() {
   apply {
-    it.plugin("org.jetbrains.dokka")
+    plugin("org.jetbrains.dokka")
   }
 
   tasks.withType(DokkaTask::class.java).configureEach {
     //https://github.com/Kotlin/dokka/issues/1455
-    it.dependsOn("assemble")
+    dependsOn("assemble")
   }
   tasks.withType(DokkaTaskPartial::class.java).configureEach {
     //https://github.com/Kotlin/dokka/issues/1455
-    it.dependsOn("assemble")
+    dependsOn("assemble")
+  }
+
+  tasks.withType(AbstractDokkaTask::class.java).configureEach {
+    pluginConfiguration<org.jetbrains.dokka.base.DokkaBase, org.jetbrains.dokka.base.DokkaBaseConfiguration> {
+      customAssets = listOf("apollo.svg").map { rootProject.file("dokka/$it") }
+      customStyleSheets = listOf("style.css", "prism.css", "logo-styles.css").map { rootProject.file("dokka/$it") }
+    }
   }
 }
 
@@ -74,19 +79,19 @@ fun Project.getOssStagingUrl(): String {
   }
 }
 
-private fun Project.configurePublishingDelayed() {
+private fun Project.configurePublishingInternal() {
   /**
    * Javadoc
    */
   val dokkaJarTaskProvider = tasks.register("defaultJavadocJar", org.gradle.jvm.tasks.Jar::class.java) {
-    it.archiveClassifier.set("javadoc")
+    archiveClassifier.set("javadoc")
 
     runCatching {
-      it.from(tasks.named("dokkaHtml").flatMap { (it as DokkaTask).outputDirectory })
+      from(tasks.named("dokkaHtml").flatMap { (it as DokkaTask).outputDirectory })
     }
   }
   val emptyJavadocJarTaskProvider = tasks.register("emptyJavadocJar", org.gradle.jvm.tasks.Jar::class.java) {
-    it.archiveClassifier.set("javadoc")
+    archiveClassifier.set("javadoc")
   }
 
   /**
@@ -100,73 +105,76 @@ private fun Project.configurePublishingDelayed() {
   }
 
   tasks.withType(Jar::class.java) {
-    it.manifest {
-      it.attributes["Built-By"] = findProperty("POM_DEVELOPER_ID") as String?
-      it.attributes["Build-Jdk"] = "${System.getProperty("java.version")} (${System.getProperty("java.vendor")} ${System.getProperty("java.vm.version")})"
-      it.attributes["Created-By"] = "Gradle ${gradle.gradleVersion}"
-      it.attributes["Implementation-Title"] = findProperty("POM_NAME") as String?
-      it.attributes["Implementation-Version"] = findProperty("VERSION_NAME") as String?
+    manifest {
+      attributes["Built-By"] = findProperty("POM_DEVELOPER_ID") as String?
+      attributes["Build-Jdk"] = "${System.getProperty("java.version")} (${System.getProperty("java.vendor")} ${System.getProperty("java.vm.version")})"
+      attributes["Created-By"] = "Gradle ${gradle.gradleVersion}"
+      attributes["Implementation-Title"] = findProperty("POM_NAME") as String?
+      attributes["Implementation-Version"] = findProperty("VERSION_NAME") as String?
     }
   }
 
-  extensions.configure(PublishingExtension::class.java) { publishingExtension ->
-    publishingExtension.publications { publicationContainer ->
+  extensions.configure(PublishingExtension::class.java) {
+    publications {
       when {
         plugins.hasPlugin("org.jetbrains.kotlin.multiplatform") -> {
           /**
            * Kotlin MPP creates nice publications.
            * It only misses the javadoc
            */
-          publicationContainer.withType(MavenPublication::class.java).configureEach {
-            if (it.name == "kotlinMultiplatform") {
+          withType(MavenPublication::class.java).configureEach {
+            if (name == "kotlinMultiplatform") {
               // Add the javadoc to the multiplatform publications
-              it.artifact(javadocJarTaskProvider.get())
+              artifact(javadocJarTaskProvider.get())
             } else {
               // And an empty one for others so as to save some space
-              it.artifact(emptyJavadocJarTaskProvider.get())
+              artifact(emptyJavadocJarTaskProvider.get())
             }
           }
         }
+
         plugins.hasPlugin("java-gradle-plugin") -> {
           /**
            * java-gradle-plugin creates 2 publications (one marker and one regular) but without source/javadoc.
            */
-          publicationContainer.withType(MavenPublication::class.java) { mavenPublication ->
-            mavenPublication.artifact(javadocJarTaskProvider.get())
+          withType(MavenPublication::class.java) {
+            artifact(javadocJarTaskProvider.get())
             // Only add sources for the main publication
             // XXX: is there a nicer way to do this?
-            if (!mavenPublication.name.lowercase().contains("marker")) {
-              mavenPublication.artifact(createJavaSourcesTask())
+            if (!name.lowercase().contains("marker")) {
+              artifact(createJavaSourcesTask())
             }
           }
         }
+
         extensions.findByName("android") != null -> {
           /**
            * Android projects do not create publications (yet?). Do it ourselves.
            */
-          publicationContainer.create("default", MavenPublication::class.java) { mavenPublication ->
+          create("default", MavenPublication::class.java) {
             afterEvaluate {
               // afterEvaluate is required for Android
-              mavenPublication.from(components.findByName("release"))
+              from(components.findByName("release"))
             }
 
-            mavenPublication.artifact(javadocJarTaskProvider.get())
-            mavenPublication.artifact(createAndroidSourcesTask().get())
+            artifact(javadocJarTaskProvider.get())
+            artifact(createAndroidSourcesTask().get())
 
-            mavenPublication.artifactId = findProperty("POM_ARTIFACT_ID") as String?
+            artifactId = findProperty("POM_ARTIFACT_ID") as String?
           }
         }
+
         else -> {
           /**
            * Kotlin JVM do not create publications (yet?). Do it ourselves.
            */
-          publicationContainer.create("default", MavenPublication::class.java) { mavenPublication ->
+          create("default", MavenPublication::class.java) {
 
-            mavenPublication.from(components.findByName("java"))
-            mavenPublication.artifact(javadocJarTaskProvider.get())
-            mavenPublication.artifact(createJavaSourcesTask().get())
+            from(components.findByName("java"))
+            artifact(javadocJarTaskProvider.get())
+            artifact(createJavaSourcesTask().get())
 
-            mavenPublication.artifactId = findProperty("POM_ARTIFACT_ID") as String?
+            artifactId = findProperty("POM_ARTIFACT_ID") as String?
           }
         }
       }
@@ -174,57 +182,57 @@ private fun Project.configurePublishingDelayed() {
       /**
        * In all cases, configure the pom
        */
-      publicationContainer.withType(MavenPublication::class.java).configureEach { mavenPublication ->
-        setDefaultPomFields(mavenPublication)
+      withType(MavenPublication::class.java).configureEach {
+        setDefaultPomFields(this)
       }
     }
 
-    publishingExtension.repositories { repositoryHandler ->
-      repositoryHandler.maven { repository ->
-        repository.name = "pluginTest"
-        repository.url = uri("file://${rootProject.buildDir}/localMaven")
+    repositories {
+      maven {
+        name = "pluginTest"
+        url = uri("file://${rootProject.buildDir}/localMaven")
       }
 
-      repositoryHandler.maven { repository ->
-        repository.name = "ossSnapshots"
-        repository.url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
-        repository.credentials { credentials ->
-          credentials.username = System.getenv("SONATYPE_NEXUS_USERNAME")
-          credentials.password = System.getenv("SONATYPE_NEXUS_PASSWORD")
+      maven {
+        name = "ossSnapshots"
+        url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
+        credentials {
+          username = System.getenv("SONATYPE_NEXUS_USERNAME")
+          password = System.getenv("SONATYPE_NEXUS_PASSWORD")
         }
       }
 
-      repositoryHandler.maven { repository ->
-        repository.name = "ossStaging"
-        repository.setUrl {
+      maven {
+        name = "ossStaging"
+        setUrl {
           uri(rootProject.getOssStagingUrl())
         }
-        repository.credentials { credentials ->
-          credentials.username = System.getenv("SONATYPE_NEXUS_USERNAME")
-          credentials.password = System.getenv("SONATYPE_NEXUS_PASSWORD")
+        credentials {
+          username = System.getenv("SONATYPE_NEXUS_USERNAME")
+          password = System.getenv("SONATYPE_NEXUS_PASSWORD")
         }
       }
 
-      repositoryHandler.maven { repository ->
-        repository.name = "repsy"
-        repository.setUrl("https://repo.repsy.io/mvn/mbonnin/default")
-        repository.credentials { credentials ->
-          credentials.username = System.getenv("REPSY_USERNAME")
-          credentials.password = System.getenv("REPSY_PASSWORD")
+      maven {
+        name = "repsy"
+        setUrl("https://repo.repsy.io/mvn/mbonnin/default")
+        credentials {
+          username = System.getenv("REPSY_USERNAME")
+          password = System.getenv("REPSY_PASSWORD")
         }
       }
     }
   }
 
-  extensions.configure(SigningExtension::class.java) { signingExtension ->
+  extensions.configure(SigningExtension::class.java) {
     // GPG_PRIVATE_KEY should contain the armoured private key that starts with -----BEGIN PGP PRIVATE KEY BLOCK-----
     // It can be obtained with gpg --armour --export-secret-keys KEY_ID
-    signingExtension.useInMemoryPgpKeys(System.getenv("GPG_PRIVATE_KEY"), System.getenv("GPG_PRIVATE_KEY_PASSWORD"))
+    useInMemoryPgpKeys(System.getenv("GPG_PRIVATE_KEY"), System.getenv("GPG_PRIVATE_KEY_PASSWORD"))
     val publicationsContainer = (extensions.getByName("publishing") as PublishingExtension).publications
-    signingExtension.sign(publicationsContainer)
+    sign(publicationsContainer)
   }
   tasks.withType(Sign::class.java).configureEach {
-    it.isEnabled = !System.getenv("GPG_PRIVATE_KEY").isNullOrBlank()
+    isEnabled = !System.getenv("GPG_PRIVATE_KEY").isNullOrBlank()
   }
 }
 
@@ -236,57 +244,57 @@ private fun Project.setDefaultPomFields(mavenPublication: MavenPublication) {
   mavenPublication.groupId = findProperty("GROUP") as String?
   mavenPublication.version = findProperty("VERSION_NAME") as String?
 
-  mavenPublication.pom { mavenPom ->
-    mavenPom.name.set(findProperty("POM_NAME") as String?)
+  mavenPublication.pom {
+    name.set(findProperty("POM_NAME") as String?)
     (findProperty("POM_PACKAGING") as String?)?.let {
       // Do not overwrite packaging if already set by the multiplatform plugin
-      mavenPom.packaging = it
+      packaging = it
     }
 
-    mavenPom.description.set(findProperty("POM_DESCRIPTION") as String?)
-    mavenPom.url.set(findProperty("POM_URL") as String?)
+    description.set(findProperty("POM_DESCRIPTION") as String?)
+    url.set(findProperty("POM_URL") as String?)
 
-    mavenPom.scm { scm ->
-      scm.url.set(findProperty("POM_SCM_URL") as String?)
-      scm.connection.set(findProperty("POM_SCM_CONNECTION") as String?)
-      scm.developerConnection.set(findProperty("POM_SCM_DEV_CONNECTION") as String?)
+    scm {
+      url.set(findProperty("POM_SCM_URL") as String?)
+      connection.set(findProperty("POM_SCM_CONNECTION") as String?)
+      developerConnection.set(findProperty("POM_SCM_DEV_CONNECTION") as String?)
     }
 
-    mavenPom.licenses { licenseSpec ->
-      licenseSpec.license { license ->
-        license.name.set(findProperty("POM_LICENCE_NAME") as String?)
-        license.url.set(findProperty("POM_LICENCE_URL") as String?)
+    licenses {
+      license {
+        name.set(findProperty("POM_LICENCE_NAME") as String?)
+        url.set(findProperty("POM_LICENCE_URL") as String?)
       }
     }
 
-    mavenPom.developers { developerSpec ->
-      developerSpec.developer { developer ->
-        developer.id.set(findProperty("POM_DEVELOPER_ID") as String?)
-        developer.name.set(findProperty("POM_DEVELOPER_NAME") as String?)
+    developers {
+      developer {
+        id.set(findProperty("POM_DEVELOPER_ID") as String?)
+        name.set(findProperty("POM_DEVELOPER_NAME") as String?)
       }
     }
   }
 }
 
 private fun Project.createJavaSourcesTask(): TaskProvider<Jar> {
-  return tasks.register("javaSourcesJar", Jar::class.java) { jar ->
+  return tasks.register("javaSourcesJar", Jar::class.java) {
     /**
      * Add a dependency on the compileKotlin task to make sure the generated sources like
      * antlr or SQLDelight get included
      * See also https://youtrack.jetbrains.com/issue/KT-47936
      */
-    jar.dependsOn("compileKotlin")
+    dependsOn("compileKotlin")
 
-    jar.archiveClassifier.set("sources")
+    archiveClassifier.set("sources")
     val sourceSets = project.extensions.getByType(JavaPluginExtension::class.java).sourceSets
-    jar.from(sourceSets.getByName("main").allSource)
+    from(sourceSets.getByName("main").allSource)
   }
 }
 
 private fun Project.createAndroidSourcesTask(): TaskProvider<Jar> {
-  return tasks.register("javaSourcesJar", Jar::class.java) { jar ->
-    val android = extensions.findByName("android") as BaseExtension
-    jar.from(android.sourceSets.getByName("main").java.getSourceFiles())
-    jar.archiveClassifier.set("sources")
+  return tasks.register("javaSourcesJar", Jar::class.java) {
+    val android = project.extensions.findByName("android") as BaseExtension
+    from(android.sourceSets.getByName("main").java.getSourceFiles())
+    archiveClassifier.set("sources")
   }
 }
diff --git a/build-logic/src/main/kotlin/Repositories.kt b/build-logic/src/main/kotlin/Repositories.kt
new file mode 100644
index 0000000000..16398af720
--- /dev/null
+++ b/build-logic/src/main/kotlin/Repositories.kt
@@ -0,0 +1,15 @@
+import org.gradle.api.Project
+
+fun Project.configureRepositories() {
+  repositories.apply {
+    mavenCentral()
+    google()
+    @Suppress("DEPRECATION")
+    jcenter {
+      content {
+        // https://github.com/Kotlin/kotlinx-nodejs/issues/16
+        includeModule("org.jetbrains.kotlinx", "kotlinx-nodejs")
+      }
+    }
+  }
+}
diff --git a/build-logic/src/main/kotlin/com/apollographql/apollo3/buildlogic/plugin/LibraryConventionPlugin.kt b/build-logic/src/main/kotlin/com/apollographql/apollo3/buildlogic/plugin/LibraryConventionPlugin.kt
new file mode 100644
index 0000000000..4da36a126f
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/apollographql/apollo3/buildlogic/plugin/LibraryConventionPlugin.kt
@@ -0,0 +1,67 @@
+package com.apollographql.apollo3.buildlogic.plugin
+
+import configureJavaAndKotlinCompilers
+import configureMppDefaults
+import configurePublishing
+import configureRepositories
+import configureTesting
+import org.gradle.api.Action
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Nested
+import org.gradle.api.tasks.bundling.Jar
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+class LibraryConventionPlugin : Plugin<Project> {
+  override fun apply(project: Project) {
+    with(project) {
+      group = property("GROUP")!!
+      version = property("VERSION_NAME")!!
+
+      configureRepositories()
+
+      extensions.create("apolloLibrary", Extension::class.java)
+
+      configureJavaAndKotlinCompilers(treatWarningsAsErrors = true)
+
+      configureTesting()
+
+      configurePublishing()
+    }
+  }
+
+  abstract class Extension(private val project: Project) {
+    interface MppConfiguration {
+      val withJs: Property<Boolean>
+      val withLinux: Property<Boolean>
+    }
+
+    @get:Nested
+    abstract val mppConfiguration: MppConfiguration
+
+    fun mpp(action: Action<MppConfiguration>) {
+      action.execute(mppConfiguration)
+      project.configureMppDefaults(
+          withJs = mppConfiguration.withJs.getOrElse(true),
+          withLinux = mppConfiguration.withLinux.getOrElse(true)
+      )
+    }
+
+    fun javaModuleName(javaModuleName: String) {
+      project.tasks.withType(Jar::class.java).configureEach {
+        manifest {
+          attributes(mapOf("Automatic-Module-Name" to javaModuleName))
+        }
+      }
+    }
+
+    fun treatWarningsAsErrors(treatWarningsAsErrors: Boolean) {
+      project.tasks.withType(KotlinCompile::class.java).configureEach {
+        kotlinOptions {
+          allWarningsAsErrors = treatWarningsAsErrors
+        }
+      }
+    }
+  }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index d8a7969264..f41ecf9e4c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,4 @@
 import JapiCmp.configureJapiCmp
-import kotlinx.validation.sourceSets
-import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import org.jetbrains.dokka.gradle.DokkaTaskPartial
 
 buildscript {
   repositories {
@@ -20,56 +17,6 @@ apply(plugin = "org.jetbrains.kotlinx.binary-compatibility-validator")
 
 version = property("VERSION_NAME")!!
 
-subprojects {
-  configureJavaAndKotlinCompilers()
-
-  tasks.withType<Test> {
-    systemProperty("updateTestFixtures", System.getProperty("updateTestFixtures"))
-    systemProperty("testFilter", System.getProperty("testFilter"))
-    systemProperty("codegenModels", System.getProperty("codegenModels"))
-  }
-  tasks.withType<AbstractTestTask> {
-    testLogging {
-      exceptionFormat = TestExceptionFormat.FULL
-    }
-  }
-
-  repositories {
-    mavenCentral()
-    google()
-    jcenter {
-      content {
-        // https://github.com/Kotlin/kotlinx-nodejs/issues/16
-        includeModule("org.jetbrains.kotlinx", "kotlinx-nodejs")
-      }
-    }
-  }
-
-  group = property("GROUP")!!
-  version = property("VERSION_NAME")!!
-
-  configurePublishing()
-
-  /**
-   * Type `echo "apollographql_android_hack=true\n" >> ~/.gradle/gradle.properties` on your development machine
-   * to make MPP modules publish an Android artifact so that IntelliJ can resolve the symbols
-   *
-   * See https://youtrack.jetbrains.com/issue/KTIJ-14471
-   */
-  if (properties["apollographql_android_hack"] == "true") {
-    pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") {
-      // Hack for autocomplete to work with android projects
-      // See https://youtrack.jetbrains.com/issue/KTIJ-14471
-      if (System.getProperty("idea.sync.active") != null) {
-        apply(plugin = "com.android.library")
-        (extensions.findByName("kotlin") as? org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension)?.apply {
-          android()
-        }
-      }
-    }
-  }
-}
-
 fun subprojectTasks(name: String): List<Task> {
   return subprojects.flatMap { subproject ->
     subproject.tasks.matching { it.name == name }
@@ -166,15 +113,6 @@ tasks.named("dokkaHtmlMultiModule").configure {
   outputDirectory.set(buildDir.resolve("dokkaHtml/kdoc"))
 }
 
-allprojects {
-  tasks.withType<org.jetbrains.dokka.gradle.AbstractDokkaTask>().configureEach {
-    pluginConfiguration<org.jetbrains.dokka.base.DokkaBase, org.jetbrains.dokka.base.DokkaBaseConfiguration> {
-      customAssets = listOf("apollo.svg").map { rootProject.file("dokka/$it") }
-      customStyleSheets = listOf("style.css", "prism.css", "logo-styles.css").map { rootProject.file("dokka/$it") }
-    }
-  }
-}
-
 tasks.named("dependencyUpdates").configure {
   (this as com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask)
   rejectVersionIf {
diff --git a/gradle.properties b/gradle.properties
index 1c4a144ce9..b2a40f0b17 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -27,3 +27,7 @@ kotlin.stdlib.default.dependency=false
 # Enable compatibility with non-hierarchical projects.
 # See https://kotlinlang.org/docs/multiplatform-hierarchy.html#for-library-authors
 kotlin.mpp.enableCompatibilityMetadataVariant=true
+
+# Enable the build cache
+# See https://docs.gradle.org/current/userguide/build_cache.html
+org.gradle.caching=true
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fb496c6aad..8516179957 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,9 +14,12 @@ includeBuild("build-logic")
 enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
 
 pluginManagement {
+  includeBuild("build-logic")
+
   repositories {
     mavenCentral()
     google()
+    gradlePluginPortal()
   }
 }
 


Test patch:
diff --git a/apollo-testing-support/build.gradle.kts b/apollo-testing-support/build.gradle.kts
index cd47b22e61..ddb5b3afd2 100644
--- a/apollo-testing-support/build.gradle.kts
+++ b/apollo-testing-support/build.gradle.kts
@@ -1,8 +1,13 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.library")
 }
 
-configureMppDefaults(withLinux = false)
+apolloLibrary {
+  mpp {
+    withLinux.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
diff --git a/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestTokenProvider.kt b/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestTokenProvider.kt
index fa2cf788af..5ae3ed0123 100644
--- a/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestTokenProvider.kt
+++ b/apollo-testing-support/src/commonMain/kotlin/com/apollographql/apollo3/testing/TestTokenProvider.kt
@@ -1,11 +1,15 @@
+@file:Suppress("DEPRECATION")
+
 package com.apollographql.apollo3.testing
 
 import com.apollographql.apollo3.annotations.ApolloExperimental
 import com.apollographql.apollo3.network.http.TokenProvider
 
 @ApolloExperimental
-class TestTokenProvider(currentAccessToken: String,
-                        val newAccessToken: String) : TokenProvider {
+class TestTokenProvider(
+    currentAccessToken: String,
+    val newAccessToken: String,
+) : TokenProvider {
   var accessToken = currentAccessToken
   override suspend fun currentToken(): String {
     return accessToken
@@ -15,4 +19,4 @@ class TestTokenProvider(currentAccessToken: String,
     accessToken = newAccessToken
     return accessToken
   }
-}
\ No newline at end of file
+}
diff --git a/build-logic/src/main/kotlin/Testing.kt b/build-logic/src/main/kotlin/Testing.kt
new file mode 100644
index 0000000000..a6d5d51787
--- /dev/null
+++ b/build-logic/src/main/kotlin/Testing.kt
@@ -0,0 +1,22 @@
+import org.gradle.api.Project
+import org.gradle.api.tasks.testing.AbstractTestTask
+import org.gradle.api.tasks.testing.Test
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.gradle.api.tasks.testing.logging.TestLogEvent
+
+fun Project.configureTesting() {
+  tasks.withType(Test::class.java) {
+    systemProperty("updateTestFixtures", System.getProperty("updateTestFixtures"))
+    systemProperty("testFilter", System.getProperty("testFilter"))
+    systemProperty("codegenModels", System.getProperty("codegenModels"))
+  }
+
+  tasks.withType(AbstractTestTask::class.java) {
+    testLogging {
+      exceptionFormat = TestExceptionFormat.FULL
+      events.add(TestLogEvent.PASSED)
+      events.add(TestLogEvent.FAILED)
+      showStandardStreams = true
+    }
+  }
+}
diff --git a/build-logic/src/main/kotlin/com/apollographql/apollo3/buildlogic/plugin/TestConventionPlugin.kt b/build-logic/src/main/kotlin/com/apollographql/apollo3/buildlogic/plugin/TestConventionPlugin.kt
new file mode 100644
index 0000000000..02ba267f38
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/apollographql/apollo3/buildlogic/plugin/TestConventionPlugin.kt
@@ -0,0 +1,51 @@
+package com.apollographql.apollo3.buildlogic.plugin
+
+import configureJavaAndKotlinCompilers
+import configureMppTestsDefaults
+import configureRepositories
+import configureTesting
+import org.gradle.api.Action
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.SetProperty
+import org.gradle.api.tasks.Nested
+import workaroundForIssueKT51970
+
+class TestConventionPlugin : Plugin<Project> {
+  override fun apply(project: Project) {
+    with(project) {
+      configureRepositories()
+
+      extensions.create("apolloTest", Extension::class.java)
+
+      configureJavaAndKotlinCompilers()
+
+      configureTesting()
+    }
+  }
+
+  abstract class Extension(private val project: Project) {
+    interface MppConfiguration {
+      val withJs: Property<Boolean>
+      val withJvm: Property<Boolean>
+      val newMemoryManager: Property<Boolean>
+      val appleTargets: SetProperty<String>
+    }
+
+    @get:Nested
+    abstract val mppConfiguration: MppConfiguration
+
+    fun mpp(action: Action<MppConfiguration>) {
+      action.execute(mppConfiguration)
+      project.configureMppTestsDefaults(
+          withJs = mppConfiguration.withJs.getOrElse(true),
+          withJvm = mppConfiguration.withJvm.getOrElse(true),
+          newMemoryManager = mppConfiguration.newMemoryManager.getOrElse(true),
+          appleTargets = mppConfiguration.appleTargets.get().ifEmpty { setOf("macosX64", "macosArm64") },
+      )
+
+      project.workaroundForIssueKT51970()
+    }
+  }
+}
diff --git a/tests/build.gradle.kts b/tests/build.gradle.kts
index ad44b7b42c..468c862836 100644
--- a/tests/build.gradle.kts
+++ b/tests/build.gradle.kts
@@ -15,46 +15,6 @@ repositories {
   mavenCentral()
 }
 
-subprojects {
-  repositories {
-    mavenCentral()
-    google()
-    jcenter {
-      content {
-        // https://github.com/Kotlin/kotlinx-nodejs/issues/16
-        includeModule("org.jetbrains.kotlinx", "kotlinx-nodejs")
-      }
-    }
-  }
-
-  // Workaround for https://youtrack.jetbrains.com/issue/KT-51970
-  afterEvaluate {
-    afterEvaluate {
-      tasks.configureEach {
-        if (
-            name.startsWith("compile")
-            && name.endsWith("KotlinMetadata")
-        ) {
-          enabled = false
-        }
-      }
-    }
-  }
-
-  configureJavaAndKotlinCompilers()
-
-  afterEvaluate {
-    tasks.withType<AbstractTestTask> {
-      testLogging {
-        exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
-        events.add(org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED)
-        events.add(org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED)
-        showStandardStreams = true
-      }
-    }
-  }
-}
-
 // See https://youtrack.jetbrains.com/issue/KT-49109#focus=Comments-27-5259190.0-0
 rootProject.configureNode()
 
diff --git a/tests/coroutines-mt/build.gradle.kts b/tests/coroutines-mt/build.gradle.kts
index 158d42a4dc..4ca4fa2d31 100644
--- a/tests/coroutines-mt/build.gradle.kts
+++ b/tests/coroutines-mt/build.gradle.kts
@@ -1,13 +1,17 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-kotlin {
-  configureAppleTargets("macosX64", "macosArm64")
-
-  enableNewMemoryManager()
+apolloTest {
+  mpp {
+    withJs.set(false)
+    withJvm.set(false)
+  }
+}
 
+kotlin {
   sourceSets {
     val commonMain by getting {
       dependencies {
@@ -18,13 +22,6 @@ kotlin {
         implementation(libs.apollo.runtime)
       }
     }
-
-    val commonTest by getting {
-      dependencies {
-        implementation(libs.kotlin.test.common)
-        implementation(libs.kotlin.test.annotations.common)
-      }
-    }
   }
 }
 
@@ -36,6 +33,7 @@ configurations {
     }
   }
 }
+
 apollo {
   packageName.set("macos.app")
   generateDataBuilders.set(true)
diff --git a/tests/data-builders/build.gradle.kts b/tests/data-builders/build.gradle.kts
index 7ed881e17e..705e38bc71 100644
--- a/tests/data-builders/build.gradle.kts
+++ b/tests/data-builders/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
-  id("com.apollographql.apollo3")
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
+  id("com.apollographql.apollo3")
 }
 
 dependencies {
@@ -15,4 +16,4 @@ apollo {
   addTypename.set("always")
   mapScalar("Long1", "MyLong", "MyLongAdapter")
   mapScalar("Long2", "MyLong")
-}
\ No newline at end of file
+}
diff --git a/tests/defer/build.gradle.kts b/tests/defer/build.gradle.kts
index b29f99dd53..3877bb7aac 100644
--- a/tests/defer/build.gradle.kts
+++ b/tests/defer/build.gradle.kts
@@ -1,9 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults()
+apolloTest {
+  mpp {}
+}
 
 kotlin {
   /**
@@ -25,7 +28,6 @@ kotlin {
       dependencies {
         implementation(libs.apollo.mockserver)
         implementation(libs.apollo.testingsupport)
-        implementation(libs.kotlin.test)
       }
     }
 
diff --git a/tests/deprecated-requires-opt-in/build.gradle.kts b/tests/deprecated-requires-opt-in/build.gradle.kts
index 57f16701b9..a57695664d 100644
--- a/tests/deprecated-requires-opt-in/build.gradle.kts
+++ b/tests/deprecated-requires-opt-in/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
-  id("com.apollographql.apollo3.external")
+  id("apollo.test")
+  id("com.apollographql.apollo3")
 }
 
 dependencies {
diff --git a/tests/enums/build.gradle.kts b/tests/enums/build.gradle.kts
index 64b23ca080..7cc1f4d3a2 100644
--- a/tests/enums/build.gradle.kts
+++ b/tests/enums/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/escaping/build.gradle.kts b/tests/escaping/build.gradle.kts
index 038c12261e..fa48bf1b99 100644
--- a/tests/escaping/build.gradle.kts
+++ b/tests/escaping/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/filesystem-sensitivity/build.gradle.kts b/tests/filesystem-sensitivity/build.gradle.kts
index 88a322f9fd..fb2d529b8a 100644
--- a/tests/filesystem-sensitivity/build.gradle.kts
+++ b/tests/filesystem-sensitivity/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/gradle.properties b/tests/gradle.properties
index b08d52b68e..185da1621b 100644
--- a/tests/gradle.properties
+++ b/tests/gradle.properties
@@ -6,3 +6,7 @@ kotlin.mpp.stability.nowarn=true
 xcodeproj=samples/multiplatform/kmp-ios-app/kmp-ios-app.xcodeproj
 
 android.useAndroidX=true
+
+# Enable the build cache
+# See https://docs.gradle.org/current/userguide/build_cache.html
+org.gradle.caching=true
diff --git a/tests/http-cache/build.gradle.kts b/tests/http-cache/build.gradle.kts
index c97b4ef736..04613ef4f3 100644
--- a/tests/http-cache/build.gradle.kts
+++ b/tests/http-cache/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/idling-resource/build.gradle.kts b/tests/idling-resource/build.gradle.kts
index abdc65ccc0..afb7a32eb7 100644
--- a/tests/idling-resource/build.gradle.kts
+++ b/tests/idling-resource/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
   id("com.android.library")
   id("org.jetbrains.kotlin.android")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/include-skip-operation-based/build.gradle.kts b/tests/include-skip-operation-based/build.gradle.kts
index 9630d2545f..187c3131cb 100644
--- a/tests/include-skip-operation-based/build.gradle.kts
+++ b/tests/include-skip-operation-based/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/integration-tests/build.gradle.kts b/tests/integration-tests/build.gradle.kts
index 607445da36..4101c0e6e6 100644
--- a/tests/integration-tests/build.gradle.kts
+++ b/tests/integration-tests/build.gradle.kts
@@ -1,9 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults()
+apolloTest {
+  mpp {}
+}
 
 kotlin {
   /**
@@ -65,11 +68,13 @@ fun configureApollo(generateKotlinModels: Boolean) {
                     "Date" to "kotlinx.datetime.LocalDate"
                 ))
               }
+
               "upload" -> {
                 customScalarsMapping.set(mapOf(
                     "Upload" to "com.apollographql.apollo3.api.Upload"
                 ))
               }
+
               "normalizer" -> {
                 generateFragmentImplementations.set(true)
                 mapScalar("Date", "kotlinx.datetime.LocalDate")
@@ -80,6 +85,7 @@ fun configureApollo(generateKotlinModels: Boolean) {
                 }
                 sealedClassesForEnumsMatching.set(listOf("Episode"))
               }
+
               "fullstack" -> {
                 customScalarsMapping.set(mapOf(
                     "Date" to "com.example.MyDate"
@@ -116,6 +122,7 @@ fun configureApollo(generateKotlinModels: Boolean) {
         }
   }
 }
+
 fun com.apollographql.apollo3.gradle.api.Service.configureConnection(generateKotlinModels: Boolean) {
   outputDirConnection {
     if (System.getProperty("idea.sync.active") == null) {
diff --git a/tests/ios-test/build.gradle.kts b/tests/ios-test/build.gradle.kts
index b1391533d4..c81afceba4 100644
--- a/tests/ios-test/build.gradle.kts
+++ b/tests/ios-test/build.gradle.kts
@@ -1,11 +1,18 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
+apolloTest {
+  mpp {
+    withJs.set(false)
+    withJvm.set(false)
+    appleTargets.set(setOf("iosArm64", "iosX64"))
+  }
+}
+
 kotlin {
-  configureAppleTargets("iosArm64", "iosX64")
-  enableNewMemoryManager()
   sourceSets {
     val commonMain by getting {
       dependencies {
@@ -16,7 +23,6 @@ kotlin {
 
     val commonTest by getting {
       dependencies {
-        implementation(libs.kotlin.test.junit)
         implementation(libs.apollo.testingsupport)
       }
     }
diff --git a/tests/java-tests/build.gradle.kts b/tests/java-tests/build.gradle.kts
index 6cf3154df8..e761d2da7c 100644
--- a/tests/java-tests/build.gradle.kts
+++ b/tests/java-tests/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("java")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/jpms/build.gradle.kts b/tests/jpms/build.gradle.kts
index 8b2a6d93e1..9c1fc6be06 100644
--- a/tests/jpms/build.gradle.kts
+++ b/tests/jpms/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
   id("java")
   id("application")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/jvmoverloads/build.gradle.kts b/tests/jvmoverloads/build.gradle.kts
index 13e436a835..6614208864 100644
--- a/tests/jvmoverloads/build.gradle.kts
+++ b/tests/jvmoverloads/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/kotlin-codegen/build.gradle.kts b/tests/kotlin-codegen/build.gradle.kts
index 369b773d42..ae46d3b6cf 100644
--- a/tests/kotlin-codegen/build.gradle.kts
+++ b/tests/kotlin-codegen/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/legacy-memory-model/build.gradle.kts b/tests/legacy-memory-model/build.gradle.kts
index ca33a0bee3..3e8ceca4b6 100644
--- a/tests/legacy-memory-model/build.gradle.kts
+++ b/tests/legacy-memory-model/build.gradle.kts
@@ -1,28 +1,27 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-kotlin {
-  configureAppleTargets("macosX64", "macosArm64")
+apolloTest {
+  mpp {
+    withJs.set(false)
+    withJvm.set(false)
+    newMemoryManager.set(false)
+  }
+}
 
+kotlin {
   sourceSets {
     val commonMain by getting {
       dependencies {
         implementation(libs.apollo.runtime)
       }
     }
-
-    val commonTest by getting {
-      dependencies {
-        implementation(libs.kotlin.test.common)
-        implementation(libs.kotlin.test.annotations.common)
-      }
-    }
   }
 }
 
 apollo {
   packageName.set("test")
 }
-
diff --git a/tests/models-compat/build.gradle.kts b/tests/models-compat/build.gradle.kts
index 3ed752443a..c3586690c9 100644
--- a/tests/models-compat/build.gradle.kts
+++ b/tests/models-compat/build.gradle.kts
@@ -2,10 +2,13 @@ import com.apollographql.apollo3.compiler.MODELS_COMPAT
 
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults()
+apolloTest {
+  mpp {}
+}
 
 kotlin {
   sourceSets {
diff --git a/tests/models-operation-based/build.gradle.kts b/tests/models-operation-based/build.gradle.kts
index 064c88a47e..673d3f4aa5 100644
--- a/tests/models-operation-based/build.gradle.kts
+++ b/tests/models-operation-based/build.gradle.kts
@@ -1,9 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults()
+apolloTest {
+  mpp {}
+}
 
 kotlin {
   /**
@@ -39,6 +42,7 @@ kotlin {
   }
 }
 
+
 fun configureApollo(generateKotlinModels: Boolean) {
   val extra = if (generateKotlinModels) "kotlin" else "java"
   apollo {
diff --git a/tests/models-response-based/build.gradle.kts b/tests/models-response-based/build.gradle.kts
index 6047477a2b..8526a91f10 100644
--- a/tests/models-response-based/build.gradle.kts
+++ b/tests/models-response-based/build.gradle.kts
@@ -1,9 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults()
+apolloTest {
+  mpp {}
+}
 
 kotlin {
   sourceSets {
diff --git a/tests/multi-module-child/build.gradle.kts b/tests/multi-module-child/build.gradle.kts
index 3c3e2dd55d..75cfc1024e 100644
--- a/tests/multi-module-child/build.gradle.kts
+++ b/tests/multi-module-child/build.gradle.kts
@@ -1,9 +1,9 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-
 dependencies {
   implementation(libs.apollo.runtime)
   implementation(projects.multiModuleRoot)
diff --git a/tests/multi-module-fragment-file-path/build.gradle.kts b/tests/multi-module-fragment-file-path/build.gradle.kts
index b675cd0d70..928295c8bc 100644
--- a/tests/multi-module-fragment-file-path/build.gradle.kts
+++ b/tests/multi-module-fragment-file-path/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/multi-module-fragment-schema-package/build.gradle.kts b/tests/multi-module-fragment-schema-package/build.gradle.kts
index 35f0f330a3..4fc0350cb3 100644
--- a/tests/multi-module-fragment-schema-package/build.gradle.kts
+++ b/tests/multi-module-fragment-schema-package/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/multi-module-root/build.gradle.kts b/tests/multi-module-root/build.gradle.kts
index ec6a87c63c..b252095dfe 100644
--- a/tests/multi-module-root/build.gradle.kts
+++ b/tests/multi-module-root/build.gradle.kts
@@ -1,9 +1,9 @@
 plugins {
-  id("com.apollographql.apollo3")
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
+  id("com.apollographql.apollo3")
 }
 
-
 dependencies {
   implementation(libs.apollo.runtime)
 }
diff --git a/tests/native-benchmarks/build.gradle.kts b/tests/native-benchmarks/build.gradle.kts
index ad0d64759a..55bb214d89 100644
--- a/tests/native-benchmarks/build.gradle.kts
+++ b/tests/native-benchmarks/build.gradle.kts
@@ -1,13 +1,17 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-kotlin {
-  configureAppleTargets("macosX64", "macosArm64")
-
-  enableNewMemoryManager()
+apolloTest {
+  mpp {
+    withJs.set(false)
+    withJvm.set(false)
+  }
+}
 
+kotlin {
   sourceSets {
     val commonMain by getting {
       dependencies {
@@ -18,9 +22,6 @@ kotlin {
 
     val commonTest by getting {
       dependencies {
-        implementation(libs.kotlin.test.common)
-        implementation(libs.kotlin.test.annotations.common)
-
         implementation(libs.apollo.testingsupport)
         implementation(libs.apollo.mockserver)
         implementation(libs.apollo.mpputils)
@@ -33,4 +34,3 @@ apollo {
   packageName.set("benchmarks")
   generateDataBuilders.set(true)
 }
-
diff --git a/tests/no-query-document/build.gradle.kts b/tests/no-query-document/build.gradle.kts
index 74e5e302bb..eec3c3478c 100644
--- a/tests/no-query-document/build.gradle.kts
+++ b/tests/no-query-document/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/no-runtime/build.gradle.kts b/tests/no-runtime/build.gradle.kts
index d3fd632b6c..ddfae247cb 100644
--- a/tests/no-runtime/build.gradle.kts
+++ b/tests/no-runtime/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/normalization-tests/build.gradle.kts b/tests/normalization-tests/build.gradle.kts
index 72d446e66a..b2dbcefd03 100644
--- a/tests/normalization-tests/build.gradle.kts
+++ b/tests/normalization-tests/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/optimistic-data/build.gradle.kts b/tests/optimistic-data/build.gradle.kts
index 38f20927e1..2a8db77627 100644
--- a/tests/optimistic-data/build.gradle.kts
+++ b/tests/optimistic-data/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/optional-variables/build.gradle.kts b/tests/optional-variables/build.gradle.kts
index 17b56f99bd..f564f9f9d2 100644
--- a/tests/optional-variables/build.gradle.kts
+++ b/tests/optional-variables/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/outofbounds/build.gradle.kts b/tests/outofbounds/build.gradle.kts
index 73a32fd850..d1834892c4 100644
--- a/tests/outofbounds/build.gradle.kts
+++ b/tests/outofbounds/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/pagination/build.gradle.kts b/tests/pagination/build.gradle.kts
index a9009ee87c..d2a8dacdfd 100644
--- a/tests/pagination/build.gradle.kts
+++ b/tests/pagination/build.gradle.kts
@@ -1,9 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults(withJs = false)
+apolloTest {
+  mpp {
+    withJs.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
diff --git a/tests/rxjava/build.gradle.kts b/tests/rxjava/build.gradle.kts
index e01ed7fedf..f79b60c668 100644
--- a/tests/rxjava/build.gradle.kts
+++ b/tests/rxjava/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/sample-server/build.gradle.kts b/tests/sample-server/build.gradle.kts
index 494e1ca654..d593600332 100644
--- a/tests/sample-server/build.gradle.kts
+++ b/tests/sample-server/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
   id("org.jetbrains.kotlin.jvm")
   id("org.jetbrains.kotlin.plugin.spring")
   id("application")
+  id("apollo.test")
 }
 
 dependencies {
diff --git a/tests/scalar-adapters/build.gradle.kts b/tests/scalar-adapters/build.gradle.kts
index c767c1eefe..bfeb724945 100644
--- a/tests/scalar-adapters/build.gradle.kts
+++ b/tests/scalar-adapters/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/schema-changes/build.gradle.kts b/tests/schema-changes/build.gradle.kts
index 6663a87a01..ffdcc34bea 100644
--- a/tests/schema-changes/build.gradle.kts
+++ b/tests/schema-changes/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/settings.gradle.kts b/tests/settings.gradle.kts
index 852ab3e737..2a49f6de2f 100644
--- a/tests/settings.gradle.kts
+++ b/tests/settings.gradle.kts
@@ -5,10 +5,20 @@ rootProject.projectDir
     .listFiles()!!
     .filter { it.isDirectory && File(it, "build.gradle.kts").exists() }
     .forEach {
-        include(it.name)
+      include(it.name)
     }
 
 includeBuild("../build-logic")
 includeBuild("../")
 
-enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
\ No newline at end of file
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+pluginManagement {
+  includeBuild("../build-logic")
+
+  repositories {
+    mavenCentral()
+    google()
+    gradlePluginPortal()
+  }
+}
diff --git a/tests/sqlite/build.gradle.kts b/tests/sqlite/build.gradle.kts
index c869b3a238..8f1154d9b2 100644
--- a/tests/sqlite/build.gradle.kts
+++ b/tests/sqlite/build.gradle.kts
@@ -1,9 +1,14 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults(withJs = false)
+apolloTest {
+  mpp {
+    withJs.set(false)
+  }
+}
 
 kotlin {
   sourceSets {
diff --git a/tests/termination/build.gradle.kts b/tests/termination/build.gradle.kts
index 0fcaa12d0c..42723f8f05 100644
--- a/tests/termination/build.gradle.kts
+++ b/tests/termination/build.gradle.kts
@@ -1,6 +1,7 @@
 plugins {
   id("application")
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/test-network-transport/build.gradle.kts b/tests/test-network-transport/build.gradle.kts
index 67c330ea72..a638a879b1 100644
--- a/tests/test-network-transport/build.gradle.kts
+++ b/tests/test-network-transport/build.gradle.kts
@@ -1,5 +1,6 @@
 plugins {
   id("org.jetbrains.kotlin.jvm")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
diff --git a/tests/websockets/build.gradle.kts b/tests/websockets/build.gradle.kts
index d9dc1adc28..40911a7b31 100644
--- a/tests/websockets/build.gradle.kts
+++ b/tests/websockets/build.gradle.kts
@@ -1,9 +1,12 @@
 plugins {
   id("org.jetbrains.kotlin.multiplatform")
+  id("apollo.test")
   id("com.apollographql.apollo3")
 }
 
-configureMppTestsDefaults()
+apolloTest {
+  mpp {}
+}
 
 kotlin {
   sourceSets {