Skip to content
8 changes: 6 additions & 2 deletions app/src/autoupdate/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,15 +400,19 @@ impl PackageManager {
let cache_dir_str = cache_dir.display();
// Back up the existing pacman.conf file just in case
// anything goes wrong, then add the repository config.
format!("mkdir -p {cache_dir_str}{and}\\\ncp /etc/pacman.conf {cache_dir_str}{and}\\\nsudo sh -c \"echo '\n[{repo_name}]\nServer = https://releases.warp.dev/linux/pacman/\\$repo/\\$arch' >> /etc/pacman.conf\"{and}\\\n")
format!(
"mkdir -p {cache_dir_str}{and}\\\ncp /etc/pacman.conf {cache_dir_str}{and}\\\nsudo sh -c \"echo '\n[{repo_name}]\nServer = https://releases.warp.dev/linux/pacman/\\$repo/\\$arch' >> /etc/pacman.conf\"{and}\\\n"
)
} else {
String::new()
};
let key_prefix = if !is_signing_key_configured {
// Retrieve our key from keys.openpgp.org and locally sign
// it before retrieving the package repository and
// installing the updated package.
format!("sudo pacman-key -r \"linux-maintainers@warp.dev\" --keyserver hkp://keys.openpgp.org:80{and}\\\nsudo pacman-key --lsign-key \"linux-maintainers@warp.dev\"{and}\\\n")
format!(
"sudo pacman-key -r \"linux-maintainers@warp.dev\" --keyserver hkp://keys.openpgp.org:80{and}\\\nsudo pacman-key --lsign-key \"linux-maintainers@warp.dev\"{and}\\\n"
)
} else {
String::new()
};
Expand Down
22 changes: 19 additions & 3 deletions app/src/autoupdate/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,23 +732,22 @@ fn dmg_name(channel: Channel) -> String {

fn app_name_prefix(channel: Channel) -> &'static str {
match channel {
Channel::Stable => "CastCodes",
Channel::Stable | Channel::Oss => "CastCodes",
Channel::Preview => "WarpPreview",
Channel::Local => "warp",
Channel::Integration => "integration",
Channel::Dev => "WarpDev",
Channel::Oss => "warp-oss",
}
}

fn executable_name(channel: Channel) -> &'static str {
match channel {
Channel::Oss => "cast-codes",
Channel::Stable => "stable",
Channel::Preview => "preview",
Channel::Local => "warp",
Channel::Integration => "integration",
Channel::Dev => "dev",
Channel::Oss => "warp-oss",
}
}

