Summary
CI caching now works but is not tuned. Measured on PR #343, the repository cache sits at 8.78 GB across 19 entries against GitHub's ~10 GB LRU budget, roughly 88% full, and a meaningful share of that is dead weight. Separately, the test-fixture-contracts nix cache is demonstrably working but still leaves 27 derivations to build, and the most likely cause is a size cap that is pruning the store before save.
This issue records the measurements so the tuning can be done against numbers rather than intuition.
What already works
Scoping the nix store cache per job (PR #343) fixed a real defect: all twelve nix jobs previously shared one key, and because actions/cache never overwrites, whichever trivial job finished first froze a 90 MB entry that every other job then "hit". Measured before and after on test-fixture-contracts:
| signal |
shared key |
job-scoped |
| cache entry size |
90 MB |
2217 MB |
| restore step duration |
8s |
51s |
| derivations to build |
166, then 411 |
27 |
last_accessed_at on the 2217 MB entry matches the restore step timestamp exactly, confirming the job consumes it.
Worth stating for anyone reading a build log: cache-nix-action restores the /nix store directory, it is not a substituter. Nix therefore never prints copying path ... from ... for cached content - it simply finds the paths present and drops them from the build plan. The observable evidence is the action's own restore, last_accessed_at, and the derivation count. Absence of substituter lines is not absence of caching.
Current cache inventory
2217MB nix-Linux-test-fixture-contracts-e28042def...
2094MB v0-rust-test-rust-Linux-Linux-x64-21820c02-b94d5870
1835MB v0-rust-test-rust-Linux-Linux-x64-4ece3d53-7027ccd5
1818MB v0-rust-test-rust-Linux-Linux-x64-4ece3d53-0a5bbf36
130MB nix-Linux-test-nix-unit-e28042def...
126MB nix-Linux-test-rust-e28042def... <- stale
110MB nix-Linux-flake-eval-x86-e28042def... <- stale
95MB nix-Linux-flake-eval-x86-outputs-e28042def... <- stale
90MB nix-Linux-test-drift-e28042def... <- stale
90MB nix-Linux-flake-eval-discover-e28042def... <- stale
90MB nix-Linux-e28042def... <- stale (original shared key)
89MB nix-Linux-test-flake-aarch64-e28042def... <- stale
total: 8.78 GB across 19 entries
Findings to tune
1. gc-max-store-size-linux: 4G is probably truncating the fixture store
test-fixture-contracts still builds 27 derivations on a warm cache. The obvious explanation - that a source change invalidated them - was tested and falsified: git diff <seed-commit>..HEAD -- packages/ is empty, so the fixture derivation is byte-identical between the seeding run and the consuming run, and the cache should have covered it entirely.
The remaining hypothesis is the size cap. gc-max-store-size-linux prunes the store before save, so anything beyond the ceiling is dropped and must rebuild. The entry is 2217 MB compressed, consistent with an uncompressed store at or above the 4G cap.
The cap was lowered from 8G to 4G during review specifically to protect the aggregate budget, so raising it is a genuine tradeoff, not a straightforward fix.
To confirm before changing anything: read the 27 derivation names from a completed test-fixture-contracts log. If they are Rust host-tool derivations, the cap is truncating. If they are per-run inputs, the cap is correct and 27 is the floor.
2. Stale per-job nix entries occupy ~730 MB
Seven nix-Linux-* entries exist for jobs that are no longer in NIX_CACHED_JOBS (test-rust, flake-eval-x86, flake-eval-x86-outputs, test-drift, flake-eval-discover, test-flake-aarch64), plus the original shared nix-Linux-<hash> key. They will never be refreshed and will not be read. They occupy budget until LRU reclaims them.
purge-prefixes only purges within a job's own scope, so a job that no longer renders a cache step never purges its own leftovers.
Options: delete them once via gh api --method DELETE, or add a scheduled workflow that reaps nix-* entries whose prefix is not in the current NIX_CACHED_JOBS.
3. rust-cache is the dominant consumer at ~5.7 GB
Three generations at ~1.9 GB each, keyed on Cargo.lock hash. This is the single largest claim on the budget and was not examined during the nix cache work.
Worth asking whether three live generations is the right number, or whether the key should be narrowed so old lockfile generations retire faster.
4. MATRIX_CHECK_SCOPE has no consumer
flake-eval-x86 shards share one ciJobId, so per-job scoping alone would let them clobber each other. A scope_suffix was added and threaded through, but that job is not in NIX_CACHED_JOBS, so the code path is currently dead. It was verified by hand (temporarily adding the job, regenerating, confirming the key renders nix-${{ runner.os }}-flake-eval-x86-${{ matrix.check }}-...) but nothing pins it.
If those shards are ever added to the cached set, that path needs a test. Note the shards each complete in under a minute today, so there is currently no case for caching them.
5. sccache is deliberately absent from CI
Recorded here so it is not re-attempted without knowing the outcome. Enabling sccache in CI breaks the build: its server forwards only a whitelist of environment variables to rustc, and CARGO_BIN_EXE_<name> - which Cargo injects so an integration test can locate the binary it exercises - is not among them. The failure is:
error: environment variable `CARGO_BIN_EXE_d2b-exec-runner` not defined at compile time
--> d2b-exec-runner/tests/tty_pty_integration.rs:359
It does not reproduce locally because CARGO_INCREMENTAL is on there, so sccache marks those units non-cacheable and passes them through untouched. CI sets CARGO_INCREMENTAL=0, sccache genuinely caches, and the variable goes missing.
sccache remains enabled for local development through packages/.cargo/config.toml and the .cargo/rustc-wrapper.sh shim, where it was verified to cache properly (miss, then 100% hit on an identical rebuild).
If CI sccache is ever wanted, the lever is excluding the CARGO_BIN_EXE_*-dependent test targets from caching - a real tradeoff needing its own decision.
Suggested order
- Read the 27 derivation names from a completed fixture log and decide whether the 4G cap is the cause.
- Reap the seven stale
nix-Linux-* entries and decide whether reaping should be automated.
- Re-measure total usage; only then adjust
NIX_CACHE_MAX_STORE.
- Separately review the rust-cache generation count, which is the largest consumer.
Context
Caching was introduced and scoped in PR #343. The .gitignore/scratch-path cleanup is tracked separately in #346.
Summary
CI caching now works but is not tuned. Measured on PR #343, the repository cache sits at 8.78 GB across 19 entries against GitHub's ~10 GB LRU budget, roughly 88% full, and a meaningful share of that is dead weight. Separately, the
test-fixture-contractsnix cache is demonstrably working but still leaves 27 derivations to build, and the most likely cause is a size cap that is pruning the store before save.This issue records the measurements so the tuning can be done against numbers rather than intuition.
What already works
Scoping the nix store cache per job (PR #343) fixed a real defect: all twelve nix jobs previously shared one key, and because
actions/cachenever overwrites, whichever trivial job finished first froze a 90 MB entry that every other job then "hit". Measured before and after ontest-fixture-contracts:last_accessed_aton the 2217 MB entry matches the restore step timestamp exactly, confirming the job consumes it.Worth stating for anyone reading a build log:
cache-nix-actionrestores the/nixstore directory, it is not a substituter. Nix therefore never printscopying path ... from ...for cached content - it simply finds the paths present and drops them from the build plan. The observable evidence is the action's own restore,last_accessed_at, and the derivation count. Absence of substituter lines is not absence of caching.Current cache inventory
Findings to tune
1.
gc-max-store-size-linux: 4Gis probably truncating the fixture storetest-fixture-contractsstill builds 27 derivations on a warm cache. The obvious explanation - that a source change invalidated them - was tested and falsified:git diff <seed-commit>..HEAD -- packages/is empty, so the fixture derivation is byte-identical between the seeding run and the consuming run, and the cache should have covered it entirely.The remaining hypothesis is the size cap.
gc-max-store-size-linuxprunes the store before save, so anything beyond the ceiling is dropped and must rebuild. The entry is 2217 MB compressed, consistent with an uncompressed store at or above the 4G cap.The cap was lowered from 8G to 4G during review specifically to protect the aggregate budget, so raising it is a genuine tradeoff, not a straightforward fix.
To confirm before changing anything: read the 27 derivation names from a completed
test-fixture-contractslog. If they are Rust host-tool derivations, the cap is truncating. If they are per-run inputs, the cap is correct and 27 is the floor.2. Stale per-job nix entries occupy ~730 MB
Seven
nix-Linux-*entries exist for jobs that are no longer inNIX_CACHED_JOBS(test-rust,flake-eval-x86,flake-eval-x86-outputs,test-drift,flake-eval-discover,test-flake-aarch64), plus the original sharednix-Linux-<hash>key. They will never be refreshed and will not be read. They occupy budget until LRU reclaims them.purge-prefixesonly purges within a job's own scope, so a job that no longer renders a cache step never purges its own leftovers.Options: delete them once via
gh api --method DELETE, or add a scheduled workflow that reapsnix-*entries whose prefix is not in the currentNIX_CACHED_JOBS.3.
rust-cacheis the dominant consumer at ~5.7 GBThree generations at ~1.9 GB each, keyed on
Cargo.lockhash. This is the single largest claim on the budget and was not examined during the nix cache work.Worth asking whether three live generations is the right number, or whether the key should be narrowed so old lockfile generations retire faster.
4.
MATRIX_CHECK_SCOPEhas no consumerflake-eval-x86shards share oneciJobId, so per-job scoping alone would let them clobber each other. Ascope_suffixwas added and threaded through, but that job is not inNIX_CACHED_JOBS, so the code path is currently dead. It was verified by hand (temporarily adding the job, regenerating, confirming the key rendersnix-${{ runner.os }}-flake-eval-x86-${{ matrix.check }}-...) but nothing pins it.If those shards are ever added to the cached set, that path needs a test. Note the shards each complete in under a minute today, so there is currently no case for caching them.
5. sccache is deliberately absent from CI
Recorded here so it is not re-attempted without knowing the outcome. Enabling sccache in CI breaks the build: its server forwards only a whitelist of environment variables to rustc, and
CARGO_BIN_EXE_<name>- which Cargo injects so an integration test can locate the binary it exercises - is not among them. The failure is:It does not reproduce locally because
CARGO_INCREMENTALis on there, so sccache marks those units non-cacheable and passes them through untouched. CI setsCARGO_INCREMENTAL=0, sccache genuinely caches, and the variable goes missing.sccache remains enabled for local development through
packages/.cargo/config.tomland the.cargo/rustc-wrapper.shshim, where it was verified to cache properly (miss, then 100% hit on an identical rebuild).If CI sccache is ever wanted, the lever is excluding the
CARGO_BIN_EXE_*-dependent test targets from caching - a real tradeoff needing its own decision.Suggested order
nix-Linux-*entries and decide whether reaping should be automated.NIX_CACHE_MAX_STORE.Context
Caching was introduced and scoped in PR #343. The
.gitignore/scratch-path cleanup is tracked separately in #346.