Identify which CLI an upgrade warning is about - #2670
Conversation
There was a problem hiding this comment.
🟡 Not ready to approve
Doctor can report false installation mismatches, omit single-install details, and hang on unresponsive executables.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Improves upgrade diagnostics for systems with multiple Stellar CLI installations.
Changes:
- Identifies the executable responsible for upgrade warnings and cache writes.
- Expands
doctordiagnostics to inspect PATH installations. - Adds request and shutdown timeouts for background upgrade checks.
File summaries
| File | Description |
|---|---|
cmd/soroban-cli/src/upgrade_check.rs |
Adds executable identification, cache attribution, and fetch timeout. |
cmd/soroban-cli/src/config/upgrade_check.rs |
Persists the cache writer with backward compatibility. |
cmd/soroban-cli/src/commands/doctor.rs |
Reports installations and cache-writer mismatches. |
cmd/soroban-cli/src/cli.rs |
Gives background checks a completion grace period. |
Review details
Suppressed comments (1)
cmd/soroban-cli/src/commands/doctor.rs:242
- The usual single-install case returns before printing the executable's path and version, so
doctordoes not actually list every PATH installation as promised. The zero-match case is also reported as “Only one.” Always enumerate nonempty results and handle zero separately.
if installs.len() <= 1 {
print.checkln("Only one Stellar CLI found on PATH".to_string());
return;
- Files reviewed: 4/4 changed files
- Comments generated: 3
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Changes recommended
Some installation listings and cache-writer diagnostics are incomplete or misleading.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
cmd/soroban-cli/src/upgrade_check.rs:140
- This assigns
last_checked_byafter a failed request without refreshing either cached version, butdoctorlater tells users the cache was “last refreshed by” this executable. That can falsely attribute version data to an install that never fetched it. Keep this assignment if the intended identity is the last writer/pacer, but update the field documentation and alldoctormessages to say “last checked/written by”; alternatively, record this field only after successful refreshes.
// A failed attempt still paces the next one, so record who
// paced it -- otherwise the file credits whichever install
// last succeeded, which may not be the one holding it back.
stats.last_checked_by = Some(check_performed_by());
cmd/soroban-cli/src/commands/doctor.rs:264
- The single-install branch does not call
list_installs, sodoctoromits that executable's PATH location and version. This contradicts the PR's stated behavior of listing every discoveredstellar/sorobanexecutable with its version; the separate “Running executable” line is not necessarily the PATH entry and has no version.
(1, _) => print.checkln("Only one Stellar CLI found on PATH".to_string()),
cmd/soroban-cli/src/commands/doctor.rs:277
common_version == Nonealso means one or more probes returnedNone, not necessarily that known versions differ. For example, two executables that cannot run are both listed as “unknown version” while this branch claims they reported different versions. Distinguish probe failures from genuinely distinct known versions so the diagnostic does not give a false cause.
(count, None) => {
print.warnln(format!(
"Found {count} Stellar CLI executables on PATH reporting different versions; \
an outdated one can report a version that disagrees with `stellar --version`:"
));
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
What: name the running executable in the upgrade warning; have `doctor` list every `stellar`/`soroban` on PATH with its version, and report which CLI last refreshed the shared version cache. Bound the crates.io request and give the background check a grace period to persist its result. Why: the warning only prints when the latest version exceeds the running one, so a stale cache cannot produce the output in stellar#2464 -- a 25.2.0 binary with a 25.1.0 cache prints nothing. `current_version` is `env!("CARGO_PKG_VERSION")`, so an older install reports its own version while looking like it speaks for the CLI the user thinks they run. The release dates agree: 22.1.0 predates the report by 15 months. Separately, returning from `main` dropped a still-running check, so a fast command never persisted the versions it had just fetched. Known limitations: the grace period is skipped on the error paths that call `process::exit`, and it can add up to 2s to the first command of the day, when the check actually goes to the network. Cache-writer reporting is diagnostic only -- no decision keys off it, so files written before the field existed behave exactly as before. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
What: record the version-cache writer as separate version and executable fields, canonicalize the executable on both write and read, and compare only the path. Warn about several executables on PATH when their versions disagree rather than when there is more than one, and report finding none. Why: the writer was a single `"<version> (<executable>)"` string, so an in-place upgrade -- same path, new version -- read as a different install; the cache is only rewritten once a day, so that warning could repeat on every `doctor` run for up to 24h. The count was misleading in the same way: this crate ships both `stellar` and `soroban`, so one ordinary install puts two files on PATH and was warned about. Zero executables fell into the same branch and reported "Only one Stellar CLI found on PATH". Same path with an earlier version is now informational rather than a warning: it is the ordinary state after an upgrade, and it corrects itself at the next refresh. When either path is unknown there is no identity to compare, so the writer is reported without claiming a match either way. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
What: add `soroban-test` integration coverage for zero, one, two agreeing and two disagreeing Stellar CLIs on PATH, and for cache writers that are this install, this install at an earlier version, a different install, and absent. Why: the externally visible behavior was covered only by parser unit tests, so nothing exercised the subprocess probing or the messages themselves -- both false positives fixed in the previous commit would have been caught here. PATH comes from fake CLIs in a temp dir, as in `plugin.rs`, and the version cache from `STELLAR_DATA_HOME`, so no production code had to change to make the inputs injectable. One fake rejects `version --only-version` so the `--version` banner fallback runs through a real subprocess. Unix only: the fake CLIs are shell scripts needing an execute bit. Assertions are on stderr, where `Print` writes. The seeded cache writer survives the run because `doctor` reads it before `has_available_upgrade` can overwrite it. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: Copilot's review of stellar#2670 found three `doctor` diagnostics claiming more than the data behind them supports, each able to send a user after a cause that was never observed. Behavior: - A check whose fetch failed still stamps the cache, but leaves the recorded versions untouched, so "last refreshed by" credited an install with version data it never fetched. Say "checked" instead, in the messages and the field docs: the writer paces the next check rather than vouching for the versions stored beside it. Recording it only after a successful fetch was the alternative, and it hides the install worth finding -- a stale one whose fetch fails still suppresses everyone else's check for a day. - The single-install branch printed a count without the listing, so the one case a listing would settle was the one case that omitted path and version. List every discovered install. - Absent agreement was reported as disagreement: two executables that cannot be run are both unknown, yet the message blamed differing versions. `InstalledVersions` now keeps Agreed, Disagree and Unanswered apart. An observed disagreement still wins over a failed probe alongside it, because that one is a fact. Tests: four unit cases for `summarize_versions`, and two integration cases driving real subprocesses through a fake `PATH` -- two unrunnable CLIs, and a disagreement sitting next to an unreadable binary. The lone-install listing and the reworded cache-writer lines are asserted too. Release impact: no breaking change and no migration. The `upgrade_check.json` shape is untouched and older files still load. Only `doctor`'s stderr wording changes -- no command, flag or help text does, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
What: canonicalize the sandbox directories the `doctor` tests build their
expected paths from, and take a whole `PATH` value rather than a `Path` in
the test helper.
Why: `find_installs` canonicalizes every executable it discovers, so an
expectation built from an uncanonicalized sandbox compares two spellings
of the same path and finds them unequal as strings. On macOS the temporary
directory sits under `/var/folders`, a symlink to `/private/var`, which
fails three of these tests. `warns_when_installs_report_different_versions`
already did before this branch; the two path assertions added since
inherited the same flaw and turned one failure into three. Linux resolves
nothing there, so local runs and CI stayed green and hid it.
Reproduced on Linux by pointing `TMPDIR` at a symlinked directory: without
the canonicalize the same three fail, with it all ten pass. The doc comment
records why the call is there, since its absence is what let the trap
through twice.
The helper's `path` is a whole `PATH` value, and a multi-entry one is
several paths joined by `:` -- not a path. `Path::new("dir1:dir2")` only
worked because `env` takes it back to an `OsStr`, so ask for
`impl AsRef<OsStr>` and let the multi-entry case stop pretending.
Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
Motivation: review of stellar#2670 found the taxonomy added in b7df621 over-claiming in two places of its own -- the same class of error it exists to remove, one size smaller. Behavior: - `Disagree` counted every executable found, including ones that never answered. Two reporting different versions beside a third that could not be run printed "Found 3 Stellar CLI executables on PATH reporting different versions", contradicted by the listing directly beneath it, where the third reads "(unknown version)". It now carries how many went unanswered and names only the ones that were heard from: "the 2 that reported a version do not agree (1 could not be asked)". - `Unanswered` discarded what the answering executables established. Two at 27.1.0 beside one that cannot run is a machine whose reachable installs agree, and that was the most useful fact on the line; the message said only that agreement could not be determined. It now carries that version and leads with it, while still declining to call the whole set agreed: a version that was never read cannot be ruled out. Every count now sits next to what it counts, so the sentence can be checked against the listing below it. Also drops the redundant `return` in the zero-install arm. `list_installs` prints nothing for an empty slice, so the early exit bought nothing and only broke the symmetry between arms. Tests: unit cases pin the new payloads, including that an agreement survives a failed probe beside it and that the disagreement count excludes the executable that never answered. An integration case drives the agreement-plus-unreadable scenario through real subprocesses, and the two existing messages that changed are re-pinned. Release impact: no breaking change and no migration -- only `doctor`'s stderr wording moves, so `FULL_HELP_DOCS.md` stands as is. Co-authored-by: Nearx-Labs <nearxlabs@nearx.com.br>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cmd/soroban-cli/src/commands/doctor.rs:266
- A single discovered executable is always shown with a success marker, even when its version probe failed and the list immediately labels it
unknown version. This makes a broken or non-Stellar executable look healthy, whereas the same failed probe correctly warns when multiple entries exist. MatchInstalledVersions::Agreedfor the success case and warn forUnanswered.
// One file, so nothing can disagree with it. Still list it: the running
// executable is not necessarily the one `PATH` resolves by name, and the
// line above carries no version.
(1, _) => print.checkln("Only one Stellar CLI found on PATH:".to_string()),
cmd/soroban-cli/src/upgrade_check.rs:48
- Using the canonical target as the installation identity is not stable across symlink-managed upgrades. For example, Homebrew keeps
/opt/homebrew/bin/stellaras the user-facing install path but retargets it from one versioned Cellar path to another; this cache value therefore changes after a normal in-place upgrade anddoctorreports a different CLI. Preserve or separately record a stable invoked/install path for identity, while retaining the resolved path only as diagnostic detail.
let path = std::env::current_exe().ok()?;
let path = path.canonicalize().unwrap_or(path);
cmd/soroban-cli/src/commands/doctor.rs:54
- This read can race with the ordinary upgrade-check task spawned in
cli.rs:81beforeroot.run(). If the cache is stale and that task completes or fails quickly, it writes this executable aslast_checked_bybefore this load, hiding the different-install warning that this snapshot is meant to preserve. Skip the global background check fordoctor, or capture/pass the previous writer before spawning it.
This issue also appears on line 263 of the same file.
// Read this before `check_version`, which refreshes the cache and would
// otherwise record this very run as the writer -- hiding the mismatch
// the report exists to reveal.
let previous_cache_writer = version_cache_writer();
What problem does your feature solve?
The upgrade warning displays incorrect version information because of multiple
stellarinstallations on the same machine. An older binary prints the warning using its own version, which users then incorrectly compare againststellar --versionfrom a different binary. Additionally, fast commands abort the background version fetch before it can complete.What would you like to see?
doctorcommand updated to list everystellar/sorobanexecutable on the systemPATHalong with its version.doctorwarning if it was a different install.What alternatives are there?
Accepting the known limitations of this implementation: the grace period is skipped on error paths that call
process::exit; it may add up to 2 seconds to the first command of the day; and recording the cache writer is strictly diagnostic (it does not affect whether the warning is shown).fix: #2464