Repositories & Content Filtering

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Gradle 9.8.1 (the current release on 2026-10-16). Every example was run on JDK 26 through the Gradle wrapper on 2026-10-16, on macOS, with the configuration cache enabled (org.gradle.configuration-cache=true in gradle.properties) unless the text says a run turned it off, and says whether the example is configuration cache compatible. The Kotlin DSL is shown first and the Groovy DSL second wherever the syntax differs, and both versions were run; where the Groovy output is identical, only the Kotlin output is shown. Paths of the scratch workspace are shortened to <work>, empty lines of the console output were removed, and the standard Run with --stacktrace ... hints that end every failure message were left out. Gradle 8.14 was not available here: what differs in it is described from the official documentation and the release notes and is labelled "not run here".

A repository, in Gradle’s vocabulary, is a place where Gradle looks for the metadata (a POM, an ivy.xml or a Gradle Module Metadata file) and the artifacts of a module named by its coordinates. Everything about which version is chosen is on Declaring Dependencies & Configurations, Platforms, Constraints & Rich Versions and Dependency Resolution, Conflicts & Capabilities; this page is only about where Gradle looks, in which order, with which credentials, and which module it is allowed to ask each repository for. What happens after a download (the cache, locking and verification) is on Dependency Locking, Verification & Caching; publishing to a repository is on Publishing.

Repositories in the three tools

All three tools resolve coordinates against a list of repositories, and all three have a way to substitute a company-internal server for a public one. They differ in where the list lives and in how much the build itself may influence it.

Ant (with Ivy) Maven Gradle

Where repositories are declared

ivysettings.xml resolvers (ibiblio, url, filesystem, chain), or the Resolver Ant Tasks

<repositories> in the POM, plus <mirrors> and <servers> in settings.xml

repositories {} in the build script, or in settings.gradle(.kts) with dependencyResolutionManagement

Public shorthands

none; ibiblio with m2compatible="true" stands for Maven Central

Maven Central is built in (repository id central)

mavenCentral(), google(), gradlePluginPortal(), mavenLocal()

Repositories named by a dependency’s own POM

decided by the Ivy settings

honoured: the POM of a dependency can add repositories

ignored: only the repositories declared by the build are used

Credentials

credentials element, host and realm

<server> in settings.xml, matched by repository id

credentials {} in the repository, usually read from Gradle properties

Mirrors

a chain or a different ibiblio root

<mirror> with mirrorOf in settings.xml

reuse of the Maven mirrors since 9.8 (incubating), or an init script

Per-repository content rules

module rules in ivysettings.xml

none in the POM; repository managers and, in Maven 3.10, remote repository filtering

content {} filters and exclusiveContent

The equivalent pages are Ivy: Settings & Resolvers, Maven: Repositories, Mirrors & Repository Managers (its section Remote repository filtering is the nearest Maven relative of the content filters below) and Maven: settings.xml & Password Encryption. The directory layout all three tools read and write, the protocols, repository managers and Maven Central are on Repositories & Publishing Across Tools; the idea of resolving coordinates at all is on Dependency Management Fundamentals, and the supply-chain reasons for restricting what a repository may serve are on Supply-Chain Security.

A short history

  • Gradle 1.0 (2012-06-12) introduced Gradle’s own dependency resolution engine, moving away from Ivy.

  • Gradle 6.0 (2019-11-08) made plain HTTP repositories emit a deprecation warning and introduced allowInsecureProtocol as the per-repository opt-in. Today the same declaration fails the build, as the run below shows.

  • Gradle 6.2 (2020-02-17) added exclusive repository content (exclusiveContent).

  • Gradle 7.0 (2021-04-09) deprecated jcenter(), because JFrog announced the sunset of JCenter in February 2021; Gradle 9.0 (2025-07-31) removed it. mavenCentral() is the replacement, and JCenter itself was redirected to Maven Central in August 2024.

  • Gradle 9.8.0 (2026-09-24) added the reuse of Maven mirror settings (org.gradle.mirror.maven.settings, incubating). Gradle 9.8.1 (2026-10-07) fixed a security issue in which repositories that fail to establish an SSL connection could expose builds to malicious artifacts.

None of these dates is repeated below; a feature is named with its release where it matters. Gradle 8.14 has everything on this list except the mirror reuse of 9.8 and the removal of jcenter() (not run here).

The lab

The running example is the tidepool toolkit of this section. For this page it is reduced to a single project, repo-lab, with one class that uses a library from an internal repository (io.tidepool:tidepool-model) and one from Maven Central (org.apache.commons:commons-csv), so that every example asks the same question: which repository answers for which module?

Hello.java
package demo;

public class Hello {
    public static void main(String[] args) {
        System.out.println(new io.tidepool.model.Constituent("M2", 1.0, 0.0));
        System.out.println(org.apache.commons.csv.CSVFormat.DEFAULT.getDelimiterString());
    }
}

The "internal" repository is a directory that a small producer build filled with the maven-publish and ivy-publish plugins (publishing is on Publishing; here it only creates fixtures). Its content, with the checksum files left out of the listing:

internal-repo/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
internal-repo/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.module
internal-repo/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
internal-repo/io/tidepool/tidepool-model/maven-metadata.xml
ivy-repo/io.tidepool/tidepool-model/1.0.0/ivy-1.0.0.xml
ivy-repo/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
ivy-repo/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.module

To see who is asked about what, the lab serves repositories over HTTP on localhost with a few lines of Python (standard library only; nothing is reachable from outside the machine): port 18081 is the internal repository, optionally protected by Basic authentication, and port 18082 is a transparent proxy to Maven Central ("public"). Both append one line per request to an access log.

repo_server.py
"""usage: repo_server.py PORT LOGFILE static DIR [USER:PASS] | proxy"""
import base64, http.server, sys, urllib.request, urllib.error, os

port, logfile, mode = int(sys.argv[1]), sys.argv[2], sys.argv[3]
root = sys.argv[4] if mode == "static" else None
cred = sys.argv[5] if len(sys.argv) > 5 else None

class H(http.server.BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def _log(self, code):
        auth = "auth" if self.headers.get("Authorization") else "noauth"
        with open(logfile, "a") as f:
            f.write(f"{self.command} {self.path} {code} {auth}\n")
    def do_HEAD(self): self.do_GET()
    def do_GET(self):
        if cred:
            want = "Basic " + base64.b64encode(cred.encode()).decode()
            if self.headers.get("Authorization") != want:
                self._log(401)
                self.send_response(401); self.send_header("WWW-Authenticate", 'Basic realm="tidepool"')
                self.send_header("Content-Length", "0"); self.end_headers(); return
        if mode == "static":
            p = os.path.join(root, self.path.lstrip("/"))
            if os.path.isfile(p):
                data = open(p, "rb").read(); self._log(200)
                self.send_response(200); self.send_header("Content-Length", str(len(data))); self.end_headers()
                if self.command == "GET": self.wfile.write(data)
            else:
                self._log(404); self.send_response(404); self.send_header("Content-Length", "0"); self.end_headers()
        else:
            try:
                r = urllib.request.urlopen(urllib.request.Request("https://repo.maven.apache.org/maven2" + self.path, method=self.command))
                data = r.read(); code = r.status
            except urllib.error.HTTPError as e:
                data = b""; code = e.code
            self._log(code)
            self.send_response(code); self.send_header("Content-Length", str(len(data))); self.end_headers()
            if self.command == "GET": self.wfile.write(data)

http.server.ThreadingHTTPServer(("127.0.0.1", port), H).serve_forever()

The runs below use a fresh Gradle user home so that nothing is served from the dependency cache, and --refresh-dependencies so that the repositories are asked again on every run.

Declaring repositories

The shorthands

Gradle has built-in shorthands for the repositories that nearly every build uses. They are methods of RepositoryHandler, and each resolves from a fixed URL.

Shorthand Repository Notes

mavenCentral()

Maven Central, the public repository of open source JVM libraries

the default choice for almost every build

google()

the Google Maven repository, which hosts the Android SDK and Android-specific artifacts

see Android Project Structure & Gradle

gradlePluginPortal()

the Gradle Plugin Portal, the default repository for plugins

only meaningful for plugins; see Plugin repositories

mavenLocal()

the Maven local repository, ~/.m2/repository unless settings.xml or maven.repo.local says otherwise

discouraged; see The case against mavenLocal()

jcenter()

JCenter

removed in Gradle 9.0; use mavenCentral()

Every repository has a name. The default name of mavenCentral() is MavenRepo (as in the messages below) and the Plugin Portal’s is Gradle Central Plugin Repository; a repository declared with a block (maven { name = "internal" }) can set its own name, which appears in error messages and is what credentials properties and Maven mirror settings are matched by.

Order, and what Gradle does with it

Repositories are searched in declaration order. When Gradle finds a module’s metadata in a repository, it downloads all of that module’s artifacts from the same repository; it does not mix the POM from one repository with the JAR from another. Searching continues down the list only while a repository answers "not found". That has three consequences that the rest of the page builds on:

  • the first repository that has a module wins, even if a later one has a newer or different build of the same coordinates;

  • every repository above the right one is asked first, so each request for a module that is not there costs a round trip (and tells that server which libraries you use);

  • a repository that is declared earlier but should not be trusted for some groups can shadow the right one (see the mavenLocal() example below).

A related rule is not about order but about scope. A Maven POM may name additional repositories, and Maven honours them; Gradle ignores them and uses only the repositories the build declares. The documentation describes this as both a reproducibility safeguard and a security measure: otherwise an updated dependency could pull artifacts from unverified sources into your build.

Centralizing repositories in settings

A build script can declare repositories {} in every project, but repositories are not part of a project’s definition; they are global build logic. The Gradle best practices therefore recommend declaring them once in the settings file with dependencyResolutionManagement (and pluginManagement for plugins), so that all projects resolve from the same repositories in the same order. The best practices page still labels dependencyResolutionManagement.repositories an incubating API while calling it the preferred way. The convention of this section is the one in the first settings file below.

Kotlin DSL: settings.gradle.kts
rootProject.name = "repo-lab"

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        exclusiveContent {
            forRepository {
                maven {
                    name = "internal"
                    url = uri("../fixtures/internal-repo")
                }
            }
            filter {
                includeGroup("io.tidepool")
            }
        }
        mavenCentral()
    }
}
Groovy DSL: settings.gradle
rootProject.name = "repo-lab"

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        exclusiveContent {
            forRepository {
                maven {
                    name = "internal"
                    url = uri("../fixtures/internal-repo")
                }
            }
            filter {
                includeGroup("io.tidepool")
            }
        }
        mavenCentral()
    }
}
Kotlin DSL: build.gradle.kts
plugins {
    `java-library`
}

dependencies {
    implementation("io.tidepool:tidepool-model:1.0.0")
    implementation("org.apache.commons:commons-csv:1.14.1")
}
Groovy DSL: build.gradle
plugins {
    id 'java-library'
}

dependencies {
    implementation 'io.tidepool:tidepool-model:1.0.0'
    implementation 'org.apache.commons:commons-csv:1.14.1'
}

A relative uri(...) in the settings file is resolved against the settings directory. This build resolves and compiles with ./gradlew compileJava --configuration-cache and is configuration cache compatible; the resolved file collection is the only thing the compile task needs from the repositories.

RepositoriesMode

repositoriesMode says what happens when a project also declares its own repositories. There are three modes.

Mode Meaning Default Use

PREFER_PROJECT

repositories declared in a project override those declared in settings

yes

teams that need different repositories in different subprojects

