Skip to content

Commit d8cb87e

Browse files
committed
feat(ubi): add support for self-hosted GitHub/GitLab
1 parent 370214d commit d8cb87e

File tree

10 files changed

+513
-101
lines changed

10 files changed

+513
-101
lines changed

Cargo.lock

Lines changed: 76 additions & 77 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ zip = { version = "=2.5.0", default-features = false, features = ["deflate"] }
151151
zstd = "0.13"
152152
gix = { version = "<1", features = ["worktree-mutation"] }
153153
jiff = "0.2"
154+
urlencoding = "2.1.3"
154155

155156
[target.'cfg(unix)'.dependencies]
156157
exec = "0.3"

docs/dev-tools/backends/ubi.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@ then this will be ignored.
6969
"ubi:BurntSushi/ripgrep" = { version = "latest", matching = "musl" }
7070
```
7171

72+
### `provider`
73+
74+
Set the provider type to use for fetching assets and release information. Either `github` or `gitlab` (default is `github`).
75+
Ensure the `provider` is set to the correct type if you use `api_url` as the type probably cannot be derived correctly
76+
from the URL.
77+
78+
```toml
79+
[tools]
80+
"ubi:gitlab-org/cli" = { version = "latest", exe = "glab", provider = "gitlab" }
81+
```
82+
83+
### `api_url`
84+
85+
Set the URL for the provider's API. This is useful when using a self-hosted instance.
86+
87+
```toml
88+
[tools]
89+
"ubi:acme/my-tool" = { version = "latest", provider= "gitlab", api_url = "https://gitlab.acme.com/api/v4" }
90+
```
91+
7292
### `extract_all`
7393

7494
Set to `true` to extract all files in the tarball instead of just the "bin". Not compatible with `exe` nor `rename_exe`.
@@ -100,6 +120,12 @@ releases.
100120
"ubi:cargo-bins/cargo-binstall" = { version = "latest", tag_regex = "^\d+\." }
101121
```
102122

123+
## Self-hosted GitHub/GitLab
124+
125+
If you are using a self-hosted GitHub/GitLab instance, you can set the `provider` and `api_url` tool options.
126+
Additionally, you can set the `MISE_GITHUB_ENTERPRISE_TOKEN` or `MISE_GITLAB_ENTERPRISE_TOKEN` environment variable to
127+
authenticate with the API.
128+
103129
## Supported Ubi Syntax
104130

105131
- **GitHub shorthand for latest release version:** `ubi:goreleaser/goreleaser`

