Closes #33903 Release Notes: - Ensured Zed reuses `GITHUB_TOKEN` env variable when querying GitHub --- Before fixing: - The `crates-lsp` extension request captured: ``` curl 'https://api.github.com/repos/MathiasPius/crates-lsp/releases' \ -H 'accept: */*' \ -H 'user-agent: Zed/0.212.3 (macos; aarch64)' \ -H 'host: api.github.com' \ ``` - `crates-lsp` extension error: ``` Language server crates-lsp: from extension "Crates LSP" version 0.2.0: status error 403, response: "{\"message\":\"API rate limit exceeded for x.x.x.x. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)\",\"documentation_url\":\"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting\"}\n" ``` After fixing: ``` export GITHUB_TOKEN=$(gh auth token) cargo run ``` - The `crates-lsp` extension request captured: ``` curl 'https://api.github.com/repos/MathiasPius/crates-lsp/releases' \ -H 'authorization: Bearer gho_Nt*****************2KXLw2' \ -H 'accept: */*' \ -H 'user-agent: Zed/0.214.0 (macos; aarch64)' \ -H 'host: api.github.com' \ ``` The API rate limitation is resolved. --- This isn't a perfect solution, but it enables users to avoid the noise.
193 lines
5.6 KiB
Rust
193 lines
5.6 KiB
Rust
use crate::{HttpClient, HttpRequestExt};
|
|
use anyhow::{Context as _, Result, anyhow, bail};
|
|
use futures::AsyncReadExt;
|
|
use http::Request;
|
|
use serde::Deserialize;
|
|
use std::sync::Arc;
|
|
use url::Url;
|
|
|
|
const GITHUB_API_URL: &str = "https://api.github.com";
|
|
|
|
pub struct GitHubLspBinaryVersion {
|
|
pub name: String,
|
|
pub url: String,
|
|
pub digest: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct GithubRelease {
|
|
pub tag_name: String,
|
|
#[serde(rename = "prerelease")]
|
|
pub pre_release: bool,
|
|
pub assets: Vec<GithubReleaseAsset>,
|
|
pub tarball_url: String,
|
|
pub zipball_url: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
pub struct GithubReleaseAsset {
|
|
pub name: String,
|
|
pub browser_download_url: String,
|
|
pub digest: Option<String>,
|
|
}
|
|
|
|
pub async fn latest_github_release(
|
|
repo_name_with_owner: &str,
|
|
require_assets: bool,
|
|
pre_release: bool,
|
|
http: Arc<dyn HttpClient>,
|
|
) -> anyhow::Result<GithubRelease> {
|
|
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");
|
|
|
|
let request = Request::get(&url)
|
|
.follow_redirects(crate::RedirectPolicy::FollowAll)
|
|
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
|
|
builder.header("Authorization", format!("Bearer {}", token))
|
|
})
|
|
.body(Default::default())?;
|
|
|
|
let mut response = http
|
|
.send(request)
|
|
.await
|
|
.context("error fetching latest release")?;
|
|
|
|
let mut body = Vec::new();
|
|
response
|
|
.body_mut()
|
|
.read_to_end(&mut body)
|
|
.await
|
|
.context("error reading latest release")?;
|
|
|
|
if response.status().is_client_error() {
|
|
let text = String::from_utf8_lossy(body.as_slice());
|
|
bail!(
|
|
"status error {}, response: {text:?}",
|
|
response.status().as_u16()
|
|
);
|
|
}
|
|
|
|
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
|
Ok(releases) => releases,
|
|
|
|
Err(err) => {
|
|
log::error!("Error deserializing: {err:?}");
|
|
log::error!(
|
|
"GitHub API response text: {:?}",
|
|
String::from_utf8_lossy(body.as_slice())
|
|
);
|
|
anyhow::bail!("error deserializing latest release: {err:?}");
|
|
}
|
|
};
|
|
|
|
let mut release = releases
|
|
.into_iter()
|
|
.filter(|release| !require_assets || !release.assets.is_empty())
|
|
.find(|release| release.pre_release == pre_release)
|
|
.context("finding a prerelease")?;
|
|
release.assets.iter_mut().for_each(|asset| {
|
|
if let Some(digest) = &mut asset.digest
|
|
&& let Some(stripped) = digest.strip_prefix("sha256:")
|
|
{
|
|
*digest = stripped.to_owned();
|
|
}
|
|
});
|
|
Ok(release)
|
|
}
|
|
|
|
pub async fn get_release_by_tag_name(
|
|
repo_name_with_owner: &str,
|
|
tag: &str,
|
|
http: Arc<dyn HttpClient>,
|
|
) -> anyhow::Result<GithubRelease> {
|
|
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");
|
|
|
|
let request = Request::get(&url)
|
|
.follow_redirects(crate::RedirectPolicy::FollowAll)
|
|
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
|
|
builder.header("Authorization", format!("Bearer {}", token))
|
|
})
|
|
.body(Default::default())?;
|
|
|
|
let mut response = http
|
|
.send(request)
|
|
.await
|
|
.context("error fetching latest release")?;
|
|
|
|
let mut body = Vec::new();
|
|
let status = response.status();
|
|
response
|
|
.body_mut()
|
|
.read_to_end(&mut body)
|
|
.await
|
|
.context("error reading latest release")?;
|
|
|
|
if status.is_client_error() {
|
|
let text = String::from_utf8_lossy(body.as_slice());
|
|
bail!(
|
|
"status error {}, response: {text:?}",
|
|
response.status().as_u16()
|
|
);
|
|
}
|
|
|
|
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
|
log::error!("Error deserializing: {err:?}");
|
|
log::error!(
|
|
"GitHub API response text: {:?}",
|
|
String::from_utf8_lossy(body.as_slice())
|
|
);
|
|
anyhow!("error deserializing GitHub release: {err:?}")
|
|
})?;
|
|
|
|
Ok(release)
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
|
pub enum AssetKind {
|
|
TarGz,
|
|
Gz,
|
|
Zip,
|
|
}
|
|
|
|
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
|
|
let mut url = Url::parse(&format!(
|
|
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
|
))?;
|
|
// We're pushing this here, because tags may contain `/` and other characters
|
|
// that need to be escaped.
|
|
let asset_filename = format!(
|
|
"{tag}.{extension}",
|
|
extension = match kind {
|
|
AssetKind::TarGz => "tar.gz",
|
|
AssetKind::Gz => "gz",
|
|
AssetKind::Zip => "zip",
|
|
}
|
|
);
|
|
url.path_segments_mut()
|
|
.map_err(|()| anyhow!("cannot modify url path segments"))?
|
|
.push(&asset_filename);
|
|
Ok(url.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::github::{AssetKind, build_asset_url};
|
|
|
|
#[test]
|
|
fn test_build_asset_url() {
|
|
let tag = "release/2.3.5";
|
|
let repo_name_with_owner = "microsoft/vscode-eslint";
|
|
|
|
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
|
|
assert_eq!(
|
|
tarball,
|
|
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
|
|
);
|
|
|
|
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
|
|
assert_eq!(
|
|
zip,
|
|
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
|
|
);
|
|
}
|
|
}
|