PREFER_SETTINGS

repositories declared in settings override those declared in a project; Gradle warns about the project ones

no

enforcing approved repositories across a large team

FAIL_ON_PROJECT_REPOS

declaring a repository in a project is a build error

no

strict enforcement; this section’s convention

The three modes with the same project, to which a stray repositories { mavenCentral() } was added at the end of the build script (the settings file is the one above, in the stated mode). The script also applies the io.github.ben-manes.versions plugin from the Gradle Plugin Portal; it plays no part in the modes and is used later, in "Plugin repositories":

Kotlin DSL: build.gradle.kts, with the stray repository
plugins {
    `java-library`
    id("io.github.ben-manes.versions") version "0.66.0"
}

dependencies {
    implementation("io.tidepool:tidepool-model:1.0.0")
    implementation("org.apache.commons:commons-csv:1.14.1")
}

repositories {
    mavenCentral()
}
Groovy DSL: build.gradle, with the stray repository
plugins {
    id 'java-library'
    id 'io.github.ben-manes.versions' version '0.66.0'
}

dependencies {
    implementation 'io.tidepool:tidepool-model:1.0.0'
    implementation 'org.apache.commons:commons-csv:1.14.1'
}

repositories {
    mavenCentral()
}

With FAIL_ON_PROJECT_REPOS (./gradlew help --no-configuration-cache) the build stops during configuration:

FAILURE: Build failed with an exception.
* Where:
Build file '<work>/lab-kts/build.gradle.kts' line: 12
* What went wrong:
Build was configured to prefer settings repositories over project repositories but repository 'MavenRepo' was added by build file 'build.gradle.kts'
BUILD FAILED in 261ms

In Groovy the same failure is wrapped in an evaluation error:

FAILURE: Build failed with an exception.
* Where:
Build file '<work>/lab-groovy/build.gradle' line: 12
* What went wrong:
A problem occurred evaluating root project 'repo-lab'.
> Build was configured to prefer settings repositories over project repositories but repository 'MavenRepo' was added by build file 'build.gradle'
BUILD FAILED in 339ms

With PREFER_SETTINGS the project repository is ignored and the build only warns (./gradlew compileJava, which succeeded):

> Configure project :
Build was configured to prefer settings repositories over project repositories but repository 'MavenRepo' was added by build file 'build.gradle.kts'
> Task :compileJava UP-TO-DATE
BUILD SUCCESSFUL in 445ms

With the default PREFER_PROJECT the project’s mavenCentral() silently replaces the settings repositories, so the internal module can no longer be found. The error message names the cause, which is worth remembering because the stack of Could not find lines alone points at the module, not at the repository list:

> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve all dependencies for configuration ':compileClasspath'.
      > Could not find io.tidepool:tidepool-model:1.0.0.
        Searched in the following locations:
          - https://repo.maven.apache.org/maven2/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
        Required by:
            root project 'repo-lab'
> The project declares repositories, effectively ignoring the repositories you have declared in the settings.
   To determine how project repositories are declared, configure your build to fail on project repositories.
   For more information, please refer to https://docs.gradle.org/9.8.1/userguide/declaring_repositories.html#sub:fail_build_on_project_repositories in the Gradle documentation.
> If the artifact you are trying to retrieve can be found in the repository but without metadata in 'Maven POM' format, you need to adjust the 'metadataSources { ... }' of the repository declaration.
BUILD FAILED in 497ms

(The failing runs used --no-configuration-cache: with the configuration cache on, a resolution failure that happens while the cache entry is being stored is reported wrapped in Configuration cache state could not be cached.)

Repository types

Gradle resolves from four kinds of repository: Maven, Ivy, flat directory, and the local forms of the first two.

Maven repositories

maven { url = ... } declares a repository in the Maven layout (group/as/path/artifact/version/artifact-version.ext), which is what Maven Central, Nexus, Artifactory and Reposilite serve and what maven-publish writes. Gradle reads the POM and, when it is published next to it, the Gradle Module Metadata file (.module); with both present, the .module file wins, because it carries variants and attributes that a POM cannot express (see Variant-Aware Resolution & Module Metadata). In the lab, the internal module is resolved from its .module file, which is why dependencyInsight lists a variant called apiElements for it and compile for the POM-only commons-csv.

URL scheme Use

file: (or a plain path through uri(...))

local directories and shared drives; no credentials

http:

refused unless allowInsecureProtocol = true (see below); basic, digest and header authentication

https:

the normal case; the same authentication schemes

sftp:

SSH file transfer; user name and password

s3:

AWS S3 buckets, with access keys or IAM; not run here

gcs:

Google Cloud Storage, with application default credentials; not run here

Plain HTTP

A URL that starts with http: is refused unless the repository opts in. Without the opt-in, Gradle 9.8.1 fails the build with this message (./gradlew compileJava --no-configuration-cache, a repository at http://localhost:18081):

> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all dependencies for configuration ':compileClasspath'.
   > Using insecure protocols with repositories, without explicit opt-in, is unsupported.
> Switch Maven repository 'internal(http://localhost:18081)' to redirect to a secure protocol (like HTTPS) or allow insecure protocols.

The opt-in is a repository property, named isAllowInsecureProtocol in the Kotlin DSL (a Boolean property whose getter starts with is) and allowInsecureProtocol in Groovy. It is intended for servers on localhost or inside a closed network, never for the public Internet; Central itself is HTTPS only.

Kotlin DSL
maven {
    name = "internal"
    url = uri("http://localhost:18081")
    isAllowInsecureProtocol = true
}
Groovy DSL
maven {
    name = "internal"
    url = uri("http://localhost:18081")
    allowInsecureProtocol = true
}

Ivy repositories

An ivy {} repository serves modules described by ivy.xml files. Its default layout (the "gradle" layout) is [organisation]/[module]/[revision]/[artifact]-[revision].[ext], which is what ivy-publish writes, so the producer’s output can be consumed directly:

Kotlin DSL: settings.gradle.kts (repositories part)
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        ivy {
            name = "ivyRepo"
            url = uri("../fixtures/ivy-repo")
            content {
                includeGroup("io.tidepool")
            }
        }
        mavenCentral()
    }
}
Groovy DSL: settings.gradle (repositories part)
dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        ivy {
            name = "ivyRepo"
            url = uri("../fixtures/ivy-repo")
            content {
                includeGroup("io.tidepool")
            }
        }
        mavenCentral()
    }
}

