Skip to content

ci: a Zig cache is one cold build, saved once - #233

Merged
foxnne merged 1 commit into
mainfrom
ci/zig-cache-once
Oct 8, 2026
Merged

foxnne merged 1 commit into
mainfrom
ci/zig-cache-once

Conversation

@foxnne

@foxnne foxnne commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #232, now merged; rebased onto main. It touches only ci.yml.

What changes

Every other CI run on main was building from nothing, and this fixes it. I first guessed that cancelled runs were the cause. The logs show otherwise:

Run What its Linux test cache did
37320334563, main, 10-05 Cold; ended at 3.67 GB, under the limit, so it was saved
37682171347, main, 10-07 Restored 683 MB (compressed); ended at 6.94 GB and was "exceeding limit of 5368709120 bytes; clearing cache", so it was saved empty
37785306976, main, 10-08 Restored that empty cache (188 B) and built from scratch, about 10 min
37786098520 and 37786730490, 10-08 7.07 GB and 5.69 GB, both cleared and saved empty

The cause is how setup-zig caches. It saves each run under a new key and restores the newest, so a warm run saves what it restored plus everything it rebuilt. For Linux, one run is enough to go past the limit, and an over-limit cache is saved empty. Cancellation only took part because cancelled runs grow too. The near-duplicates also fill the repo's 10 GB of Actions cache, which stood at 9.6 GB.

Now:

  • setup-zig's own caching is off (use-cache: false). It still points ZIG_GLOBAL_CACHE_DIR and ZIG_LOCAL_CACHE_DIR at .zig-cache.
  • Each job restores and saves .zig-cache itself with actions/cache/restore and actions/cache/save, under a fixed key: Zig version, job, OS, arch, and the hash of every build.zig.zon, taken before anything is unpacked.
  • A fixed key can't be overwritten, so only a run that missed saves, and only if it succeeded. A job's cache is one complete cold build (about 3.7 GB for Linux tests), and every later run with the same dependencies, Zig and OS starts from it. Nothing reaches a size limit, warm runs upload nothing, and a run cancelled or failed mid-build never leaves a partial cache.
  • A bug fixed on the way: Windows build (fizzy backend, cross-compiled) still computed its key inline, the bug the test job's comment already described. Restore asked for d259c36b… and save wrote a0d09007…, so it missed every time (run 37786098520: "Cache miss"). It now takes its key from a step, like the other jobs.

What a warm run keeps, and what it doesn't:

  • It reuses the expensive, unchanging parts: package downloads (no network, so no HttpConnectionClosing), the C libraries (SDL3 ×5, freetype, tree-sitter), and the build tools.
  • The app, tests and wasm are rebuilt whenever their code changed, which is true of nearly every commit. That was already the case for warm runs.
  • A cache is rebuilt from cold only when a build.zig.zon, Zig or the runner OS changes.

Not touched: web.yml's build job also uses setup-zig's caching. Its entries stay around 264 MB, so it doesn't hit the limit. It could move to the same scheme later.

SDK impact

  • None

Verified

  • The workflow parses. Each of the three jobs runs: key step → setup-zig (use-cache: false) → restore → … → save, with save gated on success() && cache-hit != 'true'. No setup-zig cache inputs remain.
  • Live: this PR's first run will miss every new key (cold) and save one entry per job. Its next run should restore them ("Cache restored from key: zig-0.16.0-test-Linux-X64-…") and skip "Save Zig cache". I'll check both before asking for a merge.
  • After merge, main's first run is cold once and saves main's copies, which every PR then restores. The old setup-zig-cache-v2-* entries go unused and age out after 7 days, or sooner under the 10 GB limit.

Follow-ups

  • Move web.yml to the same scheme if its cache ever grows.

🤖 Generated with Claude Code

Base automatically changed from ci/integration-tests to main October 8, 2026 14:11
Every other CI run on main built from nothing. setup-zig saved each run's `.zig-cache` under a
new key and restored the newest, so a warm run saved what it restored plus everything it rebuilt:
Linux's test cache went from 3.7 GB to 6-7 GB in one run, passed the 5 GiB limit, and setup-zig
cleared it and saved it empty (run 37682171347: "Cache directory reached 6943035149 bytes ...
clearing cache"). The next run restored the empty cache (37785306976: 188 B) and started cold.
The near-duplicates also filled the repo's 10 GB of Actions cache.

setup-zig's caching is off (`use-cache: false`; it still points ZIG_GLOBAL_CACHE_DIR and
ZIG_LOCAL_CACHE_DIR at `.zig-cache`). Each job restores and saves that directory with
actions/cache under a fixed key: Zig version, job, OS, arch, and the hash of every
build.zig.zon, taken before anything is unpacked. A fixed key can't be overwritten, so only a run
that missed saves, and only if it succeeded. A job's cache is one complete cold build, and every
later run with the same dependencies starts from it.

