-
Notifications
You must be signed in to change notification settings - Fork 35
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
Open
xdotli
wants to merge
16
commits into
main
Choose a base branch
from
chore/dep-cooldown-7d
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
edfa207
chore(deps)!: add a 7-day dependency cooldown (uv exclude-newer) as a…
xdotli 74a8a5c
chore(deps): auto-compute the cooldown cutoff via tools/lock.py
xdotli 61d9505
chore(deps): enforce the cooldown dynamically against uv.lock upload …
xdotli 7b92cbd
chore(deps): pin uv>=0.8.4 + ship tools/ in sdist
ea645df
Merge remote-tracking branch 'origin/main' into chore/dep-cooldown-7d
bingran-you a4ffe46
Fix dependency cooldown override expiry
bingran-you 40d71b3
chore(deps): expire pydantic cooldown override
bingran-you 7facf57
fix(types): narrow document user model before prefix checks
bingran-you 151c63d
fix(deps): require rationale for cooldown overrides
bingran-you 44133f5
Merge origin/main into dependency cooldown branch
bingran-you 956dde4
test: report cooldown age with sub-day precision
bingran-you 2b61256
Merge remote-tracking branch 'origin/main' into HEAD
bingran-you 0ac135c
chore: refresh dependency cooldown after main merge
bingran-you 0edbd8c
Merge main into dependency cooldown PR
bingran-you 7a0fd9d
Refresh cooldown lock for click advisory
bingran-you c2fe731
Allow Click 8.4 option error wording
bingran-you File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| """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 `[tool.uv.exclude-newer-package]` plus a matching structured rationale in | ||
| `[tool.benchflow.dependency-cooldown.override-rationale]`, 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_override_rationale_issues, | ||
| find_cooldown_violations, | ||
| find_expired_cooldown_overrides, | ||
| ) | ||
|
|
||
| _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 | ||
| age_days = age.total_seconds() / 86400 | ||
| assert age >= datetime.timedelta(days=COOLDOWN_DAYS), ( | ||
| f"[tool.uv] exclude-newer ({raw}) is only {age_days:.1f}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 an override with rationale." | ||
| ) | ||
|
|
||
|
|
||
| def test_exclude_newer_package_overrides_have_rationales() -> None: | ||
| cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) | ||
| tool = cfg.get("tool", {}) | ||
| uv = tool.get("uv", {}) | ||
| benchflow = tool.get("benchflow", {}) | ||
| cooldown = benchflow.get("dependency-cooldown", {}) | ||
| overrides = uv.get("exclude-newer-package", {}) | ||
| rationales = cooldown.get("override-rationale", {}) | ||
| issues = find_cooldown_override_rationale_issues(overrides, rationales) | ||
| assert not issues, ( | ||
| "[tool.uv.exclude-newer-package] overrides must carry a non-empty " | ||
| "machine-checkable rationale in " | ||
| "[tool.benchflow.dependency-cooldown.override-rationale]:\n" | ||
| + "\n".join(f" {issue}" for issue in issues) | ||
| ) | ||
|
|
||
|
|
||
| def test_exclude_newer_package_overrides_expire_with_cooldown() -> None: | ||
| cfg = tomllib.loads(_PYPROJECT.read_text(encoding="utf-8")) | ||
| overrides = cfg.get("tool", {}).get("uv", {}).get("exclude-newer-package", {}) | ||
| now = datetime.datetime.now(datetime.UTC) | ||
| expired = find_expired_cooldown_overrides(overrides, now=now) | ||
| floor = now - datetime.timedelta(days=COOLDOWN_DAYS) | ||
| assert not expired, ( | ||
| "[tool.uv.exclude-newer-package] overrides are temporary security/urgent " | ||
| f"escape hatches and must be removed once their cutoff is older than the " | ||
| f"{COOLDOWN_DAYS}-day cooldown floor ({floor:%Y-%m-%dT%H:%M:%SZ}):\n" | ||
| + "\n".join( | ||
| f" {name} = {raw!r} (override cutoff {cutoff:%Y-%m-%dT%H:%M:%SZ})" | ||
| for name, raw, cutoff in expired | ||
| ) | ||
| + "\nRemove the stale override and re-lock with `python tools/lock.py`, " | ||
| "or keep an override only for a still-fresh security/urgent pin." | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| """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 import lock as lock_tool | ||
| from tools.lock import ( | ||
| COOLDOWN_DAYS, | ||
| LockError, | ||
| compute_cooldown_cutoff, | ||
| find_cooldown_override_rationale_issues, | ||
| find_cooldown_violations, | ||
| find_expired_cooldown_overrides, | ||
| main, | ||
| 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_normalizes_exemption_names() -> None: | ||
| """Guards the fix from PR #788 so overrides are not spelling-sensitive.""" | ||
| 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) == [] | ||
|
|
||
|
|
||
| def test_expired_cooldown_overrides_flags_aged_out_entries() -> None: | ||
| """Guards the PR #788 override escape hatch from becoming a package allowlist.""" | ||
| overrides = { | ||
| "old-pkg": "2026-06-08T00:00:00Z", | ||
| "fresh-pkg": "2026-06-14T00:00:00Z", | ||
| } | ||
| expired = find_expired_cooldown_overrides(overrides, now=_NOW) | ||
| assert [(name, raw) for name, raw, _ in expired] == [ | ||
| ("old-pkg", "2026-06-08T00:00:00Z") | ||
| ] | ||
|
|
||
|
|
||
| def test_expired_cooldown_overrides_rejects_invalid_timestamp() -> None: | ||
| """Guards the PR #788 override gate against unclear TOML diagnostics.""" | ||
| with pytest.raises( | ||
| LockError, | ||
| match=r"\[tool\.uv\.exclude-newer-package\] 'bad-pkg' must be an ISO timestamp", | ||
| ): | ||
| find_expired_cooldown_overrides({"bad-pkg": "not-a-date"}, now=_NOW) | ||
|
|
||
|
|
||
| def test_override_rationale_gate_accepts_normalized_matching_entry() -> None: | ||
| """Guards PR #788: cooldown overrides require machine-checkable rationale.""" | ||
| issues = find_cooldown_override_rationale_issues( | ||
| {"Fresh_Pkg": "2026-06-14T00:00:00Z"}, | ||
| {"fresh-pkg": "GHSA fix published inside the cooldown window."}, | ||
| ) | ||
| assert issues == [] | ||
|
|
||
|
|
||
| def test_override_rationale_gate_rejects_missing_or_empty_rationale() -> None: | ||
| """Guards PR #788 against bare package allowlist overrides.""" | ||
| issues = find_cooldown_override_rationale_issues( | ||
| { | ||
| "missing-rationale": "2026-06-14T00:00:00Z", | ||
| "empty-rationale": "2026-06-14T00:00:00Z", | ||
| }, | ||
| {"empty-rationale": " "}, | ||
| ) | ||
| assert issues == [ | ||
| "'empty-rationale' override rationale must be a non-empty string.", | ||
| "'missing-rationale' has [tool.uv.exclude-newer-package] but no " | ||
| "[tool.benchflow.dependency-cooldown.override-rationale] entry.", | ||
| ] | ||
|
|
||
|
|
||
| def test_override_rationale_gate_rejects_stale_rationale_entries() -> None: | ||
| """Guards PR #788 so rationale entries expire with their overrides.""" | ||
| issues = find_cooldown_override_rationale_issues( | ||
| {}, | ||
| {"old-pkg": "no longer has an override"}, | ||
| ) | ||
| assert issues == [ | ||
| "'old-pkg' has an override rationale but no matching " | ||
| "[tool.uv.exclude-newer-package] entry." | ||
| ] | ||
|
|
||
|
|
||
| def test_override_rationale_gate_rejects_non_table() -> None: | ||
| """Guards PR #788 against unclear pyproject policy shapes.""" | ||
| issues = find_cooldown_override_rationale_issues( | ||
| {"fresh-pkg": "2026-06-14T00:00:00Z"}, | ||
| "fresh-pkg: security fix", | ||
| ) | ||
| assert issues == [ | ||
| "[tool.benchflow.dependency-cooldown.override-rationale] must be a TOML table." | ||
| ] | ||
|
|
||
|
|
||
| def test_main_wraps_pyproject_read_oserror(tmp_path, capsys) -> None: | ||
| """Guards the PR #788 lock helper from leaking raw OSError tracebacks.""" | ||
| with pytest.raises(SystemExit) as exc_info: | ||
| main(["--pyproject", str(tmp_path), "--no-lock"]) | ||
|
|
||
| assert exc_info.value.code == 1 | ||
| assert "Could not read" in capsys.readouterr().err | ||
|
|
||
|
|
||
| def test_run_uv_lock_wraps_oserror(monkeypatch, tmp_path) -> None: | ||
| """Guards the PR #788 lock helper from leaking subprocess OSError.""" | ||
|
|
||
| def raise_oserror(*args, **kwargs): | ||
| raise OSError("spawn failed") | ||
|
|
||
| monkeypatch.setattr(lock_tool.subprocess, "run", raise_oserror) | ||
|
|
||
| with pytest.raises(LockError, match="Could not run `uv lock`"): | ||
| lock_tool._run_uv_lock(tmp_path / "pyproject.toml") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back 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.
age.days, which is the integer days component of thetimedelta, not the total elapsed days rounded. For a gap like 6 days 23 h 50 min, it prints "6d old" while the assertion correctly sees it as still less than 7 days — the diagnostic makes the boundary look farther away than it is. Preferage.total_seconds() / 86400for accurate sub-day precision.