From 233ee5d9ab53826fdaaf67cbe770cb15ce3b1731 Mon Sep 17 00:00:00 2001 From: EgoMoose Date: Sun, 4 Oct 2026 17:33:34 -0400 Subject: [PATCH 1/2] Add private gh repo support --- src/backends.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/src/backends.rs b/src/backends.rs index 8961fe0..202bcac 100644 --- a/src/backends.rs +++ b/src/backends.rs @@ -1,8 +1,9 @@ use anyhow::{Context, Result, bail}; use async_trait::async_trait; use fs_err::tokio as fs; -use reqwest::Client; +use reqwest::{Client, StatusCode}; use serde::Deserialize; +use std::{env, process::Command}; use crate::config::Plugin; @@ -135,13 +136,92 @@ impl Backend for CloudBackend { pub struct GitHubBackend { client: Client, + token: Option, +} + +impl GitHubBackend { + fn get_token() -> Option { + let token = env::var("GITHUB_TOKEN") + .or_else(|_| env::var("GH_TOKEN")) + .ok() + .or_else(|| { + let output = Command::new("gh").args(["auth", "token"]).output().ok()?; + output + .status + .success() + .then(|| String::from_utf8_lossy(&output.stdout).to_string()) + })?; + + let token = token.trim().to_string(); + (!token.is_empty()).then_some(token) + } + + async fn download_with_token( + &self, + token: &str, + owner_repo: &str, + tag: &str, + asset_name: &str, + ) -> Result> { + let res = self + .client + .get(format!( + "https://api.github.com/repos/{owner_repo}/releases/tags/{tag}" + )) + .bearer_auth(token) + .header("Accept", "application/vnd.github+json") + .send() + .await?; + + if !res.status().is_success() { + bail!( + "GitHub release lookup for '{owner_repo}@{tag}' failed with status: {}", + res.status() + ); + } + + let release: ReleaseResponse = res.json().await?; + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .with_context(|| format!("Asset '{asset_name}' not found in release '{tag}'"))?; + + let res = self + .client + .get(format!( + "https://api.github.com/repos/{owner_repo}/releases/assets/{}", + asset.id + )) + .bearer_auth(token) + .header("Accept", "application/octet-stream") + .send() + .await?; + + if !res.status().is_success() { + bail!( + "GitHub release download failed with status: {}", + res.status() + ); + } + + Ok(res.bytes().await?.to_vec()) + } +} + +fn parse_release_url(url: &str) -> Option<(&str, &str, &str)> { + let rest = url.strip_prefix("https://github.com/")?; + let (rest, asset_name) = rest.rsplit_once('/')?; + let (owner_repo, tag) = rest.split_once("/releases/download/")?; + Some((owner_repo, tag, asset_name)) } #[async_trait] impl Backend for GitHubBackend { fn new() -> Result { Ok(Self { - client: Client::new(), + client: Client::builder().user_agent("drillbit").build()?, + token: Self::get_token(), }) } @@ -150,16 +230,33 @@ impl Backend for GitHubBackend { unimplemented!() }; + let ext = url.split('.').next_back().map(|s| s.to_string()); + + if let Some(token) = &self.token + && let Some((owner_repo, tag, asset_name)) = parse_release_url(url) + { + let data = self + .download_with_token(token, owner_repo, tag, asset_name) + .await?; + return Ok((data, ext)); + } + let res = self.client.get(url).send().await?; if !res.status().is_success() { + if self.token.is_none() && res.status() == StatusCode::NOT_FOUND { + bail!( + "GitHub release download failed with status: {}. If this is a private repository, set GITHUB_TOKEN or run `gh auth login`.", + res.status() + ); + } + bail!( "GitHub release download failed with status: {}", res.status() ); } - let ext = url.split('.').next_back().map(|s| s.to_string()); Ok((res.bytes().await?.to_vec(), ext)) } @@ -182,3 +279,14 @@ struct Location { struct AssetResponse { locations: Vec, } + +#[derive(Deserialize)] +struct ReleaseAsset { + id: u64, + name: String, +} + +#[derive(Deserialize)] +struct ReleaseResponse { + assets: Vec, +} From 4162c1fc6654852a42e1f5325d6cdadc3c79b3bd Mon Sep 17 00:00:00 2001 From: EgoMoose Date: Sun, 4 Oct 2026 17:33:53 -0400 Subject: [PATCH 2/2] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index d72bf42..15e4843 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,16 @@ cloud = 4621580428 github = "https://github.com/jackTabsCode/jest-companion/releases/download/v0.1.1/plugin.rbxm" ``` +### Private GitHub Repositories + +To install release artifacts from private repositories, drillbit needs a GitHub token. It looks for one in this order: + +1. The `GITHUB_TOKEN` environment variable +2. The `GH_TOKEN` environment variable +3. The output of `gh auth token`, if you're logged in with the [GitHub CLI](https://cli.github.com) + +Fine-grained tokens need the **Contents: Read** permission on the repository. Classic tokens need the `repo` scope. + ## Usage Simply run it in your project's directory: