Let CI write, developers only read the build cache
Anything that can write to your build cache can change what later builds use, so give the right to write to as few machines as possible. This page sets up a cache where CI uploads and developers only download, in Gradle and in Maven, with a read-only login the server enforces.
Give the server two logins
The server takes one read-write login and, optionally, a second read-only login. A client using the read-only one can read cache entries, and any write is refused with a 403.
FSCACHE_USERNAME=ci FSCACHE_PASSWORD=<a long secret> \
FSCACHE_RO_USERNAME=dev FSCACHE_RO_PASSWORD=<another secret> \
./fscache
The two usernames must differ, and the read-only pair only works alongside the read-write pair. Both are in the configuration table.
Gradle: let CI write, developers only read
One Gradle setting for everyone
Developers and CI use the same settings.gradle.kts. Only the environment differs:
rootProject.name = "demo"
// FosterStack Cache — remote Gradle build cache.
// https://github.com/fosterstack/cache
buildCache {
remote<HttpBuildCache> {
url = uri("https://cache.example.com/")
// CI pushes; everyone else only reads
isPush = System.getenv("CI") != null
credentials {
username = System.getenv("FSCACHE_USERNAME")
password = System.getenv("FSCACHE_PASSWORD")
}
}
}
On CI, set CI and the read-write login. On a laptop, leave CI unset and use the read-only login. Setting isPush to false on a laptop is the client choosing not to upload. The read-only login is what makes the server enforce it.
If a laptop is set up to push anyway
In our run we made a code change, so there was something new to upload, and built with CI=true but only the read-only login. The build still succeeded. With -i Gradle logged:
Could not store entry d4f2de693b561fd2c5d60b17a9232d6f in remote build cache: Storing entry at 'http://127.0.0.1:18098/d4f2de693b561fd2c5d60b17a9232d6f' response status 403: Forbidden
The remote build cache was disabled during the build due to errors.
The server stored nothing: it stayed at 1 entry (see the Gradle run table below). The same change built by CI with the read-write login was stored (2 entries). So a misconfigured laptop cannot add to the cache, and its build is not broken by that. Note that Gradle switches the remote cache off for the rest of that build once a store fails, so a laptop in this state also stops reading from it for that build.
Maven: let CI write, developers only read
One config file for everyone
Put this in .mvn/maven-build-cache-config.xml and commit it. It has no password and no saveToRemote, so on its own it only reads. Turning saving on for CI comes below.
<?xml version="1.0" encoding="UTF-8"?>
<cache xmlns="http://maven.apache.org/BUILD-CACHE-CONFIG/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/BUILD-CACHE-CONFIG/1.2.0 https://maven.apache.org/xsd/build-cache-config-1.2.0.xsd">
<configuration>
<enabled>true</enabled>
<remote enabled="true" id="fosterstack-cache">
<url>https://cache.example.com/</url>
</remote>
</configuration>
</cache>
The extension itself is set up in .mvn/extensions.xml; see Maven build cache extension: remote server setup.
Each side gets its own login
The login goes in settings.xml, in a server entry whose id matches the id in the config. CI uses the read-write login:
<settings>
<servers>
<server>
<id>fosterstack-cache</id>
<username>ci</username>
<password>your-CI-secret</password>
</server>
</servers>
</settings>
Developers put the same entry in their own settings.xml, with the login dev and its password.
Turn saving on in CI only
CI builds with:
mvn -Dmaven.build.cache.remote.save.enabled=true verify
Developers run plain mvn verify. Without that flag, and with no saveToRemote in the file, nothing is saved to the server.
If your config file already says saveToRemote="true", developers add -Dmaven.build.cache.remote.save.enabled=false. That flag won over the file in our run.
If a developer build is set to save anyway
In our runs, with only the read-only login, the build still succeeded and the server stored nothing. You will see this for each file Maven tries to save:
[INFO] Unable to save to remote cache http://.../mtest.jar
org.apache.http.client.HttpResponseException: status code: 403, reason phrase: Forbidden (403)
Those stack traces are noise. Switch saving off for developers to remove them.
If a developer has no login
The server refuses every cache read and write without a login, so Maven cannot read the cache. On Maven 3.9.9 you will see an error line, and Maven builds the project itself and succeeds:
[ERROR] Error downloading cache item: http://.../buildinfo.xml
[INFO] Remote cache is incomplete or missing, trying local build for demo:mtest
Maven 3.10.0 logs the same error line for an ordinary first-time miss, so on that version the line alone does not show a login problem.
The developer gets no speed-up, and nothing breaks. If every build looks like this, check the settings.xml login and that its id matches the config.
What this does not give you
There is no per-person identity. Everyone who holds the read-write login is the same writer to the server, so keep it to CI. See who can write to a remote build cache and the HTTP status codes. To see why laptops then reuse CI's work, see your laptop builds reuse what CI already built.
Our advice, not a test result: use HTTPS, keep the CI password in your CI secret store and never in the repository.
How we checked
Gradle run
FosterStack Cache 0.2.1 and Gradle 9.8.0. The server counts what it stores. Each line below is a build of the same small project. In our run we turned Gradle's local cache off, with local { isEnabled = false } in the same block, so that a hit could only have come from the server. You do not need that line. In the Linux repeat below, the first build on a Gradle home that had never built put 2 entries on the server, not 1, so on a new machine the server can show one more entry than the table.
| Who builds | Result | Server after |
|---|---|---|
A developer first, read-only login, CI unset | :compileJava runs. Nothing in the cache to read, and nothing uploaded. | 0 entries |
CI, read-write login, CI=true | :compileJava runs, and the result is uploaded. | 1 entry |
| The developer again, read-only login | > Task :compileJava FROM-CACHE, "1 from cache" | 1 entry, 1 hit |
The laptop never wrote to the cache, and still got the benefit of CI's work.
Maven run
We ran each case below against a real server with both logins.
| Who builds | Setting | Result | Entries on the server after |
|---|---|---|---|
| CI, read-write login | saving not turned on | built, nothing saved | 0 |
| CI, read-write login | saving on | built, 3 files saved | 3 |
| Developer, read-only login, same code | saving not turned on | restored from the cache | 3 |
| Developer, read-only login, new code | saving on | built, 3 saves refused with 403, build succeeded | 3 (unchanged) |
| CI, same new code | saving on | built, 3 files saved | 6 |
| Developer, no login, same code | saving not turned on | built, download error logged, build succeeded | 3 (unchanged) |
| Developer, read-only login, new code | config file says saveToRemote="true", flag false | built, no save attempted | 3 (unchanged) |
| Developer, read-only login, new code | config file says saveToRemote="true", no flag | built, 3 saves refused with 403, build succeeded | 3 (unchanged) |
Straight from the server with curl: a PUT with the read-write login returned 201, a PUT with the read-only login 403, a GET with the read-only login 200, and a GET or PUT with no login 401.
Setup: Maven 3.9.9, Apache Maven Build Cache Extension 1.2.3, FosterStack Cache 0.2.1, Temurin 21.0.12.1, macOS (arm64). One module with two tests, all eight plugin versions pinned. Each group of cases began on a new empty server, with a fresh project copy and an empty local build cache for every build. Rows 4 to 6 each ran after a CI build of the original code, not listed here, had put 3 entries on their server. The last two rows ran on their own server, which a CI build with the file setting had filled with 3 entries first. The server ran on 127.0.0.1 over plain HTTP; the URL in the examples is a placeholder. The plugin downloads were shared across builds. The passwords were test values.
Not tested: Maven versions other than 3.9.9, except the first-miss line on 3.10.0 (seen in our version tests and in the repeat below), a multi-module build, whether Maven keeps reading after a refused save, and logins kept anywhere other than settings.xml.
We also ran the Gradle table, the curl checks and all eight Maven cases on a GitHub-hosted Linux machine with release 0.2.2, Gradle 9.8.0 and Maven 3.9.9. The results were the same as shown here, and Maven 3.10.0 logged the first-miss error line there too. The run and its log are public.