Summary
Two related cache bugs cause unnecessary rebuilds:
- Default cache key template allows unintended prefix-match fallback - Conditional trailing segments mean one workflow's key can be a prefix of another's, causing
@actions/cache to restore a different workflow's cache.
restoreMiseCache returns undefined on cache hit - The function only returns the primary key on a miss, so the cache is never saved after a hit. Combined with (1), the correct cache is never written and the fallback repeats every run.
Problem 1: Conditional suffixes create prefix relationships
The default cache key template uses conditional {{#if}} blocks for trailing segments:
|
const DEFAULT_CACHE_KEY_TEMPLATE = |
|
'{{cache_key_prefix}}-{{platform}}-{{file_hash}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}' |
@actions/cache restoreCache performs prefix matching on the primary key even without restoreKeys. The matching cascade is: exact match on primary key → prefix match on primary key → restore keys. (docs ref)
Because trailing segments are conditional, a key without them is always a prefix of a key with them:
| Workflow |
install_args |
Key |
| A |
(none) |
mise-v0-linux-x64-<hash> |
| B |
node python |
mise-v0-linux-x64-<hash>-<args_hash> |
A's key is a prefix of B's. If only B's cache exists, A restores it via prefix match. The cache itself isn't wrong — it just may not contain all the tools A needs (or may contain tools A doesn't need), leading to unnecessary installs. The same applies across version and mise_env variations.
Problem 2: restoreMiseCache returns undefined on hit
restoreMiseCache only returns primaryKey on a cache miss (L214). On a cache hit, it falls through without a return statement, yielding undefined:
|
async function restoreMiseCache(): Promise<string | undefined> { |
|
core.startGroup('Restoring mise cache') |
|
const cachePath = miseDir() |
|
|
|
// Use custom cache key if provided, otherwise use default template |
|
const cacheKeyTemplate = |
|
core.getInput('cache_key') || DEFAULT_CACHE_KEY_TEMPLATE |
|
const primaryKey = await processCacheKeyTemplate(cacheKeyTemplate) |
|
|
|
core.saveState('PRIMARY_KEY', primaryKey) |
|
core.saveState('MISE_DIR', cachePath) |
|
|
|
const cacheKey = await cache.restoreCache([cachePath], primaryKey) |
|
core.setOutput('cache-hit', Boolean(cacheKey)) |
|
|
|
if (!cacheKey) { |
|
core.info(`mise cache not found for ${primaryKey}`) |
|
return primaryKey |
|
} |
|
|
|
core.info(`mise cache restored from key: ${cacheKey}`) |
|
} |
The caller uses the return value to gate saveCache, so on a hit, saveCache is never called:
|
if (core.getBooleanInput('install')) { |
|
await miseInstall() |
|
if (cacheKey && core.getBooleanInput('cache_save')) { |
|
await saveCache(cacheKey) |
|
} |
|
} |
Compound failure mode
With Problem 2 alone, this is harmless on an exact cache hit — the cache already exists under the correct key, so there's nothing new to save. But combined with Problem 1:
- Workflow B runs first → miss → install → cache saved under B's key
- Workflow A runs → prefix-matches B's cache → missing tools reinstalled
restoreMiseCache returned undefined (hit path) → cache NOT saved under A's key
- Next A run → same prefix match → same reinstall → never saves
- Repeat forever
If Problem 2 were fixed independently, a prefix match would cause one suboptimal run (restore wrong cache, rebuild, save correct cache), and subsequent runs would hit the correct exact-match cache. Problem 1 would go from "persistent rebuild loop" to "occasional first-run penalty."
Observed in practice
The restore log below shows a prefix match — note the requested key vs. the restored key:
Restoring mise cache
/usr/bin/ldd --version
ldd (Ubuntu GLIBC 2.39-0ubuntu8.6) 2.39
...
Cache hit for: mise-v0-linux-x64-11af6367a7fe4b577f20fd560d57f78b384692e710177df5731a4d533bffec2f
Received 4194304 of 37680007 (11.1%), 4.0 MBs/sec
Received 37680007 of 37680007 (100.0%), 28.6 MBs/sec
Cache Size: ~36 MB (37680007 B)
/usr/bin/tar -xf /home/runner/work/_temp/6422676c-cc59-4a0e-bb8a-9658bbdb83b5/cache.tzst -P -C /home/runner/work/monorepo/monorepo --use-compress-program unzstd
Cache restored successfully
mise cache restored from key: mise-v0-linux-x64-11af6367a7fe4b577f20fd560d57f78b384692e710177df5731a4d533bffec2f-e701a47bc66062ca3deccc874703ec0adcd663e90c68dd0eaadf3a8723a317cf
The requested primary key was mise-v0-linux-x64-11af6367... but the restored key has an additional -e701a47... suffix (the install_args_hash from a different workflow). Despite the cache being restored, tools were reinstalled, and due to Problem 2, the correct cache was never saved.
Proposed fixes
Fix 1 (prefix matching): Move file_hash to the end of the template so it acts as a terminator. Since file_hash is always present, no key can be a prefix of another. Bump cache_key_prefix default to mise-v1 to intentionally invalidate existing caches.
Fix 2 (missing return): Return primaryKey on cache hit in restoreMiseCache. This ensures the cache is saved under the correct key after install. If the restored key was an exact match, saveCache will no-op (key already exists). If it was a prefix match, the correct key gets saved for future runs.
PR plan
- PR 1: Fix the missing return in
restoreMiseCache (Problem 2) - minimal, unambiguous bug fix
- PR 2: Restructure default cache key template (Problem 1) - includes
cache_key_prefix bump to mise-v1
Both PRs will reference this issue.
Summary
Two related cache bugs cause unnecessary rebuilds:
@actions/cacheto restore a different workflow's cache.restoreMiseCachereturnsundefinedon cache hit - The function only returns the primary key on a miss, so the cache is never saved after a hit. Combined with (1), the correct cache is never written and the fallback repeats every run.Problem 1: Conditional suffixes create prefix relationships
The default cache key template uses conditional
{{#if}}blocks for trailing segments:mise-action/src/index.ts
Lines 42 to 43 in e371f56
@actions/cacherestoreCacheperforms prefix matching on the primary key even withoutrestoreKeys. The matching cascade is: exact match on primary key → prefix match on primary key → restore keys. (docs ref)Because trailing segments are conditional, a key without them is always a prefix of a key with them:
install_argsmise-v0-linux-x64-<hash>node pythonmise-v0-linux-x64-<hash>-<args_hash>A's key is a prefix of B's. If only B's cache exists, A restores it via prefix match. The cache itself isn't wrong — it just may not contain all the tools A needs (or may contain tools A doesn't need), leading to unnecessary installs. The same applies across
versionandmise_envvariations.Problem 2:
restoreMiseCachereturnsundefinedon hitrestoreMiseCacheonly returnsprimaryKeyon a cache miss (L214). On a cache hit, it falls through without a return statement, yieldingundefined:mise-action/src/index.ts
Lines 197 to 218 in e371f56
The caller uses the return value to gate
saveCache, so on a hit,saveCacheis never called:mise-action/src/index.ts
Lines 65 to 70 in e371f56
Compound failure mode
With Problem 2 alone, this is harmless on an exact cache hit — the cache already exists under the correct key, so there's nothing new to save. But combined with Problem 1:
restoreMiseCachereturnedundefined(hit path) → cache NOT saved under A's keyIf Problem 2 were fixed independently, a prefix match would cause one suboptimal run (restore wrong cache, rebuild, save correct cache), and subsequent runs would hit the correct exact-match cache. Problem 1 would go from "persistent rebuild loop" to "occasional first-run penalty."
Observed in practice
The restore log below shows a prefix match — note the requested key vs. the restored key:
The requested primary key was
mise-v0-linux-x64-11af6367...but the restored key has an additional-e701a47...suffix (theinstall_args_hashfrom a different workflow). Despite the cache being restored, tools were reinstalled, and due to Problem 2, the correct cache was never saved.Proposed fixes
Fix 1 (prefix matching): Move
file_hashto the end of the template so it acts as a terminator. Sincefile_hashis always present, no key can be a prefix of another. Bumpcache_key_prefixdefault tomise-v1to intentionally invalidate existing caches.Fix 2 (missing return): Return
primaryKeyon cache hit inrestoreMiseCache. This ensures the cache is saved under the correct key after install. If the restored key was an exact match,saveCachewill no-op (key already exists). If it was a prefix match, the correct key gets saved for future runs.PR plan
restoreMiseCache(Problem 2) - minimal, unambiguous bug fixcache_key_prefixbump tomise-v1Both PRs will reference this issue.