Skip to content

Commit 788badd

Browse files
authored
fix: update brew auto update version check (#6238)
### Summary * Use `https://github.com/Homebrew/homebrew-cask/blob/main/Casks/c/codex.rb` to get the latest available version for brew usage.
1 parent fbdedd9 commit 788badd

File tree

7 files changed

+188
-90
lines changed

7 files changed

+188
-90
lines changed

codex-rs/cli/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use codex_exec::Cli as ExecCli;
2121
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
2222
use codex_tui::AppExitInfo;
2323
use codex_tui::Cli as TuiCli;
24-
use codex_tui::updates::UpdateAction;
24+
use codex_tui::update_action::UpdateAction;
2525
use owo_colors::OwoColorize;
2626
use std::path::PathBuf;
2727
use supports_color::Stream;

codex-rs/tui/src/app.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::render::renderable::Renderable;
1313
use crate::resume_picker::ResumeSelection;
1414
use crate::tui;
1515
use crate::tui::TuiEvent;
16-
use crate::updates::UpdateAction;
16+
use crate::update_action::UpdateAction;
1717
use codex_ansi_escape::ansi_escape_line;
1818
use codex_core::AuthManager;
1919
use codex_core::ConversationManager;
@@ -203,7 +203,7 @@ impl App {
203203
tui,
204204
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
205205
latest_version,
206-
crate::updates::get_update_action(),
206+
crate::update_action::get_update_action(),
207207
))),
208208
)
209209
.await?;

codex-rs/tui/src/history_cell.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::style::user_message_style;
1616
use crate::text_formatting::format_and_truncate_tool_result;
1717
use crate::text_formatting::truncate_text;
1818
use crate::ui_consts::LIVE_PREFIX_COLS;
19-
use crate::updates::UpdateAction;
19+
use crate::update_action::UpdateAction;
2020
use crate::version::CODEX_CLI_VERSION;
2121
use crate::wrapping::RtOptions;
2222
use crate::wrapping::word_wrap_line;

codex-rs/tui/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ mod terminal_palette;
7171
mod text_formatting;
7272
mod tui;
7373
mod ui_consts;
74+
pub mod update_action;
7475
mod update_prompt;
75-
pub mod updates;
76+
mod updates;
7677
mod version;
7778

7879
mod wrapping;

