From 28aa254b50e69e6611ab221f75e40b86b5fffd0b Mon Sep 17 00:00:00 2001 From: Brandon Kiser Date: Thu, 6 Nov 2025 17:11:06 -0800 Subject: [PATCH 1/3] feat: add update conditions to index.json --- crates/fig_desktop/src/install.rs | 2 +- crates/fig_desktop/src/tray.rs | 3 +- crates/fig_desktop/src/update.rs | 1 - crates/fig_desktop_api/src/requests/update.rs | 3 +- crates/fig_install/src/index.rs | 306 +++++++++++++----- crates/fig_install/src/lib.rs | 8 +- crates/fig_install/test_files/test-index.json | 6 +- crates/figterm/src/update.rs | 2 +- crates/q_cli/src/cli/debug/mod.rs | 24 +- crates/q_cli/src/cli/update.rs | 6 +- 10 files changed, 244 insertions(+), 117 deletions(-) diff --git a/crates/fig_desktop/src/install.rs b/crates/fig_desktop/src/install.rs index 06497479f..a7dd40d81 100644 --- a/crates/fig_desktop/src/install.rs +++ b/crates/fig_desktop/src/install.rs @@ -115,7 +115,7 @@ pub async fn run_install(ctx: Arc, ignore_immediate_update: bool) { use tokio::time::timeout; // Check for updates but timeout after 3 seconds to avoid making the user wait too long // todo: don't download the index file twice - match timeout(Duration::from_secs(3), check_for_updates(true, true)).await { + match timeout(Duration::from_secs(3), check_for_updates(true)).await { Ok(Ok(Some(_))) => { crate::update::check_for_update(true, true).await; }, diff --git a/crates/fig_desktop/src/tray.rs b/crates/fig_desktop/src/tray.rs index a25fae463..c9c3b9636 100644 --- a/crates/fig_desktop/src/tray.rs +++ b/crates/fig_desktop/src/tray.rs @@ -97,7 +97,6 @@ fn tray_update(proxy: &EventLoopProxy) { ignore_rollout: true, interactive: true, relaunch_dashboard: true, - is_auto_update: false, }, ) .await; @@ -138,7 +137,7 @@ fn tray_update(proxy: &EventLoopProxy) { /// /// Returns `true` if we should continue with updating, `false` otherwise. async fn should_continue_with_update(ctx: &Context, proxy: &EventLoopProxy) -> bool { - match fig_install::check_for_updates(true, false).await { + match fig_install::check_for_updates(true).await { Ok(Some(pkg)) => { let file_type = get_file_type(ctx, &Variant::Full) .await diff --git a/crates/fig_desktop/src/update.rs b/crates/fig_desktop/src/update.rs index ebeed07a9..68d47d5e3 100644 --- a/crates/fig_desktop/src/update.rs +++ b/crates/fig_desktop/src/update.rs @@ -101,7 +101,6 @@ pub async fn check_for_update(show_webview: bool, relaunch_dashboard: bool) -> b ignore_rollout: false, interactive: show_webview, relaunch_dashboard, - is_auto_update: true, }) .await { diff --git a/crates/fig_desktop_api/src/requests/update.rs b/crates/fig_desktop_api/src/requests/update.rs index be2b82db5..622585df2 100644 --- a/crates/fig_desktop_api/src/requests/update.rs +++ b/crates/fig_desktop_api/src/requests/update.rs @@ -20,14 +20,13 @@ pub async fn update_application(request: UpdateApplicationRequest) -> RequestRes ignore_rollout: request.ignore_rollout.unwrap_or(true), interactive: request.interactive.unwrap_or(true), relaunch_dashboard: request.relaunch_dashboard.unwrap_or(true), - is_auto_update: false, }, )); RequestResult::success() } pub async fn check_for_updates(_request: CheckForUpdatesRequest) -> RequestResult { - fig_install::check_for_updates(true, false) + fig_install::check_for_updates(true) .await .map(|res| { Box::new(ServerOriginatedSubMessage::CheckForUpdatesResponse( diff --git a/crates/fig_install/src/index.rs b/crates/fig_install/src/index.rs index 1e862c652..ea1555190 100644 --- a/crates/fig_install/src/index.rs +++ b/crates/fig_install/src/index.rs @@ -23,7 +23,9 @@ use fig_util::system_info::get_system_id; use semver::Version; use serde::{ Deserialize, + Deserializer, Serialize, + Serializer, }; use strum::{ Display, @@ -102,16 +104,15 @@ impl Index { /// given target and variant without filtering on file type, e.g. in the case of Linux desktop /// bundles. #[allow(clippy::too_many_arguments)] - pub fn find_next_version( - &self, - target_triple: &TargetTriple, - variant: &Variant, - file_type: Option<&FileType>, - current_version: &str, - ignore_rollout: bool, - is_auto_update: bool, - threshold_override: Option, - ) -> Result, Error> { + pub fn find_next_version(&self, args: FindNextVersionArgs<'_>) -> Result, Error> { + let target_triple = args.target_triple; + let variant = args.variant; + let file_type = args.file_type; + let current_version = args.current_version; + let product_name = args.product_name; + let ignore_rollout = args.ignore_rollout; + let threshold_override = args.threshold_override; + if !self.supported.iter().any(|support| { support.target_triple.as_ref() == Some(target_triple) && support.variant == *variant @@ -139,7 +140,12 @@ impl Index { Some(rollout) => rollout.start <= right_now, None => true, }) - .filter(|version| !is_auto_update || !version.disable_autoupdate) + .filter(|version| { + version.update_conditions.is_empty() + || version.update_conditions.iter().all(|cond| match cond { + UpdateCondition::AllowedProductNames(product_names) => product_names.contains(product_name), + }) + }) .collect::>(); valid_versions.sort_unstable_by(|lhs, rhs| lhs.version.cmp(&rhs.version)); @@ -239,6 +245,17 @@ impl Index { } } +#[derive(Debug)] +pub struct FindNextVersionArgs<'a> { + pub target_triple: &'a TargetTriple, + pub variant: &'a Variant, + pub file_type: Option<&'a FileType>, + pub current_version: &'a str, + pub product_name: &'a ProductName, + pub ignore_rollout: bool, + pub threshold_override: Option, +} + #[allow(unused)] #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -256,12 +273,59 @@ struct Support { } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub(crate) struct RemoteVersion { pub version: Version, pub rollout: Option, pub packages: Vec, #[serde(default)] - pub disable_autoupdate: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub update_conditions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum UpdateCondition { + AllowedProductNames(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumString, Display)] +pub enum ProductName { + #[strum(serialize = "Amazon Q")] + AmazonQ, + #[strum(default)] + Unknown(String), +} + +impl Serialize for ProductName { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ProductName::AmazonQ => serializer.serialize_str("Amazon Q"), + ProductName::Unknown(s) => serializer.serialize_str(s), + } + } +} + +impl<'de> Deserialize<'de> for ProductName { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "Amazon Q" => Ok(ProductName::AmazonQ), + _ => Ok(ProductName::Unknown(s)), + } + } +} + +impl Default for ProductName { + fn default() -> Self { + Self::AmazonQ + } } #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -365,18 +429,18 @@ pub async fn check_for_updates( variant: &Variant, file_type: Option<&FileType>, ignore_rollout: bool, - is_auto_update: bool, ) -> Result, Error> { const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); - pull(&channel).await?.find_next_version( + let product_name = ProductName::default(); + pull(&channel).await?.find_next_version(FindNextVersionArgs { target_triple, variant, file_type, - CURRENT_VERSION, + current_version: CURRENT_VERSION, + product_name: &product_name, ignore_rollout, - is_auto_update, - None, - ) + threshold_override: None, + }) } pub async fn get_file_type(ctx: &Context, variant: &Variant) -> Result { @@ -401,6 +465,7 @@ mod tests { use fig_util::{ OLD_CLI_BINARY_NAMES, OLD_PRODUCT_NAME, + PRODUCT_NAME, }; use super::*; @@ -408,10 +473,18 @@ mod tests { macro_rules! test_ser_deser { ($ty:ident, $variant:expr, $text:expr) => { let quoted = format!("\"{}\"", $text); - assert_eq!(quoted, serde_json::to_string(&$variant).unwrap()); - assert_eq!($variant, serde_json::from_str("ed).unwrap()); - assert_eq!($variant, $ty::from_str($text).unwrap()); - assert_eq!($text, $variant.to_string()); + assert_eq!( + quoted, + serde_json::to_string(&$variant).unwrap(), + "serde to_string failed" + ); + assert_eq!( + $variant, + serde_json::from_str("ed).unwrap(), + "serde from_str failed" + ); + assert_eq!($variant, $ty::from_str($text).unwrap(), "from_str failed"); + assert_eq!($text, $variant.to_string(), "to_string failed"); }; } @@ -422,6 +495,20 @@ mod tests { test_ser_deser!(PackageArchitecture, PackageArchitecture::Universal, "universal"); } + #[test] + fn test_product_name_ser_deser() { + test_ser_deser!(ProductName, ProductName::AmazonQ, "Amazon Q"); + test_ser_deser!(ProductName, ProductName::Unknown("other".to_string()), "other"); + } + + #[test] + fn test_product_name() { + assert_eq!( + serde_json::to_string(&ProductName::default()).unwrap(), + format!("\"{}\"", PRODUCT_NAME) + ); + } + #[tokio::test] #[cfg(target_os = "macos")] async fn pull_test() { @@ -441,7 +528,6 @@ mod tests { &Variant::Full, Some(FileType::Dmg).as_ref(), false, - false, ) .await .unwrap(); @@ -574,11 +660,6 @@ mod tests { assert_eq!(index.versions.len(), 4); - assert!( - !index.versions[1].disable_autoupdate, - "missing disable_autoupdate field should default to false" - ); - // check the 1.0.0 entry matches assert_eq!(index.versions[2], RemoteVersion { version: Version::new(1, 0, 0), @@ -607,7 +688,7 @@ mod tests { cli_path: None, } ], - disable_autoupdate: true, + update_conditions: vec![], }); } @@ -617,16 +698,17 @@ mod tests { #[test] fn index_latest_version_does_not_upgrade() { + let product_name = ProductName::default(); let next = load_test_index() - .find_next_version( - &TargetTriple::AArch64UnknownLinuxMusl, - &Variant::Minimal, - Some(&FileType::TarZst), - "1.2.1", - true, - false, - None, - ) + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::AArch64UnknownLinuxMusl, + variant: &Variant::Minimal, + file_type: Some(&FileType::TarZst), + current_version: "1.2.1", + product_name: &product_name, + ignore_rollout: true, + threshold_override: None, + }) .unwrap(); assert!(next.is_none()); } @@ -634,15 +716,15 @@ mod tests { #[test] fn index_outdated_version_upgrades_to_correct_version() { let next = load_test_index() - .find_next_version( - &TargetTriple::AArch64UnknownLinuxMusl, - &Variant::Minimal, - Some(&FileType::TarZst), - "1.2.0", - true, - false, - None, - ) + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::AArch64UnknownLinuxMusl, + variant: &Variant::Minimal, + file_type: Some(&FileType::TarZst), + current_version: "1.2.0", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + threshold_override: None, + }) .unwrap() .expect("Should have UpdatePackage"); assert_eq!(next.version.to_string(), "1.2.1".to_owned()); @@ -651,71 +733,121 @@ mod tests { #[test] fn index_missing_support_returns_error() { - let next = load_test_index().find_next_version( - &TargetTriple::AArch64UnknownLinuxMusl, - &Variant::Full, - Some(&FileType::TarZst), - "1.2.1", - true, - false, - None, - ); + let next = load_test_index().find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::AArch64UnknownLinuxMusl, + variant: &Variant::Full, + file_type: Some(&FileType::TarZst), + current_version: "1.2.1", + product_name: &ProductName::AmazonQ, + ignore_rollout: true, + threshold_override: None, + }); assert!(next.is_err()); } #[test] fn index_with_optional_filetype_returns_highest_version() { let next = load_test_index() - .find_next_version( - &TargetTriple::X86_64UnknownLinuxGnu, - &Variant::Full, - None, - "1.0.5", - true, - false, - None, - ) + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + threshold_override: None, + }) .unwrap() .expect("should have update package"); assert_eq!(next.version.to_string().as_str(), "1.2.1"); } #[test] - fn index_autoupdate_does_not_update_into_disabled() { + fn index_test_allowed_product_names_update() { let mut index = load_test_index(); let next = index - .find_next_version( - &TargetTriple::X86_64UnknownLinuxGnu, - &Variant::Full, - None, - "1.0.5", - true, - true, - None, - ) + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::AmazonQ, + ignore_rollout: true, + threshold_override: None, + }) .unwrap() .expect("should have update package"); - assert_eq!(next.version.to_string().as_str(), "1.2.0"); + assert_eq!( + next.version.to_string().as_str(), + "1.2.0", + "amazon q should update into 1.2.0" + ); + + let next = index + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + threshold_override: None, + }) + .unwrap() + .expect("should have update package"); + assert_eq!( + next.version.to_string().as_str(), + "1.2.1", + "qv2 should update into 1.2.1" + ); - // Push a newer update that does not have autoupdate disabled + // Push a newer update that allows Amazon Q let mut last = index.versions.last().cloned().unwrap(); + last.update_conditions = vec![UpdateCondition::AllowedProductNames(vec![ProductName::AmazonQ])]; last.version = Version::from_str("2.0.0").unwrap(); - last.disable_autoupdate = false; index.versions.push(last); let next = index - .find_next_version( - &TargetTriple::X86_64UnknownLinuxGnu, - &Variant::Full, - None, - "1.0.5", - true, - true, - None, - ) + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::AmazonQ, + ignore_rollout: true, + threshold_override: None, + }) .unwrap() .expect("should have update package"); assert_eq!(next.version.to_string().as_str(), "2.0.0"); } + + #[test] + fn index_test_empty_allowed_product_names_does_not_update() { + let mut index = load_test_index(); + + let mut last = index.versions.last().cloned().unwrap(); + last.update_conditions = vec![UpdateCondition::AllowedProductNames(vec![])]; + last.version = Version::from_str("2.0.0").unwrap(); + index.versions.push(last); + + let next = index + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + threshold_override: None, + }) + .unwrap() + .expect("should have update package"); + assert_eq!( + next.version.to_string().as_str(), + "1.2.1", + "qv2 should update into 1.2.1" + ); + } } diff --git a/crates/fig_install/src/lib.rs b/crates/fig_install/src/lib.rs index 09bc68cb2..95d5abab2 100644 --- a/crates/fig_install/src/lib.rs +++ b/crates/fig_install/src/lib.rs @@ -144,7 +144,7 @@ pub fn get_max_channel() -> Channel { .unwrap() } -pub async fn check_for_updates(ignore_rollout: bool, is_auto_update: bool) -> Result, Error> { +pub async fn check_for_updates(ignore_rollout: bool) -> Result, Error> { let manifest = manifest(); let ctx = Context::new(); let file_type = match (&manifest.variant, ctx.platform().os()) { @@ -157,7 +157,6 @@ pub async fn check_for_updates(ignore_rollout: bool, is_auto_update: bool) -> Re &manifest.variant, file_type.as_ref(), ignore_rollout, - is_auto_update, ) .await } @@ -178,8 +177,6 @@ pub struct UpdateOptions { pub interactive: bool, /// If to relaunch into dashboard after update (false will launch in background) pub relaunch_dashboard: bool, - /// Whether or not the update is being invoked automatically without the user's approval - pub is_auto_update: bool, } /// Attempt to update if there is a newer version of Fig @@ -190,11 +187,10 @@ pub async fn update( ignore_rollout, interactive, relaunch_dashboard, - is_auto_update, }: UpdateOptions, ) -> Result { info!("Checking for updates..."); - if let Some(update) = check_for_updates(ignore_rollout, is_auto_update).await? { + if let Some(update) = check_for_updates(ignore_rollout).await? { info!("Found update: {}", update.version); debug!("Update info: {:?}", update); diff --git a/crates/fig_install/test_files/test-index.json b/crates/fig_install/test_files/test-index.json index 72d1871e1..c9cc24c18 100644 --- a/crates/fig_install/test_files/test-index.json +++ b/crates/fig_install/test_files/test-index.json @@ -487,7 +487,11 @@ { "version": "1.2.1", - "disable_autoupdate": true, + "updateConditions": [ + { + "allowedProductNames": ["qv2"] + } + ], "packages": [ { "kind": "deb", diff --git a/crates/figterm/src/update.rs b/crates/figterm/src/update.rs index 2a03afcb1..043413ccc 100644 --- a/crates/figterm/src/update.rs +++ b/crates/figterm/src/update.rs @@ -55,7 +55,7 @@ pub fn check_for_update(context: &Context) { } tokio::spawn(async { - match fig_install::check_for_updates(false, true).await { + match fig_install::check_for_updates(false).await { Ok(Some(pkg)) => { if let Err(err) = fig_settings::state::set_value(UPDATE_AVAILABLE_KEY, pkg.version.to_string()) { warn!(?err, "Error setting {UPDATE_AVAILABLE_KEY}: {err}"); diff --git a/crates/q_cli/src/cli/debug/mod.rs b/crates/q_cli/src/cli/debug/mod.rs index 4a6acdefc..67a2ba968 100644 --- a/crates/q_cli/src/cli/debug/mod.rs +++ b/crates/q_cli/src/cli/debug/mod.rs @@ -218,8 +218,6 @@ pub enum DebugSubcommand { #[arg(short = 'r', long)] enable_rollout: bool, #[arg(short, long)] - is_auto_update: bool, - #[arg(short, long)] override_threshold: Option, #[arg(short, long)] file_type: String, @@ -750,27 +748,31 @@ impl DebugSubcommand { variant, version: current_version, enable_rollout, - is_auto_update, override_threshold, file_type, } => { + use fig_install::index::{ + FindNextVersionArgs, + ProductName, + }; use fig_util::manifest::{ Channel, TargetTriple, Variant, }; + let product_name = ProductName::default(); let result = fig_install::index::pull(&Channel::from_str(channel)?) .await? - .find_next_version( - &TargetTriple::from_str(target_triple)?, - &Variant::from_str(variant)?, - Some(&FileType::from_str(file_type)?), + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::from_str(target_triple)?, + variant: &Variant::from_str(variant)?, + file_type: Some(&FileType::from_str(file_type)?), current_version, - !enable_rollout, - *is_auto_update, - *override_threshold, - ); + product_name: &product_name, + ignore_rollout: !enable_rollout, + threshold_override: *override_threshold, + }); println!("{result:#?}"); }, diff --git a/crates/q_cli/src/cli/update.rs b/crates/q_cli/src/cli/update.rs index 43ce6742b..86f669e0f 100644 --- a/crates/q_cli/src/cli/update.rs +++ b/crates/q_cli/src/cli/update.rs @@ -102,7 +102,6 @@ impl UpdateArgs { ignore_rollout: !rollout, interactive: !non_interactive, relaunch_dashboard: *relaunch_dashboard, - is_auto_update: false, }, ) .await; @@ -130,10 +129,7 @@ impl UpdateArgs { } async fn try_linux_update() -> Result { - match ( - fig_install::check_for_updates(true, false).await, - bundle_metadata().await, - ) { + match (fig_install::check_for_updates(true).await, bundle_metadata().await) { (ref update_result @ Ok(Some(ref pkg)), Some(file_type)) => { if file_type == FileType::AppImage { let should_continue = dialoguer::Select::with_theme(&dialoguer_theme()) From 6aec5826ef1dc3c15b829f4ff1c5674d0cab8f75 Mon Sep 17 00:00:00 2001 From: Brandon Kiser Date: Thu, 6 Nov 2025 19:11:44 -0800 Subject: [PATCH 2/3] update to auto update only --- crates/fig_desktop/src/install.rs | 2 +- crates/fig_desktop/src/tray.rs | 3 +- crates/fig_desktop/src/update.rs | 1 + crates/fig_desktop_api/src/requests/update.rs | 3 +- crates/fig_install/src/index.rs | 52 +++++++++++++++---- crates/fig_install/src/lib.rs | 8 ++- crates/fig_install/test_files/test-index.json | 2 +- crates/figterm/src/update.rs | 2 +- crates/q_cli/src/cli/debug/mod.rs | 4 ++ crates/q_cli/src/cli/update.rs | 6 ++- 10 files changed, 66 insertions(+), 17 deletions(-) diff --git a/crates/fig_desktop/src/install.rs b/crates/fig_desktop/src/install.rs index a7dd40d81..06497479f 100644 --- a/crates/fig_desktop/src/install.rs +++ b/crates/fig_desktop/src/install.rs @@ -115,7 +115,7 @@ pub async fn run_install(ctx: Arc, ignore_immediate_update: bool) { use tokio::time::timeout; // Check for updates but timeout after 3 seconds to avoid making the user wait too long // todo: don't download the index file twice - match timeout(Duration::from_secs(3), check_for_updates(true)).await { + match timeout(Duration::from_secs(3), check_for_updates(true, true)).await { Ok(Ok(Some(_))) => { crate::update::check_for_update(true, true).await; }, diff --git a/crates/fig_desktop/src/tray.rs b/crates/fig_desktop/src/tray.rs index c9c3b9636..a25fae463 100644 --- a/crates/fig_desktop/src/tray.rs +++ b/crates/fig_desktop/src/tray.rs @@ -97,6 +97,7 @@ fn tray_update(proxy: &EventLoopProxy) { ignore_rollout: true, interactive: true, relaunch_dashboard: true, + is_auto_update: false, }, ) .await; @@ -137,7 +138,7 @@ fn tray_update(proxy: &EventLoopProxy) { /// /// Returns `true` if we should continue with updating, `false` otherwise. async fn should_continue_with_update(ctx: &Context, proxy: &EventLoopProxy) -> bool { - match fig_install::check_for_updates(true).await { + match fig_install::check_for_updates(true, false).await { Ok(Some(pkg)) => { let file_type = get_file_type(ctx, &Variant::Full) .await diff --git a/crates/fig_desktop/src/update.rs b/crates/fig_desktop/src/update.rs index 68d47d5e3..ebeed07a9 100644 --- a/crates/fig_desktop/src/update.rs +++ b/crates/fig_desktop/src/update.rs @@ -101,6 +101,7 @@ pub async fn check_for_update(show_webview: bool, relaunch_dashboard: bool) -> b ignore_rollout: false, interactive: show_webview, relaunch_dashboard, + is_auto_update: true, }) .await { diff --git a/crates/fig_desktop_api/src/requests/update.rs b/crates/fig_desktop_api/src/requests/update.rs index 622585df2..be2b82db5 100644 --- a/crates/fig_desktop_api/src/requests/update.rs +++ b/crates/fig_desktop_api/src/requests/update.rs @@ -20,13 +20,14 @@ pub async fn update_application(request: UpdateApplicationRequest) -> RequestRes ignore_rollout: request.ignore_rollout.unwrap_or(true), interactive: request.interactive.unwrap_or(true), relaunch_dashboard: request.relaunch_dashboard.unwrap_or(true), + is_auto_update: false, }, )); RequestResult::success() } pub async fn check_for_updates(_request: CheckForUpdatesRequest) -> RequestResult { - fig_install::check_for_updates(true) + fig_install::check_for_updates(true, false) .await .map(|res| { Box::new(ServerOriginatedSubMessage::CheckForUpdatesResponse( diff --git a/crates/fig_install/src/index.rs b/crates/fig_install/src/index.rs index ea1555190..d50b48db5 100644 --- a/crates/fig_install/src/index.rs +++ b/crates/fig_install/src/index.rs @@ -111,6 +111,7 @@ impl Index { let current_version = args.current_version; let product_name = args.product_name; let ignore_rollout = args.ignore_rollout; + let is_auto_update = args.is_auto_update; let threshold_override = args.threshold_override; if !self.supported.iter().any(|support| { @@ -143,7 +144,9 @@ impl Index { .filter(|version| { version.update_conditions.is_empty() || version.update_conditions.iter().all(|cond| match cond { - UpdateCondition::AllowedProductNames(product_names) => product_names.contains(product_name), + UpdateCondition::AllowedAutoUpdateProductNames(product_names) => { + !is_auto_update || product_names.contains(product_name) + }, }) }) .collect::>(); @@ -252,6 +255,7 @@ pub struct FindNextVersionArgs<'a> { pub file_type: Option<&'a FileType>, pub current_version: &'a str, pub product_name: &'a ProductName, + pub is_auto_update: bool, pub ignore_rollout: bool, pub threshold_override: Option, } @@ -286,7 +290,7 @@ pub(crate) struct RemoteVersion { #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum UpdateCondition { - AllowedProductNames(Vec), + AllowedAutoUpdateProductNames(Vec), } #[derive(Debug, Clone, PartialEq, Eq, EnumString, Display)] @@ -429,6 +433,7 @@ pub async fn check_for_updates( variant: &Variant, file_type: Option<&FileType>, ignore_rollout: bool, + is_auto_update: bool, ) -> Result, Error> { const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); let product_name = ProductName::default(); @@ -439,6 +444,7 @@ pub async fn check_for_updates( current_version: CURRENT_VERSION, product_name: &product_name, ignore_rollout, + is_auto_update, threshold_override: None, }) } @@ -528,6 +534,7 @@ mod tests { &Variant::Full, Some(FileType::Dmg).as_ref(), false, + false, ) .await .unwrap(); @@ -707,6 +714,7 @@ mod tests { current_version: "1.2.1", product_name: &product_name, ignore_rollout: true, + is_auto_update: false, threshold_override: None, }) .unwrap(); @@ -723,6 +731,7 @@ mod tests { current_version: "1.2.0", product_name: &ProductName::Unknown("qv2".to_string()), ignore_rollout: true, + is_auto_update: false, threshold_override: None, }) .unwrap() @@ -740,6 +749,7 @@ mod tests { current_version: "1.2.1", product_name: &ProductName::AmazonQ, ignore_rollout: true, + is_auto_update: false, threshold_override: None, }); assert!(next.is_err()); @@ -755,6 +765,7 @@ mod tests { current_version: "1.0.5", product_name: &ProductName::Unknown("qv2".to_string()), ignore_rollout: true, + is_auto_update: false, threshold_override: None, }) .unwrap() @@ -763,7 +774,7 @@ mod tests { } #[test] - fn index_test_allowed_product_names_update() { + fn index_test_allowed_autoupdate_product_names_update() { let mut index = load_test_index(); let next = index @@ -774,6 +785,25 @@ mod tests { current_version: "1.0.5", product_name: &ProductName::AmazonQ, ignore_rollout: true, + is_auto_update: false, + threshold_override: None, + }) + .unwrap() + .expect("should have update package"); + assert_eq!( + next.version.to_string().as_str(), + "1.2.1", + "amazon q during manual update should update into 1.2.1" + ); + let next = index + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::AmazonQ, + ignore_rollout: true, + is_auto_update: true, threshold_override: None, }) .unwrap() @@ -781,9 +811,8 @@ mod tests { assert_eq!( next.version.to_string().as_str(), "1.2.0", - "amazon q should update into 1.2.0" + "amazon q during auto update should update into 1.2.0" ); - let next = index .find_next_version(FindNextVersionArgs { target_triple: &TargetTriple::X86_64UnknownLinuxGnu, @@ -792,6 +821,7 @@ mod tests { current_version: "1.0.5", product_name: &ProductName::Unknown("qv2".to_string()), ignore_rollout: true, + is_auto_update: true, threshold_override: None, }) .unwrap() @@ -799,12 +829,14 @@ mod tests { assert_eq!( next.version.to_string().as_str(), "1.2.1", - "qv2 should update into 1.2.1" + "qv2 during auto update should update into 1.2.1" ); // Push a newer update that allows Amazon Q let mut last = index.versions.last().cloned().unwrap(); - last.update_conditions = vec![UpdateCondition::AllowedProductNames(vec![ProductName::AmazonQ])]; + last.update_conditions = vec![UpdateCondition::AllowedAutoUpdateProductNames(vec![ + ProductName::AmazonQ, + ])]; last.version = Version::from_str("2.0.0").unwrap(); index.versions.push(last); @@ -816,6 +848,7 @@ mod tests { current_version: "1.0.5", product_name: &ProductName::AmazonQ, ignore_rollout: true, + is_auto_update: true, threshold_override: None, }) .unwrap() @@ -824,11 +857,11 @@ mod tests { } #[test] - fn index_test_empty_allowed_product_names_does_not_update() { + fn index_test_empty_allowed_autoupdate_product_names_does_not_update() { let mut index = load_test_index(); let mut last = index.versions.last().cloned().unwrap(); - last.update_conditions = vec![UpdateCondition::AllowedProductNames(vec![])]; + last.update_conditions = vec![UpdateCondition::AllowedAutoUpdateProductNames(vec![])]; last.version = Version::from_str("2.0.0").unwrap(); index.versions.push(last); @@ -840,6 +873,7 @@ mod tests { current_version: "1.0.5", product_name: &ProductName::Unknown("qv2".to_string()), ignore_rollout: true, + is_auto_update: true, threshold_override: None, }) .unwrap() diff --git a/crates/fig_install/src/lib.rs b/crates/fig_install/src/lib.rs index 95d5abab2..09bc68cb2 100644 --- a/crates/fig_install/src/lib.rs +++ b/crates/fig_install/src/lib.rs @@ -144,7 +144,7 @@ pub fn get_max_channel() -> Channel { .unwrap() } -pub async fn check_for_updates(ignore_rollout: bool) -> Result, Error> { +pub async fn check_for_updates(ignore_rollout: bool, is_auto_update: bool) -> Result, Error> { let manifest = manifest(); let ctx = Context::new(); let file_type = match (&manifest.variant, ctx.platform().os()) { @@ -157,6 +157,7 @@ pub async fn check_for_updates(ignore_rollout: bool) -> Result Result { info!("Checking for updates..."); - if let Some(update) = check_for_updates(ignore_rollout).await? { + if let Some(update) = check_for_updates(ignore_rollout, is_auto_update).await? { info!("Found update: {}", update.version); debug!("Update info: {:?}", update); diff --git a/crates/fig_install/test_files/test-index.json b/crates/fig_install/test_files/test-index.json index c9cc24c18..c3ccb63e1 100644 --- a/crates/fig_install/test_files/test-index.json +++ b/crates/fig_install/test_files/test-index.json @@ -489,7 +489,7 @@ "version": "1.2.1", "updateConditions": [ { - "allowedProductNames": ["qv2"] + "allowedAutoUpdateProductNames": ["qv2"] } ], "packages": [ diff --git a/crates/figterm/src/update.rs b/crates/figterm/src/update.rs index 043413ccc..2a03afcb1 100644 --- a/crates/figterm/src/update.rs +++ b/crates/figterm/src/update.rs @@ -55,7 +55,7 @@ pub fn check_for_update(context: &Context) { } tokio::spawn(async { - match fig_install::check_for_updates(false).await { + match fig_install::check_for_updates(false, true).await { Ok(Some(pkg)) => { if let Err(err) = fig_settings::state::set_value(UPDATE_AVAILABLE_KEY, pkg.version.to_string()) { warn!(?err, "Error setting {UPDATE_AVAILABLE_KEY}: {err}"); diff --git a/crates/q_cli/src/cli/debug/mod.rs b/crates/q_cli/src/cli/debug/mod.rs index 67a2ba968..fdb0a28c2 100644 --- a/crates/q_cli/src/cli/debug/mod.rs +++ b/crates/q_cli/src/cli/debug/mod.rs @@ -218,6 +218,8 @@ pub enum DebugSubcommand { #[arg(short = 'r', long)] enable_rollout: bool, #[arg(short, long)] + is_auto_update: bool, + #[arg(short, long)] override_threshold: Option, #[arg(short, long)] file_type: String, @@ -749,6 +751,7 @@ impl DebugSubcommand { version: current_version, enable_rollout, override_threshold, + is_auto_update, file_type, } => { use fig_install::index::{ @@ -771,6 +774,7 @@ impl DebugSubcommand { current_version, product_name: &product_name, ignore_rollout: !enable_rollout, + is_auto_update: *is_auto_update, threshold_override: *override_threshold, }); diff --git a/crates/q_cli/src/cli/update.rs b/crates/q_cli/src/cli/update.rs index 86f669e0f..43ce6742b 100644 --- a/crates/q_cli/src/cli/update.rs +++ b/crates/q_cli/src/cli/update.rs @@ -102,6 +102,7 @@ impl UpdateArgs { ignore_rollout: !rollout, interactive: !non_interactive, relaunch_dashboard: *relaunch_dashboard, + is_auto_update: false, }, ) .await; @@ -129,7 +130,10 @@ impl UpdateArgs { } async fn try_linux_update() -> Result { - match (fig_install::check_for_updates(true).await, bundle_metadata().await) { + match ( + fig_install::check_for_updates(true, false).await, + bundle_metadata().await, + ) { (ref update_result @ Ok(Some(ref pkg)), Some(file_type)) => { if file_type == FileType::AppImage { let should_continue = dialoguer::Select::with_theme(&dialoguer_theme()) From 8461a0fb1ca0791f4b48cf8c1e13aaf314dd9b2c Mon Sep 17 00:00:00 2001 From: Brandon Kiser Date: Fri, 7 Nov 2025 09:44:07 -0800 Subject: [PATCH 3/3] clean and update tests --- crates/fig_install/src/index.rs | 84 ++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/fig_install/src/index.rs b/crates/fig_install/src/index.rs index d50b48db5..38d52f416 100644 --- a/crates/fig_install/src/index.rs +++ b/crates/fig_install/src/index.rs @@ -775,8 +775,13 @@ mod tests { #[test] fn index_test_allowed_autoupdate_product_names_update() { + // Loads an index with the versions: + // - 1.1.0 | no update conditions + // - 1.2.0 | no update conditions + // - 1.2.1 | update conditions = [AllowedAutoUpdateProductNames("qv2")] let mut index = load_test_index(); + // ProductName("Amazon Q") can update into 1.2.1 during manual update let next = index .find_next_version(FindNextVersionArgs { target_triple: &TargetTriple::X86_64UnknownLinuxGnu, @@ -795,6 +800,8 @@ mod tests { "1.2.1", "amazon q during manual update should update into 1.2.1" ); + + // ProductName("Amazon Q") updates into 1.2.0 during auto update let next = index .find_next_version(FindNextVersionArgs { target_triple: &TargetTriple::X86_64UnknownLinuxGnu, @@ -813,6 +820,8 @@ mod tests { "1.2.0", "amazon q during auto update should update into 1.2.0" ); + + // ProductName("qv2") updates into 1.2.1 during auto update let next = index .find_next_version(FindNextVersionArgs { target_triple: &TargetTriple::X86_64UnknownLinuxGnu, @@ -832,10 +841,11 @@ mod tests { "qv2 during auto update should update into 1.2.1" ); - // Push a newer update that allows Amazon Q + // Push a newer update that allows both Amazon Q and qv2 to auto update into let mut last = index.versions.last().cloned().unwrap(); last.update_conditions = vec![UpdateCondition::AllowedAutoUpdateProductNames(vec![ ProductName::AmazonQ, + ProductName::Unknown("qv2".to_string()), ])]; last.version = Version::from_str("2.0.0").unwrap(); index.versions.push(last); @@ -854,6 +864,24 @@ mod tests { .unwrap() .expect("should have update package"); assert_eq!(next.version.to_string().as_str(), "2.0.0"); + let next = index + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + is_auto_update: true, + threshold_override: None, + }) + .unwrap() + .expect("should have update package"); + assert_eq!( + next.version.to_string().as_str(), + "2.0.0", + "qv2 during auto update should update into 2.0.0" + ); } #[test] @@ -884,4 +912,58 @@ mod tests { "qv2 should update into 1.2.1" ); } + + #[test] + fn index_test_multiple_update_conditions_uses_and_boolean_logic() { + let mut index = load_test_index(); + + // - 1.2.1 | update conditions = [AllowedAutoUpdateProductNames("qv2")] + let next = index + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + is_auto_update: true, + threshold_override: None, + }) + .unwrap() + .expect("should have update package"); + assert_eq!( + next.version.to_string().as_str(), + "1.2.1", + "qv2 should update into 1.2.1" + ); + + index + .versions + .last_mut() + .unwrap() + .update_conditions + .push(UpdateCondition::AllowedAutoUpdateProductNames(vec![ + ProductName::AmazonQ, + ])); + + // qv2 can no longer update into 1.2.1 + let next = index + .find_next_version(FindNextVersionArgs { + target_triple: &TargetTriple::X86_64UnknownLinuxGnu, + variant: &Variant::Full, + file_type: None, + current_version: "1.0.5", + product_name: &ProductName::Unknown("qv2".to_string()), + ignore_rollout: true, + is_auto_update: true, + threshold_override: None, + }) + .unwrap() + .expect("should have update package"); + assert_eq!( + next.version.to_string().as_str(), + "1.2.0", + "qv2 should update into 1.2.0" + ); + } }