feat: opt in to caching mise rust toolchains

This commit is contained in:
Taku Kodma 2026-05-10 20:20:37 +10:00
parent 590bfd78fa
commit 373ae063a0
No known key found for this signature in database
GPG key ID: 2FA149ECEAB1E16D
6 changed files with 238 additions and 12 deletions

View file

@ -128,6 +128,24 @@ jobs:
- run: which jq
- run: jq --version
rust_cache_setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup mise with Rust cache paths
uses: ./
with:
install: false
cache_save: false
cache_key: "rust-cache-setup-${{ github.run_id }}"
cache_rust: true
- name: Check Rust cache paths
run: |
test "$MISE_RUSTUP_HOME" = "$HOME/.local/share/mise/rustup"
test "$MISE_CARGO_HOME" = "$HOME/.local/share/mise/cargo"
test -d "$MISE_RUSTUP_HOME"
test -d "$MISE_CARGO_HOME"
fetch_from_github:
runs-on: ubuntu-latest
steps:
@ -153,6 +171,7 @@ jobs:
- specific_version
- checksum_failure
- custom_cache_key
- rust_cache_setup
- fetch_from_github
runs-on: ubuntu-latest
timeout-minutes: 1

View file

@ -20,6 +20,7 @@ jobs:
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
cache_rust: false # [default: false] also cache Rust toolchains installed by mise
experimental: true # [default: false] enable experimental features
log_level: debug # [default: info] log level
# automatically write this .tool-versions file
@ -72,6 +73,7 @@ Available template variables:
- `{{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
- `{{cache_rust}}` - Whether the Rust toolchain cache integration is enabled
- `{{default}}` - The processed default cache key (useful for extending)
Conditional logic is also supported using Handlebars syntax like `{{#if version}}...{{/if}}`.
@ -94,6 +96,22 @@ You can also extend the default cache key:
This gives you full control over cache invalidation based on the specific aspects that matter to your workflow.
### Rust Toolchain Cache
By default, mise-action only caches mise's data directory. Rust is different from most mise tools because mise delegates Rust installation to rustup, which stores toolchains in `RUSTUP_HOME` and cargo/rustup proxy binaries in `CARGO_HOME/bin`. That state may live outside the mise data directory, so a restored mise cache can otherwise make `mise install` think Rust is present while components such as `rustfmt` or `clippy` are missing.
If mise is responsible for installing Rust in your workflow, opt in to Rust toolchain caching:
```yaml
- uses: jdx/mise-action@v4
with:
cache_rust: true
```
When enabled, the action exports `MISE_RUSTUP_HOME` and `MISE_CARGO_HOME` to directories under the mise data dir before cache restore and install. Those directories are then restored and saved with the normal mise cache. The default cache key includes a `-rust` segment when this option is enabled so Rust-enabled caches do not reuse older mise-only caches. If you override `cache_key`, include `{{cache_rust}}` or otherwise invalidate that key when enabling this option.
Leave `cache_rust` as `false` when Rust is installed or cached by another action such as `rustup`, `actions-rust-lang/setup-rust-toolchain`, or `Swatinem/rust-cache`, and you use mise for other tools. `Swatinem/rust-cache` remains useful alongside this option for Cargo registry, git dependency, and `target` build artifact caching.
## 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.

View file

@ -50,8 +50,23 @@ inputs:
description: |
Override the complete cache key (ignores all other cache key options).
Supports template variables: {{version}}, {{cache_key_prefix}}, {{platform}}, {{file_hash}},
{{mise_env}}, {{install_args_hash}}, {{default}}, {{env.VAR_NAME}} for environment variables,
{{mise_env}}, {{install_args_hash}}, {{cache_rust}}, {{default}}, {{env.VAR_NAME}} for environment variables,
and conditional logic like {{#if version}}...{{/if}}
cache_rust:
required: false
default: "false"
description: |
Opt in to caching Rust toolchains installed by mise's Rust backend.
When `true`, the action exports `MISE_RUSTUP_HOME` and `MISE_CARGO_HOME`
to directories under the mise data dir before restoring the mise cache.
This lets the existing mise cache save/restore rustup toolchains,
rustup metadata, and cargo/rustup proxy binaries that mise needs for
Rust components such as rustfmt and clippy.
Default `false` keeps existing workflows unchanged, especially jobs that
install Rust with rustup, rust-cache, setup-rust-toolchain, or another
standard Rust action and use mise only for other tools.
experimental:
required: false
default: "false"

86
dist/index.js generated vendored
View file

@ -88916,11 +88916,12 @@ const MISE_CONFIG_FILE_PATTERNS = [
`**/.tool-versions`
];
// Default cache key template
const DEFAULT_CACHE_KEY_TEMPLATE = '{{cache_key_prefix}}-{{platform}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}-{{#if file_hash}}{{file_hash}}{{else}}no-config{{/if}}';
const DEFAULT_CACHE_KEY_TEMPLATE = '{{cache_key_prefix}}-{{platform}}{{#if cache_rust}}-rust{{/if}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}-{{#if file_hash}}{{file_hash}}{{else}}no-config{{/if}}';
async function run() {
try {
await setToolVersions();
await setMiseToml();
setupRustCache();
let cacheKey;
if (getBooleanInput('cache')) {
cacheKey = await restoreMiseCache();
@ -89010,6 +89011,45 @@ function setupWings() {
'is bypassed.');
}
}
/**
* Opt in to caching Rust toolchains that mise installs through rustup.
*
* mise's Rust backend delegates installation to rustup. By default, rustup
* stores toolchains in ~/.rustup and its proxies in ~/.cargo/bin, while this
* action's normal cache only stores miseDir(). Restoring only miseDir() can
* leave the cached mise install marker in place while rustup components such
* as rustfmt or clippy are missing from the runner.
*
* When `cache_rust: true`, put mise-managed Rust state under miseDir() via
* mise-specific env vars. This keeps the default behavior unchanged for
* workflows that use rust-cache, rustup, setup-rust-toolchain, or another
* Rust setup action outside of mise.
*/
function setupRustCache() {
if (!getBooleanInput('cache_rust')) {
return;
}
if (!getBooleanInput('cache')) {
warning('cache_rust is enabled, but cache is false. Rust cache setup is skipped.');
return;
}
const { rustupHome, cargoHome } = rustCacheHomes();
if (process.env.MISE_RUSTUP_HOME) {
info(`mise rust cache: using MISE_RUSTUP_HOME=${rustupHome}`);
}
else {
exportVariable('MISE_RUSTUP_HOME', rustupHome);
}
if (process.env.MISE_CARGO_HOME) {
info(`mise rust cache: using MISE_CARGO_HOME=${cargoHome}`);
}
else {
exportVariable('MISE_CARGO_HOME', cargoHome);
}
fs.mkdirSync(rustupHome, { recursive: true });
fs.mkdirSync(cargoHome, { recursive: true });
info('mise rust cache: enabled. mise-managed rustup and cargo homes will be restored and saved with the mise cache.');
}
async function exportMiseEnv() {
startGroup('Exporting mise environment variables');
const cwd = getCwd();
@ -89117,12 +89157,13 @@ async function setEnvVars() {
async function restoreMiseCache() {
startGroup('Restoring mise cache');
const cachePath = miseDir();
const cachePaths = miseCachePaths();
// Use custom cache key if provided, otherwise use default template
const cacheKeyTemplate = getInput('cache_key') || DEFAULT_CACHE_KEY_TEMPLATE;
const primaryKey = await processCacheKeyTemplate(cacheKeyTemplate);
saveState('PRIMARY_KEY', primaryKey);
saveState('MISE_DIR', cachePath);
const cacheKey = await restoreCache([cachePath], primaryKey);
const cacheKey = await restoreCache(cachePaths, primaryKey);
setOutput('cache-hit', Boolean(cacheKey));
if (!cacheKey) {
info(`mise cache not found for ${primaryKey}`);
@ -89280,16 +89321,49 @@ function miseDir() {
return path$1.join(LOCALAPPDATA, 'mise');
return path$1.join(os.homedir(), '.local', 'share', 'mise');
}
function rustCacheHomes() {
const cacheRoot = miseDir();
return {
rustupHome: process.env.MISE_RUSTUP_HOME || path$1.join(cacheRoot, 'rustup'),
cargoHome: process.env.MISE_CARGO_HOME || path$1.join(cacheRoot, 'cargo')
};
}
function miseCachePaths() {
const cacheRoot = miseDir();
const cachePaths = [cacheRoot];
if (!getBooleanInput('cache_rust')) {
return cachePaths;
}
const { rustupHome, cargoHome } = rustCacheHomes();
for (const cachePath of [rustupHome, cargoHome]) {
if (!isPathInside(cachePath, cacheRoot) &&
!cachePaths.includes(cachePath)) {
cachePaths.push(cachePath);
}
}
return cachePaths;
}
function isPathInside(child, parent) {
const relative = path$1.relative(path$1.resolve(parent), path$1.resolve(child));
return (relative === '' ||
(!!relative && !relative.startsWith('..') && !path$1.isAbsolute(relative)));
}
async function saveCache(cacheKey) {
await group(`Saving mise cache`, async () => {
const cachePath = miseDir();
const cachePaths = miseCachePaths();
if (!fs.existsSync(cachePath)) {
throw new Error(`Cache folder path does not exist on disk: ${cachePath}`);
}
const cacheId = await saveCache$1([cachePath], cacheKey);
const existingCachePaths = cachePaths.filter(fs.existsSync);
const missingCachePaths = cachePaths.filter(p => !fs.existsSync(p));
if (missingCachePaths.length > 0) {
info(`Skipping missing cache paths: ${missingCachePaths.join(', ')}`);
}
const cacheId = await saveCache$1(existingCachePaths, cacheKey);
if (cacheId === -1)
return;
info(`Cache saved from ${cachePath} with key: ${cacheKey}`);
info(`Cache saved from ${existingCachePaths.join(', ')} with key: ${cacheKey}`);
});
}
async function getTarget() {
@ -89321,6 +89395,7 @@ async function processCacheKeyTemplate(template) {
const version = getInput('version');
const installArgs = getInput('install_args');
const cacheKeyPrefix = getInput('cache_key_prefix') || 'mise-v1';
const cacheRust = getBooleanInput('cache_rust');
const miseEnv = process.env.MISE_ENV?.replace(/,/g, '-');
const platform = `${await getTarget()}-${getRunnerImageId()}`;
// Calculate file hash
@ -89344,7 +89419,8 @@ async function processCacheKeyTemplate(template) {
platform,
file_hash: fileHash,
mise_env: miseEnv,
install_args_hash: installArgsHash
install_args_hash: installArgsHash,
cache_rust: cacheRust
};
// Calculate the default cache key by processing the default template
const defaultTemplate = libExports.compile(DEFAULT_CACHE_KEY_TEMPLATE);

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View file

@ -40,12 +40,13 @@ const MISE_CONFIG_FILE_PATTERNS = [
// Default cache key template
const DEFAULT_CACHE_KEY_TEMPLATE =
'{{cache_key_prefix}}-{{platform}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}-{{#if file_hash}}{{file_hash}}{{else}}no-config{{/if}}'
'{{cache_key_prefix}}-{{platform}}{{#if cache_rust}}-rust{{/if}}{{#if version}}-{{version}}{{/if}}{{#if mise_env}}-{{mise_env}}{{/if}}{{#if install_args_hash}}-{{install_args_hash}}{{/if}}-{{#if file_hash}}{{file_hash}}{{else}}no-config{{/if}}'
async function run(): Promise<void> {
try {
await setToolVersions()
await setMiseToml()
setupRustCache()
let cacheKey: string | undefined
if (core.getBooleanInput('cache')) {
@ -141,6 +142,54 @@ function setupWings(): void {
}
}
/**
* Opt in to caching Rust toolchains that mise installs through rustup.
*
* mise's Rust backend delegates installation to rustup. By default, rustup
* stores toolchains in ~/.rustup and its proxies in ~/.cargo/bin, while this
* action's normal cache only stores miseDir(). Restoring only miseDir() can
* leave the cached mise install marker in place while rustup components such
* as rustfmt or clippy are missing from the runner.
*
* When `cache_rust: true`, put mise-managed Rust state under miseDir() via
* mise-specific env vars. This keeps the default behavior unchanged for
* workflows that use rust-cache, rustup, setup-rust-toolchain, or another
* Rust setup action outside of mise.
*/
function setupRustCache(): void {
if (!core.getBooleanInput('cache_rust')) {
return
}
if (!core.getBooleanInput('cache')) {
core.warning(
'cache_rust is enabled, but cache is false. Rust cache setup is skipped.'
)
return
}
const { rustupHome, cargoHome } = rustCacheHomes()
if (process.env.MISE_RUSTUP_HOME) {
core.info(`mise rust cache: using MISE_RUSTUP_HOME=${rustupHome}`)
} else {
core.exportVariable('MISE_RUSTUP_HOME', rustupHome)
}
if (process.env.MISE_CARGO_HOME) {
core.info(`mise rust cache: using MISE_CARGO_HOME=${cargoHome}`)
} else {
core.exportVariable('MISE_CARGO_HOME', cargoHome)
}
fs.mkdirSync(rustupHome, { recursive: true })
fs.mkdirSync(cargoHome, { recursive: true })
core.info(
'mise rust cache: enabled. mise-managed rustup and cargo homes will be restored and saved with the mise cache.'
)
}
async function exportMiseEnv(): Promise<void> {
core.startGroup('Exporting mise environment variables')
@ -267,6 +316,7 @@ async function setEnvVars(): Promise<void> {
async function restoreMiseCache(): Promise<string | undefined> {
core.startGroup('Restoring mise cache')
const cachePath = miseDir()
const cachePaths = miseCachePaths()
// Use custom cache key if provided, otherwise use default template
const cacheKeyTemplate =
@ -276,7 +326,7 @@ async function restoreMiseCache(): Promise<string | undefined> {
core.saveState('PRIMARY_KEY', primaryKey)
core.saveState('MISE_DIR', cachePath)
const cacheKey = await cache.restoreCache([cachePath], primaryKey)
const cacheKey = await cache.restoreCache(cachePaths, primaryKey)
core.setOutput('cache-hit', Boolean(cacheKey))
if (!cacheKey) {
@ -467,18 +517,64 @@ function miseDir(): string {
return path.join(os.homedir(), '.local', 'share', 'mise')
}
function rustCacheHomes(): { rustupHome: string; cargoHome: string } {
const cacheRoot = miseDir()
return {
rustupHome: process.env.MISE_RUSTUP_HOME || path.join(cacheRoot, 'rustup'),
cargoHome: process.env.MISE_CARGO_HOME || path.join(cacheRoot, 'cargo')
}
}
function miseCachePaths(): string[] {
const cacheRoot = miseDir()
const cachePaths = [cacheRoot]
if (!core.getBooleanInput('cache_rust')) {
return cachePaths
}
const { rustupHome, cargoHome } = rustCacheHomes()
for (const cachePath of [rustupHome, cargoHome]) {
if (
!isPathInside(cachePath, cacheRoot) &&
!cachePaths.includes(cachePath)
) {
cachePaths.push(cachePath)
}
}
return cachePaths
}
function isPathInside(child: string, parent: string): boolean {
const relative = path.relative(path.resolve(parent), path.resolve(child))
return (
relative === '' ||
(!!relative && !relative.startsWith('..') && !path.isAbsolute(relative))
)
}
async function saveCache(cacheKey: string): Promise<void> {
await core.group(`Saving mise cache`, async () => {
const cachePath = miseDir()
const cachePaths = miseCachePaths()
if (!fs.existsSync(cachePath)) {
throw new Error(`Cache folder path does not exist on disk: ${cachePath}`)
}
const cacheId = await cache.saveCache([cachePath], cacheKey)
const existingCachePaths = cachePaths.filter(fs.existsSync)
const missingCachePaths = cachePaths.filter(p => !fs.existsSync(p))
if (missingCachePaths.length > 0) {
core.info(`Skipping missing cache paths: ${missingCachePaths.join(', ')}`)
}
const cacheId = await cache.saveCache(existingCachePaths, cacheKey)
if (cacheId === -1) return
core.info(`Cache saved from ${cachePath} with key: ${cacheKey}`)
core.info(
`Cache saved from ${existingCachePaths.join(', ')} with key: ${cacheKey}`
)
})
}
@ -513,6 +609,7 @@ async function processCacheKeyTemplate(template: string): Promise<string> {
const version = core.getInput('version')
const installArgs = core.getInput('install_args')
const cacheKeyPrefix = core.getInput('cache_key_prefix') || 'mise-v1'
const cacheRust = core.getBooleanInput('cache_rust')
const miseEnv = process.env.MISE_ENV?.replace(/,/g, '-')
const platform = `${await getTarget()}-${getRunnerImageId()}`
@ -539,7 +636,8 @@ async function processCacheKeyTemplate(template: string): Promise<string> {
platform,
file_hash: fileHash,
mise_env: miseEnv,
install_args_hash: installArgsHash
install_args_hash: installArgsHash,
cache_rust: cacheRust
}
// Calculate the default cache key by processing the default template