-
Notifications
You must be signed in to change notification settings - Fork 4.1k
check for updates #1764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
check for updates #1764
Changes from all commits
acd18c7
8888485
d835396
50dc992
0fdafe5
a5b986c
3e9e688
089e0c8
9f0c8b2
e014398
6e37494
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,11 @@ mod text_formatting; | |
mod tui; | ||
mod user_approval_widget; | ||
|
||
#[cfg(not(debug_assertions))] | ||
mod updates; | ||
#[cfg(not(debug_assertions))] | ||
use color_eyre::owo_colors::OwoColorize; | ||
|
||
pub use cli::Cli; | ||
|
||
pub async fn run_main( | ||
|
@@ -139,6 +144,38 @@ pub async fn run_main( | |
.with(tui_layer) | ||
.try_init(); | ||
|
||
#[allow(clippy::print_stderr)] | ||
#[cfg(not(debug_assertions))] | ||
if let Some(latest_version) = updates::get_upgrade_version(&config) { | ||
let current_version = env!("CARGO_PKG_VERSION"); | ||
let exe = std::env::current_exe()?; | ||
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some(); | ||
|
||
eprintln!( | ||
"{} {current_version} -> {latest_version}.", | ||
"✨⬆️ Update available!".bold().cyan() | ||
); | ||
|
||
if managed_by_npm { | ||
let npm_cmd = "npm install -g @openai/codex@latest"; | ||
eprintln!("Run {} to update.", npm_cmd.cyan().on_black()); | ||
} else if cfg!(target_os = "macos") | ||
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local")) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't we drop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If someone didn't install it through the recommended installation paths I think okay if they have to figure out how to update? |
||
{ | ||
let brew_cmd = "brew upgrade codex"; | ||
eprintln!("Run {} to update.", brew_cmd.cyan().on_black()); | ||
} else { | ||
eprintln!( | ||
"See {} for the latest releases and installation options.", | ||
"https://github.com/openai/codex/releases/latest" | ||
.cyan() | ||
.on_black() | ||
); | ||
} | ||
|
||
eprintln!(""); | ||
} | ||
|
||
let show_login_screen = should_show_login_screen(&config); | ||
if show_login_screen { | ||
std::io::stdout() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
#![cfg(any(not(debug_assertions), test))] | ||
|
||
use chrono::DateTime; | ||
use chrono::Duration; | ||
use chrono::Utc; | ||
use serde::Deserialize; | ||
use serde::Serialize; | ||
use std::path::Path; | ||
use std::path::PathBuf; | ||
|
||
use codex_core::config::Config; | ||
|
||
pub fn get_upgrade_version(config: &Config) -> Option<String> { | ||
let version_file = version_filepath(config); | ||
let info = read_version_info(&version_file).ok(); | ||
|
||
if match &info { | ||
None => true, | ||
Some(info) => info.last_checked_at < Utc::now() - Duration::hours(20), | ||
} { | ||
// Refresh the cached latest version in the background so TUI startup | ||
// isn’t blocked by a network call. The UI reads the previously cached | ||
// value (if any) for this run; the next run shows the banner if needed. | ||
tokio::spawn(async move { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we spawn this, but nothing waits for it? I'm a bit confused what the expectation here is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems okayish - check for update on startup, then you get a banner the next time you open it. That way we don't wait for the network call in order to start. |
||
check_for_update(&version_file) | ||
.await | ||
.inspect_err(|e| tracing::error!("Failed to update version: {e}")) | ||
}); | ||
} | ||
|
||
info.and_then(|info| { | ||
let current_version = env!("CARGO_PKG_VERSION"); | ||
if is_newer(&info.latest_version, current_version).unwrap_or(false) { | ||
Some(info.latest_version) | ||
} else { | ||
None | ||
} | ||
}) | ||
} | ||
|
||
#[derive(Serialize, Deserialize, Debug, Clone)] | ||
struct VersionInfo { | ||
latest_version: String, | ||
// ISO-8601 timestamp (RFC3339) | ||
last_checked_at: DateTime<Utc>, | ||
} | ||
|
||
#[derive(Deserialize, Debug, Clone)] | ||
struct ReleaseInfo { | ||
tag_name: String, | ||
} | ||
|
||
const VERSION_FILENAME: &str = "version.json"; | ||
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest"; | ||
|
||
fn version_filepath(config: &Config) -> PathBuf { | ||
config.codex_home.join(VERSION_FILENAME) | ||
} | ||
|
||
fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> { | ||
let contents = std::fs::read_to_string(version_file)?; | ||
Ok(serde_json::from_str(&contents)?) | ||
} | ||
|
||
async fn check_for_update(version_file: &Path) -> anyhow::Result<()> { | ||
let ReleaseInfo { | ||
tag_name: latest_tag_name, | ||
} = reqwest::Client::new() | ||
.get(LATEST_RELEASE_URL) | ||
.header( | ||
"User-Agent", | ||
format!( | ||
"codex/{} (+https://github.com/openai/codex)", | ||
env!("CARGO_PKG_VERSION") | ||
), | ||
) | ||
.send() | ||
.await? | ||
.error_for_status()? | ||
.json::<ReleaseInfo>() | ||
.await?; | ||
|
||
let info = VersionInfo { | ||
latest_version: latest_tag_name | ||
.strip_prefix("rust-v") | ||
.ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))? | ||
.into(), | ||
last_checked_at: Utc::now(), | ||
}; | ||
|
||
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(()) | ||
} | ||
|
||
fn is_newer(latest: &str, current: &str) -> Option<bool> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please add some unit tests so we can be sure that something like |
||
match (parse_version(latest), parse_version(current)) { | ||
(Some(l), Some(c)) => Some(l > c), | ||
_ => None, | ||
} | ||
} | ||
|
||
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::*; | ||
|
||
#[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 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)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Low pri, but I would consider moving this to
updates.rs
so you can have a smaller thing attached to#[cfg(not(debug_assertions))]
.