diff --git a/Changes.md b/Changes.md index afda852..739dd1a 100644 --- a/Changes.md +++ b/Changes.md @@ -7,6 +7,9 @@ with a new `UbiBuilder::forge` method. - When looking for macOS assets, `ubi` will now match against `macosx` in asset names, not just `macos` and `osx`. Implemented by @kattouf (Vasiliy Kattouf). GH #80. +- Added a new `--api-url-base` CLI argument. Requested by Olaf Alders. GH #69. +- Renamed the `UbiBuilder::url_base` method to `api_base_url` and changed it to take a `&str` + instead of a `String`, which is consistent with all the other builder methods. ## 0.2.4 - 2024-11-24 diff --git a/README.md b/README.md index 85cc7c4..0c5db53 100644 --- a/README.md +++ b/README.md @@ -78,34 +78,41 @@ current directory. Usage: ubi [OPTIONS] Options: - -p, --project The project you want to install, like houseabsolute/precious or - https://github.com/houseabsolute/precious. - -t, --tag The tag to download. Defaults to the latest release. - -u, --url The url of the file to download. This can be provided instead of a - project or tag. This will not use the forge site's API, so you will - never hit its API limits. With this parameter, you do not need to set a - token env var except for private repos. - --self-upgrade Use ubi to upgrade to the latest version of ubi. The --exe, --in, - --project, --tag, and --url args will be ignored. - -i, --in The directory in which the binary should be placed. Defaults to ./bin. - -e, --exe The name of this project's executable. By default this is the same as - the project name, so for houseabsolute/precious we look for precious or - precious.exe. When running on Windows the ".exe" suffix will be added - as needed. - -m, --matching A string that will be matched against the release filename when there - are multiple matching files for your OS/arch. For example, there may be - multiple releases for an OS/arch that differ by compiler (MSVC vs. gcc) - or linked libc (glibc vs. musl). Note that this will be ignored if - there is only one matching release filename for your OS/arch. - --forge The forge to use. If this isn't set, then the value of --project or - --url will be checked for gitlab.com. If this contains any other domain - _or_ if it does not have a domain at all, then the default is GitHub. - [possible values: github, gitlab] - -v, --verbose Enable verbose output. - -d, --debug Enable debugging output. - -q, --quiet Suppresses most output. - -h, --help Print help - -V, --version Print version + -p, --project The project you want to install, like houseabsolute/precious or + https://github.com/houseabsolute/precious. + -t, --tag The tag to download. Defaults to the latest release. + -u, --url The url of the file to download. This can be provided instead + of a project or tag. This will not use the forge site's API, so + you will never hit its API limits. With this parameter, you do + not need to set a token env var except for private repos. + --self-upgrade Use ubi to upgrade to the latest version of ubi. The --exe, + --in, --project, --tag, and --url args will be ignored. + -i, --in The directory in which the binary should be placed. Defaults to + ./bin. + -e, --exe The name of this project's executable. By default this is the + same as the project name, so for houseabsolute/precious we look + for precious or precious.exe. When running on Windows the + ".exe" suffix will be added as needed. + -m, --matching A string that will be matched against the release filename when + there are multiple matching files for your OS/arch. For + example, there may be multiple releases for an OS/arch that + differ by compiler (MSVC vs. gcc) or linked libc (glibc vs. + musl). Note that this will be ignored if there is only one + matching release filename for your OS/arch. + --forge The forge to use. If this isn't set, then the value of + --project or --url will be checked for gitlab.com. If this + contains any other domain _or_ if it does not have a domain at + all, then the default is GitHub. [possible values: github, + gitlab] + --api-base-url The the base URL for the forge site's API. This is useful for + testing or if you want to operate against an Enterprise version + of GitHub or GitLab. This should be something like + `https://github.my-corp.example.com/api/v4`. + -v, --verbose Enable verbose output. + -d, --debug Enable debugging output. + -q, --quiet Suppresses most output. + -h, --help Print help + -V, --version Print version ``` ## Using a Forge Token @@ -244,6 +251,11 @@ parameter, then you should use the `--tag` parameter to specify the released ver install. Otherwise `ubi` will always download the latest version, which can lead to surprises, especially if you are running the tools you download in CI. +## Using `ubi` with GitHub Enterprise or GitLab for Enterprise + +The command line tool takes an `--api-base-url` flag for this purpose. This should be the full URL +to the root of the API, something like `https://github.my-corp.example.com/api/v4`. + ## Why This Is Useful With the rise of Go and Rust, it has become increasingly common for very useful tools like diff --git a/ubi-cli/src/main.rs b/ubi-cli/src/main.rs index f78222f..85be26a 100644 --- a/ubi-cli/src/main.rs +++ b/ubi-cli/src/main.rs @@ -128,6 +128,11 @@ fn cmd() -> Command { " does not have a domain at all, then the default is GitHub.", )), ) + .arg(Arg::new("api-base-url").long("api-base-url").help(concat!( + "The the base URL for the forge site's API. This is useful for testing or if", + " you want to operate against an Enterprise version of GitHub or GitLab. This", + " should be something like `https://github.my-corp.example.com/api/v4`.", + ))) .arg( Arg::new("verbose") .short('v') @@ -198,6 +203,9 @@ fn make_ubi<'a>( if let Some(ft) = matches.get_one::("forge") { builder = builder.forge(ForgeType::from_str(ft)?); } + if let Some(url) = matches.get_one::("api-base-url") { + builder = builder.api_base_url(url); + } Ok((builder.build()?, None)) } diff --git a/ubi/src/builder.rs b/ubi/src/builder.rs index 71fdfe5..7953edf 100644 --- a/ubi/src/builder.rs +++ b/ubi/src/builder.rs @@ -32,7 +32,7 @@ pub struct UbiBuilder<'a> { gitlab_token: Option<&'a str>, platform: Option<&'a Platform>, is_musl: Option, - url_base: Option, + api_base_url: Option<&'a str>, forge: Option, } @@ -146,11 +146,12 @@ impl<'a> UbiBuilder<'a> { self } - /// Set the base URL for the forge site's API. This is useful for testing or if you want to operate - /// against an Enterprise version of GitHub or GitLab. + /// Set the base URL for the forge site's API. This is useful for testing or if you want to + /// operate against an Enterprise version of GitHub or GitLab. This should be something like + /// `https://github.my-corp.example.com/api/v4`. #[must_use] - pub fn url_base(mut self, url_base: String) -> Self { - self.url_base = Some(url_base); + pub fn api_base_url(mut self, api_base_url: &'a str) -> Self { + self.api_base_url = Some(api_base_url); self } @@ -193,7 +194,7 @@ impl<'a> UbiBuilder<'a> { } fn new_forge(&self, project_name: String, forge_type: &ForgeType) -> Result> { - let api_base = self.url_base.as_deref().map(Url::parse).transpose()?; + let api_base = self.api_base_url.map(Url::parse).transpose()?; Ok(match forge_type { ForgeType::GitHub => Box::new(GitHub::new( project_name, diff --git a/ubi/src/github.rs b/ubi/src/github.rs index 4625805..be08c0c 100644 --- a/ubi/src/github.rs +++ b/ubi/src/github.rs @@ -17,7 +17,7 @@ use url::Url; pub(crate) struct GitHub { project_name: String, tag: Option, - api_base: Url, + api_base_url: Url, token: Option, } @@ -42,7 +42,7 @@ impl Forge for GitHub { let owner = parts.next().unwrap(); let repo = parts.next().unwrap(); - let mut url = self.api_base.clone(); + let mut url = self.api_base_url.clone(); url.path_segments_mut() .expect("could not get path segments for url") .push("repos") @@ -90,7 +90,7 @@ impl GitHub { Self { project_name, tag, - api_base: api_base.unwrap_or_else(|| ForgeType::GitHub.api_base()), + api_base_url: api_base.unwrap_or_else(|| ForgeType::GitHub.api_base()), token, } } @@ -171,4 +171,19 @@ mod tests { Ok(()) } + + #[test] + fn api_base_url() { + let github = GitHub::new( + "houseabsolute/ubi".to_string(), + None, + Some(Url::parse("https://github.example.com/api/v4").unwrap()), + None, + ); + let url = github.release_info_url(); + assert_eq!( + url.as_str(), + "https://github.example.com/api/v4/repos/houseabsolute/ubi/releases/latest" + ); + } } diff --git a/ubi/src/gitlab.rs b/ubi/src/gitlab.rs index 172e5a9..4457455 100644 --- a/ubi/src/gitlab.rs +++ b/ubi/src/gitlab.rs @@ -14,7 +14,7 @@ use url::Url; pub(crate) struct GitLab { project_name: String, tag: Option, - api_base: Url, + api_base_url: Url, token: Option, } @@ -41,7 +41,7 @@ impl Forge for GitLab { } fn release_info_url(&self) -> Url { - let mut url = self.api_base.clone(); + let mut url = self.api_base_url.clone(); url.path_segments_mut() .expect("could not get path segments for url") .push("projects") @@ -92,7 +92,7 @@ impl GitLab { Self { project_name, tag, - api_base: api_base.unwrap_or_else(|| ForgeType::GitLab.api_base()), + api_base_url: api_base.unwrap_or_else(|| ForgeType::GitLab.api_base()), token, } } @@ -176,4 +176,19 @@ mod tests { Ok(()) } + + #[test] + fn api_base_url() { + let gitlab = GitLab::new( + "houseabsolute/ubi".to_string(), + None, + Some(Url::parse("https://gitlab.example.com/api/v4").unwrap()), + None, + ); + let url = gitlab.release_info_url(); + assert_eq!( + url.as_str(), + "https://gitlab.example.com/api/v4/projects/houseabsolute%2Fubi/releases/permalink/latest" + ); + } } diff --git a/ubi/src/test.rs b/ubi/src/test.rs index 566b4a3..d704a60 100644 --- a/ubi/src/test.rs +++ b/ubi/src/test.rs @@ -145,6 +145,7 @@ async fn asset_picking() -> Result<()> { ]; let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/houseabsolute/ubi/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -173,7 +174,7 @@ async fn asset_picking() -> Result<()> { .project("houseabsolute/ubi") .platform(platform) .is_musl(false) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; let expect_ubi_url = Url::parse(&format!( @@ -196,7 +197,7 @@ async fn asset_picking() -> Result<()> { .project("houseabsolute/omegasort") .platform(platform) .is_musl(false) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; let expect_omegasort_url = Url::parse(&format!( @@ -510,6 +511,7 @@ async fn matching_unusual_names() -> Result<()> { ]; let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/protocolbuffers/protobuf/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -527,7 +529,7 @@ async fn matching_unusual_names() -> Result<()> { let mut ubi = UbiBuilder::new() .project("protocolbuffers/protobuf") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; assert_eq!( @@ -633,6 +635,7 @@ async fn mkcert_matching() -> Result<()> { ]; let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/FiloSottile/mkcert/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -650,7 +653,7 @@ async fn mkcert_matching() -> Result<()> { let mut ubi = UbiBuilder::new() .project("FiloSottile/mkcert") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; assert_eq!( @@ -727,6 +730,7 @@ async fn jq_matching() -> Result<()> { ]; let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/stedolan/jq/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -744,7 +748,7 @@ async fn jq_matching() -> Result<()> { let mut ubi = UbiBuilder::new() .project("stedolan/jq") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; assert_eq!( @@ -799,6 +803,7 @@ async fn multiple_matches() -> Result<()> { let platforms = ["x86_64-pc-windows-gnu", "i686-pc-windows-gnu"]; let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/test/multiple-matches/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -815,7 +820,7 @@ async fn multiple_matches() -> Result<()> { let mut ubi = UbiBuilder::new() .project("test/multiple-matches") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; let expect = "mm-i686-pc-windows-gnu.zip"; @@ -844,6 +849,7 @@ const MULTIPLE_MATCHES_RESPONSE: &str = r#" #[test(tokio::test)] async fn macos_arm() -> Result<()> { let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/test/macos/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -860,7 +866,7 @@ async fn macos_arm() -> Result<()> { let mut ubi = UbiBuilder::new() .project("test/macos") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; { @@ -933,6 +939,7 @@ const MACOS_RESPONSE2: &str = r#" async fn os_without_arch() -> Result<()> { { let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/test/os-without-arch/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -949,7 +956,7 @@ async fn os_without_arch() -> Result<()> { let mut ubi = UbiBuilder::new() .project("test/os-without-arch") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await?; let expect = "gvproxy-darwin"; @@ -960,6 +967,7 @@ async fn os_without_arch() -> Result<()> { { let mut server = Server::new_async().await; + let url = server.url(); let m1 = server .mock("GET", "/repos/test/os-without-arch/releases/latest") .match_header(ACCEPT.as_str(), "application/json") @@ -976,7 +984,7 @@ async fn os_without_arch() -> Result<()> { let mut ubi = UbiBuilder::new() .project("test/os-without-arch") .platform(platform) - .url_base(server.url()) + .api_base_url(&url) .build()?; let asset = ubi.asset().await; assert!(