<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/755
<!-- entire-trail-link-end -->
A cold `mise outdated`, `mise upgrade` or `mise install` downloads one
`data/<tool>.toml` from mise-versions for each tool it has to resolve.
mise now reads mise-versions' versions index (`data/index.v1.json`,
jdx/mise-versions#368) instead, so many tools take one request. The
answers do not change: the index is used only when it can prove the
per-tool list would give the same version.
For this project, a cold `mise outdated` (empty
`MISE_CACHE_DIR`/`MISE_DATA_DIR`) went from **19 requests to 14**, with
identical output:
```toml
[tools]
node = "22"
python = "3.12"
go = "latest"
terraform = "1"
jq = "latest"
ripgrep = "latest"
gh = "latest"
deno = "2"
bun = "latest"
zig = "0.14"
kubectl = "latest"
helm = "3"
shellcheck = "latest"
yq = "4"
fzf = "latest"
just = "latest"
```
The six lists for node, go, bun, deno, zig and yq became one list plus
the index. The other 13 requests are unchanged: aqua's `latest` shortcut
(`/releases/latest`) for jq, ripgrep, gh, kubectl, shellcheck, fzf and
just; kubectl's `api.github.com` lookups; and the lists for python,
terraform and helm (see *What still reads its own list*). Wall time was
within noise (0.8–1.3s both ways), because the requests already ran in
parallel.
| Command | Before | After |
| --- | --- | --- |
| `mise outdated`, cold, 16 tools | 19 requests | 14 requests |
| `mise latest node`, cold | 1 request (`node.toml`) | 1 request
(`node.toml`) |
| `mise latest bun`, index on disk, no `bun.toml` cached | 1 request | 0
requests |
### When the index is used
The index is about 140 KB compressed, against about 5 KB for one tool's
list, so it is worth fetching only when it replaces several lists:
- A tool whose own list is already cached never consults the index. That
list is complete and costs nothing.
- The first tool in a command that needs a list reads its own
`data/<tool>.toml`, exactly as before. The index is fetched when a
second tool needs one. A single-tool command such as `mise latest node`
therefore never downloads it, and a many-tool command makes two requests
instead of one per tool.
- The index is cached at `$MISE_CACHE_DIR/versions-index/` and reused
for `fetch_remote_versions_cache` (1h by default), by any command,
including single-tool ones. After that it is revalidated with
`If-None-Match`, which returns 304 when nothing changed. A `refresh`
lookup (installing a `latest` that nothing installed satisfies) reuses a
copy validated in the last 5 minutes, the window a refreshed version
list reuses.
- An explicit backend arg that #14459 maps to a registry tool's list,
such as `aqua:jqlang/jq`, uses that tool's index entry too.
- `use_versions_host = false`, offline and `prefer_offline` never use
it. Neither does any tool the per-tool list would not come from
mise-versions for (local listing options, a cache context, non-default
backends, tools the host does not list), and neither does a
`version_order` other than `source`. A failed or 404 fetch is remembered
for the command, and every tool then reads its own list.
- The subset is never stored as a tool's version list.
`mise outdated` also stops looking up `release_url` for each outdated
tool unless `--json` prints it. The table never showed it, and the
lookup read each tool's full list, which would have undone the index.
`mise outdated --json` still includes it.
### What still reads its own list
- **Tools with `version_order = "semver"`.** That is 847 of the 1013
registry entries, including terraform and helm above. The spec requires
source order, and mise re-sorting the full list would break the subset
argument. Most of these lists are already published in mise's semver
order: a scan of mise-versions' `docs/` found 824 of the 846 with lists
unchanged by `VersionOrder::Semver`. A per-tool marker in the index
saying so would let mise cover them; that needs a spec change on the
mise-versions side.
- **aqua and github `latest`.** These still come from
`/releases/latest`, which can name a different release than the list
does, so replacing it would change answers.
- **python** is not served from the shared listing, as before.
- **Installs of aqua and github tools** still read the list for the
release tag (#14442). The index has no `release_url`.
### Tests
- Property tests, one per matcher (shared, PEP 440, aqua, and a ubi
sample), resolve every index key, `latest` and a set of non-key queries
for 10 tools. They use real data from one mise-versions deploy (index
entries plus full lists, `test/fixtures/versions-index/`, 134 KB), with
prereleases on and off and release-age cutoffs from none and 0s to 30
days, including a clock two days ahead. Every index answer is compared
with full-list resolution by the backend's own matcher and filters; a
separate test checks that this full-list answer is what the real path
(`latest_version_for_query_with_selection_options`,
`list_versions_matching_with_selection_options`) gives, on a sample.
Making the coverage check always accept fails them at once. Each runs in
about 0.3s, within the suite's 1s per-test limit.
- Unit tests cover the algorithm's branches, the spec's worked examples,
format and malformed-entry rejection, and that index timestamps and
flags read the same as `data/<tool>.toml`.
- A new e2e test, `test_versions_index`, uses a local mise-versions
stand-in. It checks that one tool reads only its own list, that three
tools make one list request plus one index request, that a warm run
makes no requests, that a stale copy is revalidated with its ETag, and
that offline makes no requests.
- Ran clippy (`--all-features --all-targets -D warnings`), the
`backend::`, `toolset::`, `versions_host` and cache unit tests, and the
`test_outdated*`, `test_upgrade*`, `test_latest*`,
`test_install_dry_run`, `test_ls_remote_versions_host_cache` and
minimum-release-age e2e tests. Two failures also happen on `origin/main`
in this sandbox: `test_upgrade_lazy_missing`, and the unit test
`has_linked_version_ignores_mise_managed_absolute_targets`, which cannot
write to the root-owned `/usr/local/share/mise/installs`.
#14457 (still open) changes how `refresh` reuses a recent list; the
index follows the same 5-minute window, so the two agree when it lands.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5;
version: 2.1.295.*
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes core remote version resolution for multi-tool commands;
incorrect index logic could pick wrong versions, though fallbacks and
property tests aim to prevent that.
>
> **Overview**
> When several tools need remote versions in one command (`mise
outdated`, `upgrade`, install resolution), mise can fetch **one**
mise-versions **`index.v1.json`** instead of a **`data/<tool>.toml`**
per tool. Resolution tries the index first and only keeps an answer when
coverage markers prove it matches the full list; otherwise behavior is
unchanged. The index is loaded after the second tool needs a list
(single-tool commands still fetch only that tool’s TOML), cached under
`$MISE_CACHE_DIR/versions-index/` with **ETag** revalidation, and
skipped when offline, `use_versions_host` is off, non-source
`version_order`, a fresh per-tool cache already exists, or backends with
broader matchers (e.g. Java).
>
> **`mise outdated`** no longer resolves **`release_url`** unless
**`--json`** (`outdated_release_urls`), avoiding extra list fetches that
would negate the win. Docs for **`use_versions_host`** describe the
index; **`CacheManager::has_fresh_value`** supports skipping index use
when a tool list is already warm. New e2e **`test_versions_index`** and
large unit/property tests back the client in **`versions_index.rs`**.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1c09d20ee8. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Commands resolving versions for multiple tools can use a shared,
cached version index. When the index can’t confirm a match, mise falls
back to the tool’s full version list.
* Cached index responses are reused while fresh and can be revalidated
when stale. A usable cached index can also support offline commands.
* Outdated-version JSON output includes release URLs.
* **Documentation**
* Clarified how the version index is used and cached, including when
full version lists are consulted.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>