fix(lockfile): don't propagate ad-hoc CLI overrides into the project lockfile#9562
fix(lockfile): don't propagate ad-hoc CLI overrides into the project lockfile#9562
Conversation
Greptile SummaryThis PR fixes a bug where Confidence Score: 5/5Safe to merge — targeted, well-tested fix with no P0/P1 issues found. The fix is a single, clearly-scoped guard in one branch of update_lockfiles. All affected code paths are covered by the new e2e test and existing regression tests. The mise.lock rollback confirms the fix resolves the real-world symptom. No edge cases or logic errors were found. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[update_lockfiles called\nwith new_versions] --> B{new_version has\nsource path?}
B -->|Yes — MiseToml source| C[Direct update:\nreplace/insert lockfile entry\nfor that config path]
B -->|No — Argument or\nEnvironment source| D{Tool short-name\nexists in lockfile?}
D -->|No| E[Skip — tool not\nin project config]
D -->|Yes| F{Exactly one\nconfig entry?}
F -->|No| G[Skip — ambiguous\nconfig entries]
F -->|Yes| H{new_version.request.version\n== config request.version?}
H -->|No — specifier mismatch\ne.g. 'latest' vs '24'| I[Skip — ad-hoc CLI/env override\ndoes NOT update lockfile]
H -->|Yes — same specifier\ne.g. both '24'| J[Propagate: overwrite\nrequest with config request\nand update lockfile entry]
style I fill:#f66,color:#fff
style J fill:#6a6,color:#fff
style C fill:#6a6,color:#fff
Reviews (6): Last reviewed commit: "[autofix.ci] apply automated fixes" | Re-trigger Greptile |
There was a problem hiding this comment.
Code Review
This pull request updates the render-help task in tasks.toml by removing the explicit node@latest version from the mise x command. The review feedback suggests that the mise x -- prefix is redundant because the task already executes within the project's environment, and recommends removing it entirely to simplify the command and avoid unnecessary CLI invocations.
| run = [ | ||
| "mise render-help", | ||
| "mise run show-output-on-failure -- mise x node@latest -- npx markdown-magic", | ||
| "mise run show-output-on-failure -- mise x -- npx markdown-magic", |
There was a problem hiding this comment.
The mise x -- prefix is redundant in this context. Since render:help is defined as a mise task, it automatically executes within the project's environment, meaning the pinned Node.js version and npx are already present in the PATH. Removing the redundant mise x call simplifies the task and avoids an unnecessary extra invocation of the mise CLI, which aligns with the objective of preventing unintended mise.lock updates.
"mise run show-output-on-failure -- npx markdown-magic",
8505e76 to
dbc8c44
Compare
8d1d43c to
e076560
Compare
…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]>
e076560 to
3b7233a
Compare
|
Re: the The reviewed commit (dbc8c44) used a source-only check (
Added an explicit env-var case to MISE_DUMMY_VERSION=2 mise install
assert_contains "cat mise.lock" "1.0.0"
assert_not_contains "cat mise.lock" "2.0.0"The version-specifier match also handles the inverse: when the override happens to match the config (e.g. This comment was generated by an AI coding assistant. |
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 x -- echo |
22.1 ± 0.5 | 21.2 | 25.1 | 1.00 |
mise x -- echo |
23.2 ± 0.8 | 21.7 | 28.3 | 1.05 ± 0.04 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 env |
22.4 ± 0.6 | 21.0 | 25.2 | 1.00 |
mise env |
22.8 ± 1.0 | 21.3 | 37.5 | 1.02 ± 0.05 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 hook-env |
22.7 ± 1.0 | 21.5 | 36.5 | 1.00 |
mise hook-env |
23.0 ± 0.6 | 22.0 | 25.3 | 1.01 ± 0.05 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 ls |
20.2 ± 0.7 | 19.0 | 23.9 | 1.00 ± 0.04 |
mise ls |
20.2 ± 0.5 | 19.4 | 24.7 | 1.00 |
xtasks/test/perf
| Command | mise-2026.4.28 | mise | Variance |
|---|---|---|---|
| install (cached) | 150ms | 156ms | -3% |
| ls (cached) | 76ms | 79ms | -3% |
| bin-paths (cached) | 81ms | 83ms | -2% |
| task-ls (cached) | 829ms | 795ms | +4% |
### 🚀 Features - **(conda)** graduate conda backend out of experimental by @jdx in [#9544](#9544) - **(deps)** Add dart and flutter providers by @tjarvstrand in [#9505](#9505) - **(registry)** add neo4j by @mnm364 in [#9525](#9525) - **(registry)** add rustfs by @mnm364 in [#9530](#9530) - **(task)** support exclusion patterns in task sources by @jlarmstrongiv in [#9496](#9496) - **(vfox)** add stat function to lua file module by @esteve in [#9497](#9497) ### 🐛 Bug Fixes - **(backend)** flag regex prerelease versions by @jdx in [#9500](#9500) - **(backend)** mark -nightly/-canary/-experimental as prereleases by @jdx in [#9523](#9523) - **(backend)** suppress no-versions warning for unresolved-latest backends by @jdx in [#9548](#9548) - **(backend)** include dotnet prereleases from package flags by @jdx in [#9551](#9551) - **(backend)** scope PEP 440 prerelease detection to Python backends by @jdx in [#9558](#9558) - **(cargo)** Apply install_env during cargo install by @c22 in [#9502](#9502) - **(copr)** drop epel-9 chroots since rust >= 1.91 is unavailable by @jdx in [#9484](#9484) - **(github)** skip attestations on non-default api_url by @jdx in [#9486](#9486) - **(github)** retry ip allow list errors without auth by @risu729 in [#9506](#9506) - **(http)** update versions host tracking endpoint by @jdx in [#9527](#9527) - **(install)** don't warn for configured tools when version is passed via CLI by @jdx in [#9522](#9522) - **(install)** refresh latest before installing missing tools by @jdx in [#9545](#9545) - **(install)** don't cache nonexistent install paths by @jdx in [#9553](#9553) - **(lockfile)** don't propagate ad-hoc CLI overrides into the project lockfile by @jdx in [#9562](#9562) - **(plugin)** detect plugin types after cloning by @risu729 in [#9540](#9540) - **(release)** pass --no-git-checks to aube publish by @jdx in [#9483](#9483) - **(task)** convert PATH to MSYS Unix form when spawning POSIX shells on Windows by @JamBalaya56562 in [#9547](#9547) ### 📚 Documentation - **(contributing)** require popularity check for registry PRs by @jdx in [7bbeebe](7bbeebe) - **(watch)** update pitchfork domain to en.dev by @risu729 in [#9536](#9536) - document ghtkn GitHub token setup by @jdx in [#9546](#9546) - clarify registry backend acceptance policy by @jdx in [#9543](#9543) - Change exec command to use bash for variable echo by @kuboon in [#9567](#9567) ### 🧪 Testing - **(e2e)** run test-tool targets in parallel by @jdx in [#9564](#9564) - **(e2e)** run tests in parallel by @jdx in [#9563](#9563) - **(e2e)** bind-mount /tmp on disk and surface failed tests in CI summary by @jdx in [#9570](#9570) - **(tasks)** migrate test_task_help atask to usage field by @jdx in [#9549](#9549) ### 📦️ Dependency Updates - update fedora:45 docker digest to 8b838b3 by @renovate[bot] in [#9507](#9507) - update ghcr.io/jdx/mise:deb docker digest to f02194c by @renovate[bot] in [#9509](#9509) - update taiki-e/install-action digest to 7769b73 by @renovate[bot] in [#9512](#9512) - update ghcr.io/jdx/mise:alpine docker digest to 581f8a8 by @renovate[bot] in [#9508](#9508) - update rust crate ctor to v0.10.1 by @renovate[bot] in [#9515](#9515) - update ghcr.io/jdx/mise:rpm docker digest to a5c9655 by @renovate[bot] in [#9510](#9510) - update rust docker digest to a9cfb75 by @renovate[bot] in [#9511](#9511) - update rust crate age to v0.11.3 by @renovate[bot] in [#9514](#9514) - update rust crate jiff to v0.2.24 by @renovate[bot] in [#9516](#9516) - update dependency vitepress-plugin-tabs to ^0.9.0 by @renovate[bot] in [#9518](#9518) - update autofix-ci/action action to v1.3.4 by @renovate[bot] in [#9513](#9513) - update rust crate usage-lib to v3.2.1 by @renovate[bot] in [#9517](#9517) - update apple-actions/import-codesign-certs action to v7 by @renovate[bot] in [#9519](#9519) - update taiki-e/install-action digest to 51cd0b8 by @renovate[bot] in [#9531](#9531) - exclude taiki-e/install-action from renovate by @jdx in [#9532](#9532) - update rust crate blake3 to v1.8.5 by @renovate[bot] in [#9533](#9533) ### 📦 Registry - enable shellcheck on windows by @zeitlinger in [#9487](#9487) - add google-java-format by @zeitlinger in [#9488](#9488) - add expert ([aqua:expert-lsp/expert](https://github.com/expert-lsp/expert)) by @AlternateRT in [#9498](#9498) - update entry for checkmake by @eread in [#9504](#9504) - add systemctl-tui ([aqua:rgwood/systemctl-tui](https://github.com/rgwood/systemctl-tui)) by @2xdevv in [#9521](#9521) - add codon by @3w36zj6 in [#9538](#9538) - add tool yr (backend:github:VirusTotal/yara-x) by @adam-moss in [#9542](#9542) - add tool betterleaks (backend:aqua/betterleaks/betterleaks) by @adam-moss in [#9541](#9541) - add `git-filter-repo` by @garysassano in [#9550](#9550) - add umoci ([aqua:opencontainers/umoci](https://github.com/opencontainers/umoci)) by @2xdevv in [#9555](#9555) - add aqua backend for elixir-ls by @AlternateRT in [#9557](#9557) - deny inline backend options by @risu729 in [#9565](#9565) ### Chore - **(ci)** fail registry tests without summary by @jdx in [#9559](#9559) - **(ci)** use !cancelled() instead of always() for test-ci aggregator by @jdx in [#9569](#9569) - **(ci)** use namespace runners for ci jobs by @jdx in [#9561](#9561) - **(config)** deprecate shorthands_file setting by @risu729 in [#9534](#9534) - **(docs)** remove shrill.en.dev analytics script by @jdx in [#9539](#9539) - **(release)** replace bc with awk in release-plz star formatting by @jdx in [d7f177f](d7f177f) - bump hk to 1.44.3 by @jdx in [#9493](#9493) - invert CLAUDE.md/AGENTS.md so AGENTS.md is canonical by @jdx in [#9560](#9560) - set dev profile debug to 1 by @jdx in [#9572](#9572) ### New Contributors - @kuboon made their first contribution in [#9567](#9567) - @AlternateRT made their first contribution in [#9557](#9557) - @2xdevv made their first contribution in [#9555](#9555) - @adam-moss made their first contribution in [#9541](#9541) - @jlarmstrongiv made their first contribution in [#9496](#9496) - @tjarvstrand made their first contribution in [#9505](#9505)
Summary
mise x node@latest -- ...(andmise run-spawnedmise xcalls) is an ephemeral CLI override. WithMISE_LOCKFILE=1, that install was being merged into the project'smise.lockunder the config's existing request — e.g.mise.tomlnode = "24"paired with a freshly-installed25.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 fixescommit on everyrelease-plzPR (e.g. PR #9485 / ea7affd8eb3f46374a9451550e7b08f1bbcf6e07: node 24.15.0 → 25.9.0, platforms 11 → 1).mise run rendercallsmise x node@latest -- npx markdown-magic; on the autofix runner withMISE_LOCKFILE=1set, that install bumpedmise.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 supportmise upgradepropagating to the lockfile) overwrites a config tool's lockfile entry with any newly installedToolSource::Argumentversion that shares the short name. Butupgrade.rsalready remapsArgument→MiseToml(...)before callingupdate_lockfiles(src/cli/upgrade.rs:383-396), so its tools go through the path-sourceifbranch. The else-if branch only fires for tools that keptToolSource::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_latestrewrites the request to carry the config's version string, so the specifiers match and a legitimate fuzzy refresh still populatesmise.lock(e.g. against an empty lockfile)mise x node@24whenmise.tomlsaysnode = "24"— same specifier, propagatesmise x node@latestwhenmise.tomlsaysnode = "24"—"latest" != "24", skippedmise x node@25whenmise.tomlsaysnode = "24"—"25" != "24", skippedmise upgradekeeps working — its remap →MiseTomlhits the if-branch, never the else-ifmise install(no args) keeps working — sources areMiseTomlmise 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 isMiseTomlTest plan
e2e/cli/test_exec_lockfile— covers both directions:mise x [email protected]andmise x dummy@latestmust NOT mutatemise.lockwhenmise.tomlsaysdummy = "1"mise x dummy(no version) MUST populate an empty lockfile from the config-derived installmise 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 fix(lockfile): update global lockfile on upgrade #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— greencargo test --all-features lockfile— 29 lockfile unit tests pass🤖 Generated with Claude Code
Note
Medium Risk
Touches
update_lockfilesbehavior so some installs triggered bymise x/env overrides will no longer updatemise.lock; this could affect workflows that relied on the previous (incorrect) propagation but is guarded by an e2e regression test.Overview
Prevents
mise.lockfrom being updated by ad-hoc version overrides (e.g.mise x tool@latestorMISE_<TOOL>_VERSION=...) when the override’s version specifier doesn’t match the project config’s requested specifier.update_lockfilesnow 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 formise x toolwithout a version, andmise upgradestill updating the lockfile.Reviewed by Cursor Bugbot for commit 3b7233a. Bugbot is set up for automated code reviews on this repo. Configure here.