Skip to content

fix(env): skip remote version fetching for "latest" in prefer-offline mode#8500

Merged
jdx merged 5 commits intomainfrom
fix/prefer-offline-latest
Mar 7, 2026
Merged

fix(env): skip remote version fetching for "latest" in prefer-offline mode#8500
jdx merged 5 commits intomainfrom
fix/prefer-offline-latest

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 7, 2026

Summary

Context

After lockfiles graduated from experimental in v2026.2.0 (#7929), mise env --json with npm:pkg = "latest" and no lockfile entry falls through to a network fetch that prefer_offline mode was intended to prevent. If the private registry holds the TCP connection open (waiting for credentials), mise hangs indefinitely.

PR #7976 fixed this for fully-qualified versions (e.g. "2.3.2") but the "latest" path was not covered. This one-liner closes that gap.

See #8499 for the full root cause analysis.

Fixes #8499

Test plan

  • mise env --json with npm:pkg = "latest" and no registry auth no longer makes network calls or hangs
  • Existing behavior unchanged when prefer_offline is not set (e.g. mise install still resolves latest from registry)

🤖 Generated with Claude Code


Note

Low Risk
Small conditional change to version resolution logic that only affects prefer_offline behavior for latest; low risk but could change expectations for users relying on remote latest lookup in that mode.

Overview
Prevents network access when resolving "latest" in ToolVersion::resolve_version while prefer_offline is enabled, aligning latest behavior with the existing prefer-offline skip for fully-qualified versions.

This adds a guard so hook-env/activate/exec/env won’t call latest_version_with_opts unless explicitly doing a latest-version lookup (opts.latest_versions), avoiding hangs against registries that keep connections open.

Written by Cursor Bugbot for commit 1ecf269. This will update automatically on new commits. Configure here.

… mode

In prefer-offline mode (used by `mise env`, `hook-env`, `activate`, `exec`),
"latest" versions were not covered by the prefer_offline guard, causing
unnecessary network calls to package registries. This is especially
problematic with private npm registries where the call can hang indefinitely.

The existing prefer_offline guard (added in #7976) only covered
fully-qualified versions (e.g. "2.3.2"). This adds the same guard for
"latest" versions: if a version is already installed, it's returned;
otherwise "latest" is returned as-is without a network fetch.

Fixes #8499

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the prefer_offline functionality by ensuring that 'latest' tool versions are resolved locally without making network calls when offline mode is active. This change prevents applications like mise env from hanging when encountering private npm registries, aligning the behavior of 'latest' version resolution with that of fully-qualified versions under prefer_offline settings.

Highlights

  • Prefer Offline Guard for 'latest' Versions: Implemented a prefer_offline guard within the resolve_version() function to prevent remote fetching of 'latest' tool versions when offline mode is preferred.
  • Network Call Prevention: Ensured that mise env, hook-env, activate, and exec no longer make network calls for 'latest' versions when prefer_offline is enabled, addressing potential hangs with private npm registries.
  • Consistency with Fully-Qualified Versions: Extended the existing prefer_offline behavior for fully-qualified versions to also cover 'latest' versions, closing a gap identified after lockfiles graduated from experimental.
Changelog
  • src/toolset/tool_version.rs
    • Added a check for settings.prefer_offline() before attempting to fetch the latest version from a remote backend.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a prefer_offline guard when resolving the 'latest' version, specifically in src/toolset/tool_version.rs, to skip remote version fetching when prefer-offline mode is enabled, enhancing performance and reliability. A security audit found no vulnerabilities meeting reporting criteria. Additionally, a suggestion has been made to simplify the logic by removing a now redundant check.

Comment on lines +266 to +268
if settings.prefer_offline() {
return build(v);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This change makes the !is_offline check on line 269 redundant. If settings.prefer_offline() is false, then settings.offline() must also be false, which means is_offline is false and !is_offline is always true in that code path.

Consider simplifying this logic by removing the redundant check. The block from line 266 to 275 could be simplified to:

            if settings.prefer_offline() {
                return build(v);
            }
            if let Some(v) = backend
                .latest_version_with_opts(config, None, opts.before_date)
                .await?
            {
                return build(v);
            }
            if settings.prefer_offline() {
                return build(v);
            }
            if let Some(v) = backend
                .latest_version_with_opts(config, None, opts.before_date)
                .await?
            {
                return build(v);
            }

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR adds a one-line prefer_offline guard to prevent latest_version_with_opts() from being called when resolving "latest" versions in prefer_offline mode. The intent is to stop mise env/hook-env/activate/exec from hanging against private registries.

Key issues found:

  • Incomplete fix: The new guard at line 267 blocks the latest_version_with_opts() call, but when no installed version is found the code falls through the if v == "latest" block to list_versions_matching_with_opts() (line 297) and then resolve_prefix() (line 311). Neither of those code paths checks prefer_offline()list_remote_versions_with_info() in backend/mod.rs only gates on settings.offline() (strict offline). The TCP hang on a private npm registry can therefore still occur via this fallthrough path.
  • Misleading comment: The updated comment now says "latest" "still needs remote resolution", which directly contradicts the fix's stated goal and is confusing to future readers.

Confidence Score: 2/5

  • The fix is incomplete: it guards only one of two network-call paths for "latest" in prefer_offline mode, so the hang described in `mise env` hangs indefinitely and spawns hundreds of npm processes with private registries — root cause analysis #8499 can still occur via list_versions_matching_with_opts / resolve_prefix.
  • The new guard correctly blocks latest_version_with_opts(), but code analysis shows that list_remote_versions_with_info() — called by list_versions_matching_with_opts on line 297 and again inside resolve_prefix on line 311 — only gates on settings.offline(), not settings.prefer_offline(). A v == "latest" path with no installed version will fall through to those network calls in prefer_offline mode, leaving the original bug unfixed.
  • src/toolset/tool_version.rs — specifically the fallthrough from the if v == "latest" block to list_versions_matching_with_opts and resolve_prefix without a prefer_offline early-return.

Important Files Changed

Filename Overview
src/toolset/tool_version.rs Adds a prefer_offline guard to the "latest" branch of resolve_version, but the code still falls through to list_versions_matching_with_opts (and resolve_prefix) which can make network calls — the fix only partially prevents the hang described in #8499. The updated comment is also misleading.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["resolve_version(v, opts)"] --> B{v == 'latest'?}
    B -- Yes --> C{!opts.latest_versions AND installed version exists?}
    C -- Yes --> D[return installed version ✅]
    C -- No --> E{"!is_offline AND\n(!prefer_offline OR opts.latest_versions)?\n[NEW GUARD]"}
    E -- Yes --> F["latest_version_with_opts()\n🌐 network call"]
    F --> G[return remote latest ✅]
    E -- No --> H["⚠️ Falls through without returning"]
    B -- No --> I{!opts.latest_versions?}
    H --> I
    I -- Yes --> J["list_installed_versions_matching(v)\n(local, no network)"]
    J --> K{match found?}
    K -- Yes --> L[return installed match ✅]
    K -- No --> M{is_offline?}
    M -- Yes --> N[return v as-is ✅]
    M -- No --> O{"prefer_offline AND\nv.matches('.') >= 2?"}
    O -- Yes --> P["return v as-is ✅\n(fully-qualified only)"]
    O -- No --> Q["list_versions_matching_with_opts()\n🌐 NETWORK CALL\n⚠️ 'latest' reaches here in prefer_offline mode!"]
    Q --> R{exact match?}
    R -- Yes --> S[return match ✅]
    R -- No --> T["resolve_prefix()\n🌐 ANOTHER network call\n⚠️ also reached in prefer_offline mode"]

    style H fill:#ffcccc,stroke:#cc0000
    style Q fill:#ffcccc,stroke:#cc0000
    style T fill:#ffcccc,stroke:#cc0000
Loading

Comments Outside Diff (2)

  1. src/toolset/tool_version.rs, line 260-311 (link)

    Fix only blocks one of two network call paths for "latest" in prefer_offline mode

    The new guard at line 267 correctly prevents latest_version_with_opts() from being called, but when no installed version is found, the code falls out of the if v == "latest" block and continues to line 297:

    let matches = backend
        .list_versions_matching_with_opts(config, &v, opts.before_date)
        .await?;

    list_versions_matching_with_opts calls list_remote_versions_with_info internally, and that function only gates on settings.offline() (strict offline, line 531 of backend/mod.rs), not settings.prefer_offline(). So for v == "latest" with prefer_offline mode, no installed version, and a private registry that holds the TCP connection open, this code still reaches the network — exactly the hang described in `mise env` hangs indefinitely and spawns hundreds of npm processes with private registries — root cause analysis #8499.

    The same code then falls through to resolve_prefix(config, request, "latest", opts) at line 311, which calls list_versions_matching_with_opts a second time.

    To fully close the gap, an early-return should be added for "latest" when prefer_offline is active and latest_versions is not forced. For example, extend the fully-qualified version guard at line 292:

    if settings.prefer_offline() && !opts.latest_versions
        && (v.matches('.').count() >= 2 || v == "latest")
    {
        return build(v);
    }

    Or add a guarded return build(v) at the end of the if v == "latest" block:

    if v == "latest" {
        if !opts.latest_versions
            && let Some(v) = backend.latest_installed_version(None)?
        {
            return build(v);
        }
        if !is_offline
            && (!settings.prefer_offline() || opts.latest_versions)
            && let Some(v) = backend
                .latest_version_with_opts(config, None, opts.before_date)
                .await?
        {
            return build(v);
        }
        // NEW: avoid fallthrough to list_versions_matching_with_opts in prefer_offline mode
        if settings.prefer_offline() && !opts.latest_versions {
            return build(v);
        }
    }
  2. src/toolset/tool_version.rs, line 288-294 (link)

    Updated comment contradicts the PR's stated intent

    The comment now reads:

    Prefix versions like "2" or "latest" still need remote resolution to find e.g. "2.1.0" or the actual latest version number.

    But the PR description says the fix is precisely to prevent remote resolution for "latest" in prefer_offline mode. Adding "latest" to the list of versions that "still need remote resolution" is misleading — it describes what the code actually does (falls through to a network call) rather than what was intended. This comment should either be reverted to its original form or updated to explain that "latest" is handled by the guard above, e.g.:

Fix All in Claude Code

Last reviewed commit: 1ecf269

Move the prefer_offline guard for "latest" versions inline with the
existing is_offline check rather than using an early return. This
ensures list_installed_versions_matching() still runs, so a user with
pkg@1.2.3 installed but configured as "latest" (no latest symlink)
still gets the installed version instead of the literal string "latest".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

jdx and others added 3 commits March 7, 2026 16:38
When no version is installed and v is still "latest", it falls through
past the first prefer_offline guard (in the "latest" block) and past
list_installed_versions_matching (no matches). The second prefer_offline
guard at line 291 only covered fully-qualified versions (2+ dots), so
"latest" would still reach list_versions_matching_with_opts and make a
network call. Add "latest" to the condition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the user explicitly passes @latest (e.g. `mise x dummy@latest`),
opts.latest_versions is true — they want the actual latest, not just
whatever is installed. The prefer_offline guards should not block
network resolution in that case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The second prefer_offline guard (for list_versions_matching_with_opts)
should treat "latest" the same as prefix versions like "2" — both need
remote resolution. Only the first guard (for latest_version_with_opts
inside the "latest" block) needs to block network calls, since that's
the npm-specific path that hangs with private registries.

Restore the original dot-count heuristic at the second guard, keeping
"latest" and prefix versions consistent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx force-pushed the fix/prefer-offline-latest branch from 7ac3426 to 1ecf269 Compare March 7, 2026 16:56
@jdx jdx enabled auto-merge (squash) March 7, 2026 17:06
@jdx jdx merged commit 4300c48 into main Mar 7, 2026
36 checks passed
@jdx jdx deleted the fix/prefer-offline-latest branch March 7, 2026 17:12
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 7, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 x -- echo 23.9 ± 0.9 23.1 35.5 1.00
mise x -- echo 24.1 ± 0.7 23.3 35.6 1.01 ± 0.05

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 env 23.5 ± 0.8 22.6 28.9 1.00
mise env 23.5 ± 0.3 22.7 25.0 1.00 ± 0.04

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 hook-env 24.0 ± 0.4 23.2 26.8 1.00
mise hook-env 24.3 ± 0.5 23.4 29.1 1.01 ± 0.03

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.4 ls 23.2 ± 0.6 22.4 32.7 1.00
mise ls 23.6 ± 0.5 22.8 26.3 1.02 ± 0.03

xtasks/test/perf

Command mise-2026.3.4 mise Variance
install (cached) 151ms 151ms +0%
ls (cached) 83ms 83ms +0%
bin-paths (cached) 87ms 87ms +0%
task-ls (cached) 847ms 825ms +2%

jdx pushed a commit that referenced this pull request Mar 7, 2026
### 🚀 Features

- **(vfox)** add `RUNTIME.envType` for libc variant detection by @malept
in [#8493](#8493)
- store provenance verification results in lockfile by @jdx in
[#8495](#8495)

### 🐛 Bug Fixes

- **(env)** skip remote version fetching for "latest" in prefer-offline
mode by @jdx in [#8500](#8500)
- **(tasks)** deduplicate shared deps across task delegation by
@vadimpiven in [#8497](#8497)
- **(windows)** correctly identify mise binary without extension by @jdx
in [#8503](#8503)

### 🚜 Refactor

- **(core)** migrate cmd! callers to async with kill_on_drop by @jdx in
[a63f7d2](a63f7d2)

### Chore

- **(ci)** temporarily disable `mise up` in release-plz by @jdx in
[#8504](#8504)
- consolidate all linters into hk.pkl by @jdx in
[#8498](#8498)

## 📦 Aqua Registry Updates

#### New Packages (1)

- [`apache/ant`](https://github.com/apache/ant)
jdx added a commit that referenced this pull request Mar 9, 2026
The GitHub API endpoint for the latest release is `/releases/latest`,
not `/releases/tags/latest` (which looks for a tag literally named
"latest"). This fixes a regression in v2026.3.5 where `github:` backend
tools with `@latest` would fail with a 404 during install/exec.

Two fixes:
1. `get_release_` in github.rs now uses `/releases/latest` when tag is
   "latest"
2. Revert the prefer-offline guard on `latest_version_with_opts` from
   #8500 — "latest" must always resolve to a concrete version via
   remote lookup when no installed version is found, otherwise it
   propagates as the literal string "latest" into the install path

Fixes #8530

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Mar 9, 2026
Two fixes for the regression in v2026.3.5 where `github:` backend tools
with `@latest` fail with a 404 during install/exec (#8530):

1. `get_release_()` in github.rs now uses `/releases/latest` when tag
   is "latest", instead of `/releases/tags/latest` (which looks for a
   tag literally named "latest")

2. In prefer-offline mode, when "latest" can't be resolved locally,
   return early with "latest" so the backend's install path can handle
   it via the correct API endpoint — rather than falling through to
   general version matching which fails to find it. This preserves the
   prefer-offline guard from #8500 that prevents hangs with private
   registries.

Fixes #8530

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Mar 9, 2026
The prefer-offline guard was added in #8500 to prevent npm hangs, but
that was properly fixed in bdaf470 by making subprocess calls async
with kill_on_drop. The guard is now redundant and was causing "latest"
to not resolve to a concrete version in prefer-offline mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jdx added a commit that referenced this pull request Mar 9, 2026
## Summary

- Fix GitHub API URL for "latest" releases: use `/releases/latest`
instead of `/releases/tags/latest` (which looks for a tag literally
named "latest")
- Fully revert the prefer-offline guard on `latest_version_with_opts`
from #8500 — the npm hang it aimed to fix was properly addressed in
bdaf470 by making subprocess calls async with `kill_on_drop`, making
the guard redundant
- Add e2e test for `github:` backend with `@latest` version

## Context

Regression in v2026.3.5 where `github:` backend tools configured with
`@latest` fail with a 404 error during install/exec. Two issues
combined:

1. `get_release_()` in `github.rs` always constructed
`/releases/tags/{tag}`, but "latest" isn't a tag — GitHub's API uses
`/releases/latest` for the latest release endpoint
2. The prefer-offline guard in #8500 prevented "latest" from being
resolved to a concrete version number during `exec`/`env`/`hook-env`,
causing the literal string "latest" to propagate into the install path.
This guard was added to prevent npm hangs with private registries, but
that issue was already properly fixed in bdaf470 by using async
subprocess calls with `kill_on_drop`

Fixes #8530

## Test plan

- [x] New e2e test `test_github_latest` verifies install and exec with
`@latest`
- [x] Existing e2e tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core GitHub release fetching and generic tool version
resolution, which can affect installs/execs across backends, though the
change is narrowly scoped and covered by a new E2E regression test.
> 
> **Overview**
> Fixes `github:` tools configured with `version = "latest"` by calling
GitHub’s correct endpoint (`/releases/latest`) instead of treating
`latest` as a tag (`/releases/tags/latest`).
> 
> Adjusts tool version resolution so `latest` is still resolved via
remote lookup when not offline (avoiding propagation of the literal
`latest` string), and adds an E2E regression test
(`e2e/backend/test_github_latest`) that installs/execs a GitHub fixture
and asserts `mise ls` shows a concrete semver-like version.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5698a9a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant