Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 4 additions & 53 deletions src/backend/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::backend::asset_detector;
use crate::backend::backend_type::BackendType;
use crate::backend::static_helpers::lookup_platform_key;
use crate::backend::static_helpers::{
get_filename_from_url, install_artifact, template_string, verify_artifact,
get_filename_from_url, install_artifact, template_string, try_with_v_prefix, verify_artifact,
};
use crate::cli::args::BackendArg;
use crate::config::Config;
Expand Down Expand Up @@ -206,56 +206,6 @@ impl UnifiedGitBackend {
}
}

/// Helper to try both prefixed and non-prefixed tags for a resolver function
async fn try_with_v_prefix<F, Fut>(&self, version: &str, resolver: F) -> Result<String>
where
F: Fn(String) -> Fut,
Fut: std::future::Future<Output = Result<String>>,
{
let mut errors = vec![];
let opts = self.ba.opts();

// Generate candidates based on version prefix configuration
let candidates = if let Some(prefix) = opts.get("version_prefix") {
// If a custom prefix is configured, try both prefixed and non-prefixed versions
if version.starts_with(prefix) {
vec![
version.to_string(),
version.trim_start_matches(prefix).to_string(),
]
} else {
vec![format!("{}{}", prefix, version), version.to_string()]
}
} else {
// Fall back to 'v' prefix logic
if version.starts_with('v') {
vec![
version.to_string(),
version.trim_start_matches('v').to_string(),
]
} else {
vec![format!("v{version}"), version.to_string()]
}
};

for candidate in candidates {
match resolver(candidate.clone()).await {
Ok(url) => return Ok(url),
Err(e) => {
let is_404 = crate::http::error_code(&e) == Some(404);
if is_404 {
errors.push(e);
} else {
return Err(e);
}
}
}
}
Err(errors
.pop()
.unwrap_or_else(|| eyre::eyre!("No matching release found for {version}")))
}

