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(
36 current_version: &Version,
37 channel: &UpdateChannel,
38) -> Result<Option<UpdateInfo>> {
39 let releases = fetch_github_releases().await?;
40 find_suitable_update(current_version, channel, &releases)
41}
42
43async fn fetch_github_releases() -> Result<Vec<GitHubRelease>> {
45 let client = reqwest::Client::new();
46 let url = "https://api.github.com/repos/ferrous-systems/ferrous-forge/releases";
47
48 let response = client
49 .get(url)
50 .header("User-Agent", "ferrous-forge")
51 .send()
52 .await
53 .map_err(|e| Error::network(format!("Failed to fetch releases: {}", e)))?;
54
55 if !response.status().is_success() {
56 return Err(Error::network(format!(
57 "GitHub API request failed: {}",
58 response.status()
59 )));
60 }
61
62 response
63 .json()
64 .await
65 .map_err(|e| Error::network(format!("Failed to parse GitHub response: {}", e)))
66}
67
68fn find_suitable_update(
70 current_version: &Version,
71 channel: &UpdateChannel,
72 releases: &[GitHubRelease],
73) -> Result<Option<UpdateInfo>> {
74 for release in releases {
75 if !is_release_suitable(release, channel) {
76 continue;
77 }
78
79 if let Some(version) = parse_release_version(&release.tag_name) {
80 if version <= *current_version {
81 continue;
82 }
83
84 if let Some(update_info) = create_update_info(version, release) {
85 return Ok(Some(update_info));
86 }
87 }
88 }
89 Ok(None)
90}
91
92fn is_release_suitable(release: &GitHubRelease, channel: &UpdateChannel) -> bool {
94 !matches!(channel, UpdateChannel::Stable) || !release.prerelease
95}
96
97fn parse_release_version(tag_name: &str) -> Option<Version> {
99 let tag_version = tag_name.trim_start_matches('v');
100 Version::parse(tag_version).ok()
101}
102
103fn create_update_info(version: Version, release: &GitHubRelease) -> Option<UpdateInfo> {
105 let platform_suffix = get_platform_suffix();
106 let asset = release
107 .assets
108 .iter()
109 .find(|asset| asset.name.contains(&platform_suffix))?;
110
111 Some(UpdateInfo {
112 version,
113 download_url: asset.browser_download_url.clone(),
114 size: asset.size,
115 sha256: None, notes: release.body.clone(),
117 critical: false, })
119}
120
121fn get_platform_suffix() -> String {
123 let os = std::env::consts::OS;
124 let arch = std::env::consts::ARCH;
125
126 match (os, arch) {
127 ("linux", "x86_64") => "linux-x86_64",
128 ("macos", "x86_64") => "darwin-x86_64",
129 ("macos", "aarch64") => "darwin-aarch64",
130 ("windows", "x86_64") => "windows-x86_64.exe",
131 _ => "unknown",
132 }
133 .to_string()
134}