From be8e2ee0ad4475c7ba40b793021ccb1f71f0c254 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Wed, 15 Apr 2026 01:51:31 -0400 Subject: [PATCH 1/3] fix(go): honor install_before for module versions Populate Go module version timestamps from proxy and go list metadata so date filters can pick an older release correctly. Add an explicit backend e2e regression covering local install_before precedence over the global setting. --- .../test_install_before_explicit_go_backend | 24 +++ src/backend/go.rs | 161 +++++++++++++++--- 2 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 e2e/cli/test_install_before_explicit_go_backend diff --git a/e2e/cli/test_install_before_explicit_go_backend b/e2e/cli/test_install_before_explicit_go_backend new file mode 100644 index 0000000000..06e1b9abbe --- /dev/null +++ b/e2e/cli/test_install_before_explicit_go_backend @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +export GOPROXY="https://proxy.golang.org,direct" + +cat <<'EOF' >mise.toml +[settings] +install_before = "2100-01-01" + +[tools] +'go:github.com/roborev-dev/roborev/cmd/roborev' = { version = "latest", install_before = "2026-04-07" } +EOF + +assert_contains "mise install --dry-run 2>&1" "go:github.com/roborev-dev/roborev/cmd/roborev@0.50.0" + +cat <<'EOF' >mise.toml +[settings] +install_before = "2026-04-07" + +[tools] +'go:github.com/roborev-dev/roborev/cmd/roborev' = { version = "latest", install_before = "2100-01-01" } +EOF + +assert_contains "mise install --dry-run 2>&1" "go:github.com/roborev-dev/roborev/cmd/roborev@0.51.0" diff --git a/src/backend/go.rs b/src/backend/go.rs index 37b49c7eb1..5fbd5c43c2 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -210,13 +210,8 @@ impl GoBackend { let path = &candidates[*idx]; match result { ProxyListResult::Versions(versions) if !versions.is_empty() => { - let mut version_infos: Vec = versions - .iter() - .map(|v| VersionInfo { - version: v.trim_start_matches('v').to_string(), - ..Default::default() - }) - .collect(); + let mut version_infos = + fetch_proxy_version_infos(&proxies, path, versions).await; version_infos.retain(|v| Versioning::new(&v.version).is_some()); version_infos.sort_by_cached_key(|v| Versioning::new(&v.version)); return Ok(Some(version_infos)); @@ -225,9 +220,11 @@ impl GoBackend { // Check if @latest resolves (module using pseudo-versions) let encoded = encode_module_path(path); match query_proxy_latest(&proxies, &encoded).await { - ProxyListResult::Versions(_) => return Ok(Some(vec![])), - ProxyListResult::NotFound => continue, - ProxyListResult::Error => return Ok(None), + ProxyVersionInfoResult::Found(info) => { + return Ok(Some(vec![version_info_from_metadata(info)])); + } + ProxyVersionInfoResult::NotFound => continue, + ProxyVersionInfoResult::Error => return Ok(None), } } ProxyListResult::NotFound => continue, @@ -278,21 +275,55 @@ impl GoBackend { Err(_) => return Ok(None), }; - // remove the leading v from the versions - let versions = mod_info - .versions - .into_iter() - .map(|v| VersionInfo { - version: v.trim_start_matches('v').to_string(), - ..Default::default() - }) - .collect(); + let versions = self + .fetch_go_module_version_infos(config, mod_path, &mod_info.versions) + .await; Ok(Some(versions)) }) .await .cloned() } + + async fn fetch_go_module_version_infos( + &self, + config: &Arc, + mod_path: &str, + versions: &[String], + ) -> Vec { + let mut infos = Vec::with_capacity(versions.len()); + let env = match self.dependency_env(config).await { + Ok(env) => env, + Err(_) => { + return versions + .iter() + .map(|version| VersionInfo { + version: version.trim_start_matches('v').to_string(), + ..Default::default() + }) + .collect(); + } + }; + + for version in versions { + let module = format!("{mod_path}@{version}"); + let info = cmd!("go", "list", "-mod=readonly", "-m", "-json", module) + .full_env(env.clone()) + .read() + .ok() + .and_then(|raw| serde_json::from_str::(&raw).ok()); + + infos.push(match info { + Some(info) => version_info_from_metadata(info), + None => VersionInfo { + version: version.trim_start_matches('v').to_string(), + ..Default::default() + }, + }); + } + + infos + } } enum ProxyListResult { @@ -301,6 +332,12 @@ enum ProxyListResult { Error, } +enum ProxyVersionInfoResult { + Found(GoModuleVersionMetadata), + NotFound, + Error, +} + #[derive(Clone, Debug, PartialEq)] enum FallThrough { OnNotFound, @@ -344,11 +381,21 @@ async fn query_proxy_list(proxies: &[GoProxy], encoded_path: &str) -> ProxyListR ProxyListResult::NotFound } -async fn query_proxy_latest(proxies: &[GoProxy], encoded_path: &str) -> ProxyListResult { +async fn query_proxy_latest(proxies: &[GoProxy], encoded_path: &str) -> ProxyVersionInfoResult { + query_proxy_version_metadata(proxies, &format!("{encoded_path}/@latest")).await +} + +async fn query_proxy_version_metadata( + proxies: &[GoProxy], + endpoint: &str, +) -> ProxyVersionInfoResult { for proxy in proxies { - let url = format!("{}/{}/@latest", proxy.url, encoded_path); + let url = format!("{}/{}", proxy.url, endpoint); match HTTP_FETCH.get_text(&url).await { - Ok(_) => return ProxyListResult::Versions(vec![]), + Ok(body) => match serde_json::from_str::(&body) { + Ok(info) => return ProxyVersionInfoResult::Found(info), + Err(_) => return ProxyVersionInfoResult::Error, + }, Err(e) => { let is_not_found_or_gone = e .downcast_ref::() @@ -360,11 +407,11 @@ async fn query_proxy_latest(proxies: &[GoProxy], encoded_path: &str) -> ProxyLis if is_not_found_or_gone || proxy.fall_through == FallThrough::OnAnyError { continue; } - return ProxyListResult::Error; + return ProxyVersionInfoResult::Error; } } } - ProxyListResult::NotFound + ProxyVersionInfoResult::NotFound } fn parse_goproxy() -> Vec { @@ -432,6 +479,64 @@ pub struct GoModInfo { versions: Vec, } +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "PascalCase")] +struct GoModuleVersionMetadata { + version: String, + #[serde(default)] + time: Option, +} + +fn version_info_from_metadata(info: GoModuleVersionMetadata) -> VersionInfo { + VersionInfo { + version: info.version.trim_start_matches('v').to_string(), + created_at: info.time, + ..Default::default() + } +} + +async fn fetch_proxy_version_infos( + proxies: &[GoProxy], + path: &str, + versions: &[String], +) -> Vec { + let encoded = encode_module_path(path); + let mut join_set = tokio::task::JoinSet::new(); + + for version in versions { + let proxies = proxies.to_vec(); + let encoded = encoded.clone(); + let version = version.clone(); + join_set.spawn(async move { + let endpoint = format!("{encoded}/@v/{version}.info"); + let info = query_proxy_version_metadata(&proxies, &endpoint).await; + (version, info) + }); + } + + let mut times = BTreeMap::new(); + while let Some(result) = join_set.join_next().await { + match result { + Ok((version, ProxyVersionInfoResult::Found(info))) => { + times.insert(version, info.time); + } + Ok((version, ProxyVersionInfoResult::NotFound | ProxyVersionInfoResult::Error)) => { + times.insert(version, None); + } + Err(e) => warn!("proxy version info task panicked: {e}"), + } + } + + versions + .iter() + .map(|version| VersionInfo { + version: version.trim_start_matches('v').to_string(), + created_at: times.get(version).cloned().flatten(), + ..Default::default() + }) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -450,6 +555,14 @@ mod tests { assert_eq!(info.versions, vec!["v1.0.0", "v1.1.0"]); } + #[test] + fn parse_go_module_version_metadata() { + let raw = r#"{"Version":"v1.2.3","Time":"2026-04-08T12:56:30Z"}"#; + let info: GoModuleVersionMetadata = serde_json::from_str(raw).unwrap(); + assert_eq!(info.version, "v1.2.3"); + assert_eq!(info.time, Some("2026-04-08T12:56:30Z".to_string())); + } + #[test] fn encode_module_path_lowercase() { assert_eq!( From 24f82f6eeecf4bdf1dab3c8a3b92c0cef5eb5cbd Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Wed, 15 Apr 2026 02:17:11 -0400 Subject: [PATCH 2/3] fix(go): batch version metadata lookups Filter invalid proxy versions before requesting metadata, cap proxy info fan-out, and batch direct-mode { "Path": "command-line-arguments", "Main": true, "GoVersion": "1.26.2" } lookups into bounded chunks. This keeps the new timestamp enrichment path from doing one request or process per version without changing install_before behavior. --- src/backend/go.rs | 80 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/src/backend/go.rs b/src/backend/go.rs index 5fbd5c43c2..bb317ad15a 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -4,7 +4,7 @@ use crate::backend::backend_type::BackendType; use crate::backend::platform_target::PlatformTarget; use crate::cache::{CacheManager, CacheManagerBuilder}; use crate::cli::args::BackendArg; -use crate::cmd::CmdLineRunner; +use crate::cmd::{CmdLineRunner, cmd}; use crate::config::Config; use crate::config::Settings; use crate::hash::hash_to_str; @@ -14,8 +14,11 @@ use crate::timeout; use crate::toolset::{ToolRequest, ToolVersion}; use async_trait::async_trait; use dashmap::DashMap; -use std::collections::BTreeMap; +use serde_json::Deserializer; +use std::collections::{BTreeMap, HashMap}; +use std::ffi::OsString; use std::{fmt::Debug, sync::Arc}; +use tokio::sync::Semaphore; use versions::Versioning; use xx::regex; @@ -161,6 +164,8 @@ pub fn install_time_option_keys() -> Vec { } const DEFAULT_GOPROXY: &str = "https://proxy.golang.org,direct"; +const GO_PROXY_VERSION_INFO_CONCURRENCY: usize = 20; +const GO_LIST_VERSION_INFO_BATCH_SIZE: usize = 50; impl GoBackend { pub fn from_arg(ba: BackendArg) -> Self { @@ -210,9 +215,22 @@ impl GoBackend { let path = &candidates[*idx]; match result { ProxyListResult::Versions(versions) if !versions.is_empty() => { - let mut version_infos = - fetch_proxy_version_infos(&proxies, path, versions).await; - version_infos.retain(|v| Versioning::new(&v.version).is_some()); + let versions: Vec = versions + .iter() + .filter(|v| Versioning::new(v.trim_start_matches('v')).is_some()) + .cloned() + .collect(); + if versions.is_empty() { + let encoded = encode_module_path(path); + match query_proxy_latest(&proxies, &encoded).await { + ProxyVersionInfoResult::Found(info) => { + return Ok(Some(vec![version_info_from_metadata(info)])); + } + ProxyVersionInfoResult::NotFound => continue, + ProxyVersionInfoResult::Error => return Ok(None), + } + } + let mut version_infos = fetch_proxy_version_infos(&proxies, path, &versions).await; version_infos.sort_by_cached_key(|v| Versioning::new(&v.version)); return Ok(Some(version_infos)); } @@ -291,7 +309,6 @@ impl GoBackend { mod_path: &str, versions: &[String], ) -> Vec { - let mut infos = Vec::with_capacity(versions.len()); let env = match self.dependency_env(config).await { Ok(env) => env, Err(_) => { @@ -305,24 +322,41 @@ impl GoBackend { } }; - for version in versions { - let module = format!("{mod_path}@{version}"); - let info = cmd!("go", "list", "-mod=readonly", "-m", "-json", module) - .full_env(env.clone()) - .read() - .ok() - .and_then(|raw| serde_json::from_str::(&raw).ok()); + let mut metadata_by_version = HashMap::with_capacity(versions.len()); + for chunk in versions.chunks(GO_LIST_VERSION_INFO_BATCH_SIZE) { + let mut args = vec![ + OsString::from("list"), + OsString::from("-mod=readonly"), + OsString::from("-m"), + OsString::from("-json"), + ]; + for version in chunk { + args.push(format!("{mod_path}@{version}").into()); + } + let Ok(raw) = cmd("go", args).full_env(&env).read() else { + continue; + }; + let Ok(infos) = Deserializer::from_str(&raw) + .into_iter::() + .collect::, _>>() + else { + continue; + }; + for info in infos { + metadata_by_version.insert(info.version.clone(), info); + } + } - infos.push(match info { + versions + .iter() + .map(|version| match metadata_by_version.remove(version) { Some(info) => version_info_from_metadata(info), None => VersionInfo { version: version.trim_start_matches('v').to_string(), ..Default::default() }, - }); - } - - infos + }) + .collect() } } @@ -500,16 +534,20 @@ async fn fetch_proxy_version_infos( path: &str, versions: &[String], ) -> Vec { - let encoded = encode_module_path(path); + let encoded = Arc::new(encode_module_path(path)); + let proxies = Arc::new(proxies.to_vec()); + let sem = Arc::new(Semaphore::new(GO_PROXY_VERSION_INFO_CONCURRENCY)); let mut join_set = tokio::task::JoinSet::new(); for version in versions { - let proxies = proxies.to_vec(); + let proxies = proxies.clone(); let encoded = encoded.clone(); + let sem = sem.clone(); let version = version.clone(); join_set.spawn(async move { + let _permit = sem.acquire_owned().await.expect("semaphore closed"); let endpoint = format!("{encoded}/@v/{version}.info"); - let info = query_proxy_version_metadata(&proxies, &endpoint).await; + let info = query_proxy_version_metadata(proxies.as_slice(), &endpoint).await; (version, info) }); } From 01d02cd9086eded3951e004bcc0f5a2f4d3b626e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:21:32 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- src/backend/go.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/backend/go.rs b/src/backend/go.rs index bb317ad15a..64093543b7 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -230,7 +230,8 @@ impl GoBackend { ProxyVersionInfoResult::Error => return Ok(None), } } - let mut version_infos = fetch_proxy_version_infos(&proxies, path, &versions).await; + let mut version_infos = + fetch_proxy_version_infos(&proxies, path, &versions).await; version_infos.sort_by_cached_key(|v| Versioning::new(&v.version)); return Ok(Some(version_infos)); }