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
4 changes: 2 additions & 2 deletions docs/dev-tools/backends/npm.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ similar to how the pipx backend uses `uv` when available.
If you use `aube`, `pnpm`, or `bun` as the package manager, that package manager
must also be installed.

When [`minimum_release_age`](/configuration/settings.html#minimum_release_age) is set, the npm backend
forwards that cutoff to transitive dependency resolution during install. This relies on the
The npm backend forwards [`minimum_release_age`](/configuration/settings.html#minimum_release_age)
to transitive dependency resolution during install. This relies on the
configured package manager supporting its native release-age flag:

- `aube` using its `minimumReleaseAge` setting
Expand Down
2 changes: 1 addition & 1 deletion docs/dev-tools/backends/pipx.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This relies on having `uv` (recommended) or `pipx` installed.

If you have `uv` installed, mise will use `uv tool install` under the hood and you don't need to install `pipx` to run the commands containing "pipx:".

When [`minimum_release_age`](/configuration/settings.html#minimum_release_age) is set, mise forwards the cutoff
mise forwards [`minimum_release_age`](/configuration/settings.html#minimum_release_age)
to transitive Python dependency resolution during install. The uv install path uses uv's
`--exclude-newer` flag, and the `pipx` fallback passes pip's `--uploaded-prior-to` flag.
When using `minimum_release_age` with the `pipx` install path, `pip >= 26.0` is required for pip's `--uploaded-prior-to` support, and you are responsible for ensuring that compatible Python/pip environment is installed.
Expand Down
4 changes: 2 additions & 2 deletions docs/dev-tools/mise-lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,11 @@ When enabled, every `mise install` will cryptographically verify provenance rega

## Minimum Release Age

In addition to lockfiles, mise supports the [`minimum_release_age`](/configuration/settings.html#minimum_release_age) setting to limit supply chain risk by only installing versions that have been available for a minimum amount of time:
In addition to lockfiles, mise uses the [`minimum_release_age`](/configuration/settings.html#minimum_release_age) setting to limit supply chain risk by only installing versions that have been available for a minimum amount of time. It defaults to `24h`:

```toml
[settings]
minimum_release_age = "7d" # only resolve to versions released more than 7 days ago
minimum_release_age = "7d" # override the default 24h delay
```

This pairs well with lockfiles — use `minimum_release_age` to avoid picking up brand-new releases, and lockfiles to pin the exact versions you've vetted.
Expand Down
7 changes: 4 additions & 3 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ 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:
Use `minimum_release_age_excludes` to exclude tools or backends from the global/default setting:

```toml
[settings]
Expand All @@ -84,8 +84,9 @@ 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.
IDs like `npm:prettier`. Matching tools skip the global setting and built-in default. 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 the setting
reference.
8 changes: 6 additions & 2 deletions e2e/backend/test_npm_install_before
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ export MISE_MINIMUM_RELEASE_AGE="2023-06-01"
assert_contains "mise latest npm:prettier" "2.8.8"
unset MISE_MINIMUM_RELEASE_AGE

# Test: without minimum_release_age, latest is the absolute latest (newer than 2.8.8)
assert_not_contains "mise latest npm:prettier" "2.8.8"
# Test: minimum_release_age=0s effectively disables the delay and returns the absolute latest.
export MISE_MINIMUM_RELEASE_AGE="0s"
latest_prettier="$(mise latest npm:prettier)"
assert_matches "echo \"$latest_prettier\"" '^[0-9]+\.[0-9]+\.[0-9]+$'
assert_not_contains_text "$latest_prettier" "2.8.8"
unset MISE_MINIMUM_RELEASE_AGE
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Test: `mise latest --minimum-release-age` CLI flag should filter by date
assert_contains "mise latest npm:prettier --minimum-release-age 2023-06-01" "2.8.8"
Expand Down
6 changes: 3 additions & 3 deletions e2e/backend/test_npm_package_manager
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ export MISE_NPM_PACKAGE_MANAGER=aube

# Ensure aube is available after the default package manager test, since
# npm.package_manager=auto uses aube when it is already installed.
# Pin to the current aube release checked for this test. Bypass
# MISE_MINIMUM_RELEASE_AGE because the global filter is set to test the
# package-manager flag plumbing below.
# Pin to the current aube release checked for this test. Remove this test's
# MISE_MINIMUM_RELEASE_AGE override; the explicit version bypasses the default
# release-age filter.
AUBE_TEST_VERSION="1.16.0"
if ! command -v aube >/dev/null 2>&1; then
echo "aube not available in test environment, installing..."
Expand Down
10 changes: 10 additions & 0 deletions e2e/cli/test_install_before
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ mise install tiny@1.0.0
output=$(mise upgrade --minimum-release-age 2025-01-01 --dry-run 2>&1)
assert_contains "echo '$output'" "tiny"

rm -f mise.toml
cat <<EOF >mise.toml
[tools]
jq = "latest"
EOF
mise uninstall jq --all 2>/dev/null || true
mise install jq@1.6
output=$(mise upgrade jq --minimum-release-age 2019-01-01 --dry-run 2>&1)
assert_contains "echo '$output'" "newer jq release"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Test: install --minimum-release-age with relative duration "90d"
mise uninstall tiny --all 2>/dev/null || true
output=$(mise install tiny@latest --minimum-release-age 90d --dry-run 2>&1)
Expand Down
1 change: 1 addition & 0 deletions e2e/cli/test_ls_remote
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ assert "mise ls-remote dummy@sub-1:2" "1.0.0
# result is cached and reused for the second query.
assert_contains "mise ls-remote jq --minimum-release-age 2019-01-01" "1.6"
assert_not_contains "mise ls-remote jq --minimum-release-age 2019-01-01" "1.7.1"
assert_contains "mise ls-remote jq --minimum-release-age 2019-01-01 2>&1" "newer jq release"
assert_contains "mise ls-remote jq --minimum-release-age 2024-01-01" "1.7.1"
assert_not_empty "mise ls-remote jq --minimum-release-age 1d"
assert_contains "mise ls-remote jq --before 2019-01-01" "1.6"
Expand Down
2 changes: 1 addition & 1 deletion schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -1093,7 +1093,7 @@
},
"minimum_release_age_excludes": {
"default": [],
"description": "Tools and backends to exclude from the global minimum_release_age setting",
"description": "Tools and backends to exclude from the global/default minimum_release_age setting",
"type": "array",
"items": {
"type": "string"
Expand Down
10 changes: 6 additions & 4 deletions settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,7 @@ Example:

```toml
[settings]
minimum_release_age = "7d" # only install versions released more than 7 days ago
minimum_release_age = "7d" # default is 24h
```

Capability depends on what each backend can report or pass through:
Expand All @@ -1395,6 +1395,8 @@ cutoff date.

For `npm:` and `pipx:` package-manager support details, see the backend docs.

Defaults to `24h`. Set `minimum_release_age = "0s"` to effectively disable the delay.

Can be overridden with the `--minimum-release-age` CLI flag or per-tool with the `minimum_release_age` tool option.

```toml
Expand All @@ -1409,9 +1411,9 @@ type = "String"

[minimum_release_age_excludes]
default = []
description = "Tools and backends to exclude from the global minimum_release_age setting"
description = "Tools and backends to exclude from the global/default minimum_release_age setting"
docs = """
Exclude tools or backends from the global [`minimum_release_age`](#minimum_release_age) setting.
Exclude tools or backends from the global/default [`minimum_release_age`](#minimum_release_age) setting.
This is useful for tools where newly published releases are time-sensitive.

Exclusions match any of:
Expand All @@ -1428,7 +1430,7 @@ 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
This excludes matching tools from the global setting and built-in default. 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"
Expand Down
62 changes: 45 additions & 17 deletions src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,29 +171,36 @@ fn is_false(v: &bool) -> bool {
}

impl VersionInfo {
fn created_at_timestamp(&self) -> Option<Timestamp> {
match &self.created_at {
Some(ts) => {
let created = parse_into_timestamp(ts);
if created.is_err() {
trace!("Failed to parse timestamp: {}", ts);
}
created.ok()
}
None => None,
}
}

pub fn hidden_by_date(&self, before: Timestamp) -> bool {
self.created_at_timestamp()
.is_some_and(|created| created >= before)
}

pub fn count_hidden_by_date(versions: &[Self], before: Timestamp) -> usize {
versions.iter().filter(|v| v.hidden_by_date(before)).count()
}

/// 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
}
}
}
// Include versions without timestamps
None => true,
}
v.created_at_timestamp()
.is_none_or(|created| created < before)
})
.collect()
}
Expand Down Expand Up @@ -1520,6 +1527,27 @@ pub trait Backend: Debug + Send + Sync {
.await
}

/// Get the latest version without applying release-date cutoffs.
///
/// This is intended for diagnostics that compare a date-filtered result
/// with the absolute latest result. Normal resolution should use
/// `latest_version` so global, per-tool, and default release-age cutoffs are
/// honored.
async fn latest_version_unfiltered(
&self,
config: &Arc<Config>,
query: Option<String>,
) -> eyre::Result<Option<String>> {
let resolved_query = query.as_deref().unwrap_or("latest");
if resolved_query == "latest"
&& let Some(version) = self.latest_stable_version(config).await?
{
return Ok(Some(version));
}
self.latest_version_for_query(config, resolved_query, None, false)
.await
}

/// Like `latest_version` but with explicit refresh control. Pass
/// `refresh = true` to bypass the cached remote-versions list when falling
/// back to the full version-list path. The `latest_stable_version` fast
Expand Down
37 changes: 26 additions & 11 deletions src/cli/ls_remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,34 +123,40 @@ impl LsRemote {
};
let matches_prefix = |v: &str| prefix.as_ref().is_none_or(|p| v.starts_with(p));

let versions = filter_versions_by_date(
plugin.list_remote_versions_with_info(config).await?,
before_date,
)
.into_iter()
.filter(|v| matches_prefix(&v.version))
.collect::<Vec<_>>();
let versions_matching_prefix = plugin
.list_remote_versions_with_info(config)
.await?
.into_iter()
.filter(|v| matches_prefix(&v.version))
.collect::<Vec<_>>();
let hidden_versions = before_date
.map(|before| VersionInfo::count_hidden_by_date(&versions_matching_prefix, before))
.unwrap_or_default();
let versions = filter_versions_by_date(versions_matching_prefix, before_date);

if self.json {
miseprintln!("{}", serde_json::to_string(&versions)?);
} else {
for v in versions {
miseprintln!("{}", v.version);
}
warn_if_versions_hidden_by_minimum_release_age(plugin.id(), hidden_versions);
}
Ok(())
}

async fn run_all(self, config: &Arc<Config>, before_date: Option<Timestamp>) -> Result<()> {
let mut versions = vec![];
let mut hidden_versions = 0;
for b in backend::list() {
let tool = b.id().to_string();
let before_date =
resolve_before_date_for_backend(config, b.as_ref(), before_date).await?;
for v in filter_versions_by_date(
b.list_remote_versions_with_info(config).await?,
before_date,
) {
let all_versions = b.list_remote_versions_with_info(config).await?;
if let Some(before) = before_date {
hidden_versions += VersionInfo::count_hidden_by_date(&all_versions, before);
}
for v in filter_versions_by_date(all_versions, before_date) {
versions.push(VersionOutputAll {
tool: tool.clone(),
version: v.version,
Expand All @@ -167,6 +173,7 @@ impl LsRemote {
for v in versions {
miseprintln!("{}@{}", v.tool, v.version);
}
warn_if_versions_hidden_by_minimum_release_age("tools", hidden_versions);
}
Ok(())
}
Expand All @@ -187,6 +194,14 @@ impl LsRemote {
}
}

fn warn_if_versions_hidden_by_minimum_release_age(tool: &str, hidden_versions: usize) {
if hidden_versions == 0 {
return;
}
let s = if hidden_versions == 1 { "" } else { "s" };
warn!("{hidden_versions} newer {tool} release{s} hidden by minimum_release_age");
}

fn filter_versions_by_date(
versions: Vec<VersionInfo>,
before_date: Option<Timestamp>,
Expand Down
Loading
Loading