Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jdx/mise
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 798ca8d
Choose a base ref
...
head repository: jdx/mise
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 9225550
Choose a head ref
  • 14 commits
  • 133 files changed
  • 8 contributors

Commits on Apr 25, 2026

  1. Configuration menu
    Copy the full SHA
    dbf496e View commit details
    Browse the repository at this point in the history

Commits on Apr 26, 2026

  1. Configuration menu
    Copy the full SHA
    ccd9cd9 View commit details
    Browse the repository at this point in the history
  2. fix(schema): allow array values in tool additionalProperties (#9400)

    Tool options like rust `components` accept arrays, e.g.:
    
    ```toml
    [tools]
    rust = { version = "1.77", components = ["rustfmt", "clippy"] }
    ```
    
    The old schema though would say otherwise, and linters like tombi flag
    the above as invalid.
    
    Signed-off-by: JP-Ellis <[email protected]>
    JP-Ellis authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    aff9814 View commit details
    Browse the repository at this point in the history
  3. feat: opt-in to pre-release versions for github and aqua backends (#9329

    )
    
    Implements the opt-in proposed in discussion #9323 (approved by @jdx).
    Also wires support into the `aqua:` backend as a small follow-up, as
    requested.
    
    ## What changed
    
    Adds a per-tool `prerelease = true` option to the `github:` and `aqua:`
    backends. When set:
    
    - Releases flagged `prerelease: true` on GitHub appear in `mise
    ls-remote`
    - `latest` resolves against the full list including pre-releases
    - Fuzzy version queries (e.g. `1.2`) match pre-release tags under that
    prefix
    
    ```toml
    [tools]
    "github:myorg/mytool" = { version = "latest", prerelease = true }
    "aqua:owner/tool"     = { version = "latest", prerelease = true }
    ```
    
    Default behavior is unchanged (`prerelease = false`). Draft releases are
    always excluded.
    
    ## Where the filters lived
    
    Two independent filters needed an escape hatch:
    
    1. **GitHub API flag filter** in `src/github.rs::list_releases_`
    (`!r.prerelease`). Split so the cache holds unfiltered-except-drafts;
    `list_releases` preserves old behavior via read-time filter, new
    `list_releases_including_prereleases{,_from_url}` exposes the unfiltered
    view. Cache is shared — no extra API cost.
    2. **`VERSION_REGEX` filter** in `fuzzy_match_filter` (trait default +
    `aqua` override). Extracted into a shared
    `fuzzy_match_versions(versions, query, filter_prereleases)` helper so
    both call paths can opt out.
    
    Also, the `github:` backend's `latest_stable_version` uses the
    `/releases/latest` endpoint (stable-only by design) — when `prerelease =
    true` it now falls through to `latest_version_for_query("latest", None)`
    to resolve against the full list. Uses the `_for_query` variant
    deliberately to avoid recursing back through `latest_stable_version`.
    
    ## Tests
    
    - `fuzzy_match_versions`: unit tests pinning both the default (filters
    prereleases) and opted-in (includes them) behavior across `latest` and
    partial queries
    - `UnifiedGitBackend::fuzzy_match_filter`: unit tests verifying the
    override reads `self.ba.opts()` and toggles correctly
    - `include_prereleases`: bool-parsing sanity check
    - All 750+ existing unit tests still pass (one pre-existing failure —
    `test_inline_install_before_wins_over_config_entry` — reproduces on
    clean `main`, unrelated)
    
    ## What isn't here
    
    **No e2e test yet** — `jdx/mise-test-fixtures` only has one stable
    release, so exercising the opt end-to-end needs a fixture with at least
    one release flagged `prerelease: true`. Happy to add that in a follow-up
    if you can flag an existing tag (or I can mock this differently if you'd
    prefer).
    
    **Not touching GitLab/Forgejo** — the opt is documented as a no-op for
    those backends. Can extend later if there's demand.
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
    3 people authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    e54999c View commit details
    Browse the repository at this point in the history
  4. fix(backend): allow unresolved latest opt-in (#9401)

    ## Summary
    
    Fixes `latest` resolution for backends that can install an unresolved
    selector when their remote version list is empty.
    
    `minimum_release_age` cannot be evaluated when a backend has no version
    candidates, but falling back to literal `latest` for every empty-list
    backend is unsafe: some backends require concrete versions and must
    continue failing rather than creating literal `latest/` installs. This
    change keeps the fallback in the central resolver but makes it opt-in
    through `Backend::unresolved_latest_version()`.
    
    The resolver now only falls back when the backend’s unfiltered remote
    version list is empty and the backend explicitly provides an unresolved
    selector. If versions exist but are all filtered out by
    `minimum_release_age`, mise still reports no matching version,
    preserving the age gate.
    
    `pipx` opts in only for git-backed requests, where `latest` is an
    installable selector. Its remote version listing remains honest: GitHub
    repos with no releases return `[]` from `ls-remote` instead of reporting
    `latest` as a release. Tests also assert GitHub release timestamps are
    preserved for date filtering.
    
    ## Validation
    
    - `cargo fmt --check`
    - `cargo test backend::pipx::tests`
    - `cargo build`
    - `mise run test:e2e
    e2e/backend/test_fuzzy_versions_do_not_install_literal_dirs`
    - clean-cache config check:
    `pipx:a13xp0p0v/kernel-hardening-checker@latest` resolves to `latest`
    without `minimum_release_age`
    - clean-cache config check:
    `pipx:a13xp0p0v/kernel-hardening-checker@latest` with
    `minimum_release_age = "7d"` resolves to `latest`
    - clean-cache guard check: `pipx:psf/black@latest` with
    `minimum_release_age = "1970-01-01"` still reports no matching version
    instead of falling back to HEAD
    - clean-cache `ls-remote pipx:a13xp0p0v/kernel-hardening-checker --json`
    returns `[]`
    - pre-commit hook during amend, including `cargo check --all-features`
    
    *This PR description was generated by an AI coding assistant.*
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Medium Risk**
    > Changes central `@latest` resolution behavior by allowing an
    unresolved selector fallback for opted-in backends when remote version
    listing is empty, which could affect install outcomes for `latest`
    requests. Scope is limited via explicit backend opt-in and only triggers
    in non-offline mode with no remote versions returned.
    > 
    > **Overview**
    > Fixes `@latest` resolution for backends that can *install an
    unresolved selector* when no remote versions are discoverable.
    > 
    > Adds a new `Backend::unresolved_latest_version()` opt-in hook and
    updates `ToolVersion::resolve_version` to fall back to that value only
    when the backend’s unfiltered remote version list is empty (and only
    when not offline), preserving failure behavior when versions exist but
    don’t match date/age filters.
    > 
    > Updates `pipx` to opt in for git-based requests by returning `latest`
    as an installable unresolved selector, while making its remote version
    listing for non-GitHub git sources return an empty list (instead of a
    synthetic `latest` version), and adds tests for GitHub release->version
    mapping and timestamp preservation.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    483868d. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    jdx authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    580635a View commit details
    Browse the repository at this point in the history
  5. chore(deps): bump communique to 1.1.2 (#9402)

    ## Summary
    - update the communique mise lock entry to v1.1.2
    - refresh release asset URLs and checksums, including musl assets
    
    ## Validation
    - monitored jdx/communique release workflow 24960017639 to success
    - `mise install --locked communique`
    - mise pre-commit lint hook ran during commit
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Low Risk**
    > Low risk: only updates the `mise.lock` entry for the `communique`
    tool, changing pinned version plus release asset URLs/checksums with no
    runtime code changes.
    > 
    > **Overview**
    > Bumps the pinned `communique` tool in `mise.lock` from `v1.0.4` to
    `v1.1.2`.
    > 
    > Refreshes the per-platform release asset metadata (URLs, `url_api`,
    and checksums), including switching the Linux ARM64 *musl* entry to the
    correct musl artifact.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    c51648f. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    jdx authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    47a71fd View commit details
    Browse the repository at this point in the history
  6. chore(ci): improve pr-closer workflow (#9403)

    ## Summary
    - Backports improvements made to the same workflow in jdx/hk#876.
    - Adds explicit `permissions: pull-requests: write` so the job can't 403
    on read-only default tokens.
    - Replaces hardcoded `jdx/mise` with `${{ github.repository }}` (via
    `REPO` env).
    - Filters stale PRs server-side via `--search "updated:<CUTOFF
    -author:jdx -label:keep-open sort:updated-asc"` and bumps `--limit` to
    500. Only stale candidates are fetched, sorted oldest-first, so anything
    beyond the limit rolls into the next daily run instead of being silently
    skipped.
    
    ## Test plan
    - [x] Verified the search query against jdx/mise — currently returns 0
    stale candidates (workflow has been keeping things tidy)
    - [ ] Trigger via `workflow_dispatch` once merged to confirm a clean run
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Low Risk**
    > Low risk: limited to a GitHub Actions workflow and only changes how
    stale PRs are queried/closed; main risk is mis-filtering PRs and closing
    the wrong ones.
    > 
    > **Overview**
    > Improves the `pr-closer` GitHub Actions workflow by granting explicit
    `pull-requests: write` permissions and removing the hardcoded repository
    in favor of `${{ github.repository }}`.
    > 
    > Changes stale-PR selection to be filtered server-side using a date
    cutoff search (sorted oldest-first) and increases the fetch limit to 500
    before closing candidates, while keeping the “failing vs passing checks”
    close message behavior.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    ae0b5f8. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    jdx and claude authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    41a8887 View commit details
    Browse the repository at this point in the history
  7. feat(backend): add global libc preference (#9404)

    ## Summary
    
    Adds a global `libc` setting (`MISE_LIBC`) for selecting Linux
    precompiled binary variants across backends that can safely honor a libc
    preference.
    
    ## Details
    
    - Promotes libc preference into typed settings and schema generation,
    accepting `musl`, `glibc`, and `gnu`.
    - Threads libc through `Platform::current()` and `PlatformTarget` so
    generic GitHub asset matching can prefer musl or glibc variants
    consistently.
    - Adds aqua support in two layers:
    - switches aqua registry Linux replacements like
    `unknown-linux-gnu`/`unknown-linux-musl` according to the target/global
    libc preference;
    - keeps the fallback that prefers a very similar release asset with a
    standalone `musl` token when aqua cannot express the variant yet.
    - Applies the setting to Bun, Python precompiled builds, Node's common
    musl unofficial-build flavor, and vfox runtime `envType`.
    - Updates Docker and Node docs for the new global setting.
    
    ## Backend Audit
    
    Conda, Go, Zig, Deno, Ruby, Java, Erlang, and Rust do not currently
    expose a safe global libc selection path here because their upstream
    artifact naming is either not libc-specific, hard-coded by upstream, or
    already controlled by backend-specific settings.
    
    ## Validation
    
    - `cargo test libc`
    - `cargo test backend::aqua::lock_candidate_tests`
    - `cargo test -p vfox config::tests::test_env_type`
    - `cargo fmt --check`
    - `git diff --check`
    - commit hook: `cargo check --all-features`, schema validation,
    markdownlint, shellcheck, shfmt, taplo, prettier, actionlint, lua checks
    
    *This PR was generated by an AI coding assistant.*
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Medium Risk**
    > Adds a new global `libc` setting that changes how Linux artifacts are
    selected across multiple backends and influences cache keys, so
    mis-detection or incorrect preference could cause wrong binaries/lock
    entries to be chosen.
    > 
    > **Overview**
    > Adds a global `libc` setting (schema + `settings.toml`, env
    `MISE_LIBC`, accepting `glibc`/`gnu`/`musl`) and uses it to influence
    Linux platform qualification and libc detection.
    > 
    > Updates platform/target handling to expose libc (including compound
    qualifiers) and adjusts caching and GitHub asset matching so musl
    variants can be preferred when requested.
    > 
    > Extends backend/tool integrations to honor the preference: `aqua` now
    rewrites Linux replacements and prefers musl release assets for musl
    lock targets, Node/Python/Bun select musl/glibc variants accordingly,
    and `vfox` passes an optional runtime `envType` into plugin hooks. Docs
    are updated to describe the new setting and Docker override behavior.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    16a6a2a. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    jdx authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    09cff3d View commit details
    Browse the repository at this point in the history
  8. fix(prune): skip remote version resolution for tracked configs (#9406)

    ## Summary
    
    - `mise prune` was resolving every tracked config's tool versions
    against the network (npm registry, Go proxy, GitHub API), which could
    hang on slow or failing registries — see
    [#9405](#9405).
    - The remote calls were wasted work: prune only protects *installed*
    versions from deletion, and the resolver's early-return paths
    (`latest_installed_version`, `list_installed_versions_matching`) already
    match installed versions without hitting the network. If no installed
    version matches, the remote-resolved result can't appear in the deletion
    set anyway.
    - Adds an `offline` field to `ResolveOptions`, additive to
    `Settings::offline()`. Set to `true` in prune's path; left `false` for
    `mise upgrade` (which deliberately wants fresh remote data to decide
    what tracked configs would resolve to after an upgrade).
    - Fixes the `latest` resolver path: with offline + nothing installed,
    returns the unresolved `"latest"` string instead of erroring (safe —
    won't match any installed pathname).
    - Replaces the old workaround `MISE_OFFLINE=1 mise prune --yes` with
    default behavior.
    
    Closes [#9405](#9405).
    
    ## Test plan
    
    - [x] `cargo check` clean
    - [x] `mise run lint-fix` clean
    - [x] `mise run test:unit` (774 passed)
    - [x] `mise run test:e2e test_prune` passes
    - [ ] Reviewer: in a directory with multiple tracked configs containing
    prefix/latest/range versions (e.g. `node = "22"`, `npm = "^11"`, a `go:`
    tool), `MISE_DEBUG=1 mise prune --dry-run` should no longer show `DEBUG
    GET https://registry.npmjs.org/...`, `proxy.golang.org/...`, or
    `api.github.com/...` requests during resolution.
    - [ ] Reviewer: `MISE_DEBUG=1 mise upgrade --dry-run` should still show
    remote version queries (verifying upgrade's behavior is unchanged).
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Medium Risk**
    > Medium risk because it changes version resolution behavior in offline
    paths (including `latest`/prefix/sub semantics), which can affect which
    tool versions are considered safe to delete during `prune` and after
    `upgrade`. Scope is contained to resolution options and tracked-config
    protection logic.
    > 
    > **Overview**
    > **`mise prune` no longer performs network lookups** when resolving
    tracked config requirements, by threading a new `ResolveOptions.offline`
    flag through tracked-config resolution.
    > 
    > This updates `get_versions_needed_by_tracked_configs` to accept an
    `offline` parameter (used by prune, not upgrade) and adds conservative
    protection for `sub-N:latest` when offline by keeping all installed
    versions for that backend.
    > 
    > Resolver behavior is adjusted so `offline` resolution can return
    unresolved specs for `latest`, prefix, and `sub-N:latest` instead of
    erroring or fetching remotes, while leaving normal
    `upgrade`/`use`/`install` paths explicitly `offline: false`.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    5316331. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    jdx and claude authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    b404138 View commit details
    Browse the repository at this point in the history
  9. fix(node): route musl tarball URLs to unofficial-builds (#9409)

    ## Summary
    
    [PR #9404](#9404) (`feat(backend): add
    global libc preference`) taught Node's slug builder to append `-musl`
    for musl targets but kept routing through `nodejs.org/dist/`, which does
    not host musl tarballs (they live at `unofficial-builds.nodejs.org`).
    The visible symptom was in [PR
    #9396](#9396 `mise.lock` diff: the
    URL gained a `-musl` suffix while the checksum stayed pinned to the
    original glibc tarball.
    
    ```toml
    [tools.node."platforms.linux-x64-musl"]
    checksum = "sha256:dbf5b8665..."   # ← still the glibc checksum
    url = ".../v24.14.0/node-v24.14.0-linux-x64-musl.tar.gz"   # ← 404, wrong host
    ```
    
    Mechanically: `resolve_lock_info` builds a 404'ing URL on
    `nodejs.org/dist`, fetches the wrong `SHASUMS256.txt` (which doesn't
    list `-musl.tar.gz`), gets `None` back, and the lockfile merge preserves
    the **stale glibc checksum** alongside the new URL. Anyone running `mise
    install` against that lockfile on a musl system would either 404 or hit
    a checksum mismatch.
    
    The aqua/github-backed tools in the same release diff updated cleanly
    because their checksum source rotates with the artifact. Node is unique
    in fetching checksums from a separate `SHASUMS256.txt`.
    
    ## Changes
    
    ### `src/plugins/core/node.rs`
    
    Add `mirror_url_for(&SettingsNode, filename)` that swaps to
    `https://unofficial-builds.nodejs.org/download/release/` when a filename
    references a musl artifact and the user has not set a custom
    `node.mirror_url`. Wire it into `resolve_lock_info`, `get_tarball_url`,
    `BuildOpts::new`, and `shasums_url` so the tarball URL and the matching
    `SHASUMS256.txt` always come from the same host. Three unit tests cover
    routing (default → glibc, musl → unofficial-builds, custom mirror passes
    through unchanged).
    
    ### `src/lockfile.rs`
    
    Defense in depth: when merging `PlatformInfo` (both in
    `set_platform_info` and `merge_with`), drop the other side's
    `checksum`/`size`/`url_api` if URLs disagree — those fields describe a
    specific artifact and become stale once the URL changes. The
    pre-existing `test_platform_info_merge_prefers_sha256` was asserting
    that sha256 should win even across mismatched URLs, which is exactly the
    latent bug; updated it to use a shared URL and added
    `test_platform_info_merge_drops_stale_checksum_on_url_change`.
    
    ### `mise.lock`
    
    Re-ran `mise lock node` to fix the three corrupted node musl entries.
    Checksums verified against upstream:
    
    ```sh
    $ curl -fsSL https://unofficial-builds.nodejs.org/download/release/v24.14.0/SHASUMS256.txt | grep -E "linux-(arm64|x64)-musl\.tar\.gz"
    8f81d47b7f...  node-v24.14.0-linux-arm64-musl.tar.gz
    bae0f23204...  node-v24.14.0-linux-x64-musl.tar.gz
    ```
    
    Both match what mise now writes.
    
    ## Test plan
    
    - [x] `cargo test` — 778 passed, 0 failed (includes 3 new node tests + 1
    new lockfile test, plus an updated test that previously asserted the
    latent bug).
    - [x] `cargo clippy --all-features --tests` — clean.
    - [x] `cargo fmt` — clean.
    - [x] `mise lock node` against this branch produces correct URLs +
    checksums for all three node musl platform variants.
    - [ ] Install on Alpine / musl host: `MISE_LIBC=musl mise install
    [email protected]` should download from `unofficial-builds.nodejs.org` and
    run.
    - [ ] Glibc regression: same flow without `MISE_LIBC=musl` should still
    hit `nodejs.org/dist`.
    
    *This PR was generated by an AI coding assistant.*
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Medium Risk**
    > Changes Node.js download/checksum URL selection and lockfile merge
    behavior, which can affect installs and lockfile correctness across
    platforms; scope is contained with added tests.
    > 
    > **Overview**
    > Fixes Node.js *musl* installs/lock resolution by routing musl tarball
    URLs (and their matching `SHASUMS256.txt`/signature URLs) to
    `unofficial-builds.nodejs.org` when using the default mirror, while
    still respecting user-configured `node.mirror_url`.
    > 
    > Hardens lockfile merging so when a platform artifact `url` changes,
    stale artifact-bound fields (`checksum`, `size`, `url_api`) from the
    other side are dropped instead of preserved, preventing mismatched
    URL+checksum pairs.
    > 
    > Regenerates `mise.lock` Node musl entries to use the unofficial-builds
    URLs with updated sha256 checksums, and adds/updates unit tests covering
    the new mirror routing and merge semantics.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    dd68707. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    jdx and claude authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    bec289e View commit details
    Browse the repository at this point in the history
  10. fix(install): stop rewriting healthy runtime symlinks (#9410)

    ## Summary
    
    Fixes the `mise install` regression where a non-root user gets
    `Permission denied` while rebuilding runtime symlinks against a
    read-only system installs dir (typical Docker pattern: root populates
    `/usr/local/share/mise/installs/` at build time, non-root user runs
    `mise install` at runtime). Reported in
    [#8596](#8596) and addressed by
    [#9408](#9408) — this PR is an
    alternative.
    
    ## Root cause
    
    `rebuild_symlinks_in_dir` always called `remove_all` +
    `make_symlink_or_file` on every existing runtime symlink, because the
    "stale runtime dir" branch fired for symlinks too:
    
    ```rust
    if from.exists() {
        if is_runtime_symlink(&from) && file::resolve_symlink(&from)?.unwrap_or_default() != to {
            // Branch 1 — only fires when the symlink target is wrong
            ...
        } else if from.file_name() != to.file_name() && !concrete_installs.contains(&from_name) {
            // Branch 2 — fires for ANY symlink because file_name("latest") != file_name("22.5.0")
            file::remove_all(&from)?;
        }
        ...
    }
    ```
    
    For a perfectly healthy `latest -> ./22.5.0`:
    
    - Branch 1 fails (`target == to`).
    - Branch 2 enters because `from.file_name()` (`"latest"`) differs from
    `to.file_name()` (`"22.5.0"`) and `concrete_installs` doesn't include
    `"latest"`.
    - `remove_all` then runs on a healthy symlink — wasted I/O on every
    install in the common case, hard failure when the dir is read-only.
    
    ## Fix
    
    - Branch 1 now handles all runtime-symlink cases: rewrite if target
    differs, otherwise `continue`.
    - Branch 2 only fires for non-symlink stale dirs (its actual purpose —
    cleanup for the 2026.4 regression that created real `latest/`
    directories).
    - Drop the position-based permission tolerance added in #8722. With
    healthy symlinks taking the no-op path, read-only system dirs aren't
    touched at all. If a write IS genuinely required and can't happen, fail
    loudly so the user knows to fix permissions rather than silently ending
    up with a stale `latest` after `mise install --system`.
    
    ## Why not [#9408](#9408)
    
    That PR replaces position-based tolerance with category-based tolerance
    (any non-`Local` dir gets permission errors swallowed) and silently
    skips system/shared installs from user shims. Both:
    
    - Mask the real bug (the false-positive `remove_all` above) instead of
    fixing it. Even with category-based tolerance the install does I/O it
    shouldn't, just hidden under a `debug!`.
    - Trade one form of silent behavior for another. Reviewers (greptile,
    gemini) flagged the shims change as a regression for the "root installs
    once, all users get shims" pattern.
    - Don't match the maintainer's stated policy: if `mise install` doesn't
    touch system tools, it should work fine; if it does and the dir isn't
    writable, it should fail loudly. This PR achieves both via a delta-based
    no-op rather than tolerance.
    
    ## Tests
    
    - `e2e/cli/test_install_system_readonly` — sets up `[email protected]` in user
    dir, `[email protected]` in a fake system dir, `chmod -R a-w` the system dir,
    then runs `mise install --force [email protected]` and `mise install
    [email protected]`. Asserts both succeed AND that the `skipping symlink
    update` / `Permission denied` / `failed rm` warnings do NOT appear in
    output. Verified to fail before this fix (with `WARN skipping symlink
    update for ...: failed rm -rf ... Permission denied`) and pass after.
    
    ## Test plan
    
    - [x] `cargo test --bin mise` — 774 pass
    - [x] `mise run lint-fix` — clean
    - [x] `mise run test:e2e test_install_system_readonly` — pass
    - [x] `mise run test:e2e test_install_system test_shared_install_dirs
    test_install_before_ignores_stale_latest_dirs test_upgrade_latest_stale
    test_reshim_with_shims_on_path test_uninstall test_install_before
    test_install_short_ignores_full_backend_config test_install_dry_run
    test_install_concurrent_via_exec` — all pass
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Medium Risk**
    > Changes runtime symlink rebuild behavior across both user and
    shared/system install dirs, which could affect installs/reshims if edge
    cases exist. It also removes permission-error swallowing for shared
    dirs, so genuine mismatches in read-only system installs will now fail
    loudly.
    > 
    > **Overview**
    > Prevents `mise install` from rewriting existing runtime symlinks when
    they already point at the desired target, avoiding unnecessary
    deletes/recreates that previously caused `Permission denied` failures on
    read-only system install directories.
    > 
    > Refactors shared/system install-dir enumeration into
    `install_dirs_for` and removes the prior "skip on permission error"
    behavior; shared/system dirs are now treated like user dirs, but the
    rebuild path should be a no-op unless a change is actually needed. Adds
    an E2E regression test (`test_install_system_readonly`) that simulates a
    read-only system installs dir and asserts installs succeed without
    system-dir warnings.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    acf5798. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    jdx and claude authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    a0eff6d View commit details
    Browse the repository at this point in the history
  11. chore(docs): switch canonical domain to mise.en.dev (#9411)

    ## Summary
    
    - Swap every in-repo reference to `mise.jdx.dev` over to `mise.en.dev`
    (CLI help text, docs, schemas, install scripts, packaging metadata, e2e
    fixtures, generated files including the man page, `mise.usage.kdl`, and
    the lockfile header).
    - 86 files changed; both URLs serve the same content (symlinked), so the
    old URL keeps working — no coordinated cutover required.
    - `scripts/update-redirect.sh`: `ZONE_ID` swapped to the en.dev
    Cloudflare zone.
    
    ## Intentionally not changed
    
    - `[email protected]` / `[email protected]` email addresses
    (mailbox routing only exists on the old domain).
    - Historical `CHANGELOG.md` entry.
    
    ## Reviewer note
    
    `scripts/update-redirect.sh` still has the old `RULESET_ID`/`RULE_ID` —
    those are scoped to the jdx.dev zone and need to be re-derived against
    the en.dev zone before the script is run again. Flagging here so it
    isn't forgotten.
    
    ## Test plan
    
    - [x] `mise run build`
    - [x] `mise run lint`
    - [ ] Spot-check `man/man1/mise.1` and a couple of `docs/cli/*.md` URLs
    render correctly
    - [ ] Run one HTTP-fixture e2e (`mise run test:e2e test_http`) to
    confirm `mise.en.dev/test-fixtures/...` resolves
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    <!-- CURSOR_SUMMARY -->
    ---
    
    > [!NOTE]
    > **Medium Risk**
    > Mostly mechanical URL/hostname rewrites, but it touches
    distribution-critical paths (install script URLs, version check,
    packaging repos, Cloudflare worker/test fixtures) where a
    missed/incorrect domain could break installs or CI/e2e downloads.
    > 
    > **Overview**
    > Switches the project’s canonical hostname from `mise.jdx.dev` to
    `mise.en.dev` across the repo, updating docs, CLI help/manpage text,
    JSON schema `$id`s, lockfile header generation, and test/fixture URLs.
    > 
    > Updates release/install and packaging metadata to fetch
    assets/keys/repos from `mise.en.dev` (including bootstrap/tool-stub
    generation and `mise version` checks), and adjusts the Cloudflare worker
    proxy targets accordingly.
    > 
    > Removes the Cloudflare deploy workflow’s redirect-update step and
    deletes `scripts/update-redirect.sh`.
    > 
    > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
    57cde68. Bugbot is set up for automated
    code reviews on this repo. Configure
    [here](https://www.cursor.com/dashboard/bugbot).</sup>
    <!-- /CURSOR_SUMMARY -->
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    jdx and claude authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    8029ff0 View commit details
    Browse the repository at this point in the history
  12. registry: add llmfit (#9412)

    Signed-off-by: jylenhof <[email protected]>
    jylenhof authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    1963735 View commit details
    Browse the repository at this point in the history
  13. chore: release 2026.4.23 (#9396)

    ### 🚀 Features
    
    - **(backend)** add global libc preference by @jdx in
    [#9404](#9404)
    - opt-in to pre-release versions for github and aqua backends by
    @jakedgy in [#9329](#9329)
    
    ### 🐛 Bug Fixes
    
    - **(backend)** allow unresolved latest opt-in by @jdx in
    [#9401](#9401)
    - **(install)** stop rewriting healthy runtime symlinks by @jdx in
    [#9410](#9410)
    - **(node)** route musl tarball URLs to unofficial-builds by @jdx in
    [#9409](#9409)
    - **(prune)** skip remote version resolution for tracked configs by @jdx
    in [#9406](#9406)
    - **(schema)** allow array values in tool additionalProperties by
    @JP-Ellis in [#9400](#9400)
    
    ### 📦️ Dependency Updates
    
    - bump communique to 1.1.2 by @jdx in
    [#9402](#9402)
    
    ### 📦 Registry
    
    - use aqua for rumdl by @scop in
    [#9397](#9397)
    
    ### Chore
    
    - **(ci)** improve pr-closer workflow by @jdx in
    [#9403](#9403)
    - **(release)** stop appending sponsor blurb when communique succeeds by
    @jdx in [#9395](#9395)
    
    ### New Contributors
    
    - @JP-Ellis made their first contribution in
    [#9400](#9400)
    mise-en-dev authored Apr 26, 2026
    Configuration menu
    Copy the full SHA
    9225550 View commit details
    Browse the repository at this point in the history
Loading