Skip to content
Draft
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
10 changes: 10 additions & 0 deletions docs/how-tos/release-age-cooldown.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,13 @@ Valid values:
This is useful when a specific package is trusted enough to allow recent
versions, or when a package's release cadence makes the global cooldown
impractical.

Top-Level ``==`` Pin Exemption
------------------------------

Top-level requirements that use ``==`` (e.g. ``torch==2.5.1``) bypass the
cooldown automatically — the operator has explicitly chosen that version.

``==`` specifiers in transitive dependencies are **not** exempt; without
this distinction a malicious package could pin its own dependencies to
bypass cooldown.
17 changes: 6 additions & 11 deletions docs/proposals/release-cooldown.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,12 @@ References:
global default plus per-package overrides.
- **SSH transport** for git timestamp retrieval.

### Future consideration: `==` pin exemptions

Whether `==` pins in top-level requirements or constraints files
should automatically bypass cooldown is deferred. The per-package
`resolver_dist.min_release_age: 0` override already provides an
explicit, auditable escape hatch for packages that need to use
recently-published versions. Adding automatic `==` exemptions
would introduce a special case that weakens the security model
and requires users to understand the distinction. This can be
revisited if the per-package override proves too cumbersome in
practice.
### `==` pin exemptions (implemented)

Top-level `==` pins bypass cooldown automatically. Transitive `==` pins
remain subject to cooldown for security. See
[the how-to guide](../how-tos/release-age-cooldown.rst) for details.
Tracked in [#1123](https://github.com/python-wheel-build/fromager/issues/1123).

## How

Expand Down
16 changes: 15 additions & 1 deletion src/fromager/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def resolve(
req_type=req_type,
ignore_platform=ignore_platform,
)
provider.cooldown = resolve_package_cooldown(ctx, req)
provider.cooldown = resolve_package_cooldown(ctx, req, req_type=req_type)
max_age_cutoff = _compute_max_age_cutoff(ctx)
results = find_all_matching_from_provider(
provider, req, max_age_cutoff=max_age_cutoff
Expand Down Expand Up @@ -137,19 +137,33 @@ def default_resolver_provider(
)


def _has_equality_pin(req: Requirement) -> bool:
"""Return True if the requirement contains an ``==`` version specifier."""
return any(spec.operator == "==" for spec in req.specifier)
Comment on lines +140 to +142
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that wildcard equality is parsed and currently matches operator "=="
python - <<'PY'
from packaging.requirements import Requirement
r = Requirement("test-pkg==1.*")
for s in r.specifier:
    print("operator=", s.operator, "version=", s.version)
PY

Repository: python-wheel-build/fromager

Length of output: 97


Exclude wildcard pins from == operator exemption

The function _has_equality_pin() currently exempts any requirement with == operator, including wildcards like ==1.*. This widens the cooldown bypass beyond exact pins and weakens the protection model.

Fix
 def _has_equality_pin(req: Requirement) -> bool:
     """Return True if the requirement contains an ``==`` version specifier."""
-    return any(spec.operator == "==" for spec in req.specifier)
+    return any(
+        spec.operator == "==" and "*" not in spec.version
+        for spec in req.specifier
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/resolver.py` around lines 140 - 142, The helper
_has_equality_pin currently treats any "==" specifier as an exact pin; update it
to only return True for true exact pins by checking both spec.operator == "=="
and that the spec's version contains no wildcard characters (e.g., no '*' or
patterns like '1.*'). In other words, inside _has_equality_pin (working with the
Requirement and its specifier members), ensure you inspect spec.version (or
str(spec)) and exclude specifiers where '*' appears (or the version ends with
'.*') so wildcard pins like "==1.*" do not count as exact equality pins.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a valid concern.

The function also does not handle edge cases correctly:

>>> _has_equality_pin(Requirement("foo==1,>2"))
True

I recommend that you only return true if the requirement has exactly one specifier and the specifier is == without a glob.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tiran, agreed. Here's the updated implementation that applies both constraints — exactly one specifier, == operator, no wildcard:

def _has_equality_pin(req: Requirement) -> bool:
    """Return True if the requirement has a single exact ``==`` pin (no wildcard, no compound specifiers)."""
    specs = list(req.specifier)
    return (
        len(specs) == 1
        and specs[0].operator == "=="
        and "*" not in specs[0].version
    )

This correctly rejects:

  • foo==1.* — wildcard pin
  • foo==1,>2 — compound specifier (not a pure exact pin)
  • foo>=1.0 — non-equality specifier



def resolve_package_cooldown(
ctx: context.WorkContext,
req: Requirement,
req_type: RequirementType | None = None,
) -> Cooldown | None:
"""Compute the effective cooldown for a single package.

Args:
ctx: The current work context (provides the global cooldown).
req: The package requirement being resolved.
req_type: The requirement type (top-level, install, etc.).

Returns:
The cooldown to pass to the provider, or ``None`` if disabled.
"""
if req_type == RequirementType.TOP_LEVEL and _has_equality_pin(req):
logger.info(
"%s: cooldown bypassed — top-level requirement uses == pin",
req.name,
)
Comment on lines +161 to +164
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is no need to prefix the log message with the package name. It's automatically added with logger context.

Also please keep log lines ASCII-only. There is an em-dash.

return None

per_package_days = ctx.package_build_info(req).resolver_min_release_age
global_cooldown = ctx.cooldown
if per_package_days is None:
Expand Down
2 changes: 1 addition & 1 deletion src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def get_source_provider(
ignore_platform=pbi.resolver_ignore_platform,
),
)
provider.cooldown = resolver.resolve_package_cooldown(ctx, req)
provider.cooldown = resolver.resolve_package_cooldown(ctx, req, req_type=req_type)
return provider


Expand Down
4 changes: 3 additions & 1 deletion src/fromager/wheels.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,9 @@ def resolve_all_prebuilt_wheels(
provider = get_prebuilt_wheel_provider(
ctx=ctx, req=req, wheel_server_url=url, req_type=req_type
)
provider.cooldown = resolver.resolve_package_cooldown(ctx, req)
provider.cooldown = resolver.resolve_package_cooldown(
ctx, req, req_type=req_type
)
# The local fromager wheel server is PEP 503-only and serves
# packages that were already resolved and vetted earlier in the
# same run. Don't fail-closed on missing upload_time there.
Expand Down
45 changes: 45 additions & 0 deletions tests/test_cooldown.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from packaging.version import Version

from fromager import candidate, context, packagesettings, resolver, sources, wheels
from fromager.requirements_file import RequirementType

_BOOTSTRAP_TIME = datetime.datetime(2026, 3, 26, 0, 0, 0, tzinfo=datetime.UTC)
_COOLDOWN_7_DAYS = datetime.timedelta(days=7)
Expand Down Expand Up @@ -860,3 +861,47 @@ def test_compute_max_age_cutoff_disabled(
"""_compute_max_age_cutoff returns None when max_release_age is not set."""
cutoff = resolver._compute_max_age_cutoff(tmp_context)
assert cutoff is None


def test_resolve_package_cooldown_exempt_toplevel_equality_pin(
tmp_path: pathlib.Path,
) -> None:
"""Top-level == pin bypasses cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.3.2"), req_type=RequirementType.TOP_LEVEL
)
assert result is None


def test_resolve_package_cooldown_enforced_transitive_equality_pin(
tmp_path: pathlib.Path,
) -> None:
"""Transitive == pin does NOT bypass cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.3.2"), req_type=RequirementType.INSTALL
)
assert result is _COOLDOWN


def test_resolve_package_cooldown_enforced_toplevel_no_pin(
tmp_path: pathlib.Path,
) -> None:
"""Top-level requirement without == still gets cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg>=1.0"), req_type=RequirementType.TOP_LEVEL
)
assert result is _COOLDOWN


def test_resolve_package_cooldown_none_req_type_not_exempt(
tmp_path: pathlib.Path,
) -> None:
"""Unknown req_type (None) with == does NOT bypass cooldown."""
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
result = resolver.resolve_package_cooldown(
ctx, Requirement("test-pkg==1.3.2"), req_type=None
)
assert result is _COOLDOWN
Comment on lines +866 to +907
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a boundary test for wildcard equality (==1.*)

These tests cover the core matrix well, but they don’t guard the exact-pin boundary. Add one case asserting top-level test-pkg==1.* does not bypass cooldown.

Suggested test
+def test_resolve_package_cooldown_toplevel_wildcard_equality_not_exempt(
+    tmp_path: pathlib.Path,
+) -> None:
+    """Top-level wildcard equality is not an exact pin, so cooldown remains active."""
+    ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
+    result = resolver.resolve_package_cooldown(
+        ctx, Requirement("test-pkg==1.*"), req_type=RequirementType.TOP_LEVEL
+    )
+    assert result is _COOLDOWN
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_cooldown.py` around lines 866 - 907, Add a new test that exercises
the boundary case where a top-level wildcard equality like
Requirement("test-pkg==1.*") should NOT be treated as an exact-pin exemption:
call resolver.resolve_package_cooldown with ctx from _make_ctx(tmp_path,
cooldown=_COOLDOWN), req_type=RequirementType.TOP_LEVEL and
Requirement("test-pkg==1.*"), and assert the returned value equals _COOLDOWN;
place the test alongside the other test_resolve_package_cooldown_* functions and
name it something like
test_resolve_package_cooldown_toplevel_wildcard_equality_not_exempt to mirror
existing naming.

Loading