With the build script of the lab (./gradlew compileJava dependencies --configuration compileClasspath --configuration-cache; the configuration cache entry was stored, the example is compatible):

> Task :dependencies
------------------------------------------------------------
Root project 'repo-lab'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
+--- io.tidepool:tidepool-model:1.0.0
\--- org.apache.commons:commons-csv:1.14.1
     +--- commons-io:commons-io:2.20.0
     \--- commons-codec:commons-codec:1.19.0
A web-based, searchable dependency report is available by adding the --scan option.
> Task :compileJava
BUILD SUCCESSFUL in 602ms

The named layouts are gradle (the default), maven and ivy (layout("maven")). A repository with any other directory structure uses a patternLayout {}, where the tokens in square brackets are Ivy’s pattern variables; ivy(...) adds a pattern for descriptor files and setM2compatible(true) (Groovy: m2compatible = true) turns dots in the organisation into slashes. If a repository holds only JARs and no descriptors, metadataSources { artifact() } tells Gradle to use the existence of the artifact as the only metadata. The following repository is a directory with the layout tidepool-model/1.0.0/jar/tidepool-model.jar:

Kotlin DSL: settings.gradle.kts (repositories part)
ivy {
    name = "patternRepo"
    url = uri("../fixtures/pattern-repo")
    patternLayout {
        artifact("[module]/[revision]/[type]/[artifact].[ext]")
    }
    metadataSources {
        artifact()
    }
    content {
        includeGroup("io.tidepool")
    }
}
Groovy DSL: settings.gradle (repositories part)
ivy {
    name = "patternRepo"
    url = uri("../fixtures/pattern-repo")
    patternLayout {
        artifact "[module]/[revision]/[type]/[artifact].[ext]"
    }
    metadataSources {
        artifact()
    }
    content {
        includeGroup("io.tidepool")
    }
}

This resolves io.tidepool:tidepool-model:1.0.0 and compiles (the dependency tree is identical to the one above). Without the metadataSources block, Gradle looks for an ivy.xml that is not there. Note that the message also lists Maven Central: the content filter above restricts the Ivy repository, not Central (that is the next section’s subject).

> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not find io.tidepool:tidepool-model:1.0.0.
     Searched in the following locations:
       - file:<work>/fixtures/pattern-repo/tidepool-model/1.0.0/ivy/ivy.xml
       - https://repo.maven.apache.org/maven2/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
     Required by:
         root project 'repo-lab'

Ivy repositories also have a "dynamic resolve mode" that makes Gradle prefer the revConstraint attribute of an ivy.xml over rev; it is available only for Gradle’s own Ivy repositories (not run here).

Flat directory repositories

flatDir { dirs("libs") } makes Gradle look for JAR files in a plain directory. There is no metadata: Gradle synthesizes metadata from the file name, so the dependency has no transitive dependencies and no group. The coordinates are written with an empty group (":commons-csv:1.14.1"). The documentation discourages flat directories, because without POMs or Ivy files nothing says what the JAR needs; if the JAR must live in the source tree, a Maven or Ivy repository on a file: URL is preferred. Gradle also prefers modules with real metadata over ones synthesized by a flat directory, so a flat directory cannot override an artifact that another declared repository describes properly.

Kotlin DSL: settings.gradle.kts (repositories part), with ":commons-csv:1.14.1" in the dependencies
repositories {
    maven {
        name = "internal"
        url = uri("../fixtures/internal-repo")
    }
    flatDir {
        name = "vendored"
        dirs("../fixtures/libs")
    }
}
Groovy DSL: settings.gradle (repositories part)
repositories {
    maven {
        name = "internal"
        url = uri("../fixtures/internal-repo")
    }
    flatDir {
        name = "vendored"
        dirs "../fixtures/libs"
    }
}
> Task :dependencies
------------------------------------------------------------
Root project 'repo-lab'
------------------------------------------------------------
compileClasspath - Compile classpath for source set 'main'.
+--- io.tidepool:tidepool-model:1.0.0
\--- :commons-csv:1.14.1
A web-based, searchable dependency report is available by adding the --scan option.
> Task :compileJava
BUILD SUCCESSFUL in 235ms

commons-csv is on the compile class path, but its own dependencies commons-io and commons-codec, which the previous tree showed, are not: they would be missing at run time. That is the practical cost of a flat directory.

Local repositories and the case against mavenLocal()

A Maven or Ivy repository on a file: URL (or mavenLocal(), which is one) is treated specially: Gradle completely bypasses its dependency cache for it, because nothing guarantees the content stays the same between runs. That costs performance, makes builds harder to reproduce, and is why the documentation restricts such repositories to prototyping. The comparison page shows the missing cache directory in a measured run.

mavenLocal() adds ~/.m2/repository (Gradle follows the same logic as Maven: the localRepository of ~/.m2/settings.xml, else ~/.m2/repository). The documentation lists the reasons to avoid it:

  • it is a cache from Maven’s point of view, not a repository, so it can hold incomplete modules (for example without sources or Javadoc);

  • Gradle does not trust it, because artifacts' origins cannot be verified and files can be overwritten;

  • its cache is disabled, so it slows builds down, more so when it is declared before the remote repositories.