src/backend/ubi.rs

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ use crate::backend::Backend;
22
use crate::backend::backend_type::BackendType;
33
use crate::cli::args::BackendArg;
44
use crate::config::SETTINGS;
5-
use crate::env::GITHUB_TOKEN;
5+
use crate::env::{
6+
GITHUB_TOKEN, GITLAB_TOKEN, MISE_GITHUB_ENTERPRISE_TOKEN, MISE_GITLAB_ENTERPRISE_TOKEN,
7+
};
68
use crate::install_context::InstallContext;
79
use crate::plugins::VERSION_REGEX;
810
use crate::tokio::RUNTIME;
911
use crate::toolset::ToolVersion;
10-
use crate::{file, github, hash};
12+
use crate::{file, github, gitlab, hash};
1113
use eyre::bail;
1214
use itertools::Itertools;
1315
use regex::Regex;
@@ -39,14 +41,43 @@ impl Backend for UbiBackend {
3941
Ok(vec!["latest".to_string()])
4042
} else {
4143
let opts = self.ba.opts();
44+
let forge = match opts.get("provider") {
45+
Some(forge) => ForgeType::from_str(forge)?,
46+
None => ForgeType::default(),
47+
};
48+
let api_url = match opts.get("api_url") {
49+
Some(api_url) => api_url,
50+
None => match forge {
51+
ForgeType::GitHub => github::API_URL,
52+
ForgeType::GitLab => gitlab::API_URL,
53+
},
54+
};
4255
let tag_regex = OnceLock::new();
43-
let mut versions = github::list_releases(&self.tool_name())?
44-
.into_iter()
45-
.map(|r| r.tag_name)
46-
.collect::<Vec<String>>();
56+
let mut versions = match forge {
57+
ForgeType::GitHub => github::list_releases_from_url(api_url, &self.tool_name())?
58+
.into_iter()
59+
.map(|r| r.tag_name)
60+
.collect::<Vec<String>>(),
61+
ForgeType::GitLab => gitlab::list_releases_from_url(api_url, &self.tool_name())?
62+
.into_iter()
63+
.map(|r| r.tag_name)
64+
.collect::<Vec<String>>(),
65+
};
4766
if versions.is_empty() {
48-
versions = github::list_tags(&self.tool_name())?.into_iter().collect();
67+
match forge {
68+
ForgeType::GitHub => {
69+
versions = github::list_tags_from_url(api_url, &self.tool_name())?
70+
.into_iter()
71+
.collect();
72+
}
73+
ForgeType::GitLab => {
74+
versions = gitlab::list_tags_from_url(api_url, &self.tool_name())?
75+
.into_iter()
76+
.collect();
77+
}
78+
}
4979
}
80+
5081
Ok(versions
5182
.into_iter()
5283
// trim 'v' prefixes if they exist
@@ -75,10 +106,29 @@ impl Backend for UbiBackend {
75106
) -> eyre::Result<ToolVersion> {
76107
let mut v = tv.version.to_string();
77108
let opts = tv.request.options();
109+
let forge = match opts.get("provider") {
110+
Some(forge) => ForgeType::from_str(forge)?,
111+
None => ForgeType::default(),
112+
};
113+
let api_url = match opts.get("api_url") {
114+
Some(api_url) => api_url,
115+
None => match forge {
116+
ForgeType::GitHub => github::API_URL,
117+
ForgeType::GitLab => gitlab::API_URL,
118+
},
119+
};
78120
let extract_all = opts.get("extract_all").is_some_and(|v| v == "true");
79121
let bin_dir = tv.install_path();
80122

81-
if let Err(err) = github::get_release(&self.tool_name(), &tv.version) {
123+
let release: Result<_, eyre::Report> = match forge {
124+
ForgeType::GitHub => {
125+
github::get_release_for_url(api_url, &self.tool_name(), &v).map(|_| "github")
126+
}
127+
ForgeType::GitLab => {
128+
gitlab::get_release_for_url(api_url, &self.tool_name(), &v).map(|_| "gitlab")
129+
}
130+
};
131+
if let Err(err) = release {
82132
// this can fail with a rate limit error or 404, either way, try prefixing and if it fails, try without the prefix
83133
// if http::error_code(&err) == Some(404) {
84134
debug!(
@@ -95,10 +145,6 @@ impl Backend for UbiBackend {
95145

96146
let mut builder = UbiBuilder::new().project(&name).install_dir(&bin_dir);
97147

98-
if let Some(token) = &*GITHUB_TOKEN {
99-
builder = builder.token(token);
100-
}
101-
102148
if v != "latest" {
103149
builder = builder.tag(v);
104150
}
@@ -116,8 +162,19 @@ impl Backend for UbiBackend {
116162
if let Some(matching) = opts.get("matching") {
117163
builder = builder.matching(matching);
118164
}
119-
if let Some(forge) = opts.get("forge") {
120-
builder = builder.forge(ForgeType::from_str(forge)?);
165+
166+
let forge = match opts.get("provider") {
167+
Some(forge) => ForgeType::from_str(forge)?,
168+
None => ForgeType::default(),
169+
};
170+
builder = builder.forge(forge.clone());
171+
builder = set_token(builder, &forge);
172+
173+
if let Some(api_url) = opts.get("api_url") {
174+
if !api_url.contains("github.com") && !api_url.contains("gitlab.com") {
175+
builder = builder.api_base_url(api_url);
176+
builder = set_enterprise_token(builder, &forge);
177+
}
121178
}
122179

123180
let mut ubi = builder.build().map_err(|e| eyre::eyre!(e))?;
@@ -247,3 +304,37 @@ impl UbiBackend {
247304
fn name_is_url(n: &str) -> bool {
248305
n.starts_with("http")
249306
}
307+
308+
fn set_token<'a>(mut builder: UbiBuilder<'a>, forge: &ForgeType) -> UbiBuilder<'a> {
309+
match forge {
310+
ForgeType::GitHub => {
311+
if let Some(token) = &*GITHUB_TOKEN {
312+
builder = builder.token(token)
313+
}
314+
builder
315+
}
316+
ForgeType::GitLab => {
317+
if let Some(token) = &*GITLAB_TOKEN {
318+
builder = builder.token(token)
319+
}
320+
builder
321+
}
322+
}
323+
}
324+
325+
fn set_enterprise_token<'a>(mut builder: UbiBuilder<'a>, forge: &ForgeType) -> UbiBuilder<'a> {
326+
match forge {
327+
ForgeType::GitHub => {
328+
if let Some(token) = &*MISE_GITHUB_ENTERPRISE_TOKEN {
329+
builder = builder.token(token);
330+
}
331+
builder
332+
}
333+
ForgeType::GitLab => {
334+
if let Some(token) = &*MISE_GITLAB_ENTERPRISE_TOKEN {
335+
builder = builder.token(token);
336+
}
337+
builder
338+
}
339+
}
340+
}

src/cli/args/backend_arg.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ impl BackendArg {
177177
})
178178
}
179179

180+
pub fn set_opts(&mut self, opts: ToolVersionOptions) {
181+
self.opts = Some(opts);
182+
}
183+
180184
pub fn tool_name(&self) -> String {
181185
let full = self.full();
182186
let (_backend, tool_name) = full.split_once(':').unwrap_or(("", &full));

src/env.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,43 @@ pub static GITHUB_TOKEN: Lazy<Option<String>> = Lazy::new(|| {
223223

224224
token
225225
});
226+
pub static MISE_GITHUB_ENTERPRISE_TOKEN: Lazy<Option<String>> =
227+
Lazy::new(|| match var("MISE_GITHUB_ENTERPRISE_TOKEN") {
228+
Ok(v) if v.trim() != "" => {
229+
set_var("MISE_GITHUB_ENTERPRISE_TOKEN", &v);
230+
Some(v)
231+
}
232+
_ => {
233+
remove_var("MISE_GITHUB_ENTERPRISE_TOKEN");
234+
None
235+
}
236+
});
237+
pub static GITLAB_TOKEN: Lazy<Option<String>> =
238+
Lazy::new(
239+
|| match var("MISE_GITLAB_TOKEN").or_else(|_| var("GITLAB_TOKEN")) {
240+
Ok(v) if v.trim() != "" => {
241+
set_var("MISE_GITLAB_TOKEN", &v);
242+
set_var("GITLAB_TOKEN", &v);
243+
Some(v)
244+
}
245+
_ => {
246+
remove_var("MISE_GITLAB_TOKEN");
247+
remove_var("GITLAB_TOKEN");
248+
None
249+
}
250+
},
251+
);
252+
pub static MISE_GITLAB_ENTERPRISE_TOKEN: Lazy<Option<String>> =
253+
Lazy::new(|| match var("MISE_GITLAB_ENTERPRISE_TOKEN") {
254+
Ok(v) if v.trim() != "" => {
255+
set_var("MISE_GITLAB_ENTERPRISE_TOKEN", &v);
256+
Some(v)
257+
}
258+
_ => {
259+
remove_var("MISE_GITLAB_ENTERPRISE_TOKEN");
260+
None
261+
}
262+
});
226263

227264
pub static TEST_TRANCHE: Lazy<usize> = Lazy::new(|| var_u8("TEST_TRANCHE") as usize);
228265
pub static TEST_TRANCHE_COUNT: Lazy<usize> = Lazy::new(|| var_u8("TEST_TRANCHE_COUNT") as usize);

src/github.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ static RELEASE_CACHE: Lazy<RwLock<CacheGroup<GithubRelease>>> = Lazy::new(Defaul
4242

4343
static TAGS_CACHE: Lazy<RwLock<CacheGroup<Vec<String>>>> = Lazy::new(Default::default);
4444

45+
pub static API_URL: &str = "https://api.github.com/repos";
46+
4547
fn get_tags_cache(key: &str) -> RwLockReadGuard<'_, CacheGroup<Vec<String>>> {
4648
TAGS_CACHE
4749
.write()
@@ -85,11 +87,22 @@ pub fn list_releases(repo: &str) -> Result<Vec<GithubRelease>> {
8587
let key = repo.to_kebab_case();
8688
let cache = get_releases_cache(&key);
8789
let cache = cache.get(&key).unwrap();
88-
Ok(cache.get_or_try_init(|| list_releases_(repo))?.to_vec())
90+
Ok(cache
91+
.get_or_try_init(|| list_releases_(API_URL, repo))?
92+
.to_vec())
8993
}
9094

91-
fn list_releases_(repo: &str) -> Result<Vec<GithubRelease>> {
92-
let url = format!("https://api.github.com/repos/{repo}/releases");
95+
pub fn list_releases_from_url(api_url: &str, repo: &str) -> Result<Vec<GithubRelease>> {
96+
let key = format!("{api_url}-{repo}").to_kebab_case();
97+
let cache = get_releases_cache(&key);
98+
let cache = cache.get(&key).unwrap();
99+
Ok(cache
100+
.get_or_try_init(|| list_releases_(api_url, repo))?
101+
.to_vec())
102+
}
103+
104+
fn list_releases_(api_url: &str, repo: &str) -> Result<Vec<GithubRelease>> {
105+
let url = format!("{api_url}/{repo}/releases");
93106
let (mut releases, mut headers) =
94107
crate::http::HTTP_FETCH.json_headers::<Vec<GithubRelease>, _>(url)?;
95108

@@ -109,11 +122,22 @@ pub fn list_tags(repo: &str) -> Result<Vec<String>> {
109122
let key = repo.to_kebab_case();
110123
let cache = get_tags_cache(&key);
111124
let cache = cache.get(&key).unwrap();
112-
Ok(cache.get_or_try_init(|| list_tags_(repo))?.to_vec())
125+
Ok(cache
126+
.get_or_try_init(|| list_tags_(API_URL, repo))?
127+
.to_vec())
113128
}
114129

115-
fn list_tags_(repo: &str) -> Result<Vec<String>> {
116-
let url = format!("https://api.github.com/repos/{}/tags", repo);
130+
pub fn list_tags_from_url(api_url: &str, repo: &str) -> Result<Vec<String>> {
131+
let key = format!("{api_url}-{repo}").to_kebab_case();
132+
let cache = get_tags_cache(&key);
133+
let cache = cache.get(&key).unwrap();
134+
Ok(cache
135+
.get_or_try_init(|| list_tags_(api_url, repo))?
136+
.to_vec())
137+
}
138+
139+
fn list_tags_(api_url: &str, repo: &str) -> Result<Vec<String>> {
140+
let url = format!("{api_url}/{repo}/tags");
117141
let (mut tags, mut headers) = crate::http::HTTP_FETCH.json_headers::<Vec<GithubTag>, _>(url)?;
118142

119143
if *env::MISE_LIST_ALL_VERSIONS {
@@ -131,11 +155,22 @@ pub fn get_release(repo: &str, tag: &str) -> Result<GithubRelease> {
131155
let key = format!("{repo}-{tag}").to_kebab_case();
132156
let cache = get_release_cache(&key);
133157
let cache = cache.get(&key).unwrap();
134-
Ok(cache.get_or_try_init(|| get_release_(repo, tag))?.clone())
158+
Ok(cache
159+
.get_or_try_init(|| get_release_(API_URL, repo, tag))?
160+
.clone())
161+
}
162+
163+
pub fn get_release_for_url(api_url: &str, repo: &str, tag: &str) -> Result<GithubRelease> {
164+
let key = format!("{api_url}-{repo}-{tag}").to_kebab_case();
165+
let cache = get_release_cache(&key);
166+
let cache = cache.get(&key).unwrap();
167+
Ok(cache
168+
.get_or_try_init(|| get_release_(api_url, repo, tag))?
169+
.clone())
135170
}
136171

137-
fn get_release_(repo: &str, tag: &str) -> Result<GithubRelease> {
138-
let url = format!("https://api.github.com/repos/{repo}/releases/tags/{tag}");
172+
fn get_release_(api_url: &str, repo: &str, tag: &str) -> Result<GithubRelease> {
173+
let url = format!("{api_url}/{repo}/releases/tags/{tag}");
139174
crate::http::HTTP_FETCH.json(url)
140175
}
141176

0 commit comments

Comments
 (0)