Files
codex/codex-rs/tui/src/update_versions.rs
T
Eric Traut a5290028a2 Extend server version notices to prerelease and local clients (#46673)
## Why

Server version notices previously required both versions to be stable releases, leaving prerelease and local clients without notices about differing background services.

## What changed

- Use semantic version parsing to distinguish older servers from version mismatches.
- Compare stable clients against older releases, including prereleases. Compare prerelease clients by precedence within the same release line and report other release lines as different.
- Report differing versions for local clients (`0.0.0` or versions with build metadata). Ignore malformed versions.
- Show the corresponding `older than` or `different from` notice and `<` or `≠` overview indicator, while preserving `/daemon` guidance and the `show_server_version_notice` setting.

## Testing

Add version-policy tests and notice snapshots covering prerelease ordering, local builds, mismatches, and malformed versions. Add daemon tests for notice suppression and manual update guidance without scheduling an update.

GitOrigin-RevId: 29df4713a7dbcbddcf30d56995a8496e7e8d0c6a
2026-09-19 17:03:26 +00:00

184 lines
6.2 KiB
Rust

#[cfg(any(not(debug_assertions), test))]
pub(crate) fn is_newer(latest: &str, current: &str) -> Option<bool> {
match (parse_version(latest), parse_version(current)) {
(Some(l), Some(c)) => Some(l > c),
_ => None,
}
}
#[cfg(any(not(debug_assertions), test))]
pub(crate) fn extract_version_from_latest_tag(latest_tag_name: &str) -> anyhow::Result<String> {
latest_tag_name
.strip_prefix("rust-v")
.map(str::to_owned)
.ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))
}
#[cfg(any(not(debug_assertions), test))]
pub(crate) fn is_source_build_version(version: &str) -> bool {
parse_version(version) == Some((0, 0, 0))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ServerVersionNoticeKind {
Older,
Different,
}
/// Stable clients compare release precedence. Prerelease clients only order versions
/// within the same release line; local builds and other release lines compare identity.
pub(crate) fn server_version_notice_kind(
client: &str,
server: &str,
) -> Option<ServerVersionNoticeKind> {
let client = semver::Version::parse(client).ok()?;
let server = semver::Version::parse(server).ok()?;
let client_release = (client.major, client.minor, client.patch);
let server_release = (server.major, server.minor, server.patch);
let client_is_local = client_release == (0, 0, 0) || !client.build.is_empty();
if client_is_local || (!client.pre.is_empty() && client_release != server_release) {
return (client != server).then_some(ServerVersionNoticeKind::Different);
}
(server.build.is_empty() && server_release != (0, 0, 0) && client > server)
.then_some(ServerVersionNoticeKind::Older)
}
#[cfg(any(not(debug_assertions), test))]
fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
let mut iter = v.trim().split('.');
let maj = iter.next()?.parse::<u64>().ok()?;
let min = iter.next()?.parse::<u64>().ok()?;
let pat = iter.next()?.parse::<u64>().ok()?;
Some((maj, min, pat))
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn extracts_version_from_latest_tag() {
assert_eq!(
extract_version_from_latest_tag("rust-v1.5.0").expect("failed to parse version"),
"1.5.0"
);
}
#[test]
fn latest_tag_without_prefix_is_invalid() {
assert!(extract_version_from_latest_tag("v1.5.0").is_err());
}
#[test]
fn prerelease_version_is_not_considered_newer() {
assert_eq!(is_newer("0.11.0-beta.1", "0.11.0"), None);
assert_eq!(is_newer("1.0.0-rc.1", "1.0.0"), None);
}
#[test]
fn plain_semver_comparisons_work() {
assert_eq!(is_newer("0.11.1", "0.11.0"), Some(true));
assert_eq!(is_newer("0.11.0", "0.11.1"), Some(false));
assert_eq!(is_newer("1.0.0", "0.9.9"), Some(true));
assert_eq!(is_newer("0.9.9", "1.0.0"), Some(false));
}
#[test]
fn source_build_version_is_not_checked() {
assert!(is_source_build_version("0.0.0"));
assert!(!is_source_build_version("0.1.0"));
}
#[test]
fn whitespace_is_ignored() {
assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3)));
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
}
#[test]
fn stable_clients_only_warn_for_older_releases() {
for (client, server, expected) in [
("0.152.1", "0.152.0", Some(ServerVersionNoticeKind::Older)),
("0.153.0", "0.152.1", Some(ServerVersionNoticeKind::Older)),
(
"0.153.0",
"0.153.0-alpha.10.1",
Some(ServerVersionNoticeKind::Older),
),
(
"0.156.0",
"0.155.0-alpha.12",
Some(ServerVersionNoticeKind::Older),
),
("0.153.0", "0.153.0", None),
("0.153.0", "0.154.0", None),
("0.153.0", "0.154.0-alpha.1", None),
("0.153.0", "0.0.0", None),
("0.153.0", "0.0.0-alpha.1", None),
("0.153.0", "0.152.0+dev", None),
] {
assert_eq!(server_version_notice_kind(client, server), expected);
}
}
#[test]
fn prerelease_clients_compare_within_the_same_release_line() {
for (newer, older) in [
("0.155.0-alpha.23", "0.155.0-alpha.22"),
("0.155.0-alpha.24", "0.155.0-alpha.23"),
("0.153.0-alpha.10", "0.153.0-alpha.9"),
("0.153.0-alpha.10.1", "0.153.0-alpha.9.2"),
("0.153.0-alpha.10.10", "0.153.0-alpha.10.9"),
("0.153.0-alpha.10.1", "0.153.0-alpha.10"),
("0.155.0", "0.155.0-alpha.23"),
] {
assert_eq!(
server_version_notice_kind(newer, older),
Some(ServerVersionNoticeKind::Older)
);
assert_eq!(server_version_notice_kind(older, newer), None);
assert_eq!(server_version_notice_kind(newer, newer), None);
}
}
#[test]
fn prerelease_clients_on_other_release_lines_and_local_clients_warn_for_mismatches() {
for (client, server) in [
("0.155.0-alpha.23", "0.156.0"),
("0.155.0-alpha.12", "0.154.0"),
("0.0.0", "0.153.0"),
("0.0.0", "0.153.0-alpha.10"),
("0.153.0+dev", "0.153.0"),
("0.153.0-alpha.10", "0.0.0"),
] {
assert_eq!(
server_version_notice_kind(client, server),
Some(ServerVersionNoticeKind::Different)
);
assert_eq!(server_version_notice_kind(client, client), None);
}
}
#[test]
fn unknown_or_malformed_versions_do_not_produce_notices() {
for version in [
"unknown",
"dev",
"0.0.0.0",
"0.153",
"0.153.0.1",
" 0.153.0",
"+0.153.0",
"0.0153.0",
"0.153.0-alpha.01",
"0.153.0-alpha..1",
] {
for release in ["0.153.0", "0.153.0-alpha.10", "0.0.0"] {
assert_eq!(server_version_notice_kind(version, release), None);
assert_eq!(server_version_notice_kind(release, version), None);
}
}
}
}