From 096e5e76a2aea174ec7e30e14adaa4963840fb37 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 12 Jun 2026 09:26:08 -0700 Subject: [PATCH] Persist update dismissal without cache (#27783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Choosing “Don’t remind me” can silently fail when `version.json` disappears before dismissal because `dismiss_version` returns success without writing anything. The same update can then reappear on the next launch. Initialize a minimal `VersionInfo` from the selected version when the cache cannot be read, then persist the dismissal through the existing write path. Fixes #27147 --- codex-rs/tui/src/lib.rs | 2 + codex-rs/tui/src/updates.rs | 44 +++------------------ codex-rs/tui/src/updates_cache.rs | 52 +++++++++++++++++++++++++ codex-rs/tui/src/updates_cache_tests.rs | 29 ++++++++++++++ 4 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 codex-rs/tui/src/updates_cache.rs create mode 100644 codex-rs/tui/src/updates_cache_tests.rs diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 8c07dfa58..4ede235a0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -207,6 +207,8 @@ mod update_prompt; #[cfg(any(not(debug_assertions), test))] mod update_versions; mod updates; +#[cfg(any(not(debug_assertions), test))] +mod updates_cache; mod version; #[cfg(not(target_os = "linux"))] mod voice; diff --git a/codex-rs/tui/src/updates.rs b/codex-rs/tui/src/updates.rs index e99852ead..65a231756 100644 --- a/codex-rs/tui/src/updates.rs +++ b/codex-rs/tui/src/updates.rs @@ -8,17 +8,19 @@ use crate::update_action::UpdateAction; use crate::update_versions::extract_version_from_latest_tag; use crate::update_versions::is_newer; use crate::update_versions::is_source_build_version; -use chrono::DateTime; +use crate::updates_cache::VersionInfo; +use crate::updates_cache::read_version_info; +use crate::updates_cache::version_filepath; use chrono::Duration; use chrono::Utc; use codex_login::default_client::create_client; use serde::Deserialize; -use serde::Serialize; use std::path::Path; -use std::path::PathBuf; use crate::version::CODEX_CLI_VERSION; +pub(crate) use crate::updates_cache::dismiss_version; + pub fn get_upgrade_version(config: &Config) -> Option { if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) { return None; @@ -51,16 +53,6 @@ pub fn get_upgrade_version(config: &Config) -> Option { }) } -#[derive(Serialize, Deserialize, Debug, Clone)] -struct VersionInfo { - latest_version: String, - // ISO-8601 timestamp (RFC3339) - last_checked_at: DateTime, - #[serde(default)] - dismissed_version: Option, -} - -const VERSION_FILENAME: &str = "version.json"; // We use the latest version from the cask if installation is via homebrew - homebrew does not immediately pick up the latest release and can lag behind. const HOMEBREW_CASK_API_URL: &str = "https://formulae.brew.sh/api/cask/codex.json"; const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest"; @@ -75,15 +67,6 @@ struct HomebrewCaskInfo { version: String, } -fn version_filepath(config: &Config) -> PathBuf { - config.codex_home.join(VERSION_FILENAME).into_path_buf() -} - -fn read_version_info(version_file: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(version_file)?; - Ok(serde_json::from_str(&contents)?) -} - async fn check_for_update(version_file: &Path, action: Option) -> anyhow::Result<()> { let latest_version = match action { Some(UpdateAction::BrewUpgrade) => { @@ -159,20 +142,3 @@ pub fn get_upgrade_version_for_popup(config: &Config) -> Option { } Some(latest) } - -/// Persist a dismissal for the current latest version so we don't show -/// the update popup again for this version. -pub async fn dismiss_version(config: &Config, version: &str) -> anyhow::Result<()> { - let version_file = version_filepath(config); - let mut info = match read_version_info(&version_file) { - Ok(info) => info, - Err(_) => return Ok(()), - }; - info.dismissed_version = Some(version.to_string()); - let json_line = format!("{}\n", serde_json::to_string(&info)?); - if let Some(parent) = version_file.parent() { - tokio::fs::create_dir_all(parent).await?; - } - tokio::fs::write(version_file, json_line).await?; - Ok(()) -} diff --git a/codex-rs/tui/src/updates_cache.rs b/codex-rs/tui/src/updates_cache.rs new file mode 100644 index 000000000..8ac0776fb --- /dev/null +++ b/codex-rs/tui/src/updates_cache.rs @@ -0,0 +1,52 @@ +use crate::legacy_core::config::Config; +use chrono::DateTime; +use chrono::Utc; +use serde::Deserialize; +use serde::Serialize; +use std::path::Path; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct VersionInfo { + pub(crate) latest_version: String, + // ISO-8601 timestamp (RFC3339) + pub(crate) last_checked_at: DateTime, + #[serde(default)] + pub(crate) dismissed_version: Option, +} + +const VERSION_FILENAME: &str = "version.json"; + +pub(crate) fn version_filepath(config: &Config) -> PathBuf { + config.codex_home.join(VERSION_FILENAME).into_path_buf() +} + +pub(crate) fn read_version_info(version_file: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(version_file)?; + Ok(serde_json::from_str(&contents)?) +} + +/// Persist a dismissal for the current latest version so we don't show +/// the update popup again for this version. +pub(crate) async fn dismiss_version(config: &Config, version: &str) -> anyhow::Result<()> { + let version_file = version_filepath(config); + let mut info = match read_version_info(&version_file) { + Ok(info) => info, + Err(_) => VersionInfo { + latest_version: version.to_string(), + last_checked_at: DateTime::::UNIX_EPOCH, + dismissed_version: None, + }, + }; + info.dismissed_version = Some(version.to_string()); + let json_line = format!("{}\n", serde_json::to_string(&info)?); + if let Some(parent) = version_file.parent() { + tokio::fs::create_dir_all(parent).await?; + } + tokio::fs::write(version_file, json_line).await?; + Ok(()) +} + +#[cfg(test)] +#[path = "updates_cache_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/updates_cache_tests.rs b/codex-rs/tui/src/updates_cache_tests.rs new file mode 100644 index 000000000..b10c3f9ea --- /dev/null +++ b/codex-rs/tui/src/updates_cache_tests.rs @@ -0,0 +1,29 @@ +use super::*; +use crate::legacy_core::config::ConfigBuilder; +use pretty_assertions::assert_eq; +use tempfile::tempdir; + +#[tokio::test] +async fn dismiss_version_creates_cache_file_when_missing() { + let codex_home = tempdir().expect("temp codex home"); + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("load config"); + let version_file = version_filepath(&config); + + dismiss_version(&config, "999.0.0") + .await + .expect("dismiss version"); + + let info = read_version_info(&version_file).expect("read version info"); + assert_eq!(info.last_checked_at, DateTime::::UNIX_EPOCH); + assert_eq!( + ( + info.latest_version.as_str(), + info.dismissed_version.as_deref() + ), + ("999.0.0", Some("999.0.0")) + ); +}