Skip to content

About

No description, website, or topics provided.

Resources

Stars

407 stars

Watchers

5 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

867 Commits

Folders and files

Repository files navigation

Example Workflow

name: test
on:
  pull_request:
    branches:
      - main
  push:
    branches:
      - main
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: jdx/mise-action@v5
        with:
          version: 2026.3.10 # [default: newest release at least 24h old] mise version to install
          # minimum_release_age: 7d # default: 24h; use 0s to disable the delay
          install: true # [default: true] run `mise install`
          install_args: "bun" # [default: ""] additional arguments to `mise install`
          monorepo: true # [default: true] add `--monorepo` when monorepo.config_roots is configured
          bootstrap: false # [default: false] run `mise bootstrap` instead of `mise install`
          bootstrap_skip: "tools,task" # [default: ""] comma-separated parts to skip when bootstrapping
          bootstrap_args: "--yes" # [default: ""] additional arguments to `mise bootstrap`
          cache: true # [default: true] cache mise using GitHub's cache
          cache_restore_retry_budget: 60s # [default: 0s] keep retrying a cache restore throttled with HTTP 429 for up to this long
          cache_save_retry_budget: 60s # [default: 0s] same, for cache saves
          cache_save_post: false # [default: false] save the cache in the post step, after later steps install more tools
          experimental: true # [default: false] enable experimental features
          log_level: debug # [default: info] log level
          # automatically write this .tool-versions file
          tool_versions: |
            shellcheck 0.11.0
          # or, if you prefer .mise.toml format:
          mise_toml: |
            [tools]
            shellcheck = "0.11.0"
          working_directory: app # [default: .] directory to run mise in
          reshim: false # [default: false] run `mise reshim -f`
          env: true # [default: true] export mise environment variables
          export_path: true # [default: true] add mise PATH entries to subsequent steps
          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@v7
      - uses: jdx/mise-action@v5
      # .tool-versions will be read from repo root
      - run: node ./my_app.js

Cached mise and auto_update

With version unset, a mise binary restored from the cache is kept (after verifying it against the signed checksums for its own version) until the cache key changes, so mise may lag the latest release. To always reinstall when a newer release is available, set auto_update: true. The updated binary is cached separately by version, so mise is downloaded once per release rather than on every run. Pinning version remains the way to upgrade deliberately.

Minimum Release Age

By default, the action installs the newest stable mise release that is at least 24 hours old. To change the delay, set minimum_release_age and omit version:

- uses: jdx/mise-action@v4
  with:
    minimum_release_age: 7d

Relative durations such as 24h, 7d, 6mo, and 1y are supported, as are absolute ISO dates and timestamps. The action selects the newest stable, non-draft mise release published before the cutoff. An explicit version takes precedence over minimum_release_age and can install a release immediately. Set minimum_release_age: 0s to select the latest stable release without a delay. Release selection reads the public CDN release index and does not consume GitHub API quota. If the index is unavailable or invalid, the action fails rather than bypassing the age policy. This input controls the mise binary; it does not set the release-age policy for tools installed by mise.

Environment and PATH Export

The action exports environment variables and PATH entries configured by mise to subsequent workflow steps. PATH entries are added individually through GITHUB_PATH, so the runner's complete PATH is not copied into GITHUB_ENV. Set export_path: false to export regular environment variables without persisting mise's PATH changes.

Outputs

  • cache-hit: true if the mise cache was restored.
  • versions: JSON of the active, installed tools. Each tool maps to a list of {version, requested_version, install_path, source}.
  • One output per tool with its resolved version (for example steps.mise.outputs.bun), for tools whose name is a valid output name. Tools like npm:@scope/pkg are only in versions.

This avoids a separate mise ls --json | jq step, for example to key another cache on the resolved version:

- uses: jdx/mise-action@v5
  id: mise
- uses: actions/cache@v5
  with:
    path: ~/.bun/install/cache
    key: bun-${{ runner.os }}-${{ steps.mise.outputs.bun }}-${{ hashFiles('**/bun.lock') }}

If a tool has several active versions, the per-tool output is the first one; use fromJSON(steps.mise.outputs.versions).node[1].version for the others.

Plugins

Some tools and idiomatic version files (such as .yvmrc) need a plugin. List them in the plugins input, one per line as name or name url, and the action installs them before running mise install:

- uses: jdx/mise-action@v5
  env:
    # Idiomatic version files are opt-in per tool; this is needed for `.yvmrc`
    MISE_IDIOMATIC_VERSION_FILE_ENABLE_TOOLS: yarn
  with:
    plugins: |
      yarn
      php https://github.com/verzly/mise-php#latest

Plugins already present (for example restored from the cache) are left alone. If your repo has a mise.toml, you can declare plugins there under [plugins] instead and mise install fetches them without this input.

Cache Configuration

You can customize the cache key used by the action:

- uses: jdx/mise-action@v4
  with:
    cache_key: "my-custom-cache-key"  # Override the entire cache key
    cache_key_prefix: "mise-cache-v1"       # Or just change the prefix (default: "mise-v1")

Retrying throttled cache requests

When GitHub's cache service answers HTTP 429 (too many requests), the restore is treated as a cache miss and the save is skipped. To wait it out instead, give each a retry budget:

- uses: jdx/mise-action@v4
  with:
    cache_restore_retry_budget: 60s
    cache_save_retry_budget: 120s

Each wait is at least the Retry-After GitHub advertises (1s if absent) plus random jitter of up to 2s, and the whole wait counts against the budget. When the next wait would exceed what is left, the action gives up and falls back as before: a restore miss installs the tools, a save is skipped. Both default to 0s (no retries). Genuine cache misses and reservation conflicts (another job is saving the same key) are not throttling and are never retried. The budgets also apply to the mise binary cache.

@actions/cache hides the 429 from callers and only logs it, so the action detects throttling from the warnings and debug lines it writes. If a future @actions/cache changes their wording, throttling goes undetected and nothing is retried.

The default key also expires: it includes a period number, so the cache is rebuilt from scratch at least every cache_max_age_days days (default 30, 0 disables) and picks up the latest mise. This only helps runners that start clean: a self-hosted runner that keeps the mise data directory between jobs keeps its installed mise across a rollover, so set auto_update: true there. A custom cache_key replaces the default key and doesn't expire unless it includes {{cache_period}}:

- uses: jdx/mise-action@v4
  with:
    cache_max_age_days: 14
    cache_key: "my-key-{{cache_period}}-{{file_hash}}"

Using Another Cache Action

The built-in cache uses GitHub's cache. To store the cache elsewhere (for example with runs-on/cache and S3), turn the built-in cache off and cache the mise data directory yourself with any action that has the actions/cache interface:

- uses: runs-on/cache@v4
  with:
    path: ~/.local/share/mise
    key: mise-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/mise.toml', '**/.mise.toml', '**/mise.*.toml', '**/.mise.*.toml', '**/mise.lock', '**/.mise.lock', '**/mise.*.lock', '**/.tool-versions') }}
- uses: jdx/mise-action@v4
  with:
    cache: false

The path to cache is mise's data directory, ~/.local/share/mise by default (%LOCALAPPDATA%\mise on Windows). The action uses the first of these that is set: its mise_dir input, then MISE_DATA_DIR, then $XDG_DATA_HOME/mise, then the default above. When mise_dir is set, the action also exports it as MISE_DATA_DIR so mise installs tools into the cached directory; mise's state directory is unaffected (set MISE_STATE_DIR to move it). If several are set, path must be the directory that wins in that order. Build the key from your config files as above (list every config name your repo uses, including environment-specific mise.<env>.toml files, since the key must change whenever the tools do), and add anything else that should invalidate it (install_args, MISE_ENV, the runner image).

Cache Validated Downloads

With a committed mise.lock, mise verifies cached downloads against lockfile checksums before use, so a poisoned or stale cache cannot change what gets installed. Only downloads with checksums are reused. Tools are unpacked and installed each run; source-built tools compile each time. See mise's CI guidance for details.

- uses: actions/cache@v6
  with:
    path: ~/.cache/mise/downloads-cas
    key: mise-downloads-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('mise.lock') }}
- uses: jdx/mise-action@v5
  with:
    cache: false

The path is mise's default cache directory on Linux. On macOS, cache ~/Library/Caches/mise/downloads-cas; on Windows, mise uses %TEMP%\mise\downloads-cas by default. actions/cache does not expand %TEMP%, so on Windows set MISE_CACHE_DIR and use the matching resolved path:

env:
  MISE_CACHE_DIR: ${{ runner.temp }}\mise
steps:
  - uses: actions/cache@v6
    with:
      path: ${{ runner.temp }}\mise\downloads-cas
      key: mise-downloads-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('mise.lock') }}

If you set MISE_CACHE_DIR to a different location, cache downloads-cas under it. See mise's directories.

This is a separate cache from the action's built-in installed-tools cache. The action's cache-hit output describes only its built-in cache and does not report hits from this external downloads cache.

Release workflows

Don't restore a cache in a workflow that publishes artifacts. A release job can restore caches written on the default branch, so a lower-trust workflow that can write there, such as one triggered by pull_request_target, workflow_run or issue_comment that runs untrusted code, can put files of an attacker's choosing into the release. An ordinary pull_request build only writes to its own merge-ref scope, which a release job doesn't restore. This action's cache holds installed tools. Turn it off in release jobs:

- uses: jdx/mise-action@v5
  with:
    cache: false

Setting GitHub's cache-mode: none on the workflow or job also works. The action honors it, so it skips both restore and save, as do other steps that use the cache.

Template Variables in Cache Keys

When using cache_key, you can use template variables to reference internal values:

- uses: jdx/mise-action@v4
  with:
    cache_key: "mise-{{platform}}-{{version}}-{{file_hash}}"
    version: "2026.3.10"
    install_args: "node python"

Available template variables:

  • {{version}} - The mise version (from the version input)
  • {{cache_key_prefix}} - The cache key prefix (from cache_key_prefix input or default)
  • {{platform}} - The target platform, including the runner image (e.g., "linux-x64-ubuntu24", "macos-arm64-macos15", "linux-x64-self-hosted"). The trailing segment is process.env.ImageOS on github-hosted runners and falls back to "self-hosted" elsewhere — preventing cache collisions when the same repo runs on different runner providers (github-hosted, namespace.so, self-hosted).
  • {{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
  • {{bootstrap_hash}} - SHA256 hash of bootstrap mode, skip list, and args
  • {{plugins_hash}} - SHA256 hash of the plugins input (empty when unset)
  • {{cache_period}} - The current cache_max_age_days period number (empty when that is 0)
  • {{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@v4
  with:
    cache_key: "mise-v1-{{platform}}-{{install_args_hash}}-{{file_hash}}"
    install_args: "node@24 python@3.14"

You can also extend the default cache key:

- uses: jdx/mise-action@v4
  with:
    cache_key: "{{default}}-custom-suffix"
    install_args: "node@24 python@3.14"

This gives you full control over cache invalidation based on the specific aspects that matter to your workflow.

Rust Cache

Rust has a known cache interaction because mise installs Rust through rustup.

mise records that Rust is installed in its own data directory, but rustup keeps the toolchain, components (rustfmt, clippy, ...), and targets in ~/.rustup and ~/.cargo. The action only caches the mise data directory, so a cache restore can bring back mise's "installed" marker without the toolchain or its components. GitHub-hosted runners also ship their own rustup, which mise may reuse instead of installing its own.

With mise 2026.9.2 or newer, mise install (the action's default) notices this and repairs it: it checks the rustup toolchain and its components and re-downloads whatever is missing instead of trusting the marker. Later steps no longer fail with errors like a missing rustfmt. The remaining cost is that the toolchain is downloaded again on every run, because rustup's state isn't cached. With older mise versions, or when the action doesn't run mise install (install: false), the restored marker is trusted and those errors can still happen.

Two ways to avoid the re-download:

  1. Keep rustup's state inside the mise data directory so it is cached together with it. This is complete, but the cache can get large (around 2.5 GB with several components and targets):

    env:
      MISE_RUSTUP_HOME: /home/runner/.local/share/mise/rustup # macOS: /Users/runner/...
      MISE_CARGO_HOME: /home/runner/.local/share/mise/cargo

    These are the Linux paths (macOS runners use /Users/runner/...). On Windows the action caches %LOCALAPPDATA%\mise by default, so on GitHub-hosted Windows runners use C:\Users\runneradmin\AppData\Local\mise\rustup and C:\Users\runneradmin\AppData\Local\mise\cargo (on other runners, the same paths under that runner's %LOCALAPPDATA%). If you customize mise_dir, MISE_DATA_DIR, or XDG_DATA_HOME, keep these paths under the same directory.

  2. Cache the Rust build outputs separately. A Rust cache such as Swatinem/rust-cache caches the cargo registry and target/, not the toolchain, so it works alongside mise managing Rust: mise reinstalls the toolchain on each run (see above) and the cache covers the expensive part. If you would rather not download the toolchain at all, install Rust with rustup (or a dedicated action) and remove rust from mise.toml / .tool-versions (or keep it only in a config the CI job doesn't use, for example behind MISE_ENV), because the action runs mise install by default and would otherwise install Rust through mise and cache its install record again.

See jdx/mise-action#215 for the discussion.

Matrix Builds

To run the same job against several versions of a tool, override that tool with a MISE_<TOOL>_VERSION environment variable. The override applies on top of your repo's mise.toml, so every other tool is kept and the action installs only what the job needs:

strategy:
  matrix:
    ruby-version: ["3.3", "3.4"]
env:
  MISE_RUBY_VERSION: ${{ matrix.ruby-version }}
steps:
  - uses: actions/checkout@v7
  - uses: jdx/mise-action@v5
    with:
      cache_key: "{{default}}-ruby${{ matrix.ruby-version }}"
  - run: mise exec -- ruby --version

Set the variable at the job level, as above, so later steps that run mise exec, mise run or a shim also use the matrix version. Setting it only on the action step doesn't carry over: the action exports what mise env reports, not its own env. Add the matrix value to cache_key so each version gets its own cache (see Template Variables in Cache Keys).

You can also use mise x ruby@${{ matrix.ruby-version }} -- <command> for a single command, which keeps the other tools from mise.toml.

The mise_toml input is not a good fit for matrices: it writes a mise.toml in the job's current directory that replaces the tools from your repo's own config, and a .mise.toml in that directory takes precedence over it, so the matrix value is silently ignored. The action warns when it sees this.

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@v4
  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.

GitHub token persistence (unreleased)

persist_github_token is not available in the published @v4, @v5, or @v5.1.0 versions. Those versions still persist the action token automatically and ignore this input. The examples below pin the unreleased implementation commit that supports it. Use a release containing this change once available.

With this implementation, the token is available as MISE_GITHUB_TOKEN only during this action by default (persist_github_token: false). An existing env.MISE_GITHUB_TOKEN takes precedence over github_token. To authenticate later shims, mise exec, or lazy installs, set persist_github_token: true to export the action's token for subsequent steps:

- uses: jdx/mise-action@2f1a5eb16aec0e6793d6a7e6ec2901969cf93f03 # unreleased persistence support
  with:
    persist_github_token: true

You can also persist a separate credential, such as a read-only token. The action still authenticates its own tool installs with github_token (or an existing MISE_GITHUB_TOKEN); later steps receive the separate token:

- uses: jdx/mise-action@2f1a5eb16aec0e6793d6a7e6ec2901969cf93f03 # unreleased persistence support
  with:
    persist_github_token: ${{ secrets.READ_ONLY_TOKEN }}

Setting persist_github_token: false does not clear a token already set at the job level or exported by an earlier step. This changes the previous behavior, which persisted github_token automatically; workflows relying on it in later steps should opt in explicitly.

Lock Files

If a repo mise lock file such as mise.lock is present in the working directory or one of its parents, this action automatically runs mise install --locked. You can still pass install_args; --locked will be added automatically unless you already included it yourself.

This auto-detection is intended for repo-managed config files. If you provide mise_toml or tool_versions inputs, the action does not automatically force locked mode.

Monorepo installs

When the mise configuration defines [monorepo].config_roots, the action automatically adds --monorepo to mise install so tools configured across the monorepo are installed. This is enabled by default and does nothing for repositories without that setting. Set monorepo: false to opt out. You can also pass --monorepo explicitly through install_args.

Bootstrap

Set bootstrap: true to run mise bootstrap instead of mise install:

- uses: jdx/mise-action@v4
  with:
    bootstrap: true

When a repo mise lock file is present, the action automatically runs mise --locked bootstrap. install_args cannot be combined with bootstrap: true; use bootstrap_skip and bootstrap_args for bootstrap customization.

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

Install a subset of configured tools

When a job needs only some of the tools in your mise.toml, set mise's MISE_ENABLE_TOOLS setting at the job level:

jobs:
  validate:
    runs-on: ubuntu-latest
    env:
      MISE_ENABLE_TOOLS: rust
    steps:
      - uses: actions/checkout@v7
      - uses: jdx/mise-action@v5
        with:
          cache_key_prefix: mise-v0-${{ env.MISE_ENABLE_TOOLS }}
      - run: mise run validate

This is a mise setting, not a mise-action input. Setting it at the job level limits the tools mise enables both when the action installs tools and in later commands such as mise run. By contrast, install_args only limits the tools installed by the action's mise install invocation; a later mise run may install other configured tools. Include the setting in a custom cache key when different jobs use different allowlists, as the default key does not include MISE_ENABLE_TOOLS.

About

No description, website, or topics provided.

Resources

Stars

407 stars

Watchers

5 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages