github.rs

  1use crate::{HttpClient, HttpRequestExt};
  2use anyhow::{Context as _, Result, anyhow, bail};
  3use futures::AsyncReadExt;
  4use http::Request;
  5use serde::Deserialize;
  6use std::sync::Arc;
  7use url::Url;
  8
  9const GITHUB_API_URL: &str = "https://api.github.com";
 10
 11pub struct GitHubLspBinaryVersion {
 12    pub name: String,
 13    pub url: String,
 14    pub digest: Option<String>,
 15}
 16
 17#[derive(Deserialize, Debug)]
 18pub struct GithubRelease {
 19    pub tag_name: String,
 20    #[serde(rename = "prerelease")]
 21    pub pre_release: bool,
 22    pub assets: Vec<GithubReleaseAsset>,
 23    pub tarball_url: String,
 24    pub zipball_url: String,
 25}
 26
 27#[derive(Deserialize, Debug)]
 28pub struct GithubReleaseAsset {
 29    pub name: String,
 30    pub browser_download_url: String,
 31    pub digest: Option<String>,
 32}
 33
 34pub async fn latest_github_release(
 35    repo_name_with_owner: &str,
 36    require_assets: bool,
 37    pre_release: bool,
 38    http: Arc<dyn HttpClient>,
 39) -> anyhow::Result<GithubRelease> {
 40    let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");
 41
 42    let request = Request::get(&url)
 43        .follow_redirects(crate::RedirectPolicy::FollowAll)
 44        .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
 45            builder.header("Authorization", format!("Bearer {}", token))
 46        })
 47        .body(Default::default())?;
 48
 49    let mut response = http
 50        .send(request)
 51        .await
 52        .context("error fetching latest release")?;
 53
 54    let mut body = Vec::new();
 55    response
 56        .body_mut()
 57        .read_to_end(&mut body)
 58        .await
 59        .context("error reading latest release")?;
 60
 61    if response.status().is_client_error() {
 62        let text = String::from_utf8_lossy(body.as_slice());
 63        bail!(
 64            "status error {}, response: {text:?}",
 65            response.status().as_u16()
 66        );
 67    }
 68
 69    let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
 70        Ok(releases) => releases,
 71
 72        Err(err) => {
 73            log::error!("Error deserializing: {err:?}");
 74            log::error!(
 75                "GitHub API response text: {:?}",
 76                String::from_utf8_lossy(body.as_slice())
 77            );
 78            anyhow::bail!("error deserializing latest release: {err:?}");
 79        }
 80    };
 81
 82    let mut release = releases
 83        .into_iter()
 84        .filter(|release| !require_assets || !release.assets.is_empty())
 85        .find(|release| release.pre_release == pre_release)
 86        .context("finding a prerelease")?;
 87    release.assets.iter_mut().for_each(|asset| {
 88        if let Some(digest) = &mut asset.digest
 89            && let Some(stripped) = digest.strip_prefix("sha256:")
 90        {
 91            *digest = stripped.to_owned();
 92        }
 93    });
 94    Ok(release)
 95}
 96
 97pub async fn get_release_by_tag_name(
 98    repo_name_with_owner: &str,
 99    tag: &str,
100    http: Arc<dyn HttpClient>,
101) -> anyhow::Result<GithubRelease> {
102    let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");
103
104    let request = Request::get(&url)
105        .follow_redirects(crate::RedirectPolicy::FollowAll)
106        .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
107            builder.header("Authorization", format!("Bearer {}", token))
108        })
109        .body(Default::default())?;
110
111    let mut response = http
112        .send(request)
113        .await
114        .context("error fetching latest release")?;
115
116    let mut body = Vec::new();
117    let status = response.status();
118    response
119        .body_mut()
120        .read_to_end(&mut body)
121        .await
122        .context("error reading latest release")?;
123
124    if status.is_client_error() {
125        let text = String::from_utf8_lossy(body.as_slice());
126        bail!(
127            "status error {}, response: {text:?}",
128            response.status().as_u16()
129        );
130    }
131
132    let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
133        log::error!("Error deserializing: {err:?}");
134        log::error!(
135            "GitHub API response text: {:?}",
136            String::from_utf8_lossy(body.as_slice())
137        );
138        anyhow!("error deserializing GitHub release: {err:?}")
139    })?;
140
141    Ok(release)
142}
143
144#[derive(Debug, PartialEq, Eq, Clone, Copy)]
145pub enum AssetKind {
146    TarGz,
147    Gz,
148    Zip,
149}
150
151pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
152    let mut url = Url::parse(&format!(
153        "https://github.com/{repo_name_with_owner}/archive/refs/tags",
154    ))?;
155    // We're pushing this here, because tags may contain `/` and other characters
156    // that need to be escaped.
157    let asset_filename = format!(
158        "{tag}.{extension}",
159        extension = match kind {
160            AssetKind::TarGz => "tar.gz",
161            AssetKind::Gz => "gz",
162            AssetKind::Zip => "zip",
163        }
164    );
165    url.path_segments_mut()
166        .map_err(|()| anyhow!("cannot modify url path segments"))?
167        .push(&asset_filename);
168    Ok(url.to_string())
169}
170
171#[cfg(test)]
172mod tests {
173    use crate::github::{AssetKind, build_asset_url};
174
175    #[test]
176    fn test_build_asset_url() {
177        let tag = "release/2.3.5";
178        let repo_name_with_owner = "microsoft/vscode-eslint";
179
180        let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
181        assert_eq!(
182            tarball,
183            "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
184        );
185
186        let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
187        assert_eq!(
188            zip,
189            "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
190        );
191    }
192}