Skip to content

In-repo build artifacts survive the CI cache - #75

Merged
lann merged 1 commit into
mainfrom
ci-cache-in-repo-artifacts
Aug 13, 2026
Merged

In-repo build artifacts survive the CI cache#75
lann merged 1 commit into
mainfrom
ci-cache-in-repo-artifacts

Conversation

@lannbot

@lannbot lannbot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

  • cache-workspace-crates: true — rust-cache keeps the artifacts of crates that live in the repository (our members and the pinned .deps/iroh members) instead of deleting them on save.
  • scripts/restore-mtimes.py (new, run in CI after setup.sh) dates every tracked file by the commit that last touched it, in this repo and each .deps checkout.
  • fetch-depth: 0 on checkout, and .github/workflows/ci.yml joins the cache key.

Why

After #74 the gate is 448 s, of which ~5 min is rebuilding crates that live in the repository — measured from run 31711380488:

segment cost changes when
our crates + both LTO links (iroh-peer dominates) ~2 m 22 s iroh-peer rarely changes
iroh-relay release build (.deps/iroh/target) ~2 m 21 s the iroh pin moves
.deps crates in the host profile ~52 s a pin moves

Two independent mechanisms keep them there:

  1. Pruning. cleanTargetDir keeps only packages whose manifest is outside the workspace root, so every crate under the repo is deleted from the cache on save. cache-workspace-crates: true keeps the members of each registered workspace — ours, and iroh's in .deps/iroh/target. (The .deps/* sibling crates are path dependencies but not members of either workspace, so the ~52 s row is not addressed here.)
  2. Freshness. Cargo's path-dependency freshness is mtime-based (stable 1.97, no -Z checksum-freshness), and both actions/checkout and setup.sh's clones write sources with the current time. Verified locally: touching .deps/iroh's sources with no content change recompiles iroh, iroh-relay, iroh-peer. So restored in-repo artifacts are rejected on sight, and pruning is not even the binding constraint.

Neither half is worth landing alone — caching artifacts cargo will reject buys nothing, and dating sources with nothing cached buys nothing — so they are one change.

Verified locally, on the real .deps/iroh checkout:

  • touch every file, then restore-mtimes.pyjust build-hosts recompiles nothing (without the script: 3 crates).
  • Modify iroh-relay/src/lib.rs, then restore-mtimes.py → the file is skipped (230 of 231 stamped) and iroh-relay, iroh, iroh-peer rebuild. Backdating a modified file would produce a binary that does not match its source; this is the property that must not break.
  • .git contents are irrelevant to cargo's decisions (touching only .git rebuilds nothing).

just check passes; the gate itself is this PR's CI run.

What to expect

The key rotates (the workflow is in it), so this PR's run and the first run on main are cold — the first main run re-seeds the cache with in-repo artifacts kept. The measurement is the dispatch after that: expect the three rows above to mostly vanish on runs that do not touch their sources, and the cache entry to grow from ~1.8 GB (quota is 10 GB per repo).

Risk

We are trusting commit dates for freshness in CI. The exposure is a file whose content differs from HEAD getting backdated, which the script refuses to do (git diff --name-only HEAD is the skip list), and CI trees are pristine checkouts at a known commit. The script is not wired into any just recipe, so local builds keep using real mtimes.

The gate rebuilds every crate that lives in the repository on every run:
our two host binaries with their LTO links (~2m20), the pinned iroh
checkout's crates in the host profile (~50s), and the upstream relay
(~2m20) — about 5 of the gate's 7.5 minutes.

Two mechanisms held them there, and neither is fixed by the other.
Swatinem/rust-cache drops, from each cached target directory, every
package whose manifest lives inside that workspace root, which is all
of them; cache-workspace-crates keeps the members. And cargo decides a
path dependency is stale by mtime, while a checkout (ours) or a clone
(setup.sh's) writes every source with the current time — so a restored
artifact is always older than the sources it was built from, and keeping
it changes nothing. scripts/restore-mtimes.py dates each tracked file by
the commit that last touched it, which is stable across runs and
machines, so unchanged sources stay older than the artifacts built from
them. Files that differ from HEAD keep their mtimes: backdating a
modified file would hide the modification from cargo.

The full history the dating needs also arrives now (fetch-depth: 0);
a depth-1 checkout knows only the tip commit and would date every file
alike. The workflow joins the cache key, because it decides what the
cache holds and a save skipped on an exact-key hit would otherwise pin
the contents chosen by a superseded configuration — this change included.
@lann
lann merged commit 90f8bf8 into main Aug 13, 2026
1 check passed
@lann
lann deleted the ci-cache-in-repo-artifacts branch August 13, 2026 17:58
lannbot pushed a commit that referenced this pull request Aug 13, 2026
rust-cache keeps the cargo home; `actions/cache` keeps `target` and
`.deps/iroh/target` whole. The action's own target caching cannot serve
this gate: it saves a build directory only after deleting every package
whose manifest lives inside the workspace root, which is every crate the
gate spends its time on — ours and the pinned checkouts'. Keeping the
workspace members (#75) did not help, because they sit on top of the
path dependencies it still deleted, and a missing dependency output
rebuilds everything above it. Even among the packages it keeps, artifacts
whose file name carries no build hash are dropped: cargo's own
`deps/libiroh_relay.rlib` was the reason the relay rebuilt every run.

A green gate is what justifies saving, so the save runs after it, on main
only, and only when the restore did not already hold the key. A key that
misses still restores the newest entry under the `build-dirs-` prefix, so
cargo rebuilds what changed rather than everything.

Measured on this branch, seeded and then re-run: the gate compiles
nothing and takes 3m12, against 7m28 before; the two cache entries
together are 1.85 GB where the single one was 1.78 GB, and restoring them
costs 26s.
lannbot pushed a commit that referenced this pull request Aug 13, 2026
rust-cache keeps the cargo home; `actions/cache` keeps `target` and
`.deps/iroh/target` whole. The action's own target caching cannot serve
this gate: it saves a build directory only after deleting every package
whose manifest lives inside the workspace root, which is every crate the
gate spends its time on — ours and the pinned checkouts'. Keeping the
workspace members (#75) did not help, because they sit on top of the
path dependencies it still deleted, and a missing dependency output
rebuilds everything above it. Even among the packages it keeps, artifacts
whose file name carries no build hash are dropped: cargo's own
`deps/libiroh_relay.rlib` was the reason the relay rebuilt every run.

A green gate is what justifies saving, so the save runs after it, on main
only, and only when the restore did not already hold the key. A key that
misses still restores the newest entry under the `build-dirs-` prefix, so
cargo rebuilds what changed rather than everything.

Measured on this branch, seeded and then re-run: the gate compiles
nothing and takes 3m12, against 7m28 before; the two cache entries
together are 1.85 GB where the single one was 1.78 GB, and restoring them
costs 26s.
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.

2 participants