-
Notifications
You must be signed in to change notification settings - Fork 38
chore(deps)!: 7-day dependency cooldown (uv exclude-newer + CI assert) #788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
edfa207
74a8a5c
61d9505
7b92cbd
ea645df
a4ffe46
40d71b3
7facf57
151c63d
44133f5
956dde4
2b61256
0ac135c
0edbd8c
7a0fd9d
c2fe731
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||||||||||||||||||
| """Hard rule: the dependency cooldown — never resolve to a release younger than | ||||||||||||||||||||||
| a 7-day window. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Two layers, both evaluated against *today*: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 1. `test_uv_exclude_newer_caps_resolution` — `[tool.uv] exclude-newer` is the | ||||||||||||||||||||||
| static cutoff `uv lock` applies at resolve time; it must be >= 7 days old so | ||||||||||||||||||||||
| the default resolution can't reach a brand-new release. Set it with | ||||||||||||||||||||||
| `python tools/lock.py` (computes `now - 7d` and re-locks). | ||||||||||||||||||||||
| 2. `test_locked_packages_respect_cooldown` — the dynamic half: it reads the | ||||||||||||||||||||||
| `upload-time` uv bakes into `uv.lock` and fails if any *resolved* package is | ||||||||||||||||||||||
| younger than `now - 7d`. This tracks the current date with no stored date to | ||||||||||||||||||||||
| trust and no network calls. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| A genuinely urgent (e.g. security) fix younger than the window is allowed only | ||||||||||||||||||||||
| via an explicit, commented `[tool.uv.exclude-newer-package]` override, which | ||||||||||||||||||||||
| exempts that package from layer 2. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import datetime | ||||||||||||||||||||||
| import tomllib | ||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from tools.lock import COOLDOWN_DAYS, find_cooldown_violations | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| _PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml" | ||||||||||||||||||||||
| _LOCK = Path(__file__).resolve().parents[1] / "uv.lock" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_uv_exclude_newer_caps_resolution() -> None: | ||||||||||||||||||||||
| cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) | ||||||||||||||||||||||
| raw = cfg.get("tool", {}).get("uv", {}).get("exclude-newer") | ||||||||||||||||||||||
| assert raw, ( | ||||||||||||||||||||||
| "[tool.uv] exclude-newer is missing from pyproject.toml — it enforces the " | ||||||||||||||||||||||
| f"{COOLDOWN_DAYS}-day dependency cooldown and must be set." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| cutoff = datetime.datetime.fromisoformat(str(raw).replace("Z", "+00:00")) | ||||||||||||||||||||||
| if cutoff.tzinfo is None: | ||||||||||||||||||||||
| cutoff = cutoff.replace(tzinfo=datetime.UTC) | ||||||||||||||||||||||
| age = datetime.datetime.now(datetime.UTC) - cutoff | ||||||||||||||||||||||
| assert age >= datetime.timedelta(days=COOLDOWN_DAYS), ( | ||||||||||||||||||||||
| f"[tool.uv] exclude-newer ({raw}) is only {age.days}d old; the dependency " | ||||||||||||||||||||||
| f"cooldown requires it to be >= {COOLDOWN_DAYS} days in the past. Re-lock " | ||||||||||||||||||||||
| "with `python tools/lock.py` when taking dependency updates." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_locked_packages_respect_cooldown() -> None: | ||||||||||||||||||||||
| cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) | ||||||||||||||||||||||
| exempt = set(cfg.get("tool", {}).get("uv", {}).get("exclude-newer-package", {})) | ||||||||||||||||||||||
| violations = find_cooldown_violations( | ||||||||||||||||||||||
| _LOCK.read_text(encoding="utf-8"), | ||||||||||||||||||||||
| exempt=exempt, | ||||||||||||||||||||||
| now=datetime.datetime.now(datetime.UTC), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| assert not violations, ( | ||||||||||||||||||||||
| f"uv.lock resolves packages younger than the {COOLDOWN_DAYS}-day cooldown " | ||||||||||||||||||||||
| "without a documented [tool.uv.exclude-newer-package] override:\n" | ||||||||||||||||||||||
| + "\n".join(f" {n} {v} (uploaded {ts:%Y-%m-%d})" for n, v, ts in violations) | ||||||||||||||||||||||
| + "\nRe-lock with `python tools/lock.py`, or add a commented override." | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
+50
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| """Unit tests for the dependency-cooldown re-lock helper (`tools/lock.py`). | ||
|
|
||
| These cover the pure date/text transforms and the lock-freshness audit against | ||
| synthetic lock text; `uv lock` itself is not invoked. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import datetime | ||
| import tomllib | ||
|
|
||
| import pytest | ||
|
|
||
| from tools.lock import ( | ||
| COOLDOWN_DAYS, | ||
| LockError, | ||
| compute_cooldown_cutoff, | ||
| find_cooldown_violations, | ||
| newest_upload_times, | ||
| rewrite_exclude_newer, | ||
| ) | ||
|
|
||
| _NOW = datetime.datetime(2026, 6, 15, 12, 0, tzinfo=datetime.UTC) | ||
|
|
||
| # floor (now - 7d) = 2026-06-08T12:00Z: old-pkg is older, fresh-pkg is younger. | ||
| _SYNTH_LOCK = """ | ||
| [options] | ||
| exclude-newer = "2026-06-08T00:00:00Z" | ||
|
|
||
| [[package]] | ||
| name = "old-pkg" | ||
| version = "1.0.0" | ||
| sdist = { url = "https://example/old.tar.gz", upload-time = "2026-05-01T00:00:00Z" } | ||
|
|
||
| [[package]] | ||
| name = "fresh-pkg" | ||
| version = "2.0.0" | ||
| sdist = { url = "https://example/fresh.tar.gz", upload-time = "2026-06-13T00:00:00Z" } | ||
| wheels = [ | ||
| { url = "https://example/fresh.whl", upload-time = "2026-06-14T08:00:00Z" }, | ||
| ] | ||
|
|
||
| [[package]] | ||
| name = "git-pkg" | ||
| version = "0.1.0" | ||
| source = { git = "https://example/repo.git" } | ||
| """ | ||
|
|
||
|
|
||
| def test_cutoff_trails_today_by_cooldown_window_at_midnight() -> None: | ||
| now = datetime.datetime(2026, 6, 15, 9, 30, tzinfo=datetime.UTC) | ||
| assert compute_cooldown_cutoff(now) == "2026-06-08T00:00:00Z" | ||
|
|
||
|
|
||
| def test_cutoff_is_idempotent_within_a_day() -> None: | ||
| morning = datetime.datetime(2026, 6, 15, 0, 1, tzinfo=datetime.UTC) | ||
| night = datetime.datetime(2026, 6, 15, 23, 59, tzinfo=datetime.UTC) | ||
| assert compute_cooldown_cutoff(morning) == compute_cooldown_cutoff(night) | ||
|
|
||
|
|
||
| def test_cutoff_normalizes_non_utc_input() -> None: | ||
| # 2026-06-15 02:00 in UTC+9 is still 2026-06-14 17:00 UTC -> minus 7d. | ||
| tz = datetime.timezone(datetime.timedelta(hours=9)) | ||
| now = datetime.datetime(2026, 6, 15, 2, 0, tzinfo=tz) | ||
| assert compute_cooldown_cutoff(now) == "2026-06-07T00:00:00Z" | ||
|
|
||
|
|
||
| def test_cutoff_default_window_is_the_cooldown_constant() -> None: | ||
| now = datetime.datetime(2026, 6, 15, 12, 0, tzinfo=datetime.UTC) | ||
| expected = (now - datetime.timedelta(days=COOLDOWN_DAYS)).date() | ||
| assert compute_cooldown_cutoff(now).startswith(expected.isoformat()) | ||
|
|
||
|
|
||
| def test_cutoff_rejects_negative_window() -> None: | ||
| now = datetime.datetime(2026, 6, 15, tzinfo=datetime.UTC) | ||
| with pytest.raises(LockError): | ||
| compute_cooldown_cutoff(now, cooldown_days=-1) | ||
|
|
||
|
|
||
| def test_rewrite_replaces_only_the_exclude_newer_value() -> None: | ||
| text = ( | ||
| "[tool.uv]\n" | ||
| 'exclude-newer = "2026-06-01T00:00:00Z"\n' | ||
| "\n" | ||
| "[tool.uv.exclude-newer-package]\n" | ||
| 'litellm = "2026-06-14T00:00:00Z"\n' | ||
| ) | ||
| out = rewrite_exclude_newer(text, "2026-06-08T00:00:00Z") | ||
| assert 'exclude-newer = "2026-06-08T00:00:00Z"' in out | ||
| # The per-package override table is untouched. | ||
| assert 'litellm = "2026-06-14T00:00:00Z"' in out | ||
|
|
||
|
|
||
| def test_rewrite_requires_exactly_one_assignment() -> None: | ||
| with pytest.raises(LockError): | ||
| rewrite_exclude_newer("[tool.uv]\n", "2026-06-08T00:00:00Z") | ||
|
|
||
|
|
||
| def test_newest_upload_time_picks_latest_artifact_and_skips_sourceless() -> None: | ||
| newest = newest_upload_times(tomllib.loads(_SYNTH_LOCK)) | ||
| # The wheel (08:00) beats the sdist (00:00) for the same package. | ||
| assert newest["fresh-pkg"][1] == datetime.datetime( | ||
| 2026, 6, 14, 8, 0, tzinfo=datetime.UTC | ||
| ) | ||
| # A git-sourced package has no upload-time and is omitted entirely. | ||
| assert "git-pkg" not in newest | ||
|
|
||
|
|
||
| def test_find_cooldown_violations_flags_only_young_unexempted_packages() -> None: | ||
| violations = find_cooldown_violations(_SYNTH_LOCK, exempt=set(), now=_NOW) | ||
| assert [name for name, _, _ in violations] == ["fresh-pkg"] | ||
|
|
||
|
|
||
| def test_find_cooldown_violations_honors_exemptions() -> None: | ||
| violations = find_cooldown_violations(_SYNTH_LOCK, exempt={"fresh-pkg"}, now=_NOW) | ||
| assert violations == [] | ||
|
|
||
|
|
||
| def test_find_cooldown_violations_only_grows_more_lenient_over_time() -> None: | ||
| # The same lock, evaluated a month later: fresh-pkg has aged past the window. | ||
| later = _NOW + datetime.timedelta(days=30) | ||
| assert find_cooldown_violations(_SYNTH_LOCK, exempt=set(), now=later) == [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In environments that still have uv 0.7.x, this new table is an unknown
[tool.uv]key:uv sync --lockedreportsunknown field exclude-newer-package, ignores the lockfile cutoff as removed, and tries to re-resolve from PyPI instead of installing the committed lock. Since the repo only requires users to haveuvand does not settool.uv.required-version, local setup and any older CI image can fail or bypass the intended cooldown; pin/install a uv version that supports per-package cutoffs before relying on this syntax.Useful? React with 👍 / 👎.