Use Mill Build Caching in CI-Builds
Mill build is still my favorite least-hated build tool. (Here is my outdated introduction to it.) In this blog post I show how to use the Mill build to speed up builds on branches.
I am using Bitbucket pipelines in this post. And I’m a newcomer at that =).
You are probably using another CI system, but the concepts most likely apply as well. Jump to the conclusion for a quick summary.
Naive First Build
Let’s assume we have some build that takes a while. For example, in this blog post series I’ve a project with
core, shared-utils, domainX-service and ui modules.
Each of those has also a test, that take a while to run.
We start of with the most naive build, running ./mill __.test in our CI.
Mill is self boots trapping, including JDK etc,
so it doesn’t need any special installation (most of the time) or container image.
That allows us in Bitbucket pipelines to use most basic build container and call ./mill -j 2 __.test.
The -j 2 sets the parallelism explicitly two 2 concurrent tasks.
image: atlassian/default-image:5
pipelines:
default:
- step:
name: Build and Test
script:
- './mill -j 2 __.test'This builds and runs the test fine. However, if we run the build a few times, we notice that it downloads the dependencies on every build:
> ./mill -j 2 __.test
Downloading mill 1.0.6 from https://repo1.maven.org/maven2/com/lihaoyi/mill-dist-native-linux-amd64/1.0.6/mill-dist-native-linux-amd64-1.0.6.exe ...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 54.8M 100 54.8M 0 0 98.1M 0 --:--:-- --:--:-- --:--:-- 98.0M
Downloading https://repo1.maven.org/maven2/com/lihaoyi/mill-runner-daemon_3/1.0.6/mill-runner-daemon_3-1.0.6.pom
Downloaded https://repo1.maven.org/maven2/com/lihaoyi/mill-runner-daemon_3/1.0.6/mill-runner-daemon_3-1.0.6.pom
Downloading https://repo1.maven.org/maven2/com/lihaoyi/mill-runner-meta_3/1.0.6/mill-runner-meta_3-1.0.6.pom
Downloading https://repo1.maven.org/maven2/com/lihaoyi/os-lib_3/0.11.5/os-lib_3-0.11.5.pom
# tons more
Downloaded https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jdk_x64_linux_hotspot_21.0.8_9.tar.gz
============================== __.test ==============================
# The actual build kicking inCache the Dependency Folder ~./cache
Mill Caches long term dependencies in the ~/.cache directory. (Maybe more directories, depending on the tools used by the build) We want to cache that across builds. Check your CI manual on how you declare that.
In Bitbucket pipelines caches
have two parts to then. The directory that is cached, and optionally a set of files that act as key to these caches.
The cache is invalidated if these files changed.
I use the mill script and the *.mill files as my cache key.
definitions:
caches:
home-cache:
key:
files:
# when the script or the build definition changes, new dependencies might be downloaded
# so, trigger a cache invalidation if these files changed
- mill
- "**/*.mill"
path: ~/.cache
pipelines:
default:
- step:
name: Build and Test
caches:
- home-cache
# Previous scriptAnd TA-DA! The build doesn’t download Maven Central on every run:
> ./mill -j 2 __.test
============================== __.test ==============================
[build.mill-59/64] compile
# build logsSelective Execution and Build
Mill supports selective execution to speed up builds for branches/pull requests. See the reference and this blog post for more in detail information.
The idea is: Only build and test the parts of the system that actually can be affected by a change. To do that, I wrote a small wrapper script to do the setup:
#!/bin/bash
if [[ $BITBUCKET_BRANCH == "main" ]]; then
# On the main branch, be conservative and build everyting regularly.
./mill __.test
else
# On feature branches, use selective build and testing
# Ensure we can fetch other branches
git config remote.origin.fetch "+refs/heads/main:refs/remotes/origin/main"
git fetch --depth 1 origin
# Switch to the main branch and let mill determin the state of the source code
git switch -c main origin/main
./mill selective.prepare
# Switch back to the branch we`re building and build
# Mill will now will use the build graph and determin what modules can be affected by the difference to the main branch
git switch $BITBUCKET_BRANCH
./mill selective.run __.test
fiNow, if the main branch is build everything is built and tested:
# builds and test for all our modules. # Eg. Mill is running ~650 build tasks in ~94 seconds [664] Test info.gamlor.ui.customer.CustomerUISlowTest.slowTest4 finished, took 7.001 sec [664] Test run info.gamlor.ui.customer.CustomerUISlowTest finished: 0 failed, 0 ignored, 4 total, 21.018s [664/665] ============================== __.test ============================== 94s
Then, for branches it depends on what I touched. Eg. when I only touch the UI module, then less tasks run:
# On a branch: only some UI changed, so many modules and tests got skipped # So, Mill runs ~230 build tasks in ~38 seconds. [233] Test info.gamlor.ui.customer.CustomerUISlowTest.slowTest4 finished, took 7.002 sec [233] Test run info.gamlor.ui.customer.CustomerUISlowTest finished: 0 failed, 0 ignored, 4 total, 21.017s [1/1] ============================== selective.run __.test ============================== 38s
# On a branch: a more core module changed, now ~610 tasks build in ~65 secons [610] Test info.gamlor.ui.customer.CustomerUISlowTest.slowTest4 finished, took 7.001 sec [610] Test run info.gamlor.ui.customer.CustomerUISlowTest finished: 0 failed, 0 ignored, 4 total, 21.016s [1/1] ============================== selective.run __.test ============================== 65s
This faster build of branches helps to merge smaller pull request faster and have quicker feedback.
Use Mill Caching
Mill caches task results and will skip a task if its upsteam dependencies didn’t change.
To accelerate builds for branches more, lets cache the out directory and re-use in for
branch builds.
The plan is simple: On the main branch build, we skip the cache but store the result in the cache.
On the branch builds we use the cache but do not store it.
This took me a while to figure out in Bitbucket Pipelines:
Plan caches just cache once per week and are too static
Cache invalidation as described above only can take files as keys
I tried to create a step that created a cache-key file dynamically, but the cache key must be committed.
The solution I came up is to run an explicit clear cache command on the main build:
definitions:
caches:
home-cache:
# ... like before
mill-out-cache: out # No cache key, as we manually clear the cache in the script
pipelines:
default:
- step:
name: Build and Test
caches:
- home-cache
- mill-out-cache
script:
- ./build-ci.sh#!/bin/bash
if [[ $BITBUCKET_BRANCH == "main" ]]; then
# Clear the build cache, so it isn't used for the main branch
curl --request DELETE \
--url 'https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pipelines-config/caches?name=mill-out-cache' \
--header 'Authorization: Bearer $PIPELINE_TOKEN'
# Clear the our directory, start with a clear state
rm -rf out
./mill __.test
else
# ... like before
fiCached Tests, alternative to Selected
Now there is caching in place, there is another mechanism that speeds up tests.
Mill by default has also the testCached task. Unlike test, it won’t rerun the tests.
So, you could run mill -j __.testCached instead of the selective execution setup we did above.
Even if you are not using it, __.testCached great for local runs, as it doesn’t require extra steps.
This way to you can run the test locally and skip modules that are not affected by a chance since the last run.
Summary:
When using Mill in a CI server, then:
Ensure you share/cache the
~/.cachedirector between builds, as dependencies are cached there.Note: Depending on your build, more folder could be used, eg.
~/.npm
To speed up builds for branches/pull requests:
Try out selective execution/build.
Consider coping the
outdirectory from the latest main branch build.
The
testCachedtask exists: Great for local builds, you may try it in a CI branch/pull request build as well.

