fix(lockfile): update global lockfile on upgrade#9442
Conversation
There was a problem hiding this comment.
Code Review
This pull request enables lockfile updates for global configurations and refactors the update_lockfiles logic to better handle newly installed or upgraded tool versions. The changes include a new E2E test for global fuzzy version upgrades and internal refactoring to use ToolVersion objects during the lockfile generation process. Feedback suggests that the version replacement logic may fail to correctly supersede fuzzy requests with concrete versions and that the update heuristic might skip necessary updates when multiple versions of a tool are defined.
| versions.retain(|tv| { | ||
| tv.ba() != new_version.ba() | ||
| || tv.request.version() != new_version.request.version() | ||
| || tv.request.source() != new_version.request.source() | ||
| }); |
There was a problem hiding this comment.
The retain logic here might fail to replace stale lockfile entries if the new_version request version differs from the existing one. For example, if a configuration has dummy = "latest" and the user runs mise upgrade [email protected], the new request version will be "2.0.0", which won't match "latest". This could result in both the old and new versions being preserved in the lockfile. Consider if matching by backend and source path alone (when available and unambiguous) would be more effective for replacing fuzzy resolutions with concrete ones.
| } else if let Some((idx, request)) = versions | ||
| .iter() | ||
| .enumerate() | ||
| .exactly_one() | ||
| .ok() | ||
| .map(|(idx, tv)| (idx, tv.request.clone())) | ||
| { | ||
| let mut new_version = new_version.clone(); | ||
| new_version.request = request; | ||
| versions.remove(idx); | ||
| versions.push(new_version); | ||
| } |
There was a problem hiding this comment.
The exactly_one() heuristic used when the source path is missing (e.g., for versions provided via CLI arguments) will skip updating the lockfile if the tool has multiple versions defined across the configurations being updated (e.g., dummy = ["1", "2"] or the same tool appearing in both mise.toml and mise.local.toml). While this avoids ambiguity, it means the lockfile might remain stale in these scenarios. A more robust approach might involve matching against compatible requests if an exact source match isn't available.
Greptile SummaryThis PR fixes two gaps in the lockfile update logic: global config files were excluded from both Confidence Score: 5/5Safe to merge — targeted two-line guard removal plus a well-reasoned overlay loop, covered by a new e2e regression. No P0 or P1 findings. The overlay logic correctly handles both the config-sourced (retain + push) and CLI-argument-sourced (exactly_one swap) upgrade paths. PlatformTarget is already in scope at module level so the refactored free function compiles cleanly. The e2e test now asserts both that the lockfile is updated AND that the config file is left unchanged. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[update_lockfiles called\nwith new_versions] --> B[Group config files\nby lockfile path\n-- global configs NOW included --]
B --> C{Lockfile exists?}
C -- No --> D[Skip]
C -- Yes --> E[Build tool_versions_by_short\nfrom resolved toolset]
E --> F{new_version has\nconfig source path?}
F -- Yes, path in scope --> G[retain: remove exact match\nba + request.version + source\nthen push new_version]
F -- No path / CLI Argument --> H{Tool found in\ntool_versions_by_short?}
H -- No --> I[Skip: tool not\nin this lockfile scope]
H -- Yes --> J{exactly_one entry?}
J -- Yes --> K[Swap request onto\nnew_version clone,\nreplace old entry]
J -- No, ambiguous --> L[trace! log, skip update]
G --> M[Convert to LockfileTool\nvia lockfile_tool_from_tool_version]
K --> M
M --> N[Provenance check,\nmerge, write lockfile]
Reviews (6): Last reviewed commit: "[autofix.ci] apply automated fixes" | Re-trigger Greptile |
| .or_default(); | ||
| if let Some(source_path) = new_version.request.source().path() { | ||
| if !configs.iter().any(|config| config == source_path) { | ||
| continue; |
There was a problem hiding this comment.
Spurious empty entries added to unrelated lockfiles
Medium Severity
entry().or_default() at line 970–972 creates a new empty vec in tool_versions_by_short for a tool from new_versions before checking whether the tool's source config belongs to this lockfile. When the source path check at line 974 fails and continue is hit, the empty entry persists. It then flows through merge_tool_entries (which returns an empty vec for empty input), and existing_lockfile.tools.insert(short, vec![]) writes it into the lockfile — either adding a spurious empty tool = [] entry or, worse, deleting valid existing lock entries for that tool.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9057309. Configure here.
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.24 x -- echo |
25.8 ± 2.0 | 23.4 | 38.7 | 1.00 |
mise x -- echo |
27.2 ± 1.4 | 24.2 | 33.4 | 1.05 ± 0.10 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.24 env |
24.3 ± 1.1 | 22.9 | 39.2 | 1.00 |
mise env |
26.5 ± 1.2 | 23.3 | 32.2 | 1.09 ± 0.07 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.24 hook-env |
27.4 ± 1.2 | 25.5 | 35.6 | 1.00 |
mise hook-env |
28.2 ± 0.8 | 24.7 | 36.0 | 1.03 ± 0.05 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.24 ls |
28.0 ± 1.2 | 25.7 | 42.9 | 1.00 |
mise ls |
28.5 ± 1.1 | 25.9 | 40.5 | 1.02 ± 0.06 |
xtasks/test/perf
| Command | mise-2026.4.24 | mise | Variance |
|---|---|---|---|
| install (cached) | 166ms | 171ms | -2% |
| ls (cached) | 92ms | 94ms | -2% |
| bin-paths (cached) | 95ms | 95ms | +0% |
| task-ls (cached) | 833ms | 816ms | +2% |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit feeda60. Configure here.
…ckfile auto-lock - Restore request.version() in retain criterion so each fuzzy variant (e.g. tiny = ['latest', 'prefix:1.0', 'prefix:1.1']) keeps its own lockfile entry. Without this, all entries from the same source were removed and only the last new_version survived. - Remove the is_global_config guard in auto_lock_new_versions so it matches update_lockfiles and resolves checksums for global lockfiles on additional platforms. - Revert unrelated mise.lock change for node (the 25.9.0 entry doesn't satisfy the node = "24" constraint and dropped non-linux-x64 platforms). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
@greptileai this should only modify existing mise.lock files not create new ones |
|
Confirmed — both This comment was generated by an AI coding assistant. |
### 🚀 Features - **(task)** add --name-only flag to mise tasks ls by @jdx in [#9435](#9435) ### 🐛 Bug Fixes - **(Dockerfile)** install copr-cli via dnf for better dependency management by @bestagi in [#9421](#9421) - **(aqua)** drop empty-releases fallback to tags by @jdx in [#9443](#9443) - **(docs)** fix theme flicker on docs by @vhespanha in [#9427](#9427) - **(lockfile)** update global lockfile on upgrade by @jdx in [#9442](#9442) - **(ls-remote)** omit rolling/prerelease from JSON when false by @jdx in [#9439](#9439) - **(task)** support usage refs in dependency template tags by @jdx in [#9424](#9424) - **(task)** populate usage.cmd for subcommand-only tasks; share make_usage_ctx by @jdx in [#9431](#9431) - **(task)** resolve sandbox allow_read/allow_write against task dir by @jdx in [#9428](#9428) ### 📚 Documentation - **(site)** add self-hosted page tracker via Cloudflare Worker, drop GoatCounter by @jdx in [#9430](#9430) ### New Contributors - @vhespanha made their first contribution in [#9427](#9427) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…ckfile `mise x node@latest -- ...` (and `mise run`-spawned exec) installs an ephemeral, CLI-supplied tool version. With `MISE_LOCKFILE=1`, that install was being merged into the project's `mise.lock` under the config's existing request — e.g. mise.toml `node = "24"` paired with a freshly-installed `25.9.0`. The lockfile entry then claimed the "24" request resolved to 25.x, which is nonsensical and breaks subsequent `mise install` resolutions. Concretely: this is what was producing the recurring autofix commit on each `release-plz` PR (e.g. #9485 / ea7affd). `mise run render` calls `mise x node@latest -- npx markdown-magic`; on the autofix runner with `MISE_LOCKFILE=1` set, that bumped `mise.lock`'s node entry every cycle. `update_lockfiles`'s else-if branch was added in #9442 specifically to support `mise upgrade`, but `upgrade.rs` already remaps `ToolSource::Argument` → `ToolSource::MiseToml` before calling `update_lockfiles`, so its tools go through the path-source if-branch. The else-if branch only fires for tools that *kept* `ToolSource::Argument` — i.e. exactly the ad-hoc CLI overrides that shouldn't be persisted. Skip the propagation when the new version's request source is `ToolSource::Argument`. `mise upgrade` keeps working (its remap still hits the if-branch); `mise install <tool>` for a configured tool keeps working (`get_requested_tool_versions` pulls the config's request, so the source is `MiseToml`, not `Argument`). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ckfile `mise x node@latest -- ...` (and `mise run`-spawned exec) installs an ephemeral, CLI-supplied tool version. With `MISE_LOCKFILE=1`, that install was being merged into the project's `mise.lock` under the config's existing request — e.g. mise.toml `node = "24"` paired with a freshly-installed `25.9.0`. The lockfile entry then claimed the "24" request resolved to 25.x, which is nonsensical and breaks subsequent `mise install` resolutions. This is what was producing the recurring autofix commit on each `release-plz` PR (e.g. #9485 / ea7affd). `mise run render` calls `mise x node@latest -- npx markdown-magic`; on the autofix runner with `MISE_LOCKFILE=1` set, that bumped `mise.lock`'s node entry every cycle. `update_lockfiles`'s else-if branch was added in #9442 to support `mise upgrade`, but `upgrade.rs` already remaps `ToolSource::Argument` → `ToolSource::MiseToml` before calling `update_lockfiles`, so its tools go through the path-source if-branch. The else-if branch only fires for tools that *kept* `ToolSource::Argument` — exactly the ad-hoc CLI overrides that shouldn't be persisted. Skip the propagation only when the new install's request specifier doesn't match the config's. `mise x node` (no version) is rewritten by `with_default_to_latest` to carry the config's version string, so the specifiers match and a legitimate fuzzy refresh still updates the lockfile. `mise x node@latest` carries a "latest" specifier that doesn't match `node = "24"`, so the override is correctly skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ckfile `mise x node@latest -- ...` (and `mise run`-spawned exec) installs an ephemeral, CLI-supplied tool version. With `MISE_LOCKFILE=1`, that install was being merged into the project's `mise.lock` under the config's existing request — e.g. mise.toml `node = "24"` paired with a freshly-installed `25.9.0`. The lockfile entry then claimed the "24" request resolved to 25.x, which is nonsensical and breaks subsequent `mise install` resolutions. This is what was producing the recurring autofix commit on each `release-plz` PR (e.g. #9485 / ea7affd). `mise run render` calls `mise x node@latest -- npx markdown-magic`; on the autofix runner with `MISE_LOCKFILE=1` set, that bumped `mise.lock`'s node entry every cycle. `update_lockfiles`'s else-if branch was added in #9442 to support `mise upgrade`, but `upgrade.rs` already remaps `ToolSource::Argument` → `ToolSource::MiseToml` before calling `update_lockfiles`, so its tools go through the path-source if-branch. The else-if branch only fires for tools that *kept* `ToolSource::Argument` — exactly the ad-hoc CLI overrides that shouldn't be persisted. Skip the propagation only when the new install's request specifier doesn't match the config's. `mise x node` (no version) is rewritten by `with_default_to_latest` to carry the config's version string, so the specifiers match and a legitimate fuzzy refresh still updates the lockfile. `mise x node@latest` carries a "latest" specifier that doesn't match `node = "24"`, so the override is correctly skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…lockfile (#9562) ## Summary `mise x node@latest -- ...` (and `mise run`-spawned `mise x` calls) is an ephemeral CLI override. With `MISE_LOCKFILE=1`, that install was being merged into the project's `mise.lock` *under the config's existing request* — e.g. `mise.toml` `node = "24"` paired with a freshly-installed `25.9.0`. The lockfile entry then claimed the `"24"` request resolved to a 25.x version, which is nonsensical and breaks subsequent resolutions. ## Why this matters This is what was producing the recurring `[autofix.ci] apply automated fixes` commit on every `release-plz` PR (e.g. [PR #9485 / ea7affd](ea7affd): node 24.15.0 → 25.9.0, platforms 11 → 1). `mise run render` calls `mise x node@latest -- npx markdown-magic`; on the autofix runner with `MISE_LOCKFILE=1` set, that install bumped `mise.lock`'s node entry every release cycle. If autofix is ever blocked or fails on the release branch, the release would ship with a stale/wrong lockfile. ## Root cause `update_lockfiles`'s else-if branch (added in #9442 to support `mise upgrade` propagating to the lockfile) overwrites a config tool's lockfile entry with any newly installed `ToolSource::Argument` version that shares the short name. But `upgrade.rs` already remaps `Argument` → `MiseToml(...)` *before* calling `update_lockfiles` (`src/cli/upgrade.rs:383-396`), so its tools go through the path-source `if` branch. The else-if branch only fires for tools that *kept* `ToolSource::Argument` — exactly the ad-hoc CLI overrides that shouldn't be persisted. ## Fix Compare the new install's request specifier to the config's. Only propagate if they match. - ✅ `mise x node` (no version) — `with_default_to_latest` rewrites the request to carry the config's version string, so the specifiers match and a legitimate fuzzy refresh still populates `mise.lock` (e.g. against an empty lockfile) - ✅ `mise x node@24` when `mise.toml` says `node = "24"` — same specifier, propagates - ❌ `mise x node@latest` when `mise.toml` says `node = "24"` — `"latest" != "24"`, skipped - ❌ `mise x node@25` when `mise.toml` says `node = "24"` — `"25" != "24"`, skipped - ✅ `mise upgrade` keeps working — its remap → `MiseToml` hits the if-branch, never the else-if - ✅ `mise install` (no args) keeps working — sources are `MiseToml` - ✅ `mise install <tool>` for a configured tool keeps working — `get_requested_tool_versions` (`src/cli/install.rs:264-298`) pulls the config's request when no version is given, so the source is `MiseToml` ## Test plan - New e2e: `e2e/cli/test_exec_lockfile` — covers both directions: - `mise x [email protected]` and `mise x dummy@latest` must NOT mutate `mise.lock` when `mise.toml` says `dummy = "1"` - `mise x dummy` (no version) MUST populate an empty lockfile from the config-derived install - `mise upgrade [email protected]` MUST update the lockfile (regression check) - `mise run test:e2e e2e/cli/test_upgrade e2e/cli/test_upgrade_latest_stale` — green (regression check for #9442) - `mise run test:e2e e2e/cli/test_lock e2e/cli/test_lock_global e2e/cli/test_lock_latest e2e/cli/test_lock_creation e2e/cli/test_lock_local_config e2e/cli/test_lock_version e2e/cli/test_exec_latest` — green - `cargo test --all-features lockfile` — 29 lockfile unit tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches `update_lockfiles` behavior so some installs triggered by `mise x`/env overrides will no longer update `mise.lock`; this could affect workflows that relied on the previous (incorrect) propagation but is guarded by an e2e regression test. > > **Overview** > Prevents `mise.lock` from being updated by *ad-hoc version overrides* (e.g. `mise x tool@latest` or `MISE_<TOOL>_VERSION=...`) when the override’s version specifier doesn’t match the project config’s requested specifier. > > `update_lockfiles` now compares the new install’s request string against the config-derived request before propagating into the lockfile, skipping mismatches and logging a trace message. Adds an e2e test (`e2e/cli/test_exec_lockfile`) covering override non-propagation, expected propagation for `mise x tool` without a version, and `mise upgrade` still updating the lockfile. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3b7233a. 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]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>


Summary
Fixes automatic lockfile updates for global tool configs when upgrading a fuzzy version such as
latest.Root Cause
update_lockfilesskipped global config files when grouping lockfile targets, so an existing~/.config/mise/mise.lockwas not maintained aftermise upgrade. In addition, fuzzy requests could still resolve through the old lockfile entry while the update was being prepared, preserving the stale version.Changes
dummy = "latest"in the global config upgrading from1.0.0to2.0.0.Validation
mise run test:e2e e2e/cli/test_upgradecargo fmt --all -- --check,cargo check --all-features, shell/lint/schema checksNote
Medium Risk
Touches lockfile update logic used during
mise upgrade, including global config handling and version overlay behavior; mistakes could lead to stale or incorrect lockfile pins across configs.Overview
Fixes automatic lockfile refresh when upgrading tools declared with fuzzy versions (e.g.
latest), including global configs.update_lockfilesnow includes globalmise.tomlsources when determining which existing lockfiles to update, and overlays newly-installedToolVersions into the per-lockfile tool set so stale lockfile-resolved versions don’t persist during the update.Adds an e2e regression that upgrades a globally-configured
dummy = "latest"and asserts~/.config/mise/mise.lockis rewritten from1.0.0to2.0.0while the config stays"latest".Reviewed by Cursor Bugbot for commit 71b1e21. Bugbot is set up for automated code reviews on this repo. Configure here.