From 5205733ffcdca8943ca083b5367bb98d3632f146 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Wed, 3 Jun 2026 18:27:31 +0300 Subject: [PATCH 1/5] fix: backwards-compatibility for update manifest V1 --- crates/devolutions-agent-shared/src/lib.rs | 3 +- .../src/update_manifest.rs | 55 +++++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/crates/devolutions-agent-shared/src/lib.rs b/crates/devolutions-agent-shared/src/lib.rs index 23d155057..a6a687e2c 100644 --- a/crates/devolutions-agent-shared/src/lib.rs +++ b/crates/devolutions-agent-shared/src/lib.rs @@ -11,7 +11,8 @@ use camino::Utf8PathBuf; use cfg_if::cfg_if; pub use date_version::{DateVersion, DateVersionError}; pub use update_manifest::{ - InstalledProductUpdateInfo, ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, UpdateManifest, UpdateManifestV1, + InstalledProductUpdateInfo, ProductUpdateInfoV1, ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, + UpdateManifest, UpdateManifestV1, UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2, VersionSpecification, default_schedule_window_start, detect_update_manifest_major_version, }; diff --git a/crates/devolutions-agent-shared/src/update_manifest.rs b/crates/devolutions-agent-shared/src/update_manifest.rs index 1ab43dfc4..9ac529054 100644 --- a/crates/devolutions-agent-shared/src/update_manifest.rs +++ b/crates/devolutions-agent-shared/src/update_manifest.rs @@ -11,17 +11,17 @@ use crate::DateVersion; /// /// ```json /// { -/// "Gateway": { "Version": "1.2.3.4" }, -/// "HubService": { "Version": "latest" } +/// "Gateway": { "TargetVersion": "1.2.3.4" }, +/// "HubService": { "TargetVersion": "latest" } /// } /// ``` #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct UpdateManifestV1 { #[serde(skip_serializing_if = "Option::is_none")] - pub gateway: Option, + pub gateway: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub hub_service: Option, + pub hub_service: Option, } // ── Shared value types ──────────────────────────────────────────────────────── @@ -55,6 +55,28 @@ impl std::str::FromStr for VersionSpecification { } } +/// Product update info payload used by legacy V1 manifests. +/// +/// Canonical V1 field name is `TargetVersion`. +/// `Version` is accepted as an alias because of already released +/// gateway/agent with incorrect V1 manifest format, so we support both for +/// parsing but only emit the correct `TargetVersion` in serialization. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ProductUpdateInfoV1 { + /// The version of the product to update to. + #[serde(rename = "TargetVersion", alias = "Version")] + pub target_version: VersionSpecification, +} + +impl From for ProductUpdateInfo { + fn from(value: ProductUpdateInfoV1) -> Self { + Self { + target_version: value.target_version, + } + } +} + +/// Product update info payload used by V2 manifests (`Products` map). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProductUpdateInfo { /// The version of the product to update to. @@ -235,19 +257,18 @@ impl UpdateManifest { /// Normalise the manifest into a flat product map for uniform processing. /// /// - V2 `products` is used directly. - /// - V1 named fields are mapped to their [`UpdateProductKey`] equivalents. - /// - V1 `other` entries are best-effort converted; entries that do not match - /// [`ProductUpdateInfo`]'s schema are silently dropped. + /// - V1 named fields are mapped to their [`UpdateProductKey`] equivalents and + /// converted from `TargetVersion` payloads into [`ProductUpdateInfo`]. pub fn into_products(self) -> HashMap { match self { Self::ManifestV2(v2) => v2.products, Self::Legacy(v1) => { let mut map = HashMap::new(); if let Some(gw) = v1.gateway { - map.insert(UpdateProductKey::Gateway, gw); + map.insert(UpdateProductKey::Gateway, gw.into()); } if let Some(hs) = v1.hub_service { - map.insert(UpdateProductKey::HubService, hs); + map.insert(UpdateProductKey::HubService, hs.into()); } map } @@ -393,7 +414,7 @@ mod tests { #[test] fn v1_with_products_into_products() { - let json = r#"{"Gateway":{"Version":"2026.1.0"},"HubService":{"Version":"latest"}}"#; + let json = r#"{"Gateway":{"TargetVersion":"2026.1.0"},"HubService":{"TargetVersion":"latest"}}"#; let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); assert!(matches!(manifest, UpdateManifest::Legacy(_))); let products = manifest.into_products(); @@ -404,6 +425,20 @@ mod tests { )); } + #[test] + fn v1_accepted_with_version_alias() { + let json = r#"{"HubService":{"Version":"2026.2.1.7"}}"#; + let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); + assert!(matches!(manifest, UpdateManifest::Legacy(_))); + + let products = manifest.into_products(); + assert_eq!(products.len(), 1); + assert_eq!( + products[&UpdateProductKey::HubService].target_version, + VersionSpecification::Specific("2026.2.1.7".parse().unwrap()) + ); + } + #[test] fn bom_is_stripped() { // UTF-8 BOM prefix From e4251bb4ff3d8e429271be905b70e4606ddc5361 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Wed, 3 Jun 2026 18:33:12 +0300 Subject: [PATCH 2/5] . --- crates/devolutions-agent-shared/src/lib.rs | 7 +++---- devolutions-agent/src/updater/mod.rs | 4 ++-- devolutions-gateway/src/api/update.rs | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/devolutions-agent-shared/src/lib.rs b/crates/devolutions-agent-shared/src/lib.rs index a6a687e2c..dabbc4619 100644 --- a/crates/devolutions-agent-shared/src/lib.rs +++ b/crates/devolutions-agent-shared/src/lib.rs @@ -11,10 +11,9 @@ use camino::Utf8PathBuf; use cfg_if::cfg_if; pub use date_version::{DateVersion, DateVersionError}; pub use update_manifest::{ - InstalledProductUpdateInfo, ProductUpdateInfoV1, ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, - UpdateManifest, UpdateManifestV1, - UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2, VersionSpecification, - default_schedule_window_start, detect_update_manifest_major_version, + InstalledProductUpdateInfo, ProductUpdateInfo, ProductUpdateInfoV1, UPDATE_MANIFEST_V2_MINOR_VERSION, + UpdateManifest, UpdateManifestV1, UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2, + VersionSpecification, default_schedule_window_start, detect_update_manifest_major_version, }; pub use update_status::{UpdateStatus, UpdateStatusV2}; diff --git a/devolutions-agent/src/updater/mod.rs b/devolutions-agent/src/updater/mod.rs index 084dddb9d..9b1652b4d 100644 --- a/devolutions-agent/src/updater/mod.rs +++ b/devolutions-agent/src/updater/mod.rs @@ -503,10 +503,10 @@ async fn read_update_json(update_file_path: &Utf8Path) -> anyhow::Result { let mut products = HashMap::new(); if let Some(gw) = v1.gateway { - products.insert(UpdateProductKey::Gateway, gw); + products.insert(UpdateProductKey::Gateway, gw.into()); } if let Some(hs) = v1.hub_service { - products.insert(UpdateProductKey::HubService, hs); + products.insert(UpdateProductKey::HubService, hs.into()); } UpdateManifest::ManifestV2(UpdateManifestV2 { products, diff --git a/devolutions-gateway/src/api/update.rs b/devolutions-gateway/src/api/update.rs index 83c54491c..e439c1892 100644 --- a/devolutions-gateway/src/api/update.rs +++ b/devolutions-gateway/src/api/update.rs @@ -56,10 +56,10 @@ async fn read_manifest() -> Result<(UpdateManifestV2, bool), HttpError> { UpdateManifest::Legacy(v1) => { let mut products = HashMap::new(); if let Some(gw) = v1.gateway { - products.insert(UpdateProductKey::Gateway, gw); + products.insert(UpdateProductKey::Gateway, gw.into()); } if let Some(hs) = v1.hub_service { - products.insert(UpdateProductKey::HubService, hs); + products.insert(UpdateProductKey::HubService, hs.into()); } Ok(( UpdateManifestV2 { From 8b33137a435cc6cbfdbd27649a8f0e9edbdafcae Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 4 Jun 2026 18:53:21 +0300 Subject: [PATCH 3/5] Updated unit tests Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../src/update_manifest.rs | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/devolutions-agent-shared/src/update_manifest.rs b/crates/devolutions-agent-shared/src/update_manifest.rs index 9ac529054..5e0ed2122 100644 --- a/crates/devolutions-agent-shared/src/update_manifest.rs +++ b/crates/devolutions-agent-shared/src/update_manifest.rs @@ -425,19 +425,28 @@ mod tests { )); } - #[test] - fn v1_accepted_with_version_alias() { - let json = r#"{"HubService":{"Version":"2026.2.1.7"}}"#; - let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); - assert!(matches!(manifest, UpdateManifest::Legacy(_))); - - let products = manifest.into_products(); - assert_eq!(products.len(), 1); - assert_eq!( - products[&UpdateProductKey::HubService].target_version, - VersionSpecification::Specific("2026.2.1.7".parse().unwrap()) - ); - } +#[test] +fn v1_accepted_with_version_alias() { + let json = r#"{\"HubService\":{\"Version\":\"2026.2.1.7\"}}"#; + let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); + assert!(matches!(manifest, UpdateManifest::Legacy(_))); + + // Re-serializing the parsed V1 manifest should emit the canonical `TargetVersion` field. + let UpdateManifest::Legacy(v1) = &manifest else { + unreachable!(); + }; + assert_eq!( + serde_json::to_string(v1).unwrap(), + r#"{\"HubService\":{\"TargetVersion\":\"2026.2.1.7\"}}"# + ); + + let products = manifest.into_products(); + assert_eq!(products.len(), 1); + assert_eq!( + products[&UpdateProductKey::HubService].target_version, + VersionSpecification::Specific("2026.2.1.7".parse().unwrap()) + ); +} #[test] fn bom_is_stripped() { From 5ae1885f63b33bdf7aa71ca7690c8c74956a9580 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 4 Jun 2026 18:54:55 +0300 Subject: [PATCH 4/5] Fixed formatting --- .../src/update_manifest.rs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/devolutions-agent-shared/src/update_manifest.rs b/crates/devolutions-agent-shared/src/update_manifest.rs index 5e0ed2122..0bd770c3b 100644 --- a/crates/devolutions-agent-shared/src/update_manifest.rs +++ b/crates/devolutions-agent-shared/src/update_manifest.rs @@ -425,28 +425,28 @@ mod tests { )); } -#[test] -fn v1_accepted_with_version_alias() { - let json = r#"{\"HubService\":{\"Version\":\"2026.2.1.7\"}}"#; - let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); - assert!(matches!(manifest, UpdateManifest::Legacy(_))); - - // Re-serializing the parsed V1 manifest should emit the canonical `TargetVersion` field. - let UpdateManifest::Legacy(v1) = &manifest else { - unreachable!(); - }; - assert_eq!( - serde_json::to_string(v1).unwrap(), - r#"{\"HubService\":{\"TargetVersion\":\"2026.2.1.7\"}}"# - ); - - let products = manifest.into_products(); - assert_eq!(products.len(), 1); - assert_eq!( - products[&UpdateProductKey::HubService].target_version, - VersionSpecification::Specific("2026.2.1.7".parse().unwrap()) - ); -} + #[test] + fn v1_accepted_with_version_alias() { + let json = r#"{\"HubService\":{\"Version\":\"2026.2.1.7\"}}"#; + let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); + assert!(matches!(manifest, UpdateManifest::Legacy(_))); + + // Re-serializing the parsed V1 manifest should emit the canonical `TargetVersion` field. + let UpdateManifest::Legacy(v1) = &manifest else { + unreachable!(); + }; + assert_eq!( + serde_json::to_string(v1).unwrap(), + r#"{\"HubService\":{\"TargetVersion\":\"2026.2.1.7\"}}"# + ); + + let products = manifest.into_products(); + assert_eq!(products.len(), 1); + assert_eq!( + products[&UpdateProductKey::HubService].target_version, + VersionSpecification::Specific("2026.2.1.7".parse().unwrap()) + ); + } #[test] fn bom_is_stripped() { From 1a6d7994e61a9fff687ae8c64590a1d261966e0a Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Thu, 4 Jun 2026 20:42:19 +0300 Subject: [PATCH 5/5] . --- crates/devolutions-agent-shared/src/update_manifest.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/devolutions-agent-shared/src/update_manifest.rs b/crates/devolutions-agent-shared/src/update_manifest.rs index 0bd770c3b..3ea846b8f 100644 --- a/crates/devolutions-agent-shared/src/update_manifest.rs +++ b/crates/devolutions-agent-shared/src/update_manifest.rs @@ -427,7 +427,7 @@ mod tests { #[test] fn v1_accepted_with_version_alias() { - let json = r#"{\"HubService\":{\"Version\":\"2026.2.1.7\"}}"#; + let json = r#"{"HubService":{"Version":"2026.2.1.7"}}"#; let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); assert!(matches!(manifest, UpdateManifest::Legacy(_))); @@ -437,7 +437,7 @@ mod tests { }; assert_eq!( serde_json::to_string(v1).unwrap(), - r#"{\"HubService\":{\"TargetVersion\":\"2026.2.1.7\"}}"# + r#"{"HubService":{"TargetVersion":"2026.2.1.7"}}"# ); let products = manifest.into_products();