Skip to content

feat(config): add minimum_release_age option#2159

Open
h-michael wants to merge 2 commits into
folke:mainfrom
h-michael:feat/minimum-release-age
Open

feat(config): add minimum_release_age option#2159
h-michael wants to merge 2 commits into
folke:mainfrom
h-michael:feat/minimum-release-age

Conversation

@h-michael

@h-michael h-michael commented May 22, 2026

Copy link
Copy Markdown

Description

Add a minimum_release_age option that mitigates supply-chain attacks by ignoring commits and tags that have not been published long enough. Modeled on pnpm's minimumReleaseAge, mise's minimum_release_age, Renovate, and Dependabot's cooldown. Default is nil, so this is fully opt-in with no behavior change for existing users.

Usage

require("lazy").setup(spec, {
  defaults = {
    minimum_release_age = "7d", -- wait 7 days before adopting new commits/tags
  },
})

Accepts a non-negative integer (seconds) or a single-unit duration string ("30m", "24h", "7d", "2w", "1y"). Combined forms ("7d12h") are not supported -- use a single unit.

Per-plugin overrides are accepted on each spec entry:

{ "owner/risky",    minimum_release_age = "30d" },   -- stricter for this one
{ "owner/critical", minimum_release_age = false },   -- disable for this one

Value semantics

How each value is interpreted depending on where it is set:

Value At defaults (global) At spec entry (per-plugin)
nil feature disabled (default) inherit global
false equivalent to nil force-disable for this plugin only
integer (seconds) apply this constraint override global for this plugin
"30m"/"24h"/"7d"/"2w"/"1y" apply this constraint override global for this plugin
anything else ("7d12h", "foo", …) parsed as invalid → warning, treated as nil same

Scope

Applies only to fuzzy resolution paths. Explicit pins and pin = true plugins are exempt, matching Renovate's pin-exemption semantics:

Spec form Affected by minimum_release_age?
{ "owner/repo" } (default-branch following) yes
{ "owner/repo", branch = "main" } yes
{ "owner/repo", version = "*" } / "^1.0" (semver range) yes
{ "owner/repo", tag = "v1.0" } (explicit tag pin) no
{ "owner/repo", commit = "abc123" } (explicit commit pin) no
{ "owner/repo", pin = true } (excluded from updates) no

Update behavior matrix

info = currently checked-out commit. target = the latest commit/tag that satisfies minimum_release_age.

Scenario minimum_release_age_downgrade = false (default) = true
Fresh install, eligible target exists check out target check out target
Fresh install, every candidate is still in the cooldown window install task errors with guidance to relax the constraint or retry later same
info == target no-op no-op
info is an ancestor of target (info is older) forward update to target forward update to target
info is a descendant of target (info is newer than the eligible ceiling) keep info (no downgrade); newer commits surfaced as Pending roll back to target
Every candidate is too fresh (target is nil) no-op; held-back ref surfaced as Pending no-op

minimum_release_age_downgrade honors per-plugin overrides identically to minimum_release_age. Fresh installs always honor minimum_release_age regardless of this flag.

UI

Updates held back by minimum_release_age are surfaced in a new Pending (minimum_release_age = ...) section in the :Lazy UI, with per-line diagnostics showing the held commit or version plus an ETA:

Pending (minimum_release_age = "7d") (3)
  • fidget.nvim ...                       commit b33c466 (available in 4d)
  • lsp_signature.nvim ...                commit a1b2c3d (available in 2d)
  • md-render.nvim ...                    version 3.1.1 (available in 1d)

The ETA uses the same time source as the age filter (tag creatordate for tag targets, committer date for plain commit targets), so the displayed countdown matches when the candidate actually unlocks.

Bundled refactor

This PR also includes a small refactor that derives lazy.nvim's auto-injected self-spec from the local clone's origin URL instead of hardcoding folke/lazy.nvim. This makes the new feature dogfoodable from a fork without manually patching the self-injection line, and is a general improvement for anyone running lazy.nvim from a non-official clone. Behavior is unchanged for upstream users -- their local origin already points to folke/lazy.nvim.

Related Issue(s)

Screenshots

The :Lazy UI shows fresh updates that have aged past minimum_release_age in the regular Updates section, while newer commits/tags still inside the cooldown window are surfaced in Pending (minimum_release_age = "7d") with a per-plugin ETA:

Screenshot 2026-05-23 at 4 15 30

The self-spec auto-injection used to be a hardcoded "folke/lazy.nvim",
which forced anyone running lazy.nvim from a fork to either rewrite
this file or accept the spec/origin mismatch error ("Origin has
changed: ...") on every :Lazy update.

Read the origin URL from Config.me/.git/config at setup time and
build the self-spec from that:

  * GitHub URLs become the familiar "owner/repo" shorthand
  * Other URLs fall back to {url = ..., name = "lazy.nvim"}
  * Missing .git/config (e.g. an immutable Nix store path) falls
    back to the previous "folke/lazy.nvim" default

The behavior is unchanged for upstream users -- their local origin
already points to folke/lazy.nvim -- but forks now work out of the
box.
@github-actions github-actions Bot added the size/xl Extra large PR (100+ lines changed) label May 22, 2026
@h-michael h-michael changed the title feat: add minimum_release_age option feat(config): add minimum_release_age option May 22, 2026
Mitigate supply-chain attacks by ignoring commits and tags that have
not been published long enough. Inspired by pnpm minimumReleaseAge,
mise minimum_release_age, Renovate, and Dependabot's cooldown option.

Configure globally via opts.defaults.minimum_release_age, or per-plugin
with `minimum_release_age = "..."` / `false` to disable for that
plugin. Accepts a number of seconds or a single-unit duration string
("30m", "24h", "7d", "2w", "1y"). Combined forms ("7d12h", "1d 2h")
are not supported -- use a single unit.

Applies only to fuzzy resolution paths: semver ranges (`version=`)
and branch HEAD following. Explicit `commit=` / `tag=` pins and
`pin = true` are not affected, matching Renovate's pin-exemption
semantics.

By default, an already-installed plugin whose current commit is
newer than what minimum_release_age would otherwise pick is kept
as-is rather than rolled back to an older one. Set
`defaults.minimum_release_age_downgrade = true` (or the per-plugin
equivalent) to restore the strict "always check out the latest
commit that satisfies minimum_release_age" behavior. Fresh installs
always honor minimum_release_age regardless of this flag.

Updates that minimum_release_age is holding back are surfaced in a
new "Pending (minimum_release_age = ...)" section in the `:Lazy` UI,
with per-plugin diagnostics showing the held commit or version and
an ETA -- e.g. "commit b33c466 (available in 4d)" or "version 1.40.0
(available in 4d)" -- so the active constraint and unlock time are
visible at a glance.

Default is nil for both options, so this is purely opt-in with no
behavioral change for existing users.
@h-michael h-michael force-pushed the feat/minimum-release-age branch from 6435bb5 to 0d22980 Compare May 22, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl Extra large PR (100+ lines changed)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant