fix(config): let tool versions contain a colon - #11580
Conversation
A `:` anywhere in a `[tools]` version made config loading fail, because the
version's *selector* was parsed at TOML-deserialize time, before templates were
rendered. A template containing one -- the reported case was
`'ANSIBLE_VERSION: '` inside an `exec()` -- was split mid-expression:
[tools."npm:cowsay"]
version = '''{{ exec(command='echo VER: 1.2.3') | split(pat=': ') | last }}'''
x Invalid TOML in config file
1 | [tools."npm:cowsay"]
| ╰── invalid prefix: {{ exec(command='echo VER
Not template-specific: `"npm:cowsay" = "1.2.3:x"` failed the same way. Not a
regression either -- colon-free templates work back to v2025.7.0.
Keep the version request as the raw string until it is rendered, which is what
`[tasks.*.tools]` and `.tool-versions` already do; `mise.toml`'s `[tools]` was
the one place that parsed first and rendered second. `ToolRequest::new` parses
the rendered string, so `prefix:`/`ref:`/`path:`/`sub-N:` are still honoured.
`ToolVersionType` is deliberately untouched: it is also the version filter for
remote listings in `backend/mod.rs` and `backend/github.rs`, and relaxing it
there would let a tag containing template syntax through.
Deserialization no longer rejects a bad version, so the failure surfaces later,
in `to_tool_request_set`, which several callers `.ok()` away. Name the file, the
template, and what it rendered to, so the error is still actionable:
invalid version for terraform in ~/mise.toml:
{{ exec(command='echo bogus:1.0') }} rendered to "bogus:1.0"
Collapsing `From<ToolRequest> for MiseTomlTool` onto `ToolRequest::version()`
also drops a latent bug: the `Ref` arm passed `(ref_, ref_type)` where both
`FromStr` and `Display` take the type first, so `branch:main` round-tripped to
`main:branch`.
📝 WalkthroughWalkthroughTool version requests now remain unparsed until after template rendering. This allows colon-containing templates, preserves rendered selectors, and reports invalid rendered values with configuration context. Tests cover successful resolution, invalid selectors, and trust enforcement. ChangesTool version template parsing
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR defers parsing
Confidence Score: 5/5The PR appears safe to merge with no actionable defects identified. The changed parsing order preserves raw templates until rendering while retaining selector parsing afterward, and the added trust coverage confirms that newly accepted colon-bearing templates do not bypass the existing execution boundary. Important Files Changed
Reviews (1): Last reviewed commit: "fix(config): let tool versions contain a..." | Re-trigger Greptile |
Summary
A
:anywhere in a[tools]version makes config loading fail. The version's selector is parsed at TOML-deserialize time, before templates are rendered, so a template containing a colon gets split mid-expression:Not template-specific —
"npm:cowsay" = "1.2.3:x"fails the same way withinvalid tool: invalid prefix: 1.2.3. Not a regression either: colon-free templates work on every release back to v2025.7.0, so it has always been the colon.Reported in #5531, where the reporter's template contained
'ANSIBLE_VERSION: '. Their diagnosis was right.Fix
Keep the version request as the raw string until it is rendered.
mise.toml's[tools]was the one place that parsed first and rendered second:[tasks.*.tools]Task::render→ parse.tool-versionsmise.toml[tools]Display→ render → re-parseMiseTomlToolnow holdsrequest: String, andToolRequest::new— which already re-parses the rendered string — is where selectors are resolved.prefix:,ref:,path:,sub-N:andsystemall still work, including when they come out of a template (prefix:{{ env.X }}).ToolVersionTypeis deliberately untouched. Guarding it oncontains_template_syntaxwould have been a smaller diff, but it is also the version filter for remote listings inbackend/mod.rsandbackend/github.rs(Ok(ToolVersionType::Version(_)) => true), and relaxing it there would let an upstream tag containing template syntax through. Removing the caller is the narrower change even though the diff is larger.Collapsing
From<ToolRequest> for MiseTomlToolontoToolRequest::version()also drops a latent bug: theRefarm passed(ref_, ref_type)where bothFromStrandDisplaytake the type first, sobranch:mainround-tripped tomain:branch.Diagnostics
Deserialization no longer rejects a bad version, so the failure now surfaces in
to_tool_request_set()— further from the config, and behind five call sites that.ok()the result (install.rs,upgrade.rs×2,lock.rs×2,task_tool_installer.rs). To keep it actionable the error names the file, the template, and what it became:Making those callers stop swallowing the error is a separate change; I left it alone here.
Tests
test_colon_in_templated_tool_version— the reported case, plusref = "{{ env.BRANCH }}"and a{% if %}block. Asserts the resultingToolRequest, so it pins that the selector survives being resolved late rather than pinning the intermediate representation.test_templated_tool_version_rendering_to_bad_selector— the deferred error still names file, template and rendered value.e2e/config/test_tool_version_vars— the repro,prefix:{{ env.X }}, and a negative case. Every template in that file was colon-free before this.e2e/config/test_trust_safe_config— a colon-bearing template in[tools]must still require trust and must not execute. Previously the version parser rejected it first, so this pins that trust is what stops it now.Verified on Windows against the released v2026.7.18 binary: the repro flips, and
prefix:/path:/ref:/sub-1:/system/ plain / colon-free templates / both table forms are byte-identical, as ismise usewrite-back (an unrelated tool'sprefix:selector survives).🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes