From 30e79bd97eae340afdfb95373891b2764c120126 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:29:27 +0000 Subject: [PATCH] feat(config): add minimum release age excludes --- docs/tips-and-tricks.md | 11 ++++ e2e/cli/test_install_before | 15 +++++ schema/mise.json | 8 +++ settings.toml | 28 ++++++++ src/backend/mod.rs | 4 +- src/install_before.rs | 124 +++++++++++++++++++++++++++++++++++- src/toolset/tool_request.rs | 8 ++- src/toolset/tool_version.rs | 8 ++- 8 files changed, 198 insertions(+), 8 deletions(-) diff --git a/docs/tips-and-tricks.md b/docs/tips-and-tricks.md index 47faf7663e..b83e756d48 100644 --- a/docs/tips-and-tricks.md +++ b/docs/tips-and-tricks.md @@ -199,6 +199,17 @@ minimum_release_age = "1d" # trivy updates are time-sensitive, use a shorter wi Precedence: `--minimum-release-age` CLI flag > per-tool `minimum_release_age` > global `minimum_release_age` setting. +Use `minimum_release_age_excludes` to exclude tools or backends from the global setting: + +```toml +[settings] +minimum_release_age = "7d" +minimum_release_age_excludes = ["trivy", "npm:*"] +``` + +Exclusions can match backend wildcards like `npm:*`, tool shorthands like `trivy`, or full backend IDs like `npm:prettier`. +Per-tool `minimum_release_age` options and the CLI flag still apply even when a tool matches the exclusion list. + See [`minimum_release_age`](/configuration/settings.html#minimum_release_age) for more details. ## [`mise up --bump`](/cli/upgrade.html) diff --git a/e2e/cli/test_install_before b/e2e/cli/test_install_before index 424c55e448..5659c8054c 100644 --- a/e2e/cli/test_install_before +++ b/e2e/cli/test_install_before @@ -88,6 +88,21 @@ EOF mise install assert_contains "mise ls --installed jq" "1.6" +# Test: minimum_release_age_excludes skips the global setting for matching tools +mise uninstall jq --all 2>/dev/null || true +rm -f mise.toml +cat <mise.toml +[settings] +minimum_release_age = "2019-01-01" +minimum_release_age_excludes = ["jq"] + +[tools] +jq = "latest" +EOF +mise install +assert_not_empty "mise ls --installed jq" +assert_not_contains "mise ls --installed jq" "1.6" + # Test: CLI --minimum-release-age flag overrides per-tool minimum_release_age # per-tool=2024-01-01 would give jq 1.7.1, CLI --minimum-release-age=2019-01-01 gives jq 1.6 mise uninstall jq --all 2>/dev/null || true diff --git a/schema/mise.json b/schema/mise.json index 762940cce8..7ae472565c 100644 --- a/schema/mise.json +++ b/schema/mise.json @@ -1091,6 +1091,14 @@ "description": "Minimum release age / supply chain protection — only install versions older than this threshold", "type": "string" }, + "minimum_release_age_excludes": { + "default": [], + "description": "Tools and backends to exclude from the global minimum_release_age setting", + "type": "array", + "items": { + "type": "string" + } + }, "netrc": { "default": true, "description": "Use a netrc file for HTTP Basic authentication.", diff --git a/settings.toml b/settings.toml index d35f8ce6f7..13a6057ca0 100644 --- a/settings.toml +++ b/settings.toml @@ -1402,6 +1402,34 @@ env = "MISE_MINIMUM_RELEASE_AGE" optional = true type = "String" +[minimum_release_age_excludes] +default = [] +description = "Tools and backends to exclude from the global minimum_release_age setting" +docs = """ +Exclude tools or backends from the global [`minimum_release_age`](#minimum_release_age) setting. +This is useful for tools where newly published releases are time-sensitive. + +Exclusions match any of: + +- Backend wildcard: `npm:*`, `pipx:*`, `github:*` +- Registry tool shorthand: `trivy`, `node` +- Full backend ID: `npm:prettier`, `aqua:aquasecurity/trivy` + +Example: + +```toml +[settings] +minimum_release_age = "7d" +minimum_release_age_excludes = ["trivy", "npm:*"] +``` + +This only excludes tools from the global setting. A per-tool `minimum_release_age` option +or the `--minimum-release-age` CLI flag still applies to matching tools. +""" +env = "MISE_MINIMUM_RELEASE_AGE_EXCLUDES" +parse_env = "list_by_comma" +type = "ListString" + [netrc] default = true description = "Use a netrc file for HTTP Basic authentication." diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 15e40f192a..12a537912a 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -21,7 +21,7 @@ use crate::duration::parse_into_timestamp; use crate::file::{ canonicalize_cached, display_path, remove_all_with_progress, remove_all_with_warning, }; -use crate::install_before::resolve_before_date; +use crate::install_before::resolve_before_date_for_tool; use crate::install_context::InstallContext; use crate::lockfile::{PlatformInfo, ProvenanceType}; use crate::path_env::PathEnv; @@ -2444,7 +2444,7 @@ async fn effective_latest_before_date( } let opts = config.get_tool_opts_with_overrides(backend.ba()).await?; - resolve_before_date(None, opts.minimum_release_age()) + resolve_before_date_for_tool(backend.ba(), None, opts.minimum_release_age()) } fn latest_stable_candidate_allowed_by_before_date( diff --git a/src/install_before.rs b/src/install_before.rs index 2bef470609..ba2adf0daa 100644 --- a/src/install_before.rs +++ b/src/install_before.rs @@ -4,6 +4,7 @@ use eyre::Result; use jiff::Timestamp; use crate::backend::Backend; +use crate::cli::args::BackendArg; use crate::config::{Config, Settings}; use crate::duration::parse_into_timestamp; @@ -23,6 +24,26 @@ use crate::duration::parse_into_timestamp; pub fn resolve_before_date( before_date: Option, minimum_release_age: Option<&str>, +) -> Result> { + resolve_before_date_with_excludes(before_date, minimum_release_age, false) +} + +pub fn resolve_before_date_for_tool( + backend_arg: &BackendArg, + before_date: Option, + minimum_release_age: Option<&str>, +) -> Result> { + resolve_before_date_with_excludes( + before_date, + minimum_release_age, + is_minimum_release_age_excluded(backend_arg), + ) +} + +fn resolve_before_date_with_excludes( + before_date: Option, + minimum_release_age: Option<&str>, + excluded: bool, ) -> Result> { if let Some(before_date) = before_date { return Ok(Some(before_date)); @@ -30,12 +51,32 @@ pub fn resolve_before_date( if let Some(before) = minimum_release_age { return Ok(Some(parse_into_timestamp(before)?)); } + if excluded { + return Ok(None); + } if let Some(before) = &Settings::get().minimum_release_age { return Ok(Some(parse_into_timestamp(before)?)); } Ok(None) } +fn is_minimum_release_age_excluded(backend_arg: &BackendArg) -> bool { + let excludes = &Settings::get().minimum_release_age_excludes; + if excludes.is_empty() { + return false; + } + + let full = backend_arg.full_without_opts(); + let backend_type = backend_arg.backend_type().to_string(); + let backend_wildcard = format!("{backend_type}:*"); + + excludes.iter().any(|exclude| { + let exclude = exclude.trim(); + !exclude.is_empty() + && (exclude == backend_arg.short || exclude == full || exclude == backend_wildcard) + }) +} + pub(crate) async fn resolve_before_date_for_backend( config: &Arc, backend: &B, @@ -46,12 +87,13 @@ pub(crate) async fn resolve_before_date_for_backend( } let opts = config.get_tool_opts_with_overrides(backend.ba()).await?; - resolve_before_date(None, opts.minimum_release_age()) + resolve_before_date_for_tool(backend.ba(), None, opts.minimum_release_age()) } #[cfg(test)] mod tests { - use super::resolve_before_date; + use super::{resolve_before_date, resolve_before_date_for_tool}; + use crate::cli::args::BackendArg; use crate::config::settings::{Settings, SettingsPartial}; use confique::Layer; use jiff::Timestamp; @@ -64,6 +106,15 @@ mod tests { resolve_before_date(before_date, minimum_release_age).unwrap() } + fn resolved_tool_timestamp( + tool: &str, + before_date: Option, + minimum_release_age: Option<&str>, + ) -> Option { + let backend_arg: BackendArg = tool.into(); + resolve_before_date_for_tool(&backend_arg, before_date, minimum_release_age).unwrap() + } + #[test] fn test_effective_before_date_prefers_override() { Settings::reset(None); @@ -97,6 +148,75 @@ mod tests { Settings::reset(None); } + #[test] + fn test_effective_before_date_excludes_global_by_tool() { + let mut partial = SettingsPartial::empty(); + partial.minimum_release_age = Some("2024-01-03".to_string()); + partial.minimum_release_age_excludes = Some(vec!["node".to_string()]); + Settings::reset(Some(partial)); + assert_eq!(resolved_tool_timestamp("node", None, None), None); + Settings::reset(None); + } + + #[test] + fn test_effective_before_date_does_not_exclude_backend_by_bare_name() { + let mut partial = SettingsPartial::empty(); + partial.minimum_release_age = Some("2024-01-03".to_string()); + partial.minimum_release_age_excludes = Some(vec!["npm".to_string()]); + Settings::reset(Some(partial)); + assert_eq!( + resolved_tool_timestamp("npm:prettier", None, None), + Some(crate::duration::parse_into_timestamp("2024-01-03").unwrap()) + ); + Settings::reset(None); + } + + #[test] + fn test_effective_before_date_excludes_global_by_backend_wildcard() { + let mut partial = SettingsPartial::empty(); + partial.minimum_release_age = Some("2024-01-03".to_string()); + partial.minimum_release_age_excludes = Some(vec!["npm:*".to_string()]); + Settings::reset(Some(partial)); + assert_eq!(resolved_tool_timestamp("npm:prettier", None, None), None); + Settings::reset(None); + } + + #[test] + fn test_effective_before_date_excludes_global_by_full_backend_id() { + let mut partial = SettingsPartial::empty(); + partial.minimum_release_age = Some("2024-01-03".to_string()); + partial.minimum_release_age_excludes = Some(vec!["npm:prettier".to_string()]); + Settings::reset(Some(partial)); + assert_eq!(resolved_tool_timestamp("npm:prettier", None, None), None); + Settings::reset(None); + } + + #[test] + fn test_effective_before_date_does_not_exclude_by_bare_backend_tool_name() { + let mut partial = SettingsPartial::empty(); + partial.minimum_release_age = Some("2024-01-03".to_string()); + partial.minimum_release_age_excludes = Some(vec!["prettier".to_string()]); + Settings::reset(Some(partial)); + assert_eq!( + resolved_tool_timestamp("npm:prettier", None, None), + Some(crate::duration::parse_into_timestamp("2024-01-03").unwrap()) + ); + Settings::reset(None); + } + + #[test] + fn test_effective_before_date_exclude_does_not_override_tool_option() { + let mut partial = SettingsPartial::empty(); + partial.minimum_release_age = Some("2024-01-03".to_string()); + partial.minimum_release_age_excludes = Some(vec!["npm".to_string()]); + Settings::reset(Some(partial)); + assert_eq!( + resolved_tool_timestamp("npm:prettier", None, Some("2024-01-02")), + Some(crate::duration::parse_into_timestamp("2024-01-02").unwrap()) + ); + Settings::reset(None); + } + #[test] fn test_effective_before_date_none_when_unset() { Settings::reset(None); diff --git a/src/toolset/tool_request.rs b/src/toolset/tool_request.rs index 866ad2528c..1fab9d74eb 100644 --- a/src/toolset/tool_request.rs +++ b/src/toolset/tool_request.rs @@ -14,7 +14,7 @@ use crate::cli::args::BackendArg; use crate::config::config_file::config_root; use crate::dirs; use crate::env; -use crate::install_before::resolve_before_date; +use crate::install_before::resolve_before_date_for_tool; use crate::lockfile::LockfileTool; use crate::path::PathExt; use crate::runtime_symlinks::is_runtime_symlink; @@ -376,7 +376,11 @@ impl ToolRequest { pub fn resolve_options(&self, opts: &ResolveOptions) -> Result { let minimum_release_age = self.options().minimum_release_age().map(str::to_string); let mut opts = opts.clone(); - opts.before_date = resolve_before_date(opts.before_date, minimum_release_age.as_deref())?; + opts.before_date = resolve_before_date_for_tool( + self.ba(), + opts.before_date, + minimum_release_age.as_deref(), + )?; Ok(opts) } diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index 2f21c86fce..4c6cd91c84 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -12,7 +12,7 @@ use crate::env; #[cfg(windows)] use crate::file; use crate::hash::hash_to_str; -use crate::install_before::resolve_before_date; +use crate::install_before::resolve_before_date_for_tool; use crate::lockfile::{CondaPackageInfo, LockfileTool, PlatformInfo}; use crate::runtime_symlinks::is_runtime_symlink; use crate::toolset::{ToolRequest, ToolSource, ToolVersionOptions, tool_request}; @@ -78,7 +78,11 @@ impl ToolVersion { ) -> Result { let minimum_release_age = request.options().minimum_release_age().map(str::to_string); let mut opts = opts.clone(); - opts.before_date = resolve_before_date(opts.before_date, minimum_release_age.as_deref())?; + opts.before_date = resolve_before_date_for_tool( + request.ba(), + opts.before_date, + minimum_release_age.as_deref(), + )?; trace!("resolving {} {}", &request, opts); if opts.use_locked_version