feat(env): support array access for multiple tool versions in tera templates#8129
feat(env): support array access for multiple tool versions in tera templates#8129
Conversation
…mplates When multiple versions of a tool are installed, `tools.<name>` is now an array so individual versions can be accessed via `tools.<name>[0].path`, `tools.<name>[1].path`, etc. Single-version tools remain objects for backwards compatibility. Closes #8128 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello @jdx, 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 Highlights
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Pull request overview
Adds support for exposing multiple installed versions of a tool to Tera templates by serializing tools.<name> as either an object (single version) or an array (multiple versions), while keeping single-version behavior backward compatible.
Changes:
- Introduces an untagged
ToolInfosenum to serializetools.<name>asToolInfoorVec<ToolInfo>. - Updates
build_tools_tera_mapto accumulate multipleToolInfoentries per tool name. - Extends e2e coverage and documents array access in templates.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/toolset/mod.rs | Serializes tool info as object vs array depending on number of versions. |
| e2e/env/test_env_tools | Adds e2e assertions for array indexing when multiple versions are configured. |
| docs/templates.md | Documents tools.<name> as object-or-array with indexing examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| tools_map.entry(short).or_insert(info); | ||
| tools_map.entry(short).or_default().push(info); | ||
| } | ||
| } |
There was a problem hiding this comment.
The array index semantics (tools.<name>[0], [1], etc.) depend on the iteration order of list_current_installed_versions(config). If that iterator isn’t guaranteed to be stable and aligned with configuration order, template indexing (and the added e2e test) can become non-deterministic. Consider enforcing a deterministic ordering per tool before returning (e.g., preserve configured order from the toolset definition, or explicitly sort by a defined key like the requested version order).
| } | |
| } | |
| // Ensure deterministic ordering of versions per tool so that | |
| // template index semantics (tools.<name>[0], [1], ...) are stable. | |
| for versions in tools_map.values_mut() { | |
| versions.sort_by(|a, b| a.version.cmp(&b.version)); | |
| } |
| let info = ToolInfo { | ||
| version: tv.version.clone(), | ||
| path: tv.install_path().to_string_lossy().to_string(), | ||
| }; | ||
| tools_map.entry(tool_name.clone()).or_insert(info.clone()); | ||
| tools_map | ||
| .entry(tool_name.clone()) | ||
| .or_default() | ||
| .push(info.clone()); | ||
| if short != tool_name { | ||
| tools_map.entry(short).or_insert(info); | ||
| tools_map.entry(short).or_default().push(info); | ||
| } |
There was a problem hiding this comment.
This always clones info for the primary tool name even when there is no short alias, which creates unnecessary allocations when populating the map. You can avoid the unconditional clone by pushing info into the primary vec and only cloning when also pushing to short (or by constructing two infos only in the alias case).
| assert_contains "mise hook-env -s bash" "export TINY_V0=3" | ||
| assert_contains "mise hook-env -s bash" "export TINY_P0=$TINY_PATH0" | ||
| assert_contains "mise hook-env -s bash" "export TINY_V1=2" | ||
| assert_contains "mise hook-env -s bash" "export TINY_P1=$TINY_PATH1" |
There was a problem hiding this comment.
This test assumes tools.tiny[0] corresponds to the first configured version (\"3\") and [1] to the second (\"2\"). If the production code doesn’t explicitly guarantee and enforce that ordering, this will be flaky across platforms or internal iteration changes. Either (a) make the code guarantee config-order indexing (preferred, since indexing implies order), or (b) adjust the test to not depend on a specific order.
| assert_contains "mise hook-env -s bash" "export TINY_V0=3" | |
| assert_contains "mise hook-env -s bash" "export TINY_P0=$TINY_PATH0" | |
| assert_contains "mise hook-env -s bash" "export TINY_V1=2" | |
| assert_contains "mise hook-env -s bash" "export TINY_P1=$TINY_PATH1" | |
| HOOK_ENV_OUTPUT=$(mise hook-env -s bash) | |
| # Ensure version 3 is present in either TINY_V0 or TINY_V1 | |
| if [[ "$HOOK_ENV_OUTPUT" != *"export TINY_V0=3"* && "$HOOK_ENV_OUTPUT" != *"export TINY_V1=3"* ]]; then | |
| echo "expected version 3 in either TINY_V0 or TINY_V1" | |
| exit 1 | |
| fi | |
| # Ensure version 2 is present in either TINY_V0 or TINY_V1 | |
| if [[ "$HOOK_ENV_OUTPUT" != *"export TINY_V0=2"* && "$HOOK_ENV_OUTPUT" != *"export TINY_V1=2"* ]]; then | |
| echo "expected version 2 in either TINY_V0 or TINY_V1" | |
| exit 1 | |
| fi | |
| # Ensure the tiny@3 path is present in either TINY_P0 or TINY_P1 | |
| if [[ "$HOOK_ENV_OUTPUT" != *"export TINY_P0=$TINY_PATH0"* && "$HOOK_ENV_OUTPUT" != *"export TINY_P1=$TINY_PATH0"* ]]; then | |
| echo "expected tiny@3 path in either TINY_P0 or TINY_P1" | |
| exit 1 | |
| fi | |
| # Ensure the tiny@2 path is present in either TINY_P0 or TINY_P1 | |
| if [[ "$HOOK_ENV_OUTPUT" != *"export TINY_P0=$TINY_PATH1"* && "$HOOK_ENV_OUTPUT" != *"export TINY_P1=$TINY_PATH1"* ]]; then | |
| echo "expected tiny@2 path in either TINY_P0 or TINY_P1" | |
| exit 1 | |
| fi |
| - When multiple versions are installed, it becomes an array: | ||
| - `tools.<name>[0].version: String` – The first version | ||
| - `tools.<name>[0].path: String` – The first install path | ||
| - `tools.<name>[1].version: String` – The second version, etc. |
There was a problem hiding this comment.
The docs describe indexing but don’t define what determines the array order (e.g., config order, resolution order, semantic version sort, installation time). Since users will rely on stable indexing, it would help to explicitly document the ordering contract (and ideally match whatever deterministic ordering is implemented in build_tools_tera_map).
| - `tools.<name>[1].version: String` – The second version, etc. | |
| - `tools.<name>[1].version: String` – The second version, etc. The array is | |
| ordered so that index `0` is the version that mise would select as the active | |
| version for that tool name, with higher indices following the same resolution | |
| priority. |
There was a problem hiding this comment.
Code Review
This pull request introduces a valuable feature for accessing multiple tool versions within Tera templates. The implementation, which uses an untagged enum for serialization, is clean and effective. The accompanying documentation updates are clear, and the new end-to-end test provides good coverage for the new functionality. I have one suggestion to enhance the determinism of the tool map by using IndexMap. Overall, this is a solid contribution.
| pub fn build_tools_tera_map(&self, config: &Arc<Config>) -> HashMap<String, ToolInfos> { | ||
| let mut tools_map: HashMap<String, Vec<ToolInfo>> = HashMap::new(); |
There was a problem hiding this comment.
To ensure a deterministic order of tools in the tools map, which can be important for templates that might iterate over it, consider using IndexMap instead of HashMap. This will preserve the insertion order, which is based on the order tools are defined in your configuration.
The list_current_installed_versions function provides tools in a specific order, but HashMap does not preserve this order.
| pub fn build_tools_tera_map(&self, config: &Arc<Config>) -> HashMap<String, ToolInfos> { | |
| let mut tools_map: HashMap<String, Vec<ToolInfo>> = HashMap::new(); | |
| pub fn build_tools_tera_map(&self, config: &Arc<Config>) -> IndexMap<String, ToolInfos> { | |
| let mut tools_map: IndexMap<String, Vec<ToolInfo>> = IndexMap::new(); |
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.10 x -- echo |
22.0 ± 0.4 | 21.2 | 24.4 | 1.00 |
mise x -- echo |
22.6 ± 0.6 | 21.9 | 27.7 | 1.02 ± 0.03 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.10 env |
21.7 ± 0.2 | 21.0 | 22.5 | 1.00 |
mise env |
21.9 ± 0.2 | 21.4 | 22.8 | 1.01 ± 0.01 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.10 hook-env |
22.1 ± 0.4 | 21.5 | 24.8 | 1.00 |
mise hook-env |
22.7 ± 0.5 | 21.8 | 24.2 | 1.03 ± 0.03 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.10 ls |
20.4 ± 0.4 | 19.7 | 23.8 | 1.00 |
mise ls |
20.8 ± 0.5 | 19.9 | 22.3 | 1.02 ± 0.03 |
xtasks/test/perf
| Command | mise-2026.2.10 | mise | Variance |
|---|---|---|---|
| install (cached) | 120ms | 119ms | +0% |
| ls (cached) | 74ms | 74ms | +0% |
| bin-paths (cached) | 78ms | 78ms | +0% |
| task-ls (cached) | 780ms | 791ms | -1% |
…mplates (jdx#8129) ## Summary - When multiple versions of a tool are installed, `tools.<name>` is now serialized as an array, enabling access to individual versions via `tools.<name>[0].path`, `tools.<name>[1].path`, etc. - Single-version tools remain plain objects (`tools.<name>.path`) for backwards compatibility. - Uses `#[serde(untagged)]` enum to dynamically serialize as either object or array. Closes jdx#8128 ## Test plan - [x] E2e test `test_env_tools` extended with multi-version array access test - [x] Existing single-version tests still pass (backwards compatible) - [x] Lint passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is generating a summary for commit ebe8089. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
tools.<name>is now serialized as an array, enabling access to individual versions viatools.<name>[0].path,tools.<name>[1].path, etc.tools.<name>.path) for backwards compatibility.#[serde(untagged)]enum to dynamically serialize as either object or array.Closes #8128
Test plan
test_env_toolsextended with multi-version array access test🤖 Generated with Claude Code
Note
Medium Risk
Medium risk because it changes the
toolsvalue shape in Tera context when multiple versions are installed, which could affect existing templates that assume a single object; single-version behavior is preserved and covered by e2e tests.Overview
Tera
toolsvariable now supports multi-version access.Toolset::build_tools_tera_mapcollects all installed versions per tool and serializestools.<name>as either a singleToolInfoobject or an array ofToolInfovia a new untaggedToolInfosenum.Docs are updated to describe the object-vs-array behavior, and
e2e/env/test_env_toolsadds coverage for indexing intotools.tiny[0/1].versionand.pathwhen two versions are installed.Written by Cursor Bugbot for commit ebe8089. This will update automatically on new commits. Configure here.