Gradle manages its own cache and needs no mavenLocal() to download from a remote repository. The few legitimate uses are interoperability with a Maven project during development (prefer an internal repository, or a composite build from Composite Builds) and local experiments.

The lab shows how it goes wrong. A directory standing in for ~/.m2/repository (selected with -Dmaven.repo.local=<work>/fixtures/m2, which Gradle honours) contains a hand-made, empty commons-csv 1.14.1 JAR, which is exactly the kind of leftover a failed experiment or a corrupted install leaves behind. With mavenLocal() declared first it wins for every group, including the ones it has nothing to do with:

Kotlin DSL: settings.gradle.kts (repositories part)
repositories {
    mavenLocal()
    mavenCentral()
    maven {
        name = "internal"
        url = uri("../fixtures/internal-repo")
    }
}
Groovy DSL: settings.gradle (repositories part)
repositories {
    mavenLocal()
    mavenCentral()
    maven {
        name = "internal"
        url = uri("../fixtures/internal-repo")
    }
}

(./gradlew compileJava --refresh-dependencies --no-configuration-cache -Dmaven.repo.local=<work>/fixtures/m2)

> Task :compileJava FAILED
<work>/lab-kts/src/main/java/demo/Hello.java:6: error: package org.apache.commons.csv.CSVFormat does not exist
        System.out.println(org.apache.commons.csv.CSVFormat.DEFAULT.getDelimiterString());
                                                           ^
1 error

Restricting mavenLocal() to the groups that are meant to be installed locally with a content filter makes Central answer for everything else, and the same build compiles:

Kotlin DSL
mavenLocal {
    content {
        includeGroup("io.tidepool")
    }
}
Groovy DSL
mavenLocal {
    content {
        includeGroup("io.tidepool")
    }
}

The build then succeeded (BUILD SUCCESSFUL, with the configuration cache entry stored). The Maven-side description of the same directory and its settings.xml override is on Maven: The local repository.

Credentials and protocols

Usernames and passwords never belong in a build file. The documentation’s advice is to keep them in a gradle.properties file of the Gradle user home or to provide them through environment variables. The lab protects the internal repository with Basic authentication (user build-user, password placeholder-secret, both throwaway placeholders valid only for the local server of this page).

Without credentials, the first request is answered with 401 and Gradle reports the status:

> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve io.tidepool:tidepool-model:1.0.0.
     Required by:
         root project 'repo-lab'
      > Could not resolve io.tidepool:tidepool-model:1.0.0.
         > Could not get resource 'http://localhost:18081/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom'.
            > Could not HEAD 'http://localhost:18081/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom'.
               > Received status code 401 from server: Unauthorized
BUILD FAILED in 516ms

PasswordCredentials

credentials(PasswordCredentials::class) (Groovy: credentials(PasswordCredentials)) asks for two Gradle properties derived from the repository’s name: <name>Username and <name>Password. They can come from gradle.properties (project or Gradle user home), from -P on the command line, or from environment variables named ORG_GRADLE_PROJECT_<name>Username and ORG_GRADLE_PROJECT_<name>Password. Gradle fails the build when they are missing, and says which ones:

Kotlin DSL: settings.gradle.kts (repositories part)
maven {
    name = "internal"
    url = uri("http://localhost:18081")
    isAllowInsecureProtocol = true
    credentials(PasswordCredentials::class)
}
Groovy DSL: settings.gradle (repositories part)
maven {
    name = "internal"
    url = uri("http://localhost:18081")
    allowInsecureProtocol = true
    credentials(PasswordCredentials)
}
> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> The following Gradle properties are missing for 'internal' credentials:
    - internalUsername
    - internalPassword

With -PinternalUsername=build-user -PinternalPassword=placeholder-secret, with the two environment variables, or with the two lines in $GRADLE_USER_HOME/gradle.properties, the build succeeds; all three ways were run. The server log of the -P run shows how a username and password are used by default: Gradle first sends the request without credentials, waits for the 401 challenge, and then repeats it with them.

HEAD /io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom 401 noauth
HEAD /io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom 200 auth
GET /io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom.sha1 200 auth

Authentication schemes

By default Gradle tries every scheme the Apache HttpClient library supports. Declaring an authentication {} block restricts the repository to the schemes it names. The supported schemes are BasicAuthentication (credentials sent preemptively), DigestAuthentication and HttpHeaderAuthentication (a custom header such as a private or OAuth token, used with HttpHeaderCredentials). Some servers answer an unauthenticated request with something other than 401 (GitHub returns 404), so the challenge never comes; for them, declare BasicAuthentication explicitly to send the credentials preemptively:

Kotlin DSL
maven {
    name = "internal"
    url = uri("http://localhost:18081")
    isAllowInsecureProtocol = true
    credentials(PasswordCredentials::class)
    authentication {
        create<BasicAuthentication>("basic")
    }
}
Groovy DSL
maven {
    name = "internal"
    url = uri("http://localhost:18081")
    allowInsecureProtocol = true
    credentials(PasswordCredentials)
    authentication {
        basic(BasicAuthentication)
    }
}

The server log of this run begins with an authenticated request; there is no 401 round trip:

HEAD /io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom 200 auth
GET /io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom.sha1 200 auth
HEAD /io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.module 200 auth

Token authentication with HttpHeaderCredentials and HttpHeaderAuthentication, the digest scheme, sftp:, s3: (including IAM authentication) and gcs: repositories and their system properties (org.gradle.s3.endpoint, org.gradle.gcs.endpoint) are documented but were not run here, because they need servers and accounts that do not belong in an example. The examples of this page use values read from properties; the documentation’s snippets with literal keys are for illustration only.

With the configuration cache enabled, credentials passed to a repository can be read at configuration time and become part of the cached build configuration. The cache entry is encrypted on disk; the documentation points to the configuration cache page on credentials and secrets for the implications. The runs above, which passed the credentials through -P or environment variables, stored and reused cache entries without a problem.

