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 c5bb333da6f..5c6a636f895 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtils.kt @@ -25,23 +25,28 @@ internal object MuzzleMavenRepoUtils { private val backoffDelaysSeconds = listOf(5L, 10L, 30L) /** - * Remote repositories used to query version ranges and fetch dependencies. + * Remote repositories used by Aether to query version ranges. * * This intentionally reads the environment on each access: Gradle daemons can - * be reused across builds with different MAVEN_REPOSITORY_PROXY values. + * be reused across builds with different proxy values. Aether queries metadata + * from all repositories, preferring the first for versions present in both. */ @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) - } else { - val proxy = RemoteRepository.Builder("central-proxy", "default", mavenProxyUrl).build() - // TODO: temporary hack for Maven Central rate limiting - listOf(proxy /*, central*/) + fun defaultMuzzleRepos(): List = + defaultMuzzleRepos(System.getenv("MAVEN_REPOSITORY_PROXY"), System.getenv("MUZZLE_MAVEN_REPOSITORY_PROXY")) + + internal fun defaultMuzzleRepos(mavenProxyUrl: String?, muzzleProxyUrl: String? = null): List = + listOfNotNull( + muzzleProxyUrl?.takeUnless { it.isBlank() }?.let { + RemoteRepository.Builder("muzzle-proxy", "default", it).build() + }, + mavenProxyUrl?.takeUnless { it.isBlank() }?.let { + RemoteRepository.Builder("central-proxy", "default", it).build() + } + ).distinctBy { it.url }.ifEmpty { + // Avoid a direct Maven Central fallback while its rate limiting affects CI. + listOf(RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build()) } - } /** * Create new RepositorySystem for muzzle's Maven/Aether resolutions. 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 e6983e45b15..4e06b7f2c7d 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/muzzle/MuzzleMavenRepoUtilsTest.kt @@ -11,8 +11,6 @@ import org.eclipse.aether.util.version.GenericVersionScheme import org.gradle.api.GradleException import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test -import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource @@ -115,31 +113,73 @@ class MuzzleMavenRepoUtilsTest { .hasMessageContaining("Backoff:\n disabled") } - // The two tests below are mutually exclusive: MAVEN_REPOSITORY_PROXY is read from the real - // environment (defaultMuzzleRepos deliberately does not take it as a parameter), so each of - // them covers the branch its environment can reach -- unset locally, set in CI. - @Test - @DisabledIfEnvironmentVariable( - named = "MAVEN_REPOSITORY_PROXY", - matches = ".*", - disabledReason = "A mirror is configured; the proxy variant of this test covers that case" - ) fun `defaultMuzzleRepos is Maven Central alone when no proxy is configured`() { - assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos().map { it.id to it.url }) + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(null).map { it.id to it.url }) + .containsExactly("central" to MAVEN_CENTRAL_URL) + } + + @Test + fun `defaultMuzzleRepos prefers the Muzzle proxy over the shared proxy`() { + val sharedProxy = "https://maven.example.com/repository/" + val muzzleProxy = "https://muzzle.example.com/repository/" + + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(sharedProxy, muzzleProxy).map { it.id to it.url }) + .containsExactly( + "muzzle-proxy" to muzzleProxy, + "central-proxy" to sharedProxy + ) + } + + @Test + fun `defaultMuzzleRepos preserves the shared proxy when no Muzzle proxy is configured`() { + val customProxy = "https://maven.example.com/repository/" + + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(customProxy).map { it.id to it.url }) + .containsExactly("central-proxy" to customProxy) + } + + @Test + fun `defaultMuzzleRepos allows a Muzzle proxy without a shared proxy`() { + val muzzleProxy = "https://muzzle.example.com/repository/" + + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(null, muzzleProxy).map { it.id to it.url }) + .containsExactly("muzzle-proxy" to muzzleProxy) + } + + @Test + fun `defaultMuzzleRepos ignores blank proxy values`() { + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos("", " ").map { it.id to it.url }) .containsExactly("central" to MAVEN_CENTRAL_URL) } - // TODO: Re-enable after removing the temporary Maven Central rate limiting workaround. @Test - @Disabled("Temporarily using the configured proxy without a Maven Central fallback") - @EnabledIfEnvironmentVariable(named = "MAVEN_REPOSITORY_PROXY", matches = ".*") - fun `defaultMuzzleRepos queries the configured proxy before Maven Central`() { - val proxyUrl = System.getenv("MAVEN_REPOSITORY_PROXY") - - // Central stays in the list as a fallback, but the proxy is consulted first. - assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos().map { it.id to it.url }) - .containsExactly("central-proxy" to proxyUrl, "central" to MAVEN_CENTRAL_URL) + fun `defaultMuzzleRepos does not query the same proxy twice`() { + val proxy = "https://maven.example.com/repository/" + + assertThat(MuzzleMavenRepoUtils.defaultMuzzleRepos(proxy, proxy).map { it.id to it.url }) + .containsExactly("muzzle-proxy" to proxy) + } + + @Test + fun `resolveVersionRange uses the shared proxy when the Muzzle proxy has no metadata`() { + val sharedRepo = publishAndGetRepo("com.example", "mylib", listOf("1.0.0")) + val emptyRepo = MavenRepoFixture(File(tempDir, "empty")) + val directive = MuzzleDirective().apply { + group = "com.example" + module = "mylib" + versions = "[1.0,)" + } + + val result = MuzzleMavenRepoUtils.resolveVersionRange( + directive, + system, + newSession(), + MuzzleMavenRepoUtils.defaultMuzzleRepos(sharedRepo.url, emptyRepo.repoUrl), + enableBackoffRetries = false + ) + + assertThat(result.versions.map { it.toString() }).containsExactly("1.0.0") } @Test diff --git a/docs/how_to_work_with_gradle.md b/docs/how_to_work_with_gradle.md index 48ae406577c..8ff31b95289 100644 --- a/docs/how_to_work_with_gradle.md +++ b/docs/how_to_work_with_gradle.md @@ -188,6 +188,19 @@ However, via the command line, `buildSrc/` tests are disabled unless opted in wi Repository proxies configured through `MAVEN_REPOSITORY_PROXY` or `GRADLE_PLUGIN_PROXY` are propagated to TestKit builds through `repository-proxy.init.gradle.kts`. +### Muzzle Repository Proxies + +Muzzle's Aether version discovery uses `MAVEN_REPOSITORY_PROXY`, or Maven Central when unset. +Set `MUZZLE_MAVEN_REPOSITORY_PROXY` to add a preferred endpoint for Muzzle while retaining +`MAVEN_REPOSITORY_PROXY` as a second route. Blank values are ignored; identical URLs are queried once. +These settings do not change Gradle's dependency repositories. + +Aether queries metadata from all configured repositories and combines the versions it finds; +this is not sequential failover. A successful route can supply versions when another fails, +but resolution can still wait for the failing route to time out. +For a Fabric endpoint, enable it only where DNS, routing, and the Gradle JVM's certificate trust +are configured. Fabric is not enabled by default. + ### 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.