/// Resolves the asset URL using either explicit patterns or auto-detection
async fn resolve_asset_url(
&self,
Expand All @@ -270,14 +220,15 @@ impl UnifiedGitBackend {
}

let version = &tv.version;
let version_prefix = opts.get("version_prefix").map(|s| s.as_str());
if self.is_gitlab() {
self.try_with_v_prefix(version, |candidate| async move {
try_with_v_prefix(version, version_prefix, |candidate| async move {
self.resolve_gitlab_asset_url(tv, opts, repo, api_url, &candidate)
.await
})
.await
} else {
self.try_with_v_prefix(version, |candidate| async move {
try_with_v_prefix(version, version_prefix, |candidate| async move {
self.resolve_github_asset_url(tv, opts, repo, api_url, &candidate)
.await
})
Expand Down
53 changes: 53 additions & 0 deletions src/backend/static_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,59 @@ use crate::ui::progress_report::SingleReport;
use eyre::{Result, bail};
use std::path::Path;

/// Helper to try both prefixed and non-prefixed tags for a resolver function
pub async fn try_with_v_prefix<F, Fut, T>(
version: &str,
version_prefix: Option<&str>,
resolver: F,
) -> Result<T>
where
F: Fn(String) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
let mut errors = vec![];

// Generate candidates based on version prefix configuration
let candidates = if let Some(prefix) = version_prefix {
// If a custom prefix is configured, try both prefixed and non-prefixed versions
if version.starts_with(prefix) {
vec![
version.to_string(),
version.trim_start_matches(prefix).to_string(),
]
} else {
vec![format!("{}{}", prefix, version), version.to_string()]
}
} else {
// Fall back to 'v' prefix logic
if version.starts_with('v') {
vec![
version.to_string(),
version.trim_start_matches('v').to_string(),
]
} else {
vec![format!("v{version}"), version.to_string()]
}
};

for candidate in candidates {
match resolver(candidate.clone()).await {
Ok(res) => return Ok(res),
Err(e) => {
let is_404 = crate::http::error_code(&e) == Some(404);
if is_404 {
errors.push(e);
} else {
return Err(e);
}
}
}
}
Err(errors
.pop()
.unwrap_or_else(|| eyre::eyre!("No matching release found for {version}")))
Comment on lines +58 to +60
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should use 'version' directly instead of string interpolation syntax since this is in a closure context where version is captured.

Suggested change
Err(errors
.pop()
.unwrap_or_else(|| eyre::eyre!("No matching release found for {version}")))
.unwrap_or_else(|| eyre::eyre!(format!("No matching release found for {}", version))))

Copilot uses AI. Check for mistakes.
}

/// Returns all possible aliases for the current platform (os, arch),
/// with the preferred spelling first (macos/x64, linux/x64, etc).
pub fn platform_aliases() -> Vec<(String, String)> {
Expand Down
61 changes: 19 additions & 42 deletions src/backend/ubi.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::backend::backend_type::BackendType;
use crate::backend::static_helpers::try_with_v_prefix;
use crate::cli::args::BackendArg;
use crate::config::{Config, Settings};
use crate::env::{
Expand Down Expand Up @@ -108,57 +109,33 @@ impl Backend for UbiBackend {
ctx: &InstallContext,
mut tv: ToolVersion,
) -> eyre::Result<ToolVersion> {
let mut v = tv.version.to_string();
let v = tv.version.to_string();
let opts = tv.request.options();
let forge = match opts.get("provider") {
Some(forge) => ForgeType::from_str(forge)?,
None => ForgeType::default(),
};
let api_url = match opts.get("api_url") {
Some(api_url) => api_url.strip_suffix("/").unwrap_or(api_url),
None => match forge {
ForgeType::GitHub => github::API_URL,
ForgeType::GitLab => gitlab::API_URL,
},
};
let bin_path = opts
.get("bin_path")
.cloned()
.unwrap_or_else(|| "bin".to_string());
let extract_all = opts.get("extract_all").is_some_and(|v| v == "true");
let bin_dir = tv.install_path();

if !name_is_url(&self.tool_name()) {
let release: Result<_, eyre::Report> = match forge {
ForgeType::GitHub => github::get_release_for_url(api_url, &self.tool_name(), &v)
.await
.map(|_| "github"),
ForgeType::GitLab => gitlab::get_release_for_url(api_url, &self.tool_name(), &v)
if name_is_url(&self.tool_name()) {
install(&self.tool_name(), &v, &bin_dir, extract_all, &opts).await?;
} else {
try_with_v_prefix(&v, None, |candidate| {
let opts = opts.clone();
let bin_dir = bin_dir.clone();
async move {
install(
&self.tool_name(),
&candidate,
&bin_dir,
extract_all,
&opts,
)
.await
.map(|_| "gitlab"),
};
if let Err(err) = release {
// this can fail with a rate limit error or 404, either way, try prefixing and if it fails, try without the prefix
// if http::error_code(&err) == Some(404) {
debug!(
"Failed to get release for {}, trying with 'v' prefix: {}",
tv, err
);
v = format!("v{v}");
// }
}
}

if let Err(err) = install(&self.tool_name(), &v, &bin_dir, extract_all, &opts).await {
debug!(
"Failed to install with ubi version '{}': {}, trying with '{}'",
v, err, tv
);
if let Err(err) =
install(&self.tool_name(), &tv.version, &bin_dir, extract_all, &opts).await
{
bail!("Failed to install with ubi '{}': {}", tv, err);
}
}
})
.await?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: UBI Backend Ignores Custom Version Prefix

The UBI backend's install_version_ function now ignores custom version_prefix options, always defaulting to 'v' when retrying. The version retry logic, previously designed to attempt prefixed versions on any installation error (like rate limits or 404s), now only retries specifically on HTTP 404 errors, reducing resilience.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was as is, tag_regex seems not to be used to pick the version up.

}

let mut possible_exes = vec![
Expand Down
Loading