Content filtering

When a build declares several repositories, every one of them is by default a candidate for every module. The Gradle best practices give three reasons to restrict that:

  • performance, because a repository is only asked for modules it can have;

  • security, because asking every repository for every module leaks the names of your internal modules and gives an attacker who can register a public module with the same coordinates a chance to be chosen (a "dependency confusion" attack), and because fewer downloads means fewer chances to receive a malicious artifact;

  • reliability, because a repository with wrong metadata for some module cannot break a build that never asks it.

Repository-level filters

content {} inside a repository restricts what that repository is asked for. By default a repository includes everything and excludes nothing; once you declare an include, it excludes everything else; once you declare an exclude, it includes everything else; with both, it contains what is included and not excluded. A filter works on a group, a module or a version, strictly or with a regular expression.

Method Effect

includeGroup(group), excludeGroup(group)

exactly this group

includeGroupAndSubgroups(group), excludeGroupAndSubgroups(group)

this group and the groups below it (org.apache and org.apache.commons)

includeGroupByRegex(regex), excludeGroupByRegex(regex)

groups matching a regular expression

includeModule(group, module), excludeModule(group, module)

one module

includeModuleByRegex(groupRegex, moduleRegex)

modules matching expressions

includeVersion(group, module, version), excludeVersion(...)

one version; the version may be a Gradle version range when matching strictly

includeVersionByRegex(...), excludeVersionByRegex(...)

versions matching expressions

onlyForConfigurations(...), notForConfigurations(...)

restrict the filter to the resolution of the named configurations (the documentation also lists filtering by configuration attributes)

onlyForAttribute(attribute, values)

restrict the filter to resolutions that request an attribute value

The lab compares four variants with the access logs of the two servers. "Internal" is the repository on port 18081, "public" the proxy to Central on port 18082, and the build is the lab build. After each run, two commands count the requests that should never have happened: requests to the internal server for a module outside io.tidepool, and requests to the public server for io.tidepool:

echo "requests to internal about other groups: $(grep -vc 'io/tidepool' internal.log)"
echo "requests to public about io.tidepool:    $(grep -c 'io/tidepool' public.log)"
Kotlin DSL: settings.gradle.kts (repositories part), "filtered"
repositories {
    maven {
        name = "internal"
        url = uri("http://localhost:18081")
        isAllowInsecureProtocol = true
        content {
            includeGroup("io.tidepool")
        }
    }
    maven {
        name = "public"
        url = uri("http://localhost:18082")
        isAllowInsecureProtocol = true
        content {
            excludeGroup("io.tidepool")
        }
    }
}
Groovy DSL: settings.gradle (repositories part), "filtered"
repositories {
    maven {
        name = "internal"
        url = uri("http://localhost:18081")
        allowInsecureProtocol = true
        content {
            includeGroup("io.tidepool")
        }
    }
    maven {
        name = "public"
        url = uri("http://localhost:18082")
        allowInsecureProtocol = true
        content {
            excludeGroup("io.tidepool")
        }
    }
}

The results, with the repositories in the order and with the filters stated (all runs were ./gradlew compileJava --refresh-dependencies, configuration cache on, and all succeeded; the Kotlin and Groovy runs gave identical numbers):

Variant Internal asked about other groups Public asked about io.tidepool

internal first, no filters

6

0

public first, no filters

0

1

public first, internal has includeGroup only

0

1

internal and public both filtered ("filtered" above)

0

0

public first, exclusiveContent for the internal repository

0

0

The reading of the table is the heart of this section. Without filters, the order decides who is bothered: with the internal repository first, it receives six HEAD requests that end in 404, for the POMs of commons-csv, of its dependencies and of its parent POMs; with the public repository first, Central receives a request for io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom, which is the name of a company-internal module, and answers 404. The third row is the subtle one: a filter on the internal repository does not stop an earlier repository from being asked. A repository-level filter declares what this repository contains; it says nothing about whether other repositories may also have it.

exclusiveContent

exclusiveContent states the stronger fact: these artifacts are found in this repository and in no other. Gradle then asks no other repository for them, whatever the declaration order, and the rule is declared in one place. The documentation calls exclusive content filters a best practice. The settings file shown at the start of this page is the form used throughout the section, and the last row of the table shows the exclusiveContent repository declared after the public one without that being a problem:

Kotlin DSL: settings.gradle.kts (repositories part), "public first, exclusive"
repositories {
    maven {
        name = "public"
        url = uri("http://localhost:18082")
        isAllowInsecureProtocol = true
    }
    exclusiveContent {
        forRepository {
            maven {
                name = "internal"
                url = uri("http://localhost:18081")
                isAllowInsecureProtocol = true
            }
        }
        filter {
            includeGroup("io.tidepool")
        }
    }
}
Groovy DSL: settings.gradle (repositories part), "public first, exclusive"
repositories {
    maven {
        name = "public"
        url = uri("http://localhost:18082")
        allowInsecureProtocol = true
    }
    exclusiveContent {
        forRepository {
            maven {
                name = "internal"
                url = uri("http://localhost:18081")
                allowInsecureProtocol = true
            }
        }
        filter {
            includeGroup("io.tidepool")
        }
    }
}

Three details from the documentation. First, an exclusive filter must be written in extension: it says what the repository contains, and that is all it says, so the transitive dependencies of the exclusive modules must also be found in some declared repository. Second, exclusiveContent takes an inclusive descriptor, so only the include... methods exist inside its filter {}. Third, using exclusive content in the pluginManagement section of the settings file makes it illegal to add more repositories through a project’s buildscript.repositories; the build then fails, and the options are to declare all repositories in settings or to use non-exclusive filters.

A last caution about the pattern at the end of the best-practices page: the last repository is often declared without any filter so that it serves as the default for everything the others did not claim. That is convenient, and the documentation warns that it can be a security risk, since anything not claimed by an earlier filter falls through to a repository that may serve it from anywhere.

