Can feature branches share the build cache with main?
Your feature-branch builds do reuse what main already built. Give them the read-only login and turn pushing off, so a branch reads main's results without adding its own.
The short answer
- A branch restores what main built. After a method-body change in one module, that module ran and the other three came from the cache. After a new public method in that module, all four ran.
- In our runs the branch name did not matter, only the code. A second branch with the identical change restored the first branch's result, and so did main after a fast-forward merge.
- With the write login, a branch adds its own entries. The body change added one entry, and a new public method added four. All of main's entries were still there, and main still restored everything.
- With the read-only login, the branch added nothing. But with pushing on, the first refused upload switched the remote cache off for the rest of that build. On feature-a, the three unchanged modules then ran instead of being restored.
What to do
Our advice, not a test result:
- Let main, or CI you trust, write to the cache. It gives branches something to restore. See let CI write, developers only read.
- Give branch and pull-request builds the read-only login. Whoever can write can change what every build restores. See can someone poison our shared build cache.
- For those builds, also turn pushing off. In Gradle that is
isPush = false. Pushing off keeps the restores working; it is not a lock. In one run (feature-a) a read-only build with pushing on lost its restores after the first refused upload.
To see why a laptop restores what CI built, see your laptop builds reuse what CI already built.
How we checked
We made a real git repository with a four-module project on main and three branches. feature-a changes the body of a method in core. feature-a2 makes the same change on another branch. feature-b adds a public method to core. We ran the builds below in order against one new server with a read-write login and a read-only login.
| Build | Login, pushing | core | util | api | app | Entries before and after |
|---|---|---|---|---|---|---|
| main | write, on | ran | ran | ran | ran | 0 to 13 |
| feature-a | read-only, on | ran | ran | ran | ran | 13 to 13 |
| feature-b | read-only, on | ran | ran | ran | ran | 13 to 13 |
| feature-a | read-only, off | ran | from cache | from cache | from cache | 13 to 13 |
| feature-a | write, on | ran | from cache | from cache | from cache | 13 to 14 |
| main afterwards | read-only, off | from cache | from cache | from cache | from cache | 14 to 14 |
| feature-b | write, on | ran | ran | ran | ran | 14 to 18 |
| main afterwards | read-only, off | from cache | from cache | from cache | from cache | 18 to 18 |
| feature-a2 | read-only, off | from cache | from cache | from cache | from cache | 18 to 18 |
| main after merging feature-a (a fast-forward) | write, on | from cache | from cache | from cache | from cache | 18 to 18 |
Each cell is the compile task of that module. Each build used a clean checkout of the branch, a new empty Gradle home, no Gradle daemon and no local build cache. The first build stored 13 entries: four compile outputs and nine other entries, the compiled build scripts. After the read-only builds with pushing on, the entry names on the server were the same as before. After the write builds, every entry that main had stored was still there. The two refused-upload builds each printed one line saying the entry could not be stored, with response status 403, and ended with a line saying the remote cache was disabled during the build due to errors.
Setup: FosterStack Cache 0.2.1, Gradle 9.8.0, Temurin 21.0.12.1, a Mac (arm64), over plain HTTP on the same machine, with test passwords. The project is the four-module project from Gradle build cache for a monorepo. We make no claim about build times.
Not tested: Maven, tasks other than compile, a branch that changes a build script or a dependency, a branch from a fork, pull requests as such, branches with more than one commit, a merge that is not a fast-forward (ours was a fast-forward), other Gradle versions, TLS, and many branches building at once.