ferrous_forge/updater/
github.rs

1//! GitHub API integration for fetching updates
2
3use 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
29/// Fetch update information from GitHub releases
30pub 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
38/// Fetch releases from GitHub API
39async 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
63/// Find a suitable update from the list of releases
64fn 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
87/// Check if a release is suitable for the given channel
88fn is_release_suitable(release: &GitHubRelease, channel: &UpdateChannel) -> bool {
89    !matches!(channel, UpdateChannel::Stable) || !release.prerelease
90}
91
92/// Parse version from release tag
93fn 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
98/// Create update info from release data
99fn 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, // GitHub doesn't provide SHA256 in API
111        notes: release.body.clone(),
112        critical: false, // Would need to parse from release notes
113    })
114}
115
116/// Get platform-specific binary suffix
117fn 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}