codex-rs/tui/src/update_action.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/// Update action the CLI should perform after the TUI exits.
2+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3+
pub enum UpdateAction {
4+
/// Update via `npm install -g @openai/codex@latest`.
5+
NpmGlobalLatest,
6+
/// Update via `bun install -g @openai/codex@latest`.
7+
BunGlobalLatest,
8+
/// Update via `brew upgrade codex`.
9+
BrewUpgrade,
10+
}
11+
12+
impl UpdateAction {
13+
/// Returns the list of command-line arguments for invoking the update.
14+
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
15+
match self {
16+
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex"]),
17+
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex"]),
18+
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "codex"]),
19+
}
20+
}
21+
22+
/// Returns string representation of the command-line arguments for invoking the update.
23+
pub fn command_str(self) -> String {
24+
let (command, args) = self.command_args();
25+
shlex::try_join(std::iter::once(command).chain(args.iter().copied()))
26+
.unwrap_or_else(|_| format!("{command} {}", args.join(" ")))
27+
}
28+
}
29+
30+
#[cfg(not(debug_assertions))]
31+
pub(crate) fn get_update_action() -> Option<UpdateAction> {
32+
let exe = std::env::current_exe().unwrap_or_default();
33+
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
34+
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
35+
36+
detect_update_action(
37+
cfg!(target_os = "macos"),
38+
&exe,
39+
managed_by_npm,
40+
managed_by_bun,
41+
)
42+
}
43+
44+
#[cfg(any(not(debug_assertions), test))]
45+
fn detect_update_action(
46+
is_macos: bool,
47+
current_exe: &std::path::Path,
48+
managed_by_npm: bool,
49+
managed_by_bun: bool,
50+
) -> Option<UpdateAction> {
51+
if managed_by_npm {
52+
Some(UpdateAction::NpmGlobalLatest)
53+
} else if managed_by_bun {
54+
Some(UpdateAction::BunGlobalLatest)
55+
} else if is_macos
56+
&& (current_exe.starts_with("/opt/homebrew") || current_exe.starts_with("/usr/local"))
57+
{
58+
Some(UpdateAction::BrewUpgrade)
59+
} else {
60+
None
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod tests {
66+
use super::*;
67+
68+
#[test]
69+
fn detects_update_action_without_env_mutation() {
70+
assert_eq!(
71+
detect_update_action(false, std::path::Path::new("/any/path"), false, false),
72+
None
73+
);
74+
assert_eq!(
75+
detect_update_action(false, std::path::Path::new("/any/path"), true, false),
76+
Some(UpdateAction::NpmGlobalLatest)
77+
);
78+
assert_eq!(
79+
detect_update_action(false, std::path::Path::new("/any/path"), false, true),
80+
Some(UpdateAction::BunGlobalLatest)
81+
);
82+
assert_eq!(
83+
detect_update_action(
84+
true,
85+
std::path::Path::new("/opt/homebrew/bin/codex"),
86+
false,
87+
false
88+
),
89+
Some(UpdateAction::BrewUpgrade)
90+
);
91+
assert_eq!(
92+
detect_update_action(
93+
true,
94+
std::path::Path::new("/usr/local/bin/codex"),
95+
false,
96+
false
97+
),
98+
Some(UpdateAction::BrewUpgrade)
99+
);
100+
}
101+
}

codex-rs/tui/src/update_prompt.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use crate::selection_list::selection_option_row;
1010
use crate::tui::FrameRequester;
1111
use crate::tui::Tui;
1212
use crate::tui::TuiEvent;
13+
use crate::update_action::UpdateAction;
1314
use crate::updates;
14-
use crate::updates::UpdateAction;
1515
use codex_core::config::Config;
1616
use color_eyre::Result;
1717
use crossterm::event::KeyCode;
@@ -39,7 +39,7 @@ pub(crate) async fn run_update_prompt_if_needed(
3939
let Some(latest_version) = updates::get_upgrade_version_for_popup(config) else {
4040
return Ok(UpdatePromptOutcome::Continue);
4141
};
42-
let Some(update_action) = crate::updates::get_update_action() else {
42+
let Some(update_action) = crate::update_action::get_update_action() else {
4343
return Ok(UpdatePromptOutcome::Continue);
4444
};
4545

codex-rs/tui/src/updates.rs

Lines changed: 79 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
#![cfg(not(debug_assertions))]
2+
3+
use crate::update_action;
4+
use crate::update_action::UpdateAction;
15
use chrono::DateTime;
26
use chrono::Duration;
37
use chrono::Utc;
8+
use codex_core::config::Config;
9+
use codex_core::default_client::create_client;
410
use serde::Deserialize;
511
use serde::Serialize;
612
use std::path::Path;
713
use std::path::PathBuf;
814

9-
use codex_core::config::Config;
10-
use codex_core::default_client::create_client;
11-
1215
use crate::version::CODEX_CLI_VERSION;
1316

1417
pub fn get_upgrade_version(config: &Config) -> Option<String> {
@@ -47,14 +50,17 @@ struct VersionInfo {
4750
dismissed_version: Option<String>,
4851
}
4952

53+
const VERSION_FILENAME: &str = "version.json";
54+
// 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.
55+
const HOMEBREW_CASK_URL: &str =
56+
"https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/c/codex.rb";
57+
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";
58+
5059
#[derive(Deserialize, Debug, Clone)]
5160
struct ReleaseInfo {
5261
tag_name: String,
5362
}
5463

55-
const VERSION_FILENAME: &str = "version.json";
56-
const LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest";
57-
5864
fn version_filepath(config: &Config) -> PathBuf {
5965
config.codex_home.join(VERSION_FILENAME)
6066
}
@@ -65,23 +71,35 @@ fn read_version_info(version_file: &Path) -> anyhow::Result<VersionInfo> {
6571
}
6672

6773
async fn check_for_update(version_file: &Path) -> anyhow::Result<()> {
68-
let ReleaseInfo {
69-
tag_name: latest_tag_name,
70-
} = create_client()
71-
.get(LATEST_RELEASE_URL)
72-
.send()
73-
.await?
74-
.error_for_status()?
75-
.json::<ReleaseInfo>()
76-
.await?;
74+
let latest_version = match update_action::get_update_action() {
75+
Some(UpdateAction::BrewUpgrade) => {
76+
let cask_contents = create_client()
77+
.get(HOMEBREW_CASK_URL)
78+
.send()
79+
.await?
80+
.error_for_status()?
81+
.text()
82+
.await?;
83+
extract_version_from_cask(&cask_contents)?
84+
}
85+
_ => {
86+
let ReleaseInfo {
87+
tag_name: latest_tag_name,
88+
} = create_client()
89+
.get(LATEST_RELEASE_URL)
90+
.send()
91+
.await?
92+
.error_for_status()?
93+
.json::<ReleaseInfo>()
94+
.await?;
95+
extract_version_from_latest_tag(&latest_tag_name)?
96+
}
97+
};
7798

7899
// Preserve any previously dismissed version if present.
79100
let prev_info = read_version_info(version_file).ok();
80101
let info = VersionInfo {
81-
latest_version: latest_tag_name
82-
.strip_prefix("rust-v")
83-
.ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))?
84-
.into(),
102+
latest_version,
85103
last_checked_at: Utc::now(),
86104
dismissed_version: prev_info.and_then(|p| p.dismissed_version),
87105
};
@@ -101,6 +119,25 @@ fn is_newer(latest: &str, current: &str) -> Option<bool> {
101119
}
102120
}
103121

122+
fn extract_version_from_cask(cask_contents: &str) -> anyhow::Result<String> {
123+
cask_contents
124+
.lines()
125+
.find_map(|line| {
126+
let line = line.trim();
127+
line.strip_prefix("version \"")
128+
.and_then(|rest| rest.strip_suffix('"'))
129+
.map(ToString::to_string)
130+
})
131+
.ok_or_else(|| anyhow::anyhow!("Failed to find version in Homebrew cask file"))
132+
}
133+
134+
fn extract_version_from_latest_tag(latest_tag_name: &str) -> anyhow::Result<String> {
135+
latest_tag_name
136+
.strip_prefix("rust-v")
137+
.map(str::to_owned)
138+
.ok_or_else(|| anyhow::anyhow!("Failed to parse latest tag name '{latest_tag_name}'"))
139+
}
140+
104141
/// Returns the latest version to show in a popup, if it should be shown.
105142
/// This respects the user's dismissal choice for the current latest version.
106143
pub fn get_upgrade_version_for_popup(config: &Config) -> Option<String> {
@@ -140,56 +177,35 @@ fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
140177
Some((maj, min, pat))
141178
}
142179

143-
/// Update action the CLI should perform after the TUI exits.
144-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145-
pub enum UpdateAction {
146-
/// Update via `npm install -g @openai/codex@latest`.
147-
NpmGlobalLatest,
148-
/// Update via `bun install -g @openai/codex@latest`.
149-
BunGlobalLatest,
150-
/// Update via `brew upgrade codex`.
151-
BrewUpgrade,
152-
}
180+
#[cfg(test)]
181+
mod tests {
182+
use super::*;
153183

154-
#[cfg(any(not(debug_assertions), test))]
155-
pub(crate) fn get_update_action() -> Option<UpdateAction> {
156-
let exe = std::env::current_exe().unwrap_or_default();
157-
let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
158-
let managed_by_bun = std::env::var_os("CODEX_MANAGED_BY_BUN").is_some();
159-
if managed_by_npm {
160-
Some(UpdateAction::NpmGlobalLatest)
161-
} else if managed_by_bun {
162-
Some(UpdateAction::BunGlobalLatest)
163-
} else if cfg!(target_os = "macos")
164-
&& (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
165-
{
166-
Some(UpdateAction::BrewUpgrade)
167-
} else {
168-
None
184+
#[test]
185+
fn parses_version_from_cask_contents() {
186+
let cask = r#"
187+
cask "codex" do
188+
version "0.55.0"
189+
end
190+
"#;
191+
assert_eq!(
192+
extract_version_from_cask(cask).expect("failed to parse version"),
193+
"0.55.0"
194+
);
169195
}
170-
}
171196

172-
impl UpdateAction {
173-
/// Returns the list of command-line arguments for invoking the update.
174-
pub fn command_args(self) -> (&'static str, &'static [&'static str]) {
175-
match self {
176-
UpdateAction::NpmGlobalLatest => ("npm", &["install", "-g", "@openai/codex@latest"]),
177-
UpdateAction::BunGlobalLatest => ("bun", &["install", "-g", "@openai/codex@latest"]),
178-
UpdateAction::BrewUpgrade => ("brew", &["upgrade", "--cask", "codex"]),
179-
}
197+
#[test]
198+
fn extracts_version_from_latest_tag() {
199+
assert_eq!(
200+
extract_version_from_latest_tag("rust-v1.5.0").expect("failed to parse version"),
201+
"1.5.0"
202+
);
180203
}
181204

182-
/// Returns string representation of the command-line arguments for invoking the update.
183-
pub fn command_str(self) -> String {
184-
let (command, args) = self.command_args();
185-
let args_str = args.join(" ");
186-
format!("{command} {args_str}")
205+
#[test]
206+
fn latest_tag_without_prefix_is_invalid() {
207+
assert!(extract_version_from_latest_tag("v1.5.0").is_err());
187208
}
188-
}
189-
190-
#[cfg(test)]
191-
mod tests {
192-
use super::*;
193209

194210
#[test]
195211
fn prerelease_version_is_not_considered_newer() {
@@ -210,24 +226,4 @@ mod tests {
210226
assert_eq!(parse_version(" 1.2.3 \n"), Some((1, 2, 3)));
211227
assert_eq!(is_newer(" 1.2.3 ", "1.2.2"), Some(true));
212228
}
213-
214-
#[test]
215-
fn test_get_update_action() {
216-
let prev = std::env::var_os("CODEX_MANAGED_BY_NPM");
217-
218-
// First: no npm var -> expect None (we do not run from brew in CI)
219-
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
220-
assert_eq!(get_update_action(), None);
221-
222-
// Then: with npm var -> expect NpmGlobalLatest
223-
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", "1") };
224-
assert_eq!(get_update_action(), Some(UpdateAction::NpmGlobalLatest));
225-
226-
// Restore prior value to avoid leaking state
227-
if let Some(v) = prev {
228-
unsafe { std::env::set_var("CODEX_MANAGED_BY_NPM", v) };
229-
} else {
230-
unsafe { std::env::remove_var("CODEX_MANAGED_BY_NPM") };
231-
}
232-
}
233229
}

0 commit comments

Comments
 (0)