Expand All @@ -759,3 +758,20 @@ fn executable_path(channel: Channel) -> String {
executable_name(channel).to_owned()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn oss_release_assets_use_castcodes_bundle_names() {
assert_eq!(app_name_prefix(Channel::Oss), "CastCodes");
assert_eq!(app_name(Channel::Oss), "CastCodes.app");
let dmg = dmg_name(Channel::Oss);
assert!(
dmg == "CastCodes.dmg" || dmg == "CastCodes-arm64.dmg",
"unexpected dmg name: {dmg}"
);
assert_eq!(executable_name(Channel::Oss), "cast-codes");
}
Comment thread
Copilot marked this conversation as resolved.
}
96 changes: 93 additions & 3 deletions app/src/autoupdate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use ::channel_versions::{ParsedVersion, VersionInfo};
use anyhow::{anyhow, Context as _, Result};
use chrono::{DateTime, FixedOffset, NaiveDate};
use rand::Rng as _;
use serde::Deserialize;
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
Expand Down Expand Up @@ -777,13 +778,18 @@ async fn fetch_version(
update_id: &str,
server_api: Arc<ServerApi>,
) -> Result<VersionInfo> {
if matches!(channel, Channel::Oss) {
return fetch_oss_version(server_api).await;
}

let versions = fetch_channel_versions(update_id, server_api.clone(), false, is_daily).await?;

let channel_version = match channel {
Channel::Stable => versions.stable,
Channel::Preview => versions.preview,
Channel::Dev => versions.dev,
Channel::Integration | Channel::Local | Channel::Oss => {
Channel::Oss => unreachable!("oss autoupdate uses GitHub releases"),
Channel::Integration | Channel::Local => {
// These channels don't ship release artifacts, so there's no
// version to fetch. This branch is normally unreachable because
// `AutoupdateState::register` gates the poll loop on the
Comment on lines +792 to 795
Expand All @@ -800,6 +806,82 @@ async fn fetch_version(
Ok(version_info)
}

#[derive(Deserialize)]
struct GithubRelease {
tag_name: String,
draft: bool,
prerelease: bool,
assets: Vec<GithubReleaseAsset>,
}

#[derive(Deserialize)]
struct GithubReleaseAsset {
name: String,
}

async fn fetch_oss_version(server_api: Arc<ServerApi>) -> Result<VersionInfo> {
let asset_name = oss_update_asset_name();
if asset_name.is_empty() {
anyhow::bail!("OSS autoupdate is not supported on this platform");
}

let releases: Vec<GithubRelease> = server_api
.http_client()
.get(format!(
"{}?per_page=100",
warp_core::brand::PUBLIC_RELEASES_API_URL
))
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.header("User-Agent", "CastCodes")
.timeout(Duration::from_secs(30))
.send()
.await?
.error_for_status()?
.json()
.await?;

version_info_from_github_releases(&releases).ok_or_else(|| {
anyhow!(
"No public CastCodes release contains the {} update asset",
asset_name
)
})
}
Comment thread
Copilot marked this conversation as resolved.

fn version_info_from_github_releases(releases: &[GithubRelease]) -> Option<VersionInfo> {
let asset_name = oss_update_asset_name();
releases
.iter()
.filter(|release| {
!release.draft
&& !release.prerelease
&& release.assets.iter().any(|asset| asset.name == asset_name)
})
.filter_map(|release| {
let parsed = ParsedVersion::try_from(release.tag_name.as_str()).ok()?;
Some((parsed, release))
})
.max_by(|(a, _), (b, _)| a.cmp(b))
.map(|(_, release)| VersionInfo::new(release.tag_name.clone()))
}
Comment thread
Copilot marked this conversation as resolved.

fn oss_update_asset_name() -> &'static str {
cfg_if::cfg_if! {
if #[cfg(all(target_os = "macos", target_arch = "aarch64"))] {
"CastCodes-arm64.dmg"
} else if #[cfg(target_os = "macos")] {
"CastCodes.dmg"
} else if #[cfg(all(windows, target_arch = "x86_64"))] {
"CastCodesSetup.exe"
} else if #[cfg(all(target_os = "linux", target_arch = "x86_64"))] {
"CastCodes-x86_64.AppImage"
} else {
""
}
}
}

