Skip to content

Commit 4294f40

Browse files
feat(resolver): exempt top-level == pins from release cooldown
Closes: #1123 Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent b5b31d9 commit 4294f40

6 files changed

Lines changed: 104 additions & 14 deletions

File tree

docs/how-tos/release-age-cooldown.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,13 @@ Valid values:
130130
This is useful when a specific package is trusted enough to allow recent
131131
versions, or when a package's release cadence makes the global cooldown
132132
impractical.
133+
134+
Top-Level ``==`` Pin Exemption
135+
------------------------------
136+
137+
Top-level requirements that use ``==`` (e.g. ``torch==2.5.1``) bypass the
138+
cooldown automatically — the operator has explicitly chosen that version.
139+
140+
``==`` specifiers in transitive dependencies are **not** exempt; without
141+
this distinction a malicious package could pin its own dependencies to
142+
bypass cooldown.

docs/proposals/release-cooldown.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,12 @@ References:
5555
global default plus per-package overrides.
5656
- **SSH transport** for git timestamp retrieval.
5757

58-
### Future consideration: `==` pin exemptions
59-
60-
Whether `==` pins in top-level requirements or constraints files
61-
should automatically bypass cooldown is deferred. The per-package
62-
`resolver_dist.min_release_age: 0` override already provides an
63-
explicit, auditable escape hatch for packages that need to use
64-
recently-published versions. Adding automatic `==` exemptions
65-
would introduce a special case that weakens the security model
66-
and requires users to understand the distinction. This can be
67-
revisited if the per-package override proves too cumbersome in
68-
practice.
58+
### `==` pin exemptions (implemented)
59+
60+
Top-level `==` pins bypass cooldown automatically. Transitive `==` pins
61+
remain subject to cooldown for security. See
62+
[the how-to guide](../how-tos/release-age-cooldown.rst) for details.
63+
Tracked in [#1123](https://github.com/python-wheel-build/fromager/issues/1123).
6964

7065
## How
7166

src/fromager/resolver.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def resolve(
103103
req_type=req_type,
104104
ignore_platform=ignore_platform,
105105
)
106-
provider.cooldown = resolve_package_cooldown(ctx, req)
106+
provider.cooldown = resolve_package_cooldown(ctx, req, req_type=req_type)
107107
max_age_cutoff = _compute_max_age_cutoff(ctx)
108108
results = find_all_matching_from_provider(
109109
provider, req, max_age_cutoff=max_age_cutoff
@@ -137,19 +137,35 @@ def default_resolver_provider(
137137
)
138138

139139

140+
def _has_equality_pin(req: Requirement) -> bool:
141+
"""Return True if the requirement has a single exact ``==`` pin.
142+
143+
Rejects wildcard pins (``==1.*``) and compound specifiers (``==1,>2``)
144+
which are not true exact version pins.
145+
"""
146+
specs = list(req.specifier)
147+
return len(specs) == 1 and specs[0].operator == "==" and "*" not in specs[0].version
148+
149+
140150
def resolve_package_cooldown(
141151
ctx: context.WorkContext,
142152
req: Requirement,
153+
req_type: RequirementType | None = None,
143154
) -> Cooldown | None:
144155
"""Compute the effective cooldown for a single package.
145156
146157
Args:
147158
ctx: The current work context (provides the global cooldown).
148159
req: The package requirement being resolved.
160+
req_type: The requirement type (top-level, install, etc.).
149161
150162
Returns:
151163
The cooldown to pass to the provider, or ``None`` if disabled.
152164
"""
165+
if req_type == RequirementType.TOP_LEVEL and _has_equality_pin(req):
166+
logger.info("cooldown bypassed -- top-level requirement uses == pin")
167+
return None
168+
153169
per_package_days = ctx.package_build_info(req).resolver_min_release_age
154170
global_cooldown = ctx.cooldown
155171
if per_package_days is None:

src/fromager/sources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def get_source_provider(
153153
ignore_platform=pbi.resolver_ignore_platform,
154154
),
155155
)
156-
provider.cooldown = resolver.resolve_package_cooldown(ctx, req)
156+
provider.cooldown = resolver.resolve_package_cooldown(ctx, req, req_type=req_type)
157157
return provider
158158

159159

src/fromager/wheels.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,9 @@ def resolve_all_prebuilt_wheels(
549549
provider = get_prebuilt_wheel_provider(
550550
ctx=ctx, req=req, wheel_server_url=url, req_type=req_type
551551
)
552-
provider.cooldown = resolver.resolve_package_cooldown(ctx, req)
552+
provider.cooldown = resolver.resolve_package_cooldown(
553+
ctx, req, req_type=req_type
554+
)
553555
# The local fromager wheel server is PEP 503-only and serves
554556
# packages that were already resolved and vetted earlier in the
555557
# same run. Don't fail-closed on missing upload_time there.

tests/test_cooldown.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from packaging.version import Version
2121

2222
from fromager import candidate, context, packagesettings, resolver, sources, wheels
23+
from fromager.requirements_file import RequirementType
2324

2425
_BOOTSTRAP_TIME = datetime.datetime(2026, 3, 26, 0, 0, 0, tzinfo=datetime.UTC)
2526
_COOLDOWN_7_DAYS = datetime.timedelta(days=7)
@@ -860,3 +861,69 @@ def test_compute_max_age_cutoff_disabled(
860861
"""_compute_max_age_cutoff returns None when max_release_age is not set."""
861862
cutoff = resolver._compute_max_age_cutoff(tmp_context)
862863
assert cutoff is None
864+
865+
866+
def test_resolve_package_cooldown_exempt_toplevel_equality_pin(
867+
tmp_path: pathlib.Path,
868+
) -> None:
869+
"""Top-level == pin bypasses cooldown."""
870+
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
871+
result = resolver.resolve_package_cooldown(
872+
ctx, Requirement("test-pkg==1.3.2"), req_type=RequirementType.TOP_LEVEL
873+
)
874+
assert result is None
875+
876+
877+
def test_resolve_package_cooldown_enforced_transitive_equality_pin(
878+
tmp_path: pathlib.Path,
879+
) -> None:
880+
"""Transitive == pin does NOT bypass cooldown."""
881+
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
882+
result = resolver.resolve_package_cooldown(
883+
ctx, Requirement("test-pkg==1.3.2"), req_type=RequirementType.INSTALL
884+
)
885+
assert result is _COOLDOWN
886+
887+
888+
def test_resolve_package_cooldown_enforced_toplevel_no_pin(
889+
tmp_path: pathlib.Path,
890+
) -> None:
891+
"""Top-level requirement without == still gets cooldown."""
892+
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
893+
result = resolver.resolve_package_cooldown(
894+
ctx, Requirement("test-pkg>=1.0"), req_type=RequirementType.TOP_LEVEL
895+
)
896+
assert result is _COOLDOWN
897+
898+
899+
def test_resolve_package_cooldown_none_req_type_not_exempt(
900+
tmp_path: pathlib.Path,
901+
) -> None:
902+
"""Unknown req_type (None) with == does NOT bypass cooldown."""
903+
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
904+
result = resolver.resolve_package_cooldown(
905+
ctx, Requirement("test-pkg==1.3.2"), req_type=None
906+
)
907+
assert result is _COOLDOWN
908+
909+
910+
def test_resolve_package_cooldown_toplevel_wildcard_equality_not_exempt(
911+
tmp_path: pathlib.Path,
912+
) -> None:
913+
"""Top-level wildcard equality (==1.*) is not an exact pin — cooldown applies."""
914+
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
915+
result = resolver.resolve_package_cooldown(
916+
ctx, Requirement("test-pkg==1.*"), req_type=RequirementType.TOP_LEVEL
917+
)
918+
assert result is _COOLDOWN
919+
920+
921+
def test_resolve_package_cooldown_toplevel_compound_specifier_not_exempt(
922+
tmp_path: pathlib.Path,
923+
) -> None:
924+
"""Top-level compound specifier (==1.0,>0.9) is not a single exact pin."""
925+
ctx = _make_ctx(tmp_path, cooldown=_COOLDOWN)
926+
result = resolver.resolve_package_cooldown(
927+
ctx, Requirement("test-pkg==1.0,>0.9"), req_type=RequirementType.TOP_LEVEL
928+
)
929+
assert result is _COOLDOWN

0 commit comments

Comments
 (0)