June 26, 2023·5 min read·engineeringnixcicachinggithub-actions

Magic Nix Cache connects Nix substitution to GitHub's cache

Magic Nix Cache exposes GitHub Actions cache storage through a local Nix binary-cache daemon. Store-path granularity lets workflows reuse intermediate results without assembling a custom cache export step.

A fresh CI runner loses the local Nix store even when the next job asks for many of the same packages. Rebuilding the final project is only part of the cost. The runner may also have to retrieve source trees and realize tools that earlier jobs already obtained. Reusing individual store objects can avoid repeating that work without treating the whole runner filesystem as one cache artifact.

Determinate Systems and Zhaofeng Li introduce Magic Nix Cache in the June 26 announcement. The integration runs a local daemon that looks like a normal binary cache to Nix while storing data through the GitHub Actions Cache API. That makes GitHub's existing workflow storage available through Nix's own substitution mechanism.

I value Nix's ability to use the same declared inputs in development and automation. CI caching is useful when it preserves that model and reduces repeated realization. This design does that by reusing store objects identified by Nix, rather than asking each project to invent a separate directory-cache convention.

Realization can fetch an existing object

The Nix glossary available before this launch defines realization as ensuring a store path is valid. That can mean running the corresponding builder or fetching a prebuilt object from a substituter. A substituter is an additional store from which Nix can copy objects it does not already have.

Magic Nix Cache uses that existing relationship. Its Rust daemon serves Nix's binary-cache interface, while its backing storage is GitHub's cache service. The build commands remain Nix commands. They can obtain an available object through substitution instead of reproducing it locally.

This mechanism is more precise than saying every Nix store path hashes its output contents. The glossary distinguishes input-addressed objects from output-addressed objects. Ordinary input-addressed derivations identify outputs through the build inputs. The cache can reuse an object under the expected store identity without implying that all store paths are direct hashes of final bytes.

That distinction matters to reproducibility claims. Fetching a previously built object saves work, but it does not independently rebuild that object and compare its contents. Substitution and reproducibility verification answer different questions within the same build model.

The action observes the workflow's store growth

The launch account describes an initial step that downloads and starts the daemon. It then obtains a list of objects already present in the GitHub cache. The rest of the workflow proceeds, including its normal Nix commands.

At the end of the workflow, the daemon determines which store paths have not yet been written to GitHub's cache and writes them. That end-of-job behavior removes the need for a project-specific script selecting and uploading final artifacts. The integration deals with the store objects created during the workflow.

The announcement says the coverage includes source trees, development environments, and intermediate build dependencies. An upstream package source can therefore benefit from reuse even if it is not itself the final project output. A Nix-built container image can also be among the store objects the workflow produces.

This broad coverage is relevant because an expensive dependency does not need to be the workflow's headline artifact to dominate elapsed time. A cache that only exports the final project package can miss tools and intermediate results needed before that package exists. Tracking store paths lets the integration work at the same granularity as Nix realization.

The glossary describes a closure as the paths reachable through store references. A complete runtime deployment needs its closure, while a derivation's closure describes build-time dependencies. Those relationships explain why a cache intended for builds has a wider concern than copying one executable from a final output directory.

GitHub determines the sharing boundary

The cache service supplies scope rules as well as storage. The announcement explains that workflow runs do not restore caches created for child or sibling branches. Under that model, a pull request's cached artifacts do not become inputs restored by the default branch merely because they belong to the same repository.

This is the launch account's specific cache-isolation argument. It should not be expanded into a general claim that every workflow is secure regardless of its permissions and other actions. The mechanism described concerns which branch-scoped cache entries a workflow can restore.

The integration also inherits a repository boundary. The announcement says caches are shared across workflow runs within the project, not across repositories. A second repository cannot treat the first project's Actions cache as a public Nix distribution endpoint.

That limits the role of the tool in a useful way. It accelerates repeated workflow execution within its supported cache scope. Distributing packages to developers or other projects still calls for a normal binary cache. The announcement explicitly allows using both arrangements together.

I find that separation preferable to hiding distribution policy inside CI setup. A workflow-local cache can be automatic while a published binary cache remains an explicit destination with its own consumers. They can contain related artifacts without serving the same audience.

Rate limits change the amount of reuse

GitHub's cache API imposes rate limits, and the launch article describes how the integration handles them. On the fetch side, a rate-limited cache request leaves Nix to build the dependency. On the upload side, the daemon stops caching and leaves later workflow runs to upload missing paths.

This makes caching an optimization around realization. The article says rate limiting can increase build time but should not fail a workflow merely because the cache limit was reached. That statement concerns the cache integration's fallback. An underlying package build can still fail for its own reasons if it has to run.

The behavior also means that a successful job does not prove every new object reached the cache. Upload throttling can leave gaps. Later jobs can therefore repeat work until the relevant entries become available, without indicating that Nix selected a different derivation.

Magic Nix Cache's launch contribution is the adapter between these two systems. Nix requests store objects through substitution, and the daemon stores them under GitHub's workflow-cache rules. The resulting reuse covers the build's intermediate objects while leaving public package distribution and independent rebuild verification as separate operations.