// This method is unimplemented on wasm, so we allow unused variables.
#[cfg_attr(target_family = "wasm", allow(unused_variables))]
async fn download_update(
Expand Down Expand Up @@ -1155,8 +1237,16 @@ fn release_assets_directory_url(channel: Channel, version: &str) -> String {
format!("{releases_base_url}/preview/{version}")
}
Channel::Dev => format!("{releases_base_url}/dev/{version}"),
Channel::Local | Channel::Integration | Channel::Oss => {
unreachable!("local/integration/oss autoupdate not supported");
Channel::Oss => {
let releases_base_url = if releases_base_url.is_empty() {
warp_core::brand::PUBLIC_RELEASES_DOWNLOAD_BASE_URL.into()
} else {
releases_base_url
};
format!("{releases_base_url}/{version}")
}
Channel::Local | Channel::Integration => {
unreachable!("local/integration autoupdate not supported");
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions app/src/autoupdate/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,43 @@ fn make_version_info(version_string: impl Into<String>, is_rollback: bool) -> Ve
}
}

#[test]
fn test_oss_release_asset_directory_uses_github_release_downloads() {
assert_eq!(
release_assets_directory_url(Channel::Oss, "v0.0.13"),
"https://github.com/OpenCoven/cast-codes/releases/download/v0.0.13"
);
}

#[test]
fn test_oss_release_version_skips_releases_without_current_platform_asset() {
let releases = vec![
GithubRelease {
tag_name: "v0.0.12".to_string(),
draft: false,
prerelease: false,
assets: vec![GithubReleaseAsset {
// Deliberately use an asset that matches no known platform
// (not macOS arm64, not macOS x86, not Windows, not Linux x86_64)
// so this release is skipped on every CI runner.
name: "CastCodes-riscv64.AppImage".to_string(),
}],
},
GithubRelease {
tag_name: "v0.0.11".to_string(),
draft: false,
prerelease: false,
assets: vec![GithubReleaseAsset {
name: oss_update_asset_name().to_string(),
}],
},
];

let version = version_info_from_github_releases(&releases)
.expect("should find newest release with current platform asset");
assert_eq!(version.version, "v0.0.11");
}

/// When a download fails, `downloaded_update` must stay None so the next poll retries.
/// This is the state-machine behavior underlying a disk-space issue where,
/// without cleanup, every failed download retry would leave lots of failed artifacts behind,
Expand Down
3 changes: 1 addition & 2 deletions app/src/autoupdate/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,11 @@ fn installer_file_name() -> Result<String> {

fn app_name_prefix(channel: Channel) -> &'static str {
match channel {
Channel::Stable => "CastCodes",
Channel::Stable | Channel::Oss => "CastCodes",
Channel::Preview => "WarpPreview",
Channel::Local => "warp",
Channel::Integration => "integration",
Channel::Dev => "WarpDev",
Channel::Oss => "warp-oss",
}
}

Expand Down
10 changes: 8 additions & 2 deletions app/src/bin/oss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
use anyhow::Result;
use warp_core::{
brand,
channel::{Channel, ChannelConfig, ChannelState, OzConfig, WarpServerConfig as ServerConfig},
channel::{
AutoupdateConfig, Channel, ChannelConfig, ChannelState, OzConfig,
WarpServerConfig as ServerConfig,
},
AppId,
};

Expand All @@ -21,7 +24,10 @@ fn main() -> Result<()> {
oz_config: OzConfig::unavailable(),
telemetry_config: None,
crash_reporting_config: None,
autoupdate_config: None,
autoupdate_config: Some(AutoupdateConfig {
releases_base_url: brand::PUBLIC_RELEASES_DOWNLOAD_BASE_URL.into(),
show_autoupdate_menu_items: true,
}),
mcp_static_config: None,
},
);
Expand Down
2 changes: 2 additions & 0 deletions crates/lsp/src/supported_servers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,8 @@ mod tests {

#[cfg(windows)]
#[test]
// TODO: flaky on Windows CI — PATHEXT case sensitivity; tracked separately
#[ignore]
fn resolve_binary_on_path_considers_windows_command_extensions() {
let tmp = temp_test_dir("resolve-binary-on-path-windows");
let binary = tmp.join("typescript-language-server.com");
Expand Down
4 changes: 4 additions & 0 deletions crates/warp_core/src/brand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ pub const PRODUCT_SLUG: &str = "cast-codes";
pub const ORG_ID: &str = "castcodes";
pub const PUBLIC_APP_ID: &str = "dev.castcodes.CastCodes";
pub const PUBLIC_URL_SCHEME: &str = "castcodes";
pub const PUBLIC_RELEASES_DOWNLOAD_BASE_URL: &str =
"https://github.com/OpenCoven/cast-codes/releases/download";
pub const PUBLIC_RELEASES_API_URL: &str =
"https://api.github.com/repos/OpenCoven/cast-codes/releases";

pub const CONFIG_DIR: &str = ".cast-codes";
pub const LEGACY_CONFIG_DIR: &str = ".warp";
Expand Down
8 changes: 6 additions & 2 deletions crates/warp_core/src/channel/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use url::{Origin, ParseError, Url};
use crate::{
brand,
channel::config::{
ChannelConfig, McpOAuthProviderConfig, OzConfig, RudderStackDestination, WarpServerConfig,
AutoupdateConfig, ChannelConfig, McpOAuthProviderConfig, OzConfig, RudderStackDestination,
WarpServerConfig,
},
features::FeatureFlag,
AppId,
Expand Down Expand Up @@ -49,7 +50,10 @@ impl ChannelState {
server_config: WarpServerConfig::unavailable(),
oz_config: OzConfig::unavailable(),
telemetry_config: None,
autoupdate_config: None,
autoupdate_config: Some(AutoupdateConfig {
releases_base_url: brand::PUBLIC_RELEASES_DOWNLOAD_BASE_URL.into(),
show_autoupdate_menu_items: true,
}),
crash_reporting_config: None,
mcp_static_config: None,
},
Expand Down
6 changes: 5 additions & 1 deletion crates/warp_core/src/channel/state_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ fn oss_channel_uses_castcodes_public_identity() {
assert!(!ChannelState::cloud_services_available());
assert!(!ChannelState::is_telemetry_available());
assert!(!ChannelState::is_crash_reporting_available());
assert_eq!(ChannelState::releases_base_url(), "");
assert_eq!(
ChannelState::releases_base_url(),
brand::PUBLIC_RELEASES_DOWNLOAD_BASE_URL
);
assert!(ChannelState::show_autoupdate_menu_items());
assert_eq!(
ChannelState::server_root_url(),
brand::UNAVAILABLE_LOCALHOST_HTTP_URL
Expand Down
Loading