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
6 changes: 6 additions & 0 deletions docs/cli/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ Show installation output

This argument will print plugin output such as download, configuration, and compilation output.

### `--before <BEFORE>`

Only install versions released before this date

Supports absolute dates like "2024-06-01" and relative durations like "90d" or "1y".

### `--raw`

Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1
Expand Down
10 changes: 10 additions & 0 deletions docs/cli/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ would change your config to `node = "22"`.

Just print what would be done, don't actually do it

### `--before <BEFORE>`

Only upgrade to versions released before this date

Supports absolute dates like "2024-06-01" and relative durations like "90d" or "1y".
This can be useful for reproducibility or security purposes.

This only affects fuzzy version matches like "20" or "latest".
Explicitly pinned versions like "22.5.0" are not filtered.

### `--raw`

Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1
Expand Down
6 changes: 6 additions & 0 deletions docs/cli/use.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ Specify a path to a config file or directory

If a directory is specified, it will look for a config file in that directory following the rules above.

### `--before <BEFORE>`

Only install versions released before this date

Supports absolute dates like "2024-06-01" and relative durations like "90d" or "1y".

### `--fuzzy`

Save fuzzy version to config file
Expand Down
43 changes: 43 additions & 0 deletions e2e/cli/test_install_before
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Test --before flag for date-based version filtering

set -euo pipefail

# Clean up any existing installations
mise uninstall tiny --all 2>/dev/null || true
rm -f mise.toml .tool-versions

# Test: install --before with dry-run should show the tool
output=$(mise install tiny@latest --before 2020-01-01 --dry-run 2>&1)
assert_contains "echo '$output'" "tiny"

# Test: install --before should install a version
mise install tiny@latest --before 2025-01-01
assert_contains "mise ls --installed tiny" "3.1.0"

# Test: use --before with dry-run
mise uninstall tiny --all 2>/dev/null || true
output=$(mise use tiny@latest --before 2025-01-01 --dry-run 2>&1)
assert_contains "echo '$output'" "tiny"
rm -f mise.toml

# Test: MISE_INSTALL_BEFORE environment variable works
mise uninstall tiny --all 2>/dev/null || true
export MISE_INSTALL_BEFORE="2025-01-01"
mise install tiny@latest
assert_contains "mise ls --installed tiny" "3.1.0"
unset MISE_INSTALL_BEFORE

# Test: upgrade --before with dry-run
cat <<EOF >mise.toml
[tools]
tiny = "1"
EOF
mise uninstall tiny --all 2>/dev/null || true
mise install tiny@1.0.0
output=$(mise upgrade --before 2025-01-01 --dry-run 2>&1)
assert_contains "echo '$output'" "tiny"

# Clean up
mise uninstall tiny --all 2>/dev/null || true
rm -f mise.toml .tool-versions
19 changes: 19 additions & 0 deletions man/man1/mise.1
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,11 @@ Show installation output

This argument will print plugin output such as download, configuration, and compilation output.
.TP
\fB\-\-before\fR \fI<BEFORE>\fR
Only install versions released before this date

Supports absolute dates like "2024\-06\-01" and relative durations like "90d" or "1y".
.TP
\fB\-\-raw\fR
Directly pipe stdin/stdout/stderr from plugin to user Sets \-\-jobs=1
\fBArguments:\fR
Expand Down Expand Up @@ -2695,6 +2700,15 @@ would change your config to `node = "22"`.
\fB\-n, \-\-dry\-run\fR
Just print what would be done, don't actually do it
.TP
\fB\-\-before\fR \fI<BEFORE>\fR
Only upgrade to versions released before this date

Supports absolute dates like "2024\-06\-01" and relative durations like "90d" or "1y".
This can be useful for reproducibility or security purposes.

This only affects fuzzy version matches like "20" or "latest".
Explicitly pinned versions like "22.5.0" are not filtered.
.TP
\fB\-\-raw\fR
Directly pipe stdin/stdout/stderr from plugin to user Sets \-\-jobs=1
\fBArguments:\fR
Expand Down Expand Up @@ -2746,6 +2760,11 @@ Specify a path to a config file or directory

If a directory is specified, it will look for a config file in that directory following the rules above.
.TP
\fB\-\-before\fR \fI<BEFORE>\fR
Only install versions released before this date

Supports absolute dates like "2024\-06\-01" and relative durations like "90d" or "1y".
.TP
\fB\-\-fuzzy\fR
Save fuzzy version to config file

