Problem
-------
mise-action hashes ALL mise config files in the repo to compute a single
default cache key. In a monorepo with multiple projects (e.g.,
apps/frontend, apps/backend), this causes cache pollution:
1. Job A runs for apps/frontend, installs only frontend tools
2. Cache is saved with a key based on ALL configs
3. Job B runs for apps/backend, gets cache HIT (same key)
4. Job B finds frontend tools but not backend tools
5. Job B has to install all tools because they are missing from cache
Additionally, any change to an unrelated project config would bust the
cache for all projects.
Solution
--------
When working_directory is set, compute the default cache key using only
the config files that affect that directory (detected via `mise config
ls --json`) instead of globbing all configs in the repo.
This required separating binary and tools caching:
- Binary cache: restored first so mise is available for `mise config ls`
- Tools cache: default key computed after mise is installed
Key Implementation Details
--------------------------
1. Cache separation:
- restoreMiseBinaryCache/saveMiseBinaryCache for the mise binary
- restoreToolsCache/saveToolsCache for the full mise directory
- Binary cache key: `{prefix}-binary-{platform}-{version}-{dirHash}`
- Tools default cache key: based on config file contents for
working_directory
2. Binary backup during tools cache restore: The tools cache includes
bin/, which could overwrite the binary that setupMise() just installed.
We use withBinaryBackup() to backup the binary before restoring the
tools cache and restore it afterward.
An alternative approach would be to only cache installs/ and shims/
instead of the full miseDir(), but that would change the caching
behavior for existing users. Using withBinaryBackup() retains the
original caching behavior while preventing the binary from being
overwritten.
3. Binary cache key includes mise_dir hash: Prevents cache collision
when users change mise_dir between runs. Without this, a cache hit could
restore the binary to the wrong location.
4. Explicit mise binary path: Uses full path to mise binary instead of
relying on PATH lookup, avoiding potential race conditions with
core.addPath().
5. Lock file handling:
- .toml files: look for corresponding .lock file
- .tool-versions: look for mise.lock in the same directory
6. Graceful degradation: If `mise config ls` fails when
working_directory is set, caching is disabled with a warning rather
than:
- Failing the action entirely, or
- Falling back to glob patterns (which would reintroduce the bug)
Backward Compatibility
----------------------
- working_directory not set: No change, uses existing glob of all
configs
- working_directory set: Default cache key based on `mise config ls`
output
Note on cache_key input: The `cache_key` input now only controls the
tools cache key. The binary cache key is always computed automatically
based on platform, version, and mise_dir. This is generally better since
the binary cache is version-stable and does not need custom key logic.
Test Coverage
-------------
Added test-monorepo-cache.yml with 8 test scenarios:
- install-backend/restore-frontend: Verify cache isolation
- install-frontend/unrelated-change-no-bust: Verify unrelated changes do
not bust cache
- parent-config-change: Verify parent config changes bust child cache
- lock-file-change: Verify lock file changes bust cache
- install-default-mise-dir/restore-custom-mise-dir: Verify mise_dir in
cache key
Final Note
----------
Currently, the default tool cache key includes the mise version. This
was in place prior, so it was left intact. With this change and the
splitting of the mise version cache from the tool cache, we could safely
remove the mise version from the tool cache key. Left this for a
subsequent change if desired.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Fixes cache key pollution in monorepos by scoping tool cache keys to
the `working_directory`'s config hierarchy and separating binary vs.
tools caching.
>
> - New cache flow: `restoreMiseBinaryCache/saveMiseBinaryCache` (key:
`{prefix}-binary-{platform}-{version}-{dirHash}`) runs before
installation; `restoreToolsCache/saveToolsCache` uses a default key
derived from `mise config ls --json` for the specified
`working_directory`
> - Uses explicit `mise` binary path and preserves it during tools cache
restore via `withBinaryBackup` to avoid overwrites
> - Default tools key still supports template inputs; includes lockfile
handling and guards to disable caching on failures
> - `mise_dir` hash included in binary cache key to prevent cross-dir
collisions
> - Adds `.github/workflows/test-monorepo-cache.yml` with scenarios
verifying monorepo cache isolation, unrelated-config no-bust,
parent-config change bust, lockfile change bust, identical-content
different-path isolation, and `mise_dir`-key differentiation
> - Updates compiled `dist/` artifacts; minor docs entry `AGENTS.md`
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
|
||
|---|---|---|
| .github | ||
| .husky | ||
| dist | ||
| scripts | ||
| src | ||
| .eslintrc.yml | ||
| .gitattributes | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| action.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| cliff.toml | ||
| CODEOWNERS | ||
| eslint.config.mjs | ||
| LICENSE | ||
| mise.toml | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
Example Workflow
name: test
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v3
with:
version: 2024.10.0 # [default: latest] mise version to install
install: true # [default: true] run `mise install`
install_args: "bun" # [default: ""] additional arguments to `mise install`
cache: true # [default: true] cache mise using GitHub's cache
experimental: true # [default: false] enable experimental features
log_level: debug # [default: info] log level
# automatically write this .tool-versions file
tool_versions: |
shellcheck 0.9.0
# or, if you prefer .mise.toml format:
mise_toml: |
[tools]
shellcheck = "0.9.0"
working_directory: app # [default: .] directory to run mise in
reshim: false # [default: false] run `mise reshim -f`
github_token: ${{ secrets.GITHUB_TOKEN }} # [default: ${{ github.token }}] GitHub token for API authentication
- run: shellcheck scripts/*.sh
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v3
# .tool-versions will be read from repo root
- run: node ./my_app.js
Cache Configuration
You can customize the cache key used by the action:
- uses: jdx/mise-action@v3
with:
cache_key: "my-custom-cache-key" # Override the entire cache key
cache_key_prefix: "mise-v1" # Or just change the prefix (default: "mise-v0")
Template Variables in Cache Keys
When using cache_key, you can use template variables to reference internal values:
- uses: jdx/mise-action@v3
with:
cache_key: "mise-{{platform}}-{{version}}-{{file_hash}}"
version: "2024.10.0"
install_args: "node python"
Available template variables:
{{version}}- The mise version (from theversioninput){{cache_key_prefix}}- The cache key prefix (fromcache_key_prefixinput or default){{platform}}- The target platform (e.g., "linux-x64", "macos-arm64"){{file_hash}}- Hash of all mise configuration files{{mise_env}}- The MISE_ENV environment variable value{{install_args_hash}}- SHA256 hash of the sorted tools from install args{{default}}- The processed default cache key (useful for extending)
Conditional logic is also supported using Handlebars syntax like {{#if version}}...{{/if}}.
Example using multiple variables:
- uses: jdx/mise-action@v3
with:
cache_key: "mise-v1-{{platform}}-{{install_args_hash}}-{{file_hash}}"
install_args: "node@20 python@3.12"
You can also extend the default cache key:
- uses: jdx/mise-action@v3
with:
cache_key: "{{default}}-custom-suffix"
install_args: "node@20 python@3.12"
This gives you full control over cache invalidation based on the specific aspects that matter to your workflow.
GitHub API Rate Limits
When installing tools hosted on GitHub (like gh, node, bun, etc.), mise needs to make API calls to GitHub's releases API. Without authentication, these calls are subject to GitHub's rate limit of 60 requests per hour, which can cause installation failures.
- uses: jdx/mise-action@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# your other configuration
Note: The action automatically uses ${{ github.token }} as the default, so in most cases you don't need to explicitly provide it. However, if you encounter rate limit errors, make sure the token is being passed correctly.
Alternative Installation
Alternatively, mise is easy to use in GitHub Actions even without this:
jobs:
build:
steps:
- run: |
curl https://mise.run | sh
echo "$HOME/.local/share/mise/bin" >> $GITHUB_PATH
echo "$HOME/.local/share/mise/shims" >> $GITHUB_PATH