diff --git a/core/engine/src/ipc/handlers/update.rs b/core/engine/src/ipc/handlers/update.rs index b7f3174..f2bd1da 100644 --- a/core/engine/src/ipc/handlers/update.rs +++ b/core/engine/src/ipc/handlers/update.rs @@ -14,10 +14,10 @@ pub fn check_for_updates( let url = get_string(&data, "manifestUrl") .unwrap_or_else(|| "https://raw.githubusercontent.com/vantoan1511/orbit/main/update-manifest.json".to_string()); - match crate::updater::UpdateManifest::fetch(&url).await { + let current_engine = env!("CARGO_PKG_VERSION"); + + match crate::updater::UpdateManifest::fetch(&url, current_engine).await { Ok(manifest) => { - let current_engine = env!("CARGO_PKG_VERSION"); - let has_update = manifest.has_update(current_engine).unwrap_or(false); let _ = Bridge::send_event( diff --git a/core/engine/src/updater.rs b/core/engine/src/updater.rs index 1e16fb1..af5b64c 100644 --- a/core/engine/src/updater.rs +++ b/core/engine/src/updater.rs @@ -8,21 +8,24 @@ pub struct UpdateManifest { pub release_notes: Option, } +const GITHUB_REPO: &str = "vantoan1511/orbit"; + #[derive(Deserialize)] -struct GithubReleaseResponse { +struct GithubReleaseItem { + tag_name: Option, body: Option, } impl UpdateManifest { /// Fetch the latest update manifest from the given URL. - pub async fn fetch(url: &str) -> Result> { + pub async fn fetch(url: &str, current_version: &str) -> Result> { let mut manifest = reqwest::get(url) .await? .json::() .await?; if manifest.release_notes.is_none() || manifest.release_notes.as_deref() == Some("") { - if let Some(notes) = Self::fetch_github_release_notes(&manifest.version).await { + if let Some(notes) = Self::fetch_github_release_notes(&manifest.version, current_version).await { manifest.release_notes = Some(notes); } else { manifest.release_notes = Some("No release notes provided for this version.".to_string()); @@ -32,35 +35,58 @@ impl UpdateManifest { Ok(manifest) } - async fn fetch_github_release_notes(version: &str) -> Option { - let tag = if version.starts_with('v') { - version.to_string() - } else { - format!("v{}", version) - }; - let api_url = format!("https://api.github.com/repos/vantoan1511/orbit/releases/tags/{}", tag); - + async fn fetch_github_release_notes(target_version: &str, current_version: &str) -> Option { let client = reqwest::Client::builder() .user_agent("orbit-engine") .timeout(std::time::Duration::from_secs(5)) .build() .ok()?; - let resp = client.get(&api_url).send().await.ok()?; - if resp.status().is_success() { - let release = resp.json::().await.ok()?; - release.body + if let Some(notes) = Self::fetch_aggregated_release_notes(&client, target_version, current_version).await { + return Some(notes); + } + + Self::fetch_single_release_notes(&client, target_version).await + } + + async fn fetch_aggregated_release_notes( + client: &reqwest::Client, + target_version: &str, + current_version: &str, + ) -> Option { + let list_api_url = format!("https://api.github.com/repos/{}/releases?per_page=100", GITHUB_REPO); + let resp = client.get(&list_api_url).send().await.ok()?; + if !resp.status().is_success() { + return None; + } + + let releases = resp.json::>().await.ok()?; + Self::aggregate_release_notes(&releases, target_version, current_version) + } + + async fn fetch_release_by_url(client: &reqwest::Client, url: &str) -> Option { + let resp = client.get(url).send().await.ok()?; + if !resp.status().is_success() { + return None; + } + let release = resp.json::().await.ok()?; + release.body + } + + async fn fetch_single_release_notes(client: &reqwest::Client, target_version: &str) -> Option { + let tag = if target_version.starts_with('v') { + target_version.to_string() } else { - // Fallback: try latest release endpoint if specific tag lookup fails - let fallback_url = "https://api.github.com/repos/vantoan1511/orbit/releases/latest"; - let resp = client.get(fallback_url).send().await.ok()?; - if resp.status().is_success() { - let release = resp.json::().await.ok()?; - release.body - } else { - None - } + format!("v{}", target_version) + }; + let tag_url = format!("https://api.github.com/repos/{}/releases/tags/{}", GITHUB_REPO, tag); + if let Some(body) = Self::fetch_release_by_url(client, &tag_url).await { + return Some(body); } + + // Fallback: try latest release endpoint if specific tag lookup fails + let fallback_url = format!("https://api.github.com/repos/{}/releases/latest", GITHUB_REPO); + Self::fetch_release_by_url(client, &fallback_url).await } /// Check if an update is available. @@ -103,5 +129,140 @@ impl UpdateManifest { Ok(temp_path) } + + fn aggregate_release_notes( + releases: &[GithubReleaseItem], + target_version: &str, + current_version: &str, + ) -> Option { + let target_sem = semver::Version::parse(target_version.trim_start_matches('v')).ok()?; + let current_sem = semver::Version::parse(current_version.trim_start_matches('v')).ok()?; + + let mut matching_releases: Vec<(semver::Version, &str, &Option)> = releases + .iter() + .filter_map(|release| { + let tag = release.tag_name.as_deref()?; + let release_sem = semver::Version::parse(tag.trim_start_matches('v')).ok()?; + if release_sem > current_sem && release_sem <= target_sem { + Some((release_sem, tag, &release.body)) + } else { + None + } + }) + .collect(); + + if matching_releases.is_empty() { + return None; + } + + // Sort descending (newest first) + matching_releases.sort_by(|a, b| b.0.cmp(&a.0)); + + let mut combined_notes = String::new(); + + for (_, tag, body) in matching_releases { + let body_text = body + .as_deref() + .map(str::trim) + .filter(|b| !b.is_empty()) + .unwrap_or("_No release notes provided._"); + + if !combined_notes.is_empty() { + combined_notes.push_str("\n\n"); + } + combined_notes.push_str(&format!("## Release {}\n\n{}", tag, body_text)); + } + + Some(combined_notes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_aggregate_release_notes_multiple_intermediate_versions() { + // Provided in arbitrary/reverse order to verify that sorting guarantees descending order + let releases = vec![ + GithubReleaseItem { + tag_name: Some("v0.7.0".to_string()), + body: Some("### Features\n- Add feature B".to_string()), + }, + GithubReleaseItem { + tag_name: Some("v0.7.1".to_string()), + body: Some("### Features\n- Fix bug A".to_string()), + }, + GithubReleaseItem { + tag_name: Some("v0.6.0".to_string()), + body: Some("### Features\n- Old feature".to_string()), + }, + ]; + + let result = UpdateManifest::aggregate_release_notes(&releases, "0.7.1", "0.6.0"); + assert!(result.is_some()); + let notes = result.unwrap(); + assert!(notes.contains("## Release v0.7.1\n\n### Features\n- Fix bug A")); + assert!(notes.contains("## Release v0.7.0\n\n### Features\n- Add feature B")); + assert!(!notes.contains("v0.6.0")); + + // Verify descending order (newest first) + let pos_v071 = notes.find("## Release v0.7.1").unwrap(); + let pos_v070 = notes.find("## Release v0.7.0").unwrap(); + assert!(pos_v071 < pos_v070, "v0.7.1 should appear before v0.7.0 in descending order"); + } + + #[test] + fn test_aggregate_release_notes_single_version() { + let releases = vec![ + GithubReleaseItem { + tag_name: Some("v0.7.1".to_string()), + body: Some("### Features\n- Fix bug A".to_string()), + }, + GithubReleaseItem { + tag_name: Some("v0.7.0".to_string()), + body: Some("### Features\n- Add feature B".to_string()), + }, + ]; + + let result = UpdateManifest::aggregate_release_notes(&releases, "v0.7.1", "v0.7.0"); + assert!(result.is_some()); + let notes = result.unwrap(); + assert!(notes.contains("## Release v0.7.1")); + assert!(!notes.contains("v0.7.0")); + } + + #[test] + fn test_aggregate_release_notes_no_matching_versions() { + let releases = vec![ + GithubReleaseItem { + tag_name: Some("v0.6.0".to_string()), + body: Some("### Features\n- Old feature".to_string()), + }, + ]; + + let result = UpdateManifest::aggregate_release_notes(&releases, "0.7.1", "0.7.0"); + assert!(result.is_none()); + } + + #[test] + fn test_aggregate_release_notes_empty_body_fallback() { + let releases = vec![ + GithubReleaseItem { + tag_name: Some("v0.7.1".to_string()), + body: None, + }, + GithubReleaseItem { + tag_name: Some("v0.7.0".to_string()), + body: Some(" ".to_string()), + }, + ]; + + let result = UpdateManifest::aggregate_release_notes(&releases, "v0.7.1", "v0.6.0"); + assert!(result.is_some()); + let notes = result.unwrap(); + assert!(notes.contains("## Release v0.7.1\n\n_No release notes provided._")); + assert!(notes.contains("## Release v0.7.0\n\n_No release notes provided._")); + } }