mirror of
https://github.com/jdx/mise-action.git
synced 2026-05-18 15:31:54 +00:00
feat: opt in to caching mise rust toolchains
This commit is contained in:
parent
590bfd78fa
commit
373ae063a0
6 changed files with 238 additions and 12 deletions
19
.github/workflows/test.yml
vendored
19
.github/workflows/test.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -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.
|
||||
|
|
|
|||
17
action.yml
17
action.yml
|
|
@ -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
86
dist/index.js
generated
vendored
|
|
@ -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
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
108
src/index.ts
108
src/index.ts
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue