Skip to content

Speeding up caching on Cirrus Runners

Adding Cirrus Runners to your GitHub project has a great benefit of increasing the build speed, yet reducing the costs and making spendings much more predictable. However, Cirrus Runners act as self-hosted runners which means that the runners now are outside the GitHub infrastructure hosted predominantly on Azure, and this comes at a cost of cache accesses potentially being slower than they could be.

Do you use actions/cache in your workflows? Then this might be especially of interest to you.

One does not simply plug their own cache infrastructure

Since actions/cache is a primary way of implementing caching on GitHub Actions, ideally we wanted to integrate with it and point it to our own caching infrastructure that is more local to Cirrus Runners.

Unfortunately, actions/cache does not support S3-compatible storage or something like a simple HTTP cache protocol that Bazel and many other build tools implement. In fact, it does not support anything except for its own GHA actions cache protocol. In addition, it's impossible to override the caching endpoint for a given self-hosted runner to point to a custom implementation of the cache protocol.

These findings meant we have two practical choices:

  1. Implement cache protocol, fork the actions/cache and get the burden of maintaining a fork. Actually, two forks, because caching logic is implemented in actions/toolkit dependency.

  2. Write our own implementation of a caching action from scratch and get the burden of providing compatibility of inputs and behaviour for things like wildcard expansion, archiving/unarchiving, compression formats, etc.

Fortunately, we've found a middle ground solution to the problem that allows us to maintain a fork in an automatic fashion and keep 100% compatibility with the original action at the same time.

Simplicity can be hacky sometimes

While tinkering with a TypeScript Action template, we've realized that just setting the process.env["ACTIONS_CACHE_URL"] from within a Node.js runtime bypasses the limitation imposed by the GitHub Actions Runner outlined in the PR above and automatically applies to all the action dependencies including actions/toolkit.

Moreso, we don't even need to touch TypeScript! We can patch the minified JavaScript release version of the actions/cache by adding a few lines of vanilla JavaScript at the start of each .js file:

const httpCacheHost = process.env["CIRRUS_HTTP_CACHE_HOST"];

if (httpCacheHost) {
    const newActionsCacheURL = "http://" + httpCacheHost + "/";

    console.log("Redefining the ACTIONS_CACHE_URL to " + newActionsCacheURL + " to make the cache faster...");

    process.env["ACTIONS_CACHE_URL"] = newActionsCacheURL;
}

Note that we still needed to implement the cache protocol, but thankfully it consists of only four simple HTTP methods.

Introducing cirruslabs/cache action

We’ve forked actions/cache into cirruslabs/cache and made a CI workflow that fetches the latest repository contents daily and applies the modification directly to minified JavaScript in the dist/ folder of the major tags like v3 and v4.

So, in order to use it, simply change your workflows as follows:

-- uses: actions/cache@v4
+- uses: cirruslabs/cache@v4
   with:
     path: node_modules
     key: node_modules

Note that our cirruslabs/cache action will keep working exactly the same as actions/cache when run on an infrastructure other than Cirrus Runners. This makes it easier to migrate when you have a matrix which runs on both GitHub-managed runners and Cirrus Runners.

The results? Consistent and stable downloads and twice as fast uploads!

Foreword

While we keep an eye on cache performance, you can keep an eye on our blog, follow us on X and don’t hesitate to reach out to our support team at support@cirruslabs.org if you have any questions or reach out to sales@cirruslabs.org if you want to trial Cirrus Runners for free!