ferrous_forge/updater/
github.rs1use super::types::{UpdateChannel, UpdateInfo};
4use crate::{Error, Result};
5use semver::Version;
6use serde::Deserialize;
7
8#[derive(Deserialize)]
9struct GitHubRelease {
10 tag_name: String,
11 body: String,
12 prerelease: bool,
13 assets: Vec<GitHubAsset>,
14}
15
16#[derive(Deserialize)]
17struct GitHubAsset {
18 name: String,
19 browser_download_url: String,
20 size: u64,
21}
22
23#[derive(Deserialize)]
24#[allow(dead_code)]
25struct GitHubReleases {
26 releases: Vec<GitHubRelease>,
27}
28
29pub async fn fetch_update_info(
31 current_version: &Version,
32 channel: &UpdateChannel,
33) -> Result<Option<UpdateInfo>> {
34 let releases = fetch_github_releases().await?;
35 find_suitable_update(current_version, channel, &releases)
36}
37
38async fn fetch_github_releases() -> Result<Vec<GitHubRelease>> {
40 let client = reqwest::Client::new();
41 let url = "https://api.github.com/repos/ferrous-systems/ferrous-forge/releases";
42
43 let response = client
44 .get(url)
45 .header("User-Agent", "ferrous-forge")
46 .send()
47 .await
48 .map_err(|e| Error::network(format!("Failed to fetch releases: {}", e)))?;
49
50 if !response.status().is_success() {
51 return Err(Error::network(format!(
52 "GitHub API request failed: {}",
53 response.status()
54 )));
55 }
56
57 response
58 .json()
59 .await
60 .map_err(|e| Error::network(format!("Failed to parse GitHub response: {}", e)))
61}
62
63fn find_suitable_update(
65 current_version: &Version,
66 channel: &UpdateChannel,
67 releases: &[GitHubRelease],
68) -> Result<Option<UpdateInfo>> {
69 for release in releases {
70 if !is_release_suitable(release, channel) {
71 continue;
72 }
73
74 if let Some(version) = parse_release_version(&release.tag_name) {
75 if version <= *current_version {
76 continue;
77 }
78
79 if let Some(update_info) = create_update_info(version, release) {
80 return Ok(Some(update_info));
81 }
82 }
83 }
84 Ok(None)
85}
86
87fn is_release_suitable(release: &GitHubRelease, channel: &UpdateChannel) -> bool {
89 !matches!(channel, UpdateChannel::Stable) || !release.prerelease
90}
91
92fn parse_release_version(tag_name: &str) -> Option<Version> {
94 let tag_version = tag_name.trim_start_matches('v');
95 Version::parse(tag_version).ok()
96}
97
98fn create_update_info(version: Version, release: &GitHubRelease) -> Option<UpdateInfo> {
100 let platform_suffix = get_platform_suffix();
101 let asset = release
102 .assets
103 .iter()
104 .find(|asset| asset.name.contains(&platform_suffix))?;
105
106 Some(UpdateInfo {
107 version,
108 download_url: asset.browser_download_url.clone(),
109 size: asset.size,
110 sha256: None, notes: release.body.clone(),
112 critical: false, })
114}
115
116fn get_platform_suffix() -> String {
118 let os = std::env::consts::OS;
119 let arch = std::env::consts::ARCH;
120
121 match (os, arch) {
122 ("linux", "x86_64") => "linux-x86_64",
123 ("macos", "x86_64") => "darwin-x86_64",
124 ("macos", "aarch64") => "darwin-aarch64",
125 ("windows", "x86_64") => "windows-x86_64.exe",
126 _ => "unknown",
127 }
128 .to_string()
129}