Detect new releases of codex-acp (#39388)

Now we use GitHub Releases to detect when there's a new version of
codex-acp out, and we notify the user in the same way we do for the
other external agents.

This also moves `github_download.rs` out of the `languages` crate and
into `http_client`, because now we're not just using it for language
servers anymore, we're also using it for external agents.

Release Notes:

- N/A

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Richard Feldman
2025-10-03 12:10:40 +02:00
committed by GitHub
co-authored by Lukas Wirth
parent 3ab5103de1
commit 662ec9977f
12 changed files with 69 additions and 83 deletions
+33 -49
View File
@@ -16,6 +16,7 @@ use futures::StreamExt as _;
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
};
use http_client::github::AssetKind;
use node_runtime::NodeRuntime;
use remote::RemoteClient;
use rpc::{AnyProtoClient, TypedEnvelope, proto};
@@ -1022,7 +1023,7 @@ impl ExternalAgentServer for LocalCodex {
// Find or install the latest Codex release (no update checks for now).
let http = cx.update(|cx| Client::global(cx).http_client())?;
let release = ::http_client::github::latest_github_release(
"zed-industries/codex-acp",
CODEX_ACP_REPO,
true,
false,
http.clone(),
@@ -1032,42 +1033,27 @@ impl ExternalAgentServer for LocalCodex {
let version_dir = dir.join(&release.tag_name);
if !fs.is_dir(&version_dir).await {
// Assemble release download URL from prefix, tag, and filename based on target triple.
// If unsupported, silently skip download.
let tag = release.tag_name.clone(); // e.g. "v0.1.0"
let tag = release.tag_name.clone();
let version_number = tag.trim_start_matches('v');
if let Some(asset_url) = codex_release_url(version_number) {
let http = http.clone();
let mut response = http
.get(&asset_url, Default::default(), true)
.await
.with_context(|| {
format!("downloading Codex binary from {}", asset_url)
})?;
anyhow::ensure!(
response.status().is_success(),
"failed to download Codex release: {}",
response.status()
);
// Extract archive into the version directory.
if asset_url.ends_with(".zip") {
let reader = futures::io::BufReader::new(response.body_mut());
util::archive::extract_zip(&version_dir, reader)
.await
.context("extracting Codex binary from zip")?;
let asset_name = asset_name(version_number)
.context("codex acp is not supported for this architecture")?;
let asset = release
.assets
.into_iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
::http_client::github_download::download_server_binary(
&*http,
&asset.browser_download_url,
asset.digest.as_deref(),
&version_dir,
if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") {
AssetKind::Zip
} else {
// Decompress and extract the tar.gz into the version directory.
let reader = futures::io::BufReader::new(response.body_mut());
let decoder =
async_compression::futures::bufread::GzipDecoder::new(reader);
let archive = async_tar::Archive::new(decoder);
archive
.unpack(&version_dir)
.await
.context("extracting Codex binary from tar.gz")?;
}
}
AssetKind::TarGz
},
)
.await?;
}
let bin_name = if cfg!(windows) {
@@ -1101,11 +1087,13 @@ impl ExternalAgentServer for LocalCodex {
}
}
pub const CODEX_ACP_REPO: &str = "zed-industries/codex-acp";
/// Assemble Codex release URL for the current OS/arch and the given version number.
/// Returns None if the current target is unsupported.
/// Example output:
/// https://github.com/zed-industries/codex-acp/releases/download/v{version}/codex-acp-{version}-{arch}-{platform}.{ext}
fn codex_release_url(version: &str) -> Option<String> {
fn asset_name(version: &str) -> Option<String> {
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
} else if cfg!(target_arch = "aarch64") {
@@ -1131,11 +1119,7 @@ fn codex_release_url(version: &str) -> Option<String> {
"tar.gz"
};
let prefix = "https://github.com/zed-industries/codex-acp/releases/download";
Some(format!(
"{prefix}/v{version}/codex-acp-{version}-{arch}-{platform}.{ext}"
))
Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}"))
}
struct LocalCustomAgent {
@@ -1191,18 +1175,18 @@ mod tests {
// Additionally, it verifies that our logic for assembling URLs
// correctly resolves to a known-good URL on each of our targets.
let allowed = [
"https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-apple-darwin.tar.gz",
"https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-pc-windows-msvc.tar.gz",
"https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-aarch64-unknown-linux-gnu.tar.gz",
"https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-apple-darwin.tar.gz",
"https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-pc-windows-msvc.zip",
"https://github.com/zed-industries/codex-acp/releases/download/v0.1.0/codex-acp-0.1.0-x86_64-unknown-linux-gnu.tar.gz",
"codex-acp-0.1.0-aarch64-apple-darwin.tar.gz",
"codex-acp-0.1.0-aarch64-pc-windows-msvc.tar.gz",
"codex-acp-0.1.0-aarch64-unknown-linux-gnu.tar.gz",
"codex-acp-0.1.0-x86_64-apple-darwin.tar.gz",
"codex-acp-0.1.0-x86_64-pc-windows-msvc.zip",
"codex-acp-0.1.0-x86_64-unknown-linux-gnu.tar.gz",
];
if let Some(url) = super::codex_release_url(version_number) {
if let Some(url) = super::asset_name(version_number) {
assert!(
allowed.contains(&url.as_str()),
"Assembled URL {} not in allowed list",
"Assembled asset name {} not in allowed list",
url
);
} else {