Skip to main content

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
30///
31/// # Errors
32///
33/// Returns an error if the GitHub API request fails or the response
34/// cannot be parsed.
35pub 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
43/// Fetch releases from GitHub API
44async 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
68/// Find a suitable update from the list of releases
69fn 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
92/// Check if a release is suitable for the given channel
93fn is_release_suitable(release: &GitHubRelease, channel: &UpdateChannel) -> bool {
94    !matches!(channel, UpdateChannel::Stable) || !release.prerelease
95}
96
97/// Parse version from release tag
98fn 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
103/// Create update info from release data
104fn 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, // GitHub doesn't provide SHA256 in API
116        notes: release.body.clone(),
117        critical: false, // Would need to parse from release notes
118    })
119}
120
121/// Get platform-specific binary suffix
122fn 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}