Releases and snapshots

For Maven repositories, mavenContent {} declares whether a repository holds releases or snapshots, which is how company repositories are usually split. A repository declared snapshotsOnly() is not asked for a release at all; the following internal repository (which holds the release 1.0.0) is skipped, and the error lists only Central:

Kotlin DSL: settings.gradle.kts (repositories part)
maven {
    name = "internal"
    url = uri("../fixtures/internal-repo")
    mavenContent {
        snapshotsOnly()
    }
}
mavenCentral()
Groovy DSL: settings.gradle (repositories part)
maven {
    name = "internal"
    url = uri("../fixtures/internal-repo")
    mavenContent {
        snapshotsOnly()
    }
}
mavenCentral()
> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not find io.tidepool:tidepool-model:1.0.0.
     Searched in the following locations:
       - https://repo.maven.apache.org/maven2/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
     Required by:
         root project 'repo-lab'

releasesOnly() is the mirror image. Together with the -SNAPSHOT version suffix and the change-detection rules for dynamic and changing versions (on Platforms, Constraints & Rich Versions), this is what keeps snapshot repositories from being consulted for release builds.

Reusing Maven mirror settings (Gradle 9.8, incubating)

Organizations that build with Maven often route all traffic through an internal repository manager declared as a <mirror> in ~/.m2/settings.xml. Since Gradle 9.8.0, Gradle can read the same mirrors, so the configuration is not duplicated. The feature is incubating and off by default. It is meant for teams with a working Maven mirror setup; the documentation says it is not a general-purpose way to redirect repositories, and, for builds that do not already use Maven mirrors, recommends configuring repositories in the build or a mirror init script instead.

The switch is the Gradle property org.gradle.mirror.maven.settings. It is a Gradle property, read from gradle.properties or passed as a system property (-Dorg.gradle.mirror.maven.settings=true); it is not a project property, so -P has no effect, which the last run below demonstrates. With it set, Gradle reads the mirrors of the user and global settings.xml, matches each HTTP or HTTPS Maven repository against them with Maven’s mirrorOf grammar and resolves from the mirror. Credentials come from the <server> whose id equals the mirror id, and encrypted passwords are decrypted with the Maven master password.

Gradle repositories have no Maven ids, so Gradle matches by name: the name set with name = or the default name; a repository with Maven Central’s URL is matched as central; the Plugin Portal is matched by the exact name Gradle Central Plugin Repository. Local file:, S3, GCS and Ivy repositories are unaffected, and publishing is never mirrored. A mirror pattern of * or external:* also matches the Plugin Portal, so a mirror that should not catch it needs *,!Gradle Central Plugin Repository. Only mirrors and their servers are supported; profiles, proxies and pluginGroups of settings.xml, several headers per server, mirrorOfLayouts and the Maven 4 password format are not.

The lab follows the documentation’s example. A project declares a repository on a host name that never resolves (.invalid), so the build can only work if the mirror replaces it, and the mirror points at Maven Central (to keep the example real without a company server). To avoid touching the real ~/.m2, the runs set -Duser.home to a scratch directory that contains this .m2/settings.xml:

settings.xml
<settings>
    <mirrors>
        <mirror>
            <id>corp-mirror</id>
            <name>Stands in for every repository the build declares</name>
            <mirrorOf>*</mirrorOf>
            <url>https://repo.maven.apache.org/maven2</url>
        </mirror>
    </mirrors>
</settings>
Kotlin DSL: build.gradle.kts
plugins {
    `java-library`
}

repositories {
    maven {
        name = "corp-repo"
        // Nothing is served here: .invalid never resolves, so the build only
        // works if the mirror in settings.xml takes its place
        url = uri("https://repo.example.invalid/maven2")
    }
}

dependencies {
    implementation("org.apache.commons:commons-lang3:3.19.0")
}
Groovy DSL: build.gradle
plugins {
    id 'java-library'
}

repositories {
    maven {
        name = 'corp-repo'
        // Nothing is served here: .invalid never resolves, so the build only
        // works if the mirror in settings.xml takes its place
        url = 'https://repo.example.invalid/maven2'
    }
}

dependencies {
    implementation 'org.apache.commons:commons-lang3:3.19.0'
}

(This project declares its repository in the build script, because it is a one-file example of the documentation; the rest of this section declares repositories in settings.) Without the property, the project cannot resolve its dependency (./gradlew compileJava --no-configuration-cache -Duser.home=<work>/fakehome):

> Task :compileJava FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':compileJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve org.apache.commons:commons-lang3:3.19.0.
     Required by:
         root project 'mirror-lab'
      > Could not resolve org.apache.commons:commons-lang3:3.19.0.
         > Could not get resource 'https://repo.example.invalid/maven2/org/apache/commons/commons-lang3/3.19.0/commons-lang3-3.19.0.pom'.
            > Could not HEAD 'https://repo.example.invalid/maven2/org/apache/commons/commons-lang3/3.19.0/commons-lang3-3.19.0.pom'.
               > repo.example.invalid: nodename nor servname provided, or not known

With -Dorg.gradle.mirror.maven.settings=true the same project compiles; Gradle prints the incubating notice:

> Configure project :
Reusing Maven mirror settings is an incubating feature.
> Task :compileJava
BUILD SUCCESSFUL in 571ms

Passed as a project property, -Porg.gradle.mirror.maven.settings=true changes nothing, and the build fails as before (output trimmed to its first lines). With the property in gradle.properties the configuration cache stored an entry and reused it on the second run, so the feature is configuration cache compatible. The Groovy project gave the same results. Gradle 8.14 does not have this property (not run here); the same effect needs a repository declaration or an init script that rewrites the repositories.

Plugin repositories

