Skip to content

ci: cache cargo output and stop redundant runs - #572

Merged
PathGao merged 1 commit into
masterfrom
ci/cache-rust-builds
Aug 10, 2026
Merged

ci: cache cargo output and stop redundant runs#572
PathGao merged 1 commit into
masterfrom
ci/cache-rust-builds

Conversation

@PathGao

@PathGao PathGao commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

What the measurement said

The Linux build-test job of run 31382758646 took 12m14s:

step time
run cargo test (debug compile) 2m31s
Build app (release compile) 7m57s
everything else 1m46s

86% of the job is Rust compilation. No workflow in this repository cached any cargo output, so every run recompiled the whole dependency graph from scratch, on every runner.

What this changes

1. Swatinem/rust-cache@v2 in all three workflows, after the toolchain install, pointed at workspaces: src-tauri — that is the cargo workspace root, and without naming it the action looks for Cargo.lock at the repository root and caches nothing.

The action's automatic key covers the job id, the rustc release/host and a hash of the Cargo files. That is not enough for either matrix, so both get an explicit key:

  • test_build.yml — all three entries share the job id build-test, leaving the rustc host as the only discriminator, and the target list the matrix varies never enters the key. Keyed on matrix.os-name.
  • build.yml — the two windows-latest entries share the job id and the rustc host, differing only in arch. Without a key the ARM64 job would restore the x64 job's target directory. Keyed on matrix.os-matrix.arch.

2. concurrency with cancel-in-progress in test.yml, the shape test_build.yml already has. Grouped on github.ref, which is refs/pull/<n>/merge under the pull_request trigger and refs/heads/master under the push trigger, so the two can never cancel each other.

One deliberate deviation from a plain cancel-in-progress: true: it is ${{ github.event_name == 'pull_request' }}. The push trigger exists precisely so that a commit reaching master without ever having been a pull request is still tested — the comment above it says so. Cancelling one master run because a second commit landed would leave the first commit untested, putting that hole straight back. Pull request branches, which are where the redundant runs actually pile up, cancel as normal.

3. paths-ignore on test_build.yml's pull_request trigger**.md, pics/**, .github/ISSUE_TEMPLATE/**.

I did make this change, because I could confirm it is safe. Two independent checks, both at time of writing:

So build-test is not a required status check, and a pull request whose paths are all ignored is not left waiting on a job that never starts. If build-test is ever made required, this filter has to go in the same change — the reasoning is written into the workflow so that cannot happen quietly.

