diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt index b4f672b4d25..fdf16630095 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt @@ -23,22 +23,32 @@ import java.nio.file.Files internal object MuzzleMavenRepoUtils { private val log = Logging.getLogger(MuzzleMavenRepoUtils::class.java) private val backoffDelaysSeconds = listOf(5L, 10L, 30L) + private const val MAVEN_CENTRAL_URL = "https://repo1.maven.org/maven2/" /** * Remote repositories used to query version ranges and fetch dependencies. * + * When MAVEN_REPOSITORY_PROXY is set, the proxy *replaces* Maven Central instead of being + * prepended to it. Aether merges `maven-metadata.xml` from **every** repository of a + * [VersionRangeRequest] rather than stopping at the first hit, so keeping Central in the list + * would issue a request to repo1.maven.org for each muzzle directive even when the proxy + * already serves it -- exactly the Maven Central rate limiting the proxy exists to avoid. + * The trade-off is that a proxy miss now fails instead of silently falling back to Central. + * * This intentionally reads the environment on each access: Gradle daemons can * be reused across builds with different MAVEN_REPOSITORY_PROXY values. */ @JvmStatic - fun defaultMuzzleRepos(): List { - val central = RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build() - val mavenProxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY") - return if (mavenProxyUrl == null) { - listOf(central) + fun defaultMuzzleRepos(): List = + defaultMuzzleRepos(System.getenv("MAVEN_REPOSITORY_PROXY")) + + @JvmStatic + fun defaultMuzzleRepos(mavenProxyUrl: String?): List { + val proxyUrl = mavenProxyUrl?.takeIf { it.isNotBlank() } + return if (proxyUrl == null) { + listOf(RemoteRepository.Builder("central", "default", MAVEN_CENTRAL_URL).build()) } else { - val proxy = RemoteRepository.Builder("central-proxy", "default", mavenProxyUrl).build() - listOf(proxy, central) + listOf(RemoteRepository.Builder("central-proxy", "default", proxyUrl).build()) } } @@ -173,8 +183,13 @@ internal object MuzzleMavenRepoUtils { } } + // Backoff only buys anything when a repository can be *transiently* unavailable. Local + // file: repositories (test fixtures, `mvn install` output) cannot, so sleeping 45s on them + // is pure waste. + val backoffApplies = enableBackoffRetries && rangeRequest.repositories.any { !it.url.startsWith("file:") } + var waitedSeconds = 0L - if (enableBackoffRetries) { + if (backoffApplies) { for (delaySeconds in backoffDelaysSeconds) { sleepBeforeBackoffRetry(delaySeconds, directiveArtifact) waitedSeconds += delaySeconds @@ -196,7 +211,7 @@ internal object MuzzleMavenRepoUtils { failure, attemptCount, waitedSeconds, - enableBackoffRetries + backoffApplies ), failure ) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt index b89ee5937a9..9829d48d01e 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixture.kt @@ -21,6 +21,9 @@ open class GradleFixture { private val testKitDir: File get() = sharedTestKitDir + private val repositoryProxyInitScript: File + get() = File(testKitDir, "repository-proxy.init.gradle") + companion object { // JVM-wide testkit dir shared across all GradleFixture instances. One daemon // pool serves every test method, so kotlinc work on .gradle.kts scripts is @@ -35,10 +38,17 @@ open class GradleFixture { // fields. private val sharedTestKitDir: File by lazy { Files.createTempDirectory("gradle-testkit-").toFile().also { dir -> + // Register the cleanup hook before anything that can throw, so a failure below + // does not leak the temp dir. Runtime.getRuntime().addShutdownHook(Thread { stopDaemonsIn(dir) dir.deleteRecursively() }) + val initScript = File(dir, "repository-proxy.init.gradle") + GradleFixture::class.java + .getResourceAsStream("/repository-proxy.init.gradle") + ?.use { input -> initScript.outputStream().use(input::copyTo) } + ?: error("Missing repository-proxy.init.gradle test resource") } } @@ -110,6 +120,9 @@ open class GradleFixture { * @param args Gradle task names and arguments * @param expectFailure Whether the build is expected to fail * @param env Environment variables to set (merged with system environment) + * @param unsetEnv Environment variables to remove from the system environment. Needed to + * exercise "no repository proxy configured" behaviour, since CI exports + * MAVEN_REPOSITORY_PROXY/GRADLE_PLUGIN_PROXY globally and [env] can only add. * @param forwardOutput Forward the build's stdout/stderr to the test's output * @param gradleProjectDir Override the project directory used by Gradle (useful for git worktree tests); * defaults to the fixture's project directory. @@ -119,6 +132,7 @@ open class GradleFixture { vararg args: String, expectFailure: Boolean = false, env: Map = emptyMap(), + unsetEnv: Set = emptySet(), forwardOutput: Boolean = false, gradleProjectDir: File = projectDir, ): BuildResult { @@ -127,8 +141,8 @@ open class GradleFixture { .withPluginClasspath() .withProjectDir(gradleProjectDir) // Using withDebug prevents starting a daemon, but it doesn't work with withEnvironment - .withEnvironment(System.getenv() + env) - .withArguments(*args) + .withEnvironment(System.getenv() - unsetEnv + env) + .withArguments("--init-script", repositoryProxyInitScript.absolutePath, *args) if (forwardOutput) { runner.forwardOutput() } @@ -225,6 +239,14 @@ open class GradleFixture { return builder.parse(xmlFile) } + /** + * Creates a fake Maven repository under the project directory. + * + * Pass its [MavenRepoFixture.repoUrl] as `MAVEN_REPOSITORY_PROXY` to [run] to make it + * shadow Maven Central for the TestKit build. + */ + fun createMavenRepoFixture(): MavenRepoFixture = MavenRepoFixture(projectDir) + /** * Returns a File handle under the project directory. * Does not touch the filesystem. diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixtureTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixtureTest.kt new file mode 100644 index 00000000000..b8c9f397a19 --- /dev/null +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/GradleFixtureTest.kt @@ -0,0 +1,147 @@ +package datadog.gradle.plugin + +import org.assertj.core.api.Assertions.assertThat +import org.gradle.testkit.runner.TaskOutcome.SUCCESS +import org.junit.jupiter.api.Test + +class GradleFixtureTest : GradleFixture() { + + private companion object { + const val MAVEN_CENTRAL = "https://repo.maven.apache.org/maven2" + const val PLUGIN_PORTAL = "https://plugins.gradle.org/m2" + } + + @Test + fun `TestKit build routes Maven Central through configured proxy`() { + val proxyRepository = createMavenRepoFixture() + proxyRepository.publishVersions("com.example", "proxy-only", listOf("1.0.0")) + writeRootProject( + """ + plugins { + id("java") + } + + repositories { + mavenCentral() + } + + dependencies { + implementation("com.example:proxy-only:1.0.0") + } + """ + ) + writeJavaSource("Example", "public class Example {}") + + val result = run( + "compileJava", + env = mapOf("MAVEN_REPOSITORY_PROXY" to proxyRepository.repoUrl), + ) + + assertThat(result.task(":compileJava")?.outcome).isEqualTo(SUCCESS) + } + + /** + * Contributors without access to the internal Depot mirror run with no proxy configured. + * The init script must then be completely inert. Assert on the repository URLs of every + * container it touches rather than resolving anything, so the check stays hermetic. + */ + @Test + fun `TestKit build leaves public repositories untouched when no proxy is configured`() { + writeSettings( + """ + import org.gradle.api.artifacts.repositories.MavenArtifactRepository + + pluginManagement { + repositories { + gradlePluginPortal() + } + } + dependencyResolutionManagement { + repositories { + mavenCentral() + } + } + rootProject.name = "no-proxy" + + gradle.settingsEvaluated { + (pluginManagement.repositories + dependencyResolutionManagement.repositories) + .filterIsInstance() + .forEach { println("REPOSITORY=" + it.url) } + } + """ + ) + writeRootProject( + """ + import org.gradle.api.artifacts.repositories.MavenArtifactRepository + + buildscript { + repositories { + mavenCentral() + } + } + + plugins { + id("java") + } + + tasks.register("printRepositories") { + val urls = buildscript.repositories + .filterIsInstance() + .map { it.url.toString() } + doLast { urls.forEach { println("REPOSITORY=" + it) } } + } + """ + ) + + val result = run( + "printRepositories", + unsetEnv = setOf("MAVEN_REPOSITORY_PROXY", "GRADLE_PLUGIN_PROXY"), + ) + + val repositories = result.output.lines() + .filter { it.startsWith("REPOSITORY=") } + .map { it.removePrefix("REPOSITORY=") } + assertThat(repositories) + .withFailMessage("Expected repositories to be printed, output was:\n%s", result.output) + .isNotEmpty() + assertThat(repositories) + .withFailMessage("No proxy is configured, so nothing may be rewritten, got %s", repositories) + .allMatch { it.startsWith(MAVEN_CENTRAL) || it.startsWith(PLUGIN_PORTAL) } + assertThat(repositories).anyMatch { it.startsWith(MAVEN_CENTRAL) } + assertThat(repositories).anyMatch { it.startsWith(PLUGIN_PORTAL) } + } + + @Test + fun `TestKit build routes settings-level Maven Central through configured proxy`() { + val proxyRepository = createMavenRepoFixture() + proxyRepository.publishVersions("com.example", "settings-proxy-only", listOf("1.0.0")) + writeSettings( + """ + dependencyResolutionManagement { + repositories { + mavenCentral() + } + } + """ + ) + writeRootProject( + """ + plugins { + id("java") + } + + dependencies { + implementation("com.example:settings-proxy-only:1.0.0") + } + """ + ) + writeJavaSource("Example", "public class Example {}") + + val result = run( + "compileJava", + env = mapOf("MAVEN_REPOSITORY_PROXY" to proxyRepository.repoUrl), + ) + + assertThat(result.task(":compileJava")?.outcome).isEqualTo(SUCCESS) + } +} diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt index 97d4576d098..faf08622dae 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt @@ -16,6 +16,7 @@ import org.junit.jupiter.params.provider.CsvSource import java.io.File import java.lang.reflect.Proxy import java.util.concurrent.atomic.AtomicInteger +import kotlin.system.measureTimeMillis import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -109,6 +110,53 @@ class MuzzleMavenRepoUtilsTest { .hasMessageContaining("Backoff:\n disabled") } + @Test + fun `defaultMuzzleRepos falls back to Maven Central when no proxy is configured`() { + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(null).map { it.id to it.url }) + .containsExactly("central" to "https://repo1.maven.org/maven2/") + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(" ").map { it.id to it.url }) + .containsExactly("central" to "https://repo1.maven.org/maven2/") + } + + @Test + fun `defaultMuzzleRepos replaces Maven Central with the proxy so range queries do not hit it`() { + // Aether merges metadata from *every* repository of a range request (see + // `resolveVersionRange includes directive extra repositories`), so leaving Central in the + // list would query repo1.maven.org on every directive even when the proxy serves it. + val repos = MuzzleMavenRepoUtils.defaultMuzzleRepos("https://proxy.example/maven2/") + + assertThat(repos.map { it.id to it.url }) + .containsExactly("central-proxy" to "https://proxy.example/maven2/") + assertThat(repos.map { it.url }).noneMatch { it.contains("repo1.maven.org") } + } + + @Test + fun `resolveVersionRange skips backoff when every repository is local`() { + val emptyRepo = RemoteRepository.Builder( + "empty", + "default", + File(tempDir, "empty").apply { mkdirs() }.toURI().toString() + ).build() + val directive = MuzzleDirective().apply { + group = "com.example" + module = "nonexistent" + versions = "[1.0,)" + } + + // enableBackoffRetries is left at its production default; a file: repository cannot be + // transiently unavailable, so the 5s/10s/30s sleeps must not run. + val elapsedMillis = measureTimeMillis { + assertThatThrownBy { + MuzzleMavenRepoUtils.resolveVersionRange(directive, system, newSession(), listOf(emptyRepo)) + }.isInstanceOf(IllegalStateException::class.java) + .hasMessageContaining("Backoff:\n disabled") + } + + assertThat(elapsedMillis) + .withFailMessage("Expected no backoff sleeps for a file: repository, took %dms", elapsedMillis) + .isLessThan(5_000) + } + @Test fun `resolveVersionRange failure includes thrown resolution failure details`() { val directive = MuzzleDirective().apply { diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt index 4c878a0e32f..917d058ce38 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginFunctionalTest.kt @@ -344,6 +344,7 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { @Test fun `non-existent artifact fails with clear error message`() { + val emptyRepository = createMavenRepoFixture() writeProject( """ plugins { @@ -365,7 +366,7 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { val result = run( ":dd-java-agent:instrumentation:demo:muzzle", "--stacktrace", - env = mapOf("MAVEN_REPOSITORY_PROXY" to "https://repo1.maven.org/maven2/") + env = mapOf("MAVEN_REPOSITORY_PROXY" to emptyRepository.repoUrl) ) assertThat(result.output).withFailMessage("Build should fail for non-existent artifact").contains("BUILD FAILED") @@ -538,6 +539,14 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { module = "with-transitive", versions = listOf("1.0.0") ) + // Publish the transitive dependency too, so the test still discriminates: without the + // exclusion guava resolves and lands on the classpath, and the scan plugin below fails. + // (Resolving it from the real Maven Central would defeat routing everything through the proxy.) + mavenRepoFixture.publishVersions( + group = "com.google.guava", + module = "guava", + versions = listOf("31.0-jre") + ) // Manually create a POM with a transitive dependency // Write into MavenRepoFixture's repoDir, not GradleFixture's projectDir. @@ -575,7 +584,6 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { artifact() } } - mavenCentral() } muzzle { @@ -589,15 +597,15 @@ class MuzzlePluginFunctionalTest : MuzzlePluginTestFixture() { """ ) - // Scan plugin verifies that guava is NOT in the classpath (it was excluded) + // Scan plugin verifies that guava is NOT in the classpath (it was excluded). + // The fixture jar carries no guava classes, so probe the Maven descriptor every + // MavenRepoFixture artifact embeds instead of loading a class. writeScanPlugin( """ - try { - testApplicationClassLoader.loadClass("com.google.common.collect.ImmutableList"); + if (testApplicationClassLoader.getResource("META-INF/maven/com.google.guava/guava/pom.properties") != null) { throw new RuntimeException("Unexpected excluded dependency (guava) SHOULD NOT be in test classpath but was found"); - } catch (ClassNotFoundException e) { - System.out.println("Excluded dependency (guava) correctly not in test classpath"); } + System.out.println("Excluded dependency (guava) correctly not in test classpath"); """ ) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt index ba9945cbe4a..ae286c16fa2 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzlePluginTestFixture.kt @@ -1,7 +1,6 @@ package datadog.gradle.plugin.muzzle import datadog.gradle.plugin.GradleFixture -import datadog.gradle.plugin.MavenRepoFixture import org.intellij.lang.annotations.Language import java.io.File @@ -10,8 +9,6 @@ import java.io.File * Extends GradleFixture with muzzle-specific functionality. */ open class MuzzlePluginTestFixture : GradleFixture() { - fun createMavenRepoFixture(): MavenRepoFixture = MavenRepoFixture(projectDir) - /** * Writes the basic Gradle project structure for muzzle testing. * Creates a multi-project build with agent-bootstrap, agent-tooling, and instrumentation modules. diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt index 2c47645bdc7..88df371dd0a 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/RangeQueryTest.kt @@ -1,27 +1,41 @@ package datadog.gradle.plugin.muzzle +import datadog.gradle.plugin.MavenRepoFixture import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.repository.RemoteRepository import org.eclipse.aether.resolution.VersionRangeRequest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File class RangeQueryTest { + @TempDir + lateinit var tempDir: File + private val system = MuzzleMavenRepoUtils.newRepositorySystem() private val session = MuzzleMavenRepoUtils.newRepositorySystemSession(system) @Test fun `test range request`() { - // compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.0', ext: 'pom' - val directiveArtifact: Artifact = DefaultArtifact("org.codehaus.groovy", "groovy-all", "jar", "[2.5.0,2.5.8)") + val repository = MavenRepoFixture(tempDir) + repository.publishVersions( + "org.codehaus.groovy", + "groovy-all", + (0..8).map { "2.5.$it" }, + ) + val directiveArtifact: Artifact = + DefaultArtifact("org.codehaus.groovy", "groovy-all", "jar", "[2.5.0,2.5.8)") val rangeRequest = VersionRangeRequest().apply { - repositories = MuzzleMavenRepoUtils.defaultMuzzleRepos() + repositories = + listOf(RemoteRepository.Builder("fixture", "default", repository.repoUrl).build()) artifact = directiveArtifact } - // This call makes an actual network request, which may fail if network access is limited. val rangeResult = system.resolveVersionRange(session, rangeRequest) - assertThat(rangeResult.versions.size).isGreaterThanOrEqualTo(8) + assertThat(rangeResult.versions.map { it.toString() }) + .containsExactlyElementsOf((0..7).map { "2.5.$it" }) } } diff --git a/buildSrc/src/test/resources/repository-proxy.init.gradle b/buildSrc/src/test/resources/repository-proxy.init.gradle new file mode 100644 index 00000000000..80bbf8a7f79 --- /dev/null +++ b/buildSrc/src/test/resources/repository-proxy.init.gradle @@ -0,0 +1,57 @@ +import org.gradle.api.artifacts.repositories.MavenArtifactRepository + +// Routes the public repositories declared by TestKit builds (see GradleFixture) through the +// mirrors CI configures, so buildSrc tests do not hit Maven Central and get rate limited. +// +// This deliberately *replaces* the well-known public URLs rather than prepending a mirror the +// way settings.gradle.kts, gradle/repositories.gradle and build-logic's +// proxy-repositories.init.gradle.kts do. Those keep Maven Central as a fallback; here a fallback +// would defeat the purpose, because several tests point MAVEN_REPOSITORY_PROXY at a fake file: +// repository and rely on it fully shadowing Maven Central. +// +// Covers project, buildscript and settings-level repository containers. It does not reach +// included builds (no TestKit fixture uses one) nor the *implicit* gradlePluginPortal() default +// that applies when a build declares no pluginManagement repositories at all -- TestKit fixtures +// inject plugins with withPluginClasspath(), so they never resolve a plugin remotely. + +def mavenProxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY")?.trim() +def pluginProxyUrl = System.getenv("GRADLE_PLUGIN_PROXY")?.trim() + +if (mavenProxyUrl || pluginProxyUrl) { + def mavenCentralUrls = [ + "https://repo.maven.apache.org/maven2", + "https://repo1.maven.org/maven2", + ] + def pluginPortalUrls = [ + "https://plugins.gradle.org/m2", + ] + + def redirectRepository = { MavenArtifactRepository repository -> + def repositoryUrl = repository.url.toString().replaceFirst('/+$', '') + def target = null + if (mavenProxyUrl && mavenCentralUrls.contains(repositoryUrl)) { + target = mavenProxyUrl + } else if (pluginProxyUrl && pluginPortalUrls.contains(repositoryUrl)) { + target = pluginProxyUrl + } + if (target) { + repository.url = uri(target) + repository.allowInsecureProtocol = target.startsWith("http://") + } + } + + def redirectAll = { repositories -> + repositories.withType(MavenArtifactRepository).configureEach(redirectRepository) + } + + gradle.beforeSettings { settings -> + redirectAll(settings.buildscript.repositories) + redirectAll(settings.pluginManagement.repositories) + redirectAll(settings.dependencyResolutionManagement.repositories) + } + + gradle.beforeProject { project -> + redirectAll(project.repositories) + redirectAll(project.buildscript.repositories) + } +} diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index 7eeed81dcd0..5c159d907cd 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -172,6 +172,28 @@ In a well-organized Gradle project, build logic lives in specific places: > Script plugins are not recommended. The best practice for developing our build logic in plugins is > to create _convention plugins_ or _binary plugins_. +### Running Build Logic Tests + +`buildSrc` tests are disabled unless you opt in, because they are slow (most of them spin up a +real Gradle build through TestKit): + +```shell +./gradlew -p buildSrc :test -PrunBuildSrcTests # whole suite +./gradlew -p buildSrc :test -PrunBuildSrcTests --tests '*MuzzlePluginFunctionalTest' +``` + +Without `-PrunBuildSrcTests` the task reports `SKIPPED` rather than failing, so it is easy to +believe a green build ran them when it did not. IntelliJ sets `idea.active`, which enables them +too. + +These tests need nothing but a plain internet connection: a handful of TestKit builds download +real dependencies (Byte Buddy, JUnit) from Maven Central, and the rest use a fake local Maven +repository. If `MAVEN_REPOSITORY_PROXY` / `GRADLE_PLUGIN_PROXY` are set — CI points them at the +internal Depot mirror — `buildSrc/src/test/resources/repository-proxy.init.gradle` rewrites +Maven Central and the Gradle plugin portal to those mirrors for every TestKit build. Because it +*replaces* rather than prepends, a mirror you cannot reach makes the tests fail; unset both +variables to go straight to Maven Central. + ### How Gradle Compiles Build Scripts During the **Configuration phase**, Gradle doesn't simply execute build scripts top-to-bottom. Instead, it first extracts and processes certain special blocks before compiling the rest of the script. This is necessary because Gradle needs to know which plugins to apply before it can understand the DSL extensions they provide.