Gradle resolves the plugins of a build script from a different set of repositories than the project’s dependencies. Plugin dependencies use the repositories of pluginManagement (by default the Gradle Plugin Portal); project dependencies use repositories {} and ignore the plugin repositories. Declaring mavenCentral() for your code therefore does nothing for a plugins { id(...) } line, and the reverse is also true.

Kotlin DSL: settings.gradle.kts
pluginManagement {
    repositories {
        gradlePluginPortal()
    }
}

rootProject.name = "repo-lab"

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        exclusiveContent {
            forRepository {
                maven {
                    name = "internal"
                    url = uri("../fixtures/internal-repo")
                }
            }
            filter {
                includeGroup("io.tidepool")
            }
        }
        mavenCentral()
    }
}
Groovy DSL: settings.gradle
pluginManagement {
    repositories {
        gradlePluginPortal()
    }
}

rootProject.name = "repo-lab"

dependencyResolutionManagement {
    repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
    repositories {
        exclusiveContent {
            forRepository {
                maven {
                    name = "internal"
                    url = uri("../fixtures/internal-repo")
                }
            }
            filter {
                includeGroup("io.tidepool")
            }
        }
        mavenCentral()
    }
}

With the build script shown earlier (the one that applies io.github.ben-manes.versions, without its stray repository), the plugin resolves from the Portal and ./gradlew help --task dependencyUpdates --configuration-cache shows its task. The first lines of that output:

> Task :help
Detailed task information for dependencyUpdates
Path
     :dependencyUpdates (registered by plugin 'io.github.ben-manes.versions')
Type
     DependencyUpdatesTask (com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask)
Options
...

(The line with the three dots stands for the options of the task, which are not the subject of this page.) If pluginManagement names only mavenCentral(), the Portal is not consulted, and the message lists what was searched, including the repositories of the plugin:

FAILURE: Build failed with an exception.
* Where:
Build file '<work>/lab-kts/build.gradle.kts' line: 1
* What went wrong:
Plugin [id: 'io.github.ben-manes.versions', version: '0.66.0'] was not found in any of the following sources:
- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Included Builds (No included builds contain this plugin)
- Plugin Repositories (could not resolve plugin artifact 'io.github.ben-manes.versions:io.github.ben-manes.versions.gradle.plugin:0.66.0')
  Searched in the following repositories:
    MavenRepo

Two details of this message: the plugin marker (io.github.ben-manes.versions:io.github.ben-manes.versions.gradle.plugin:0.66.0) is a tiny artifact named after the plugin id that points to the real plugin JAR, and it is what Gradle looks for first; and the repository that was searched is shown by its default name, MavenRepo. The same error with the Groovy DSL differs in an instructive way when pluginManagement is not the first statement of the settings file: Groovy requires it to come first, and the build does not even start (the Kotlin DSL run, where the block followed rootProject.name, did not complain, but the documented placement is first in both DSLs):

FAILURE: Build failed with an exception.
* Where:
Settings file '<work>/lab-groovy/settings.gradle' line: 3
* What went wrong:
Groovy DSL script compilation problem
  Could not compile settings file '<work>/lab-groovy/settings.gradle'.
    Location: <work>/lab-groovy/settings.gradle:3
> startup failed:
  settings file '<work>/lab-groovy/settings.gradle': 3: The pluginManagement {} block must appear before any other statements in the script.
  For more information on the pluginManagement {} block, please refer to https://docs.gradle.org/9.8.1/userguide/plugins.html#sec:plugin_management in the Gradle documentation.
   @ line 3, column 1.
     pluginManagement {
     ^
  1 error

Plugins that are not on the Portal (a company plugin on an internal repository, for example) are found by adding that repository to pluginManagement.repositories, or by including the plugin’s build with includeBuild in pluginManagement; how plugins are applied, versioned and resolved is on Plugins Overview, and how to publish one is on Testing & Publishing Plugins. The settings.gradle(.kts) file itself is described on Settings & Build Scripts.

Classic problems

  • A project’s repositories {} silently replaces the settings. The default PREFER_PROJECT mode means a copied build script can undo the company policy. Use FAIL_ON_PROJECT_REPOS.

  • Could not find an internal module that exists. Check the order, the filters (an includeGroup that is spelled differently, or a snapshotsOnly() on a release repository), and whether the module is in a flat directory or on a file: repository that Gradle caches differently.

  • mavenLocal() shadows other repositories. It is a trap when declared first and without a filter; remove it or restrict it by group.

  • HTTP repository refused. The 9.8.1 message above; use HTTPS, or opt in only for localhost.

  • Credentials in the build file. Use Gradle properties or environment variables, and keep gradle.properties with secrets out of version control.

  • A server answering 404 instead of 401 for anonymous requests. Declare BasicAuthentication so the credentials are sent preemptively.

  • jcenter() no longer compiles. It was removed in 9.0; migrate to mavenCentral() and, for artifacts that were only on JCenter, to the repository that actually hosts them.

  • Plugin not found although the dependency repository is fine. Plugins have their own repositories (pluginManagement).

  • Changing the Maven mirror property re-downloads. The documentation notes that enabling or disabling org.gradle.mirror.maven.settings may make Gradle recheck previously downloaded artifacts.

Where the books are outdated

Gradle in Action (Gradle 1.7, 2014) shows repositories { mavenCentral(); mavenLocal(); flatDir ... } and jcenter() and treats mavenLocal() as an ordinary convenience. The shorthands still exist, with these changes: jcenter() was deprecated in 7.0 and removed in 9.0 (Bintray shut down in 2021); mavenLocal() is now explicitly discouraged; flat directories are discouraged too; plain HTTP repositories need an explicit opt-in; and repositories are best declared in the settings file with dependencyResolutionManagement, a mechanism the book predates, as it does exclusiveContent and content filtering. The book’s apply plugin: and its absence of the Plugin Portal are covered on Plugins Overview.