Expand Down
12 changes: 12 additions & 0 deletions mise.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@ cmd install help="Install a tool version" {
flag "-v --verbose" help="Show installation output" var=#true count=#true {
long_help "Show installation output\n\nThis argument will print plugin output such as download, configuration, and compilation output."
}
flag --before help="Only install versions released before this date" {
long_help "Only install versions released before this date\n\nSupports absolute dates like \"2024-06-01\" and relative durations like \"90d\" or \"1y\"."
arg <BEFORE>
}
flag --raw help="Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1"
arg "[TOOL@VERSION]…" help="Tool(s) to install e.g.: node@20" required=#false var=#true
}
Expand Down Expand Up @@ -1070,6 +1074,10 @@ cmd upgrade help="Upgrades outdated tools" {
long_help "Upgrades to the latest version available, bumping the version in mise.toml\n\nFor example, if you have `node = \"20.0.0\"` in your mise.toml but 22.1.0 is the latest available,\nthis will install 22.1.0 and set `node = \"22.1.0\"` in your config.\n\nIt keeps the same precision as what was there before, so if you instead had `node = \"20\"`, it\nwould change your config to `node = \"22\"`."
}
flag "-n --dry-run" help="Just print what would be done, don't actually do it"
flag --before help="Only upgrade to versions released before this date" {
long_help "Only upgrade to versions released before this date\n\nSupports absolute dates like \"2024-06-01\" and relative durations like \"90d\" or \"1y\".\nThis can be useful for reproducibility or security purposes.\n\nThis only affects fuzzy version matches like \"20\" or \"latest\".\nExplicitly pinned versions like \"22.5.0\" are not filtered."
arg <BEFORE>
}
flag --raw help="Directly pipe stdin/stdout/stderr from plugin to user Sets --jobs=1"
arg "[TOOL@VERSION]…" help="Tool(s) to upgrade\ne.g.: node@20 python@3.10\nIf not specified, all current tools will be upgraded" required=#false var=#true
}
Expand All @@ -1093,6 +1101,10 @@ cmd use help="Installs a tool and adds the version to mise.toml." {
long_help "Specify a path to a config file or directory\n\nIf a directory is specified, it will look for a config file in that directory following the rules above."
arg <PATH>
}
flag --before help="Only install versions released before this date" {
long_help "Only install versions released before this date\n\nSupports absolute dates like \"2024-06-01\" and relative durations like \"90d\" or \"1y\"."
arg <BEFORE>
}
flag --fuzzy help="Save fuzzy version to config file" {
long_help "Save fuzzy version to config file\n\ne.g.: `mise use --fuzzy node@20` will save 20 as the version\nthis is the default behavior unless `MISE_PIN=1`"
}
Expand Down
4 changes: 4 additions & 0 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,10 @@
"type": "string"
}
},
"install_before": {
"description": "Only install versions released before this date",
"type": "string"
},
"jobs": {
"default": 8,
"description": "How many jobs to run concurrently such as tool installs.",
Expand Down
24 changes: 24 additions & 0 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,30 @@ parse_env = "list_by_colon"
rust_type = "BTreeSet<PathBuf>"
type = "ListPath"

[install_before]
description = "Only install versions released before this date"
docs = """
Filter tool versions by release date. Supports:

- Absolute dates: `2024-06-01`, `2024-06-01T12:00:00Z`
- Relative durations: `90d` (90 days ago), `1y` (1 year ago), `6m` (6 months ago)

This is useful for reproducible builds or security purposes where you want to ensure
you're only installing versions that existed before a certain point in time.

Only affects backends that provide release timestamps (aqua, cargo, npm, pipx, and some core plugins).
Versions without timestamps are included by default.

**Behavior**: This filter only applies when resolving fuzzy version requests like `node@20` or `latest`.
Explicitly pinned versions like `node@22.5.0` are not filtered, allowing you to selectively
use newer versions for specific tools while keeping others behind the cutoff date.

Can be overridden with the `--before` CLI flag.
"""
env = "MISE_INSTALL_BEFORE"
optional = true
type = "String"

[jobs]
default = 8
description = "How many jobs to run concurrently such as tool installs."
Expand Down
110 changes: 109 additions & 1 deletion src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use tokio::sync::Mutex as TokioMutex;

use jiff::Timestamp;

use crate::cli::args::{BackendArg, ToolVersionType};
use crate::cmd::CmdLineRunner;
use crate::config::{Config, Settings};
Expand All @@ -20,7 +22,9 @@ use crate::plugins::{PluginType, VERSION_REGEX};
use crate::registry::{REGISTRY, full_to_url, normalize_remote, tool_enabled};
use crate::runtime_symlinks::is_runtime_symlink;
use crate::toolset::outdated_info::OutdatedInfo;
use crate::toolset::{ToolRequest, ToolVersion, Toolset, install_state, is_outdated_version};
use crate::toolset::{
ResolveOptions, ToolRequest, ToolVersion, Toolset, install_state, is_outdated_version,
};
use crate::ui::progress_report::SingleReport;
use crate::{
cache::{CacheManager, CacheManagerBuilder},
Expand Down Expand Up @@ -87,6 +91,35 @@ pub struct VersionInfo {
pub created_at: Option<String>,
}

impl VersionInfo {
/// Filter versions to only include those released before the given timestamp.
/// Versions without a created_at timestamp are included by default.
pub fn filter_by_date(versions: Vec<Self>, before: Timestamp) -> Vec<Self> {
use crate::duration::parse_into_timestamp;
versions
.into_iter()
.filter(|v| {
match &v.created_at {
Some(ts) => {
// Parse the timestamp using parse_into_timestamp which handles
// RFC3339, date-only (YYYY-MM-DD), and other formats
match parse_into_timestamp(ts) {
Ok(created) => created < before,
Err(_) => {
// If we can't parse the timestamp, include the version
trace!("Failed to parse timestamp: {}", ts);
true
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Date-only timestamps fail to parse in version filtering

The VersionInfo::filter_by_date method only tries parsing timestamps with ts.parse::<Timestamp>(), which requires RFC3339 format. However, when falling back from versions host to native APIs, Node and Zig backends provide date-only strings like "2024-01-09" which cannot be parsed by Timestamp. These versions silently bypass the filter (returning true on parse failure). This is inconsistent with parse_into_timestamp in duration.rs which correctly handles date-only strings by parsing as jiff::civil::Date first. The --before filter will not work for Node and Zig when the versions host is unavailable.

Additional Locations (2)

Fix in Cursor Fix in Web

}
// Include versions without timestamps
None => true,
}
})
.collect()
}
}

static TOOLS: Mutex<Option<Arc<BackendMap>>> = Mutex::new(None);

pub async fn load_tools() -> Result<Arc<BackendMap>> {
Expand Down Expand Up @@ -515,6 +548,34 @@ pub trait Backend: Debug + Send + Sync {
let versions = self.list_remote_versions(config).await?;
Ok(self.fuzzy_match_filter(versions, query))
}

/// List versions matching a query, optionally filtered by release date.
/// Use this when you have a `before_date` from ResolveOptions.
async fn list_versions_matching_with_opts(
&self,
config: &Arc<Config>,
query: &str,
before_date: Option<Timestamp>,
) -> eyre::Result<Vec<String>> {
let versions = match before_date {
Some(before) => {
// Use version info to filter by date
let versions_with_info = self.list_remote_versions_with_info(config).await?;
let filtered = VersionInfo::filter_by_date(versions_with_info, before);
// Warn if no versions have timestamps
if filtered.iter().all(|v| v.created_at.is_none()) && !filtered.is_empty() {
debug!(
"Backend {} does not provide release dates; --before filter may not work as expected",
self.id()
);
}
filtered.into_iter().map(|v| v.version).collect()
}
None => self.list_remote_versions(config).await?,
};
Ok(self.fuzzy_match_filter(versions, query))
}

async fn latest_version(
&self,
config: &Arc<Config>,
Expand All @@ -531,6 +592,52 @@ pub trait Backend: Debug + Send + Sync {
None => self.latest_stable_version(config).await,
}
}

/// Get the latest version, optionally filtered by release date.
/// Use this when you have a `before_date` from ResolveOptions.
async fn latest_version_with_opts(
&self,
config: &Arc<Config>,
query: Option<String>,
before_date: Option<Timestamp>,
) -> eyre::Result<Option<String>> {
match query {
Some(query) => {
let mut matches = self
.list_versions_matching_with_opts(config, &query, before_date)
.await?;
if matches.is_empty() && query == "latest" {
// Fall back to all versions if no match
matches = match before_date {
Some(before) => {
let versions_with_info =
self.list_remote_versions_with_info(config).await?;
VersionInfo::filter_by_date(versions_with_info, before)
.into_iter()
.map(|v| v.version)
.collect()
}
None => self.list_remote_versions(config).await?,
};
}
Ok(find_match_in_list(&matches, &query))
}
None => {
// For stable version, apply date filter if provided
match before_date {
Some(before) => {
let versions_with_info =
self.list_remote_versions_with_info(config).await?;
let filtered = VersionInfo::filter_by_date(versions_with_info, before);
let versions: Vec<String> =
filtered.into_iter().map(|v| v.version).collect();
Ok(find_match_in_list(&versions, "latest"))
}
None => self.latest_stable_version(config).await,
}
}
}
}
fn latest_installed_version(&self, query: Option<String>) -> eyre::Result<Option<String>> {
match query {
Some(query) => {
Expand Down Expand Up @@ -1051,6 +1158,7 @@ pub trait Backend: Debug + Send + Sync {
_config: &Arc<Config>,
_tv: &ToolVersion,
_bump: bool,
_opts: &ResolveOptions,
) -> Result<Option<OutdatedInfo>> {
Ok(None)
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl Exec {
ResolveOptions {
latest_versions: true,
use_locked_version: false,
..Default::default()
}
} else {
Default::default()
Expand Down
Loading
Loading