Why it is safe on the merits: no Markdown file is bundled as a resource (tauri.conf.json has no resources key), so documentation cannot change what the three platforms compile. The frontend suites that read samples/*.md as fixtures still run on every pull request through test.yml, which has no path filter — that job also has to keep running because releaseWorkflow.test.ts asserts on README.md, RELEASING.md and the workflow files themselves, so a docs-only pull request can genuinely break it.

samples/** is deliberately not in the list, though it was on the table. It holds only .md files today, so **.md already covers it; naming the directory would additionally drop a future non-Markdown fixture out of the matrix without anyone noticing.

What I cannot promise

  • The speedup depends on the cache hit rate, which cannot be known before the caches exist. Treat the numbers above as what is being attacked, not as what will be saved. The next pull request is what measures it.
  • test_build.yml's cache is the weaker one. A branch can only restore caches written on itself or on the base branch, and this workflow has no push trigger — its caches are only ever written on pull request branches. So the first run of a new pull request is still a cold compile; later pushes to the same branch are the ones that hit. Only test.yml runs on push to master, which is what seeds a fresh branch at all. Giving test_build.yml a master trigger, or a shared-key with test.yml, would change that — both have costs and are a call for you, not for this pull request.
  • The per-repository cache limit is 10 GB, and this may strain it. Three platforms × two profiles (debug for cargo test, release for Build app) in test_build.yml, plus four more entries in build.yml. Eviction is least-recently-used, so the rarely-run release caches from build.yml could plausibly push out the pull-request caches that actually matter. If that shows up, dropping rust-cache from build.yml is the first thing to try — it is a workflow_dispatch-only workflow and does nothing for pull request latency.

Not done

build.yml still has no cache: npm on its setup-node steps, unlike the two test workflows. Left alone: it is release-only, so it does not affect pull request time, and it is outside what was measured.

Testing

  • npm test — 960 pass, 0 fail. (releaseWorkflow.test.ts reads all three of these files as text; nothing it asserts on was touched.)
  • npm run check — 674 files, 0 errors, 0 warnings.
  • All three workflows re-parsed as YAML to confirm paths-ignore and concurrency sit where intended.

🤖 Generated with Claude Code

Run 31382758646 spent 12m14s on the Linux build-test job: 2m31s in the
debug compile behind `cargo test`, 7m57s in the release compile behind
`Build app`, 1m46s on everything else. 86% of that job was Rust
compilation, and no workflow in this repository cached any of it, so
every run paid it again from scratch.

Three changes, in decreasing order of what they are worth:

- `Swatinem/rust-cache@v2` in all three workflows, pointed at the
  `src-tauri` workspace root. Its automatic key covers the job id, the
  rustc release/host and a hash of the Cargo files, which is not enough
  for the two matrices: every entry of test_build.yml's shares the job
  id `build-test`, and build.yml's two windows-latest entries share the
  rustc host too and differ only in `arch`. Both get an explicit `key`.

- `concurrency` with `cancel-in-progress` in test.yml, the shape
  test_build.yml has had. Grouped by `github.ref`, which differs between
  the two triggers, and cancelling only pull requests: the push trigger
  exists so a commit reaching master without a pull request is still
  tested, and cancelling one master run because another commit landed
  would put that hole straight back.

- `paths-ignore` on test_build.yml's pull_request trigger. Documentation
  cannot change what the three platforms compile -- no Markdown file is
  bundled as a resource -- and the suites that read `samples/*.md` as
  fixtures still run through test.yml, which has no path filter. Safe
  only while `build-test` is not a required status check, which it is
  not: master's protection reports no required contexts and `gh pr
  checks --required` reports none. The reasoning is in the workflow so
  that making it required cannot quietly strand a pull request.

How much of the compile is actually skipped depends on the cache hit
rate, which is not knowable before the caches exist. The next pull
request measures it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit d1c0cbf into master Aug 10, 2026
4 checks passed
@PathGao
PathGao deleted the ci/cache-rust-builds branch August 10, 2026 12:32
PathGao added a commit that referenced this pull request Aug 10, 2026
…ding it out (#574)

* ci: write the cargo cache on master only, and stop writing it four more times

#572 added the cargo cache and named two things it could not settle. Both
turned out to matter, and the measurement is now available: the repository
holds 13.22 GB of Actions cache against a 10 GB limit -- 8.76 GB in ~113 npm
caches spread over 50 refs, and 4.45 GB in the four cargo caches a single
run of #572 wrote.

So the limit binds today, and over it GitHub evicts least-recently-used.
Cargo caches are the largest entries in the repository, which makes them the
ones that vanish: without a bound, two pull requests in flight overflow the
budget and the cache becomes a slower way of not caching.

`save-if` bounds it. Pull requests restore and save nothing, so the total is
one set rather than one set per branch in flight.

That only works if something writes on master, and #572 recorded that nothing
did: test_build.yml ran on `pull_request` alone, so every cache it wrote was
scoped to a pull request branch and no other branch could read it. Every new
pull request paid a full cold compile regardless. It now also runs on push to
master, carrying the same paths-ignore, and cancel-in-progress is restricted
to pull requests there for the reason test.yml already gives -- cancelling a
master run because a second merge landed would discard the compile that was
going to fill the cache.

build.yml loses its cache entirely. Four more entries is another ~4.5 GB of a
budget that is already over, spent on the least valuable minutes here: it is
workflow_dispatch-only, runs about weekly, and nobody waits on a release
build the way they wait on a pull request check. First place to add one back
if the limit stops binding.

965 tests pass; svelte-check clean; all three workflows re-parsed as YAML.
The one thing not knowable before merging: the first master run is a cold
compile that writes the caches every later pull request reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: drop the npm cache, which was spending the budget the cargo cache needs

The 10 GB per-repository cache limit is being exceeded, and `save-if` in the
previous commit bounds the cargo half of it. This is the other half, and it
was the larger one: 113 npm cache entries, 8.76 GB, across 50 refs, most of
them pull requests closed weeks earlier.

Nothing can bound them. GitHub scopes caches by ref, so setup-node writes one
per branch per platform, and there is no `save-if` equivalent -- deleting
them by hand, as was done alongside this branch, buys a few weeks before they
grow back.

What they bought, measured on runs that hit them: `npm ci` in 6s on Linux, 9s
on macOS, 15-17s on Windows. `cache: npm` caches `~/.npm`, the download
directory, not `node_modules`; `npm ci` still runs and still links all 190
packages, so the saving is the registry fetch alone and the runners sit close
to the registry. Seconds per job, in exchange for the budget that makes an
eight-minute saving possible.

Two assertions in scripts/ciCacheBudget.test.ts hold both halves: every
rust-cache use restricts its save to master, and no workflow re-enables the
npm cache. Both were mutation-checked -- removing `save-if` fails the first
and not the second, re-adding `cache: npm` fails the second and not the
first. Neither is a law of nature and the comments say so; the point is that
the next change to either has to re-do the measurement rather than discover
the ceiling again.

965 tests pass; svelte-check clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local>
Co-authored-by: Claude Opus 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