unsloth/.github/workflows
Daniel Han a2cb0410b9
Stop 13 jobs rebuilding the same frontend on every commit (#9375)
* Stop 13 jobs rebuilding the same frontend on every commit

The uv download cache in this action works: it hits exactly (`Cache hit for:
uv-Linux-<hash>`) with one 31 kB straggler still fetched. So what is left in
`Install Unsloth (--local, --no-torch)` is not download, it is compute, and the
elapsed-second prefix added in #9153 says where it goes:

     2s  venv
     5s  overlaying local repo (editable)
    13s  unsloth installed
    16s  node
    20s  bun installed
    58s  frontend built      <- 38s in one phase
    75s  whisper.cpp prebuilt

Measured across 13 distinct Linux jobs on main: the frontend build is a median
36s of a 74s install, 49% of it, and 468s per commit producing byte-identical
output. The spread is 31 to 42s, so it is a deterministic compute cost rather
than variance.

The key, and why it is sound
------------------------------------------------------------------------
studio/setup.sh already decides whether to rebuild, by mtime: it looks for
anything under frontend/ (maxdepth 1, minus bun.lock), frontend/src or
frontend/public NEWER than frontend/dist, and skips the build when it finds
nothing. The cache key hashes exactly those three path groups, so a hit means the
build inputs are byte-identical. That is a strictly stronger statement than the
mtime test it rides on, and it is what makes a restored dist correct by
construction rather than by luck.

bun.lock is IN the key even though the staleness check excludes it. The check has
to exclude it because the install regenerates it and it would self-trigger every
run; the cache has no such problem, and a lockfile change means different
dependencies and so a different bundle. Deliberate, and it makes the cache safer
than the check it rides on.

Three ways this could have looked like it worked
------------------------------------------------------------------------
Each is handled, and each is pinned by tests/studio/test_frontend_dist_cache.py,
because all three are silent.

1. restore-keys. The uv cache above wants them: a near-miss download still
   supplies most of the wheels. A near-miss dist is a bundle built from different
   source, which is wrong rather than partial, so this cache has none.

2. mtimes. actions/cache restores through tar, which preserves the ORIGINAL
   mtimes. A dist restored that way is older than the checkout that just wrote
   every source file, so setup.sh's `find -newer dist` would see the whole tree as
   newer and rebuild anyway: a download paid for, nothing saved, and a cache hit
   reported. One `touch` of the directory is what makes the hit count, and it is
   honest because the key already proved the inputs identical.

3. an empty hashFiles. It returns "" when a glob matches nothing, which collapses
   every commit onto one key and serves an arbitrary dist, with the restore
   succeeding and the build skipped. A step refuses that outright.

The guard
------------------------------------------------------------------------
The failure that matters is not the cache breaking, it is the cache and setup.sh
drifting apart: the key stops covering an input, the cache keeps hitting, and
every job downstream tests a stale bundle that passes. So the guard reads
setup.sh's own staleness block and asserts the key covers the paths found there,
rather than comparing against a list written down in the test.

Mutation-tested, each failing exactly one test: drop src from the key; add
restore-keys; remove the touch; save off main; and add a directory to setup.sh's
check without adding it to the key.

A test I had to change rather than route around
------------------------------------------------------------------------
test_the_cache_holds_uvs_downloads_and_not_the_venv asserted every cache step in
this action points at .uv-cache, and this is the second cache. Its argument is
worth keeping: uv's cache is content-addressed, so a stale entry cannot serve
wrong content, and that property is the whole justification.

A built frontend does not get that argument and needs its own. It is a directory
of static assets with no absolute paths, no interpreter coupling and no console
scripts, which is precisely what makes a venv unsafe to cache and this safe. So
the test now allows exactly two named paths, each with its reasoning recorded at
the list, and keeps the forbidden-install-paths check applying to every cache step
regardless. Verified it still has teeth: pointing the new cache at
~/.unsloth/studio/venv fails it.

Verification
------------------------------------------------------------------------
72 passed across test_uv_cache_discipline, test_frontend_dist_cache,
test_workflow_guards_run_unfiltered and test_cache_budget_discipline.
scripts/lint_workflow_triggers.py: OK across 41 workflow files.
The action still parses; step order is restore, touch, key check, install, save.

Expected effect: about 36s off each of 13 jobs per commit. Cache size is one
built frontend per distinct source state, saved on main only, which is the rule
every other cache here follows.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-20 04:22:14 -07:00
..
cache-janitor.yml CI: prune superseded Actions caches (#8167) 2026-08-08 04:20:22 -07:00
ci-capacity.yml Bump the actions group across 1 directory with 11 updates (#8101) 2026-08-07 14:22:15 +02:00
clean-machine-install-ci.yml Let every main run finish instead of cancelling it before it starts (#9082) 2026-08-17 03:59:13 -07:00
consolidated-tests-ci.yml Run the zoo suite in parallel, minus the two files that cannot share a worker (#9285) 2026-08-19 06:25:09 -07:00
cross-platform-parity-ci.yml Stop spending the Actions cache budget on caches nothing reads (#9151) 2026-08-18 04:47:22 -07:00
desktop-app-clean-machine-ci.yml Desktop: ship a complete Linux AppImage (#9113) 2026-08-19 10:09:29 -03:00
interrupted-install-ci.yml Make apt fail fast, so the retry has something to retry (#9260) 2026-08-19 00:19:50 -07:00
kaggle-t4-notebook-ci.yml Give Unsloth Studio its first CUDA coverage, on a Kaggle T4 (#8489) 2026-08-14 06:30:18 -07:00
kaggle-t4-studio-gpu-ci.yml Give Unsloth Studio its first CUDA coverage, on a Kaggle T4 (#8489) 2026-08-14 06:30:18 -07:00
lint-ci.yml Make apt fail fast, so the retry has something to retry (#9260) 2026-08-19 00:19:50 -07:00
local-agent-guides-ci.yml Support OpenCode V2 in unsloth start (#9275) 2026-08-20 04:42:41 +02:00
lockfile-audit.yml Run two short jobs as background lanes of Lint CI, not on their own runners (#9176) 2026-08-18 05:27:55 -07:00
mlx-ci.yml Move the two version-incidental macOS jobs onto the emptier pool (#9277) 2026-08-19 03:21:25 -07:00
model-catalog-network-check.yml Reject an incompatible GGUF and base pair before the download, not after (#8158) 2026-08-09 04:37:02 -07:00
notebooks-ci.yml Give the Colab freeze a pip cache, like the job beside it already has (#9276) 2026-08-19 03:22:16 -07:00
ossf.yml Let every main run finish instead of cancelling it before it starts (#9082) 2026-08-17 03:59:13 -07:00
pester-guard-ci.yml Bump the actions group across 1 directory with 11 updates (#8101) 2026-08-07 14:22:15 +02:00
publish-desktop-updater.yml Make publish-desktop-updater.yml manual-dispatch only (#8356) 2026-08-10 20:47:31 -07:00
release-desktop.yml Desktop: ship a complete Linux AppImage (#9113) 2026-08-19 10:09:29 -03:00
security-audit.yml Take two ubuntu jobs off their own runners: absorb one, delete the other (#9360) 2026-08-20 00:05:28 -07:00
stale.yml Bump the actions group across 1 directory with 11 updates (#8101) 2026-08-07 14:22:15 +02:00
startup-profile-ci.yml Make the startup profile a gate, with budgets from its own measurements (#8965) 2026-08-16 02:37:32 -07:00
studio-api-smoke.yml Make apt fail fast, so the retry has something to retry (#9260) 2026-08-19 00:19:50 -07:00
studio-backend-ci.yml Stop spending the Actions cache budget on caches nothing reads (#9151) 2026-08-18 04:47:22 -07:00
studio-export-capability-ci.yml Take two ubuntu jobs off their own runners: absorb one, delete the other (#9360) 2026-08-20 00:05:28 -07:00
studio-frontend-ci.yml Drain the sampling debounce long enough for the node CI actually runs (#9332) 2026-08-19 23:55:01 -07:00
studio-inference-smoke.yml Make apt fail fast, so the retry has something to retry (#9260) 2026-08-19 00:19:50 -07:00
studio-load-orchestrator-ci.yml Run two short jobs as background lanes of Lint CI, not on their own runners (#9176) 2026-08-18 05:27:55 -07:00
studio-mac-install-matrix.yml Stop running every macOS workflow on every commit to main (#9174) 2026-08-18 04:22:15 -07:00
studio-mac-ui-smoke.yml Print what the server said when an inference smoke request 4xx's (#9202) 2026-08-18 21:02:07 -07:00
studio-tauri-smoke.yml Move the two version-incidental macOS jobs onto the emptier pool (#9277) 2026-08-19 03:21:25 -07:00
studio-ui-smoke.yml Cache the .deb set webkit needs, so a dead mirror costs a miss not a shard (#9289) 2026-08-19 06:57:19 -07:00
studio-update-smoke.yml Make apt fail fast, so the retry has something to retry (#9260) 2026-08-19 00:19:50 -07:00
studio-windows-api-smoke.yml Stop spending the Actions cache budget on caches nothing reads (#9151) 2026-08-18 04:47:22 -07:00
studio-windows-inference-smoke.yml Print what the server said when an inference smoke request 4xx's (#9202) 2026-08-18 21:02:07 -07:00
studio-windows-ui-smoke.yml Stop spending the Actions cache budget on caches nothing reads (#9151) 2026-08-18 04:47:22 -07:00
studio-windows-update-smoke.yml Time the install from CI, without changing the installers (#9153) 2026-08-18 04:23:09 -07:00
version-compat-ci.yml Stop spending the Actions cache budget on caches nothing reads (#9151) 2026-08-18 04:47:22 -07:00
wheel-smoke.yml Let every main run finish instead of cancelling it before it starts (#9082) 2026-08-17 03:59:13 -07:00
windows-application-control-ci.yml Let every main run finish instead of cancelling it before it starts (#9082) 2026-08-17 03:59:13 -07:00
workflow-trigger-lint.yml Stop 13 jobs rebuilding the same frontend on every commit (#9375) 2026-08-20 04:22:14 -07:00