The Windows cross-build's key was still worked out inline, the bug the test job's comment
describes: it saved under a hash taken after the fetch unpacked zig-pkg/, so every restore
missed. It now takes its key from a step like the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne
foxnne force-pushed the ci/zig-cache-once branch from e304a95 to 19eb70d Compare October 8, 2026 14:12
@foxnne
foxnne merged commit 2cb8fd8 into main Oct 8, 2026
7 checks passed
@foxnne
foxnne deleted the ci/zig-cache-once branch October 8, 2026 14:26
foxnne added a commit that referenced this pull request Oct 8, 2026
A pull request restores its Zig caches from its own earlier runs or from main's; a cache a pull
request saves is visible to that pull request alone. A push to main ran only the Linux jobs, so
main never held a macOS, Windows or cross-build cache and every pull request's first run built
those three cold (#233's warm runs took 43 s, 285 s and 50 s against 389 s, 501 s and 365 s
cold).

A push to main now runs what a pull request runs: the three-platform test matrix and the Windows
cross-build. The merge that changes a dependency saves the new caches under main, a cache GitHub
evicts after a quiet week comes back on the next merge, and every merge is tested on all three
platforms. Nothing waits on a main run, and with a cache hit the extra jobs are short.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
A pull request restores its Zig caches from its own earlier runs or from main's; a cache a pull
request saves is visible to that pull request alone. A push to main ran only the Linux jobs, so
main never held a macOS, Windows or cross-build cache and every pull request's first run built
those three cold (#233's warm runs took 43 s, 285 s and 50 s against 389 s, 501 s and 365 s
cold).

A push to main now runs what a pull request runs: the three-platform test matrix and the Windows
cross-build. The merge that changes a dependency saves the new caches under main, a cache GitHub
evicts after a quiet week comes back on the next merge, and every merge is tested on all three
platforms. Nothing waits on a main run, and with a cache hit the extra jobs are short.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 8, 2026
)

Follow-up from #233.

## What changes

**Every pull request's first run now gets warm macOS, Windows and
cross-build caches.**

The only caches every PR can restore are the ones saved by a run on
`main`; a cache a PR saves is visible to that PR alone. A push to `main`
ran only the Linux jobs, so `main` never held a macOS, Windows or
cross-build cache, and each PR's first run built those three cold. From
#233's runs:

| Job | Cold | Warm |
|---|---|---|
| macOS tests | 389 s | 43 s |
| Windows tests | 501 s | 285 s |
| Windows cross-build | 365 s | 50 s |

The warm numbers were on unchanged source; a typical PR rebuilds the app
and gains less.

**Now a push to `main` runs what a PR runs:** the three-platform test
matrix and the Windows cross-build. That gives three things with no
schedule and no new trigger:
- **The merge that changes a dependency saves the new caches to `main`
in the same run.** PRs opened after that merge start warm.
- **A cache GitHub evicts after 7 unused days comes back on the next
merge.**
- **Every merge is tested on all three platforms, not only Linux.**

**Cost:** three more jobs per merge. They're free on a public repo,
short on a cache hit, and nothing waits on a `main` run, since PRs are
gated by `ci-ok`.

(This replaces a first version that added a nightly scheduled run
instead.)

## SDK impact

- [x] None

## Verified

- [x] The workflow parses.
- Triggers are unchanged: `push` to main, `pull_request`, `merge_group`,
`workflow_dispatch`.
  - The matrix is the three platforms for every event.
  - The cross-build runs whenever the builds run.
- No other check reads `github.event_name` except the `changes` job's
diff base.
- [ ] **Live, after merge:** the merge's own push run should save
`zig-0.16.0-test-macOS-ARM64-…`, `…-test-Windows-X64-…` and
`…-windows-fizzy-backend-Linux-X64-…` under `refs/heads/main`. `main`
already holds the Linux test and integration caches. The next PR's first
run should then restore all five.

## Follow-ups

None.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant