Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .github/hook-pin-cutoffs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"config_sha256": "db72e009c83a9a9f82ac5746e02c4f0e1a0a94cb84826091e81fe6f0b5e3131c",
"pins": {
"ruamel-yaml==0.19.1": "2026-01-02T16:50:31.840645+00:00",
"ruff==0.17.3": "2026-10-27T00:34:05.914554+00:00",
"zizmor==1.32.0": "2026-11-02T23:31:21.600887+00:00"
}
}
4 changes: 4 additions & 0 deletions .github/workflows/update-python-dependencies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ jobs:
done <"$RUNNER_TEMP/file-groups"
- name: ⏳ Wait for locked release tools
wait: tools
- name: 🔒 Refresh hook pin cutoffs
uses: $/cooled-prek
with:
args: refresh-pins
- id: install-hooks
name: 🧹 Install prek hooks
uses: $/cooled-prek
Expand Down
9 changes: 6 additions & 3 deletions docs/locked-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ The check runs in three places:
and takes no inputs:

```yaml
- uses: tox-dev/github-workflows/check-nab-exceptions@SHA
- uses: tox-dev/github-workflows/check-nab-exceptions@SHA
```

## Run prek under the cutoff: `cooled-prek`
Expand All @@ -100,8 +100,11 @@ cutoff, so a hook bumped to a release younger than seven days installs. The pin
read. Once the release is a week old, nothing is written and the normal cutoff applies. A newer release of the pinned
package, uploaded after the pinned one, stays blocked in every hook environment. The pinned release's dependencies and
any ranges stay cooled. A pin and a `packages.<name>.uploaded-prior-to` exception for the same package: the later date
wins. `cooled-prek` fetches the hook repository and looks the pinned release up on PyPI; a failed fetch or lookup fails
the run. Pitfall: hook environments read exceptions only from `pyproject.toml`, not from a standalone `nab.toml`.
wins. `cooled-prek refresh-pins` fetches hook repositories and PyPI upload times and writes
`.github/hook-pin-cutoffs.json`. Commit that file with `.pre-commit-config.yaml`; the normal check validates its hash
and reads it without fetching metadata. The dependency update workflow refreshes it before installing hooks. A missing
or stale file fails with a refresh instruction. Pitfall: hook environments read exceptions only from `pyproject.toml`,
not from a standalone `nab.toml`.

## Install locked tools: `install-locked-tools`

Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
import json
import os
import re
Expand Down Expand Up @@ -125,6 +126,12 @@ def repository(tmp_path: Path, github_api: tuple[HTTPServer, dict[str, str]], mo
" - id: mdformat\n name: mdformat\n entry: mdformat --wrap 120\n"
" language: system\n types: [markdown]\n",
)
(worktree / ".github").mkdir()
manifest = {
"config_sha256": hashlib.sha256((worktree / ".pre-commit-config.yaml").read_bytes()).hexdigest(),
"pins": {},
}
(worktree / ".github/hook-pin-cutoffs.json").write_text(f"{json.dumps(manifest, indent=2, sort_keys=True)}\n")
(worktree / "CHANGELOG.md").write_text("# Changelog\n\n<!-- towncrier release notes start -->\n")
(worktree / "changelog.d").mkdir()
run(worktree, "git", "add", ".")
Expand Down
114 changes: 113 additions & 1 deletion tests/cooled_prek/test_cooled_prek.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ def route_open(request, *args, **kwargs):
f"import sitecustomize; exec(compile(sys.stdin.read(), {str(SCRIPT.with_name('hook_pin_cutoffs.py'))!r}, 'exec'))"
)
fake = directory / "uv"
fake.write_text(f"#!/bin/sh\nexec {sys.executable!r} -I -c {shlex.quote(code)}\n")
fake.write_text(
f'#!/bin/sh\ncase "$*" in *--refresh*) set -- --refresh ;; *) set -- ;; esac\n'
f'exec {sys.executable!r} -I -c {shlex.quote(code)} "$@"\n'
)
fake.chmod(0o755)


Expand Down Expand Up @@ -239,6 +242,16 @@ def _cooled_prek(root: Path, httpserver: HTTPServer) -> subprocess.CompletedProc
fake.write_text('#!/bin/sh\ncat "${UV_CONFIG_FILE:-/dev/null}" >config\n')
fake.chmod(0o755)
_fake_uv(root, httpserver)
refreshed = subprocess.run(
[str(SCRIPT), "refresh-pins"],
cwd=root,
env=os.environ | {"PATH": f"{root}{os.pathsep}{os.environ['PATH']}", "NO_PROXY": "localhost,127.0.0.1"},
capture_output=True,
text=True,
check=False,
)
if refreshed.returncode:
return refreshed
return subprocess.run(
[str(SCRIPT), "install-hooks"],
cwd=root,
Expand Down Expand Up @@ -427,6 +440,105 @@ def test_cooled_prek_does_not_write_pin_older_than_window(tmp_path: Path, httpse
assert (tmp_path / "config").read_text() == ""


def test_cooled_prek_uses_committed_pin_dates_without_metadata_requests(tmp_path: Path, httpserver: HTTPServer) -> None:
uploaded = _uploaded(1)
_simple(httpserver, {"0.7.1": uploaded})
repo = _hook_repo(tmp_path, {"setup.py": 'setup(install_requires=["simplint==0.7.1"])\n'})
_config(tmp_path, repo, {"language": "python"})
assert _cooled_prek(tmp_path, httpserver).returncode == 0
requests = len(httpserver.log)
for path in repo.iterdir():
if path.is_file():
path.unlink()

result = subprocess.run(
[str(SCRIPT), "install-hooks"],
cwd=tmp_path,
env=os.environ | {"PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}"},
capture_output=True,
text=True,
check=False,
)

assert result.returncode == 0, result.stderr
assert len(httpserver.log) == requests
assert (tmp_path / "config").read_text().splitlines() == ["[exclude-newer-package]", _admitted(uploaded)]


def test_cooled_prek_rejects_stale_pin_dates(tmp_path: Path, httpserver: HTTPServer) -> None:
_config(tmp_path, _hook_repo(tmp_path, {}), {"language": "python"})
assert _cooled_prek(tmp_path, httpserver).returncode == 0
(tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n")

result = subprocess.run(
[str(SCRIPT), "install-hooks"],
cwd=tmp_path,
env=os.environ | {"PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}"},
capture_output=True,
text=True,
check=False,
)

assert result.returncode != 0
assert "is stale; run cooled-prek refresh-pins" in result.stderr


def test_cooled_prek_refresh_keeps_unchanged_manifest(tmp_path: Path, httpserver: HTTPServer) -> None:
(tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n")
assert _cooled_prek(tmp_path, httpserver).returncode == 0
path = tmp_path / ".github/hook-pin-cutoffs.json"
previous = path.stat().st_mtime_ns

result = subprocess.run(
[str(SCRIPT), "refresh-pins"],
cwd=tmp_path,
env=os.environ | {"PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}"},
capture_output=True,
text=True,
check=False,
)

assert result.returncode == 0, result.stderr
assert path.stat().st_mtime_ns == previous


def test_cooled_prek_requires_manifest(tmp_path: Path, httpserver: HTTPServer) -> None:
(tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n")
_fake_uv(tmp_path, httpserver)

result = subprocess.run(
[str(SCRIPT), "install-hooks"],
cwd=tmp_path,
env=os.environ | {"PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}"},
capture_output=True,
text=True,
check=False,
)

assert result.returncode != 0
assert "is missing; run cooled-prek refresh-pins" in result.stderr


def test_cooled_prek_rejects_invalid_manifest(tmp_path: Path, httpserver: HTTPServer) -> None:
(tmp_path / ".pre-commit-config.yaml").write_text("repos: []\n")
_fake_uv(tmp_path, httpserver)
path = tmp_path / ".github/hook-pin-cutoffs.json"
path.parent.mkdir()
path.write_text("{")

result = subprocess.run(
[str(SCRIPT), "install-hooks"],
cwd=tmp_path,
env=os.environ | {"PATH": f"{tmp_path}{os.pathsep}{os.environ['PATH']}"},
capture_output=True,
text=True,
check=False,
)

assert result.returncode != 0
assert "cannot read .github/hook-pin-cutoffs.json" in result.stderr


@pytest.mark.parametrize(
("nab", "expected"),
[
Expand Down
5 changes: 3 additions & 2 deletions tests/prepare_towncrier/test_prepare.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from __future__ import annotations

import sys
from pathlib import Path
from textwrap import dedent

Expand Down Expand Up @@ -252,6 +251,7 @@ def test_prepare_formats_rst(
" name: docstrfmt\n entry: sed -i s/Before/After/\n"
" language: system\n files: CHANGELOG.rst\n"
)
run(repository, sys.executable, str(Path(__file__).resolve().parents[2] / "tools/hook_pin_cutoffs.py"), "--refresh")
run(repository, "git", "add", "-A")
run(repository, "git", "commit", "-m", "Set RST hook")
run(repository, "git", "push", "origin", "main")
Expand Down Expand Up @@ -290,6 +290,7 @@ def test_prepare_avoids_rst_conflict_marker(
" name: docstrfmt\n entry: bash -c true\n"
" language: system\n files: CHANGELOG.rst\n"
)
run(repository, sys.executable, str(Path(__file__).resolve().parents[2] / "tools/hook_pin_cutoffs.py"), "--refresh")
run(repository, "git", "add", "-A")
run(repository, "git", "commit", "-m", "Set RST category")
run(repository, "git", "push", "origin", "main")
Expand Down
25 changes: 16 additions & 9 deletions tools/cooled_prek.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

def main() -> int:
arguments = sys.argv[1:]
if arguments == ["refresh-pins"]:
return _pin_cutoffs(["--refresh"]).returncode
if arguments[:1] != ["run"]:
return _install_hooks(arguments)
hooks = []
Expand All @@ -26,15 +28,7 @@ def main() -> int:

def _install_hooks(arguments: list[str]) -> int:
environment = clean_uv_environment() | {"UV_EXCLUDE_NEWER": "7 days", "UV_NO_CONFIG": "1", "UV_NO_SOURCES": "1"}
result = subprocess.run(
["uv", "run", "--no-project", "--with", "pyyaml", "python", "-I", "-"],
input=Path(__file__).with_name("hook_pin_cutoffs.py").read_text(),
text=True,
capture_output=True,
env=environment,
check=False,
)
sys.stderr.write(result.stderr)
result = _pin_cutoffs([], environment)
if result.returncode:
return result.returncode
exceptions = result.stdout.strip()
Expand All @@ -60,4 +54,17 @@ def _install_hooks(arguments: list[str]) -> int:
).returncode


def _pin_cutoffs(arguments: list[str], environment: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
result = subprocess.run(
["uv", "run", "--no-project", "--with", "pyyaml", "python", "-I", "-", *arguments],
input=Path(__file__).with_name("hook_pin_cutoffs.py").read_text(),
text=True,
capture_output=True,
env=environment or clean_uv_environment(),
check=False,
)
sys.stderr.write(result.stderr)
return result


raise SystemExit(main())
60 changes: 51 additions & 9 deletions tools/hook_pin_cutoffs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ast
import configparser
import datetime
import hashlib
import json
import pathlib
import re
Expand All @@ -23,6 +24,11 @@ class Hook(TypedDict):
additional_dependencies: NotRequired[list[str]]


class PinManifest(TypedDict):
config_sha256: str
pins: dict[str, str]


def normalize(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()

Expand Down Expand Up @@ -174,7 +180,47 @@ def as_datetime(value: str | datetime.datetime | None) -> datetime.datetime | No
return value if isinstance(value, datetime.datetime) and value.tzinfo is not None else None


def pin_manifest() -> PinManifest:
config_path = pathlib.Path(".pre-commit-config.yaml")
config_bytes = config_path.read_bytes() if config_path.is_file() else b""
config = yaml.safe_load(config_bytes) or {}
pins = {
f"{name}=={version}": upload_time(repo["repo"], name, version).isoformat()
for repo in config.get("repos", [])
if repo["repo"] not in {"local", "meta"}
for name, version in hook_pins(repo["repo"], repo["rev"], repo.get("hooks", []))
}
return {"config_sha256": hashlib.sha256(config_bytes).hexdigest(), "pins": pins}


def refresh() -> None:
path = pathlib.Path(".github/hook-pin-cutoffs.json")
path.parent.mkdir(parents=True, exist_ok=True)
content = f"{json.dumps(pin_manifest(), indent=2, sort_keys=True)}\n"
if not path.is_file() or path.read_text() != content:
path.write_text(content)


def read_pin_manifest() -> dict[str, str]:
path = pathlib.Path(".github/hook-pin-cutoffs.json")
if not path.is_file():
if not pathlib.Path(".pre-commit-config.yaml").is_file():
return {}
sys.exit(f"cooled-prek: {path} is missing; run cooled-prek refresh-pins and commit it")
try:
manifest = json.loads(path.read_text())
config = pathlib.Path(".pre-commit-config.yaml")
if manifest["config_sha256"] != hashlib.sha256(config.read_bytes() if config.is_file() else b"").hexdigest():
sys.exit(f"cooled-prek: {path} is stale; run cooled-prek refresh-pins and commit it")
return manifest["pins"]
except (OSError, ValueError, KeyError, TypeError) as error:
sys.exit(f"cooled-prek: cannot read {path}: {error}")


def main() -> None:
if sys.argv[1:] == ["--refresh"]:
refresh()
return
path = pathlib.Path("pyproject.toml")
packages = (
tomllib.loads(path.read_text()).get("tool", {}).get("nab", {}).get("packages", {}) if path.is_file() else {}
Expand All @@ -184,17 +230,13 @@ def main() -> None:
for name, settings in packages.items()
if settings.get("uploaded-prior-to") is not None
}
path = pathlib.Path(".pre-commit-config.yaml")
config = (yaml.safe_load(path.read_text()) if path.is_file() else None) or {}
seven_days_ago = datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=7)
pins = {}
for repo in config.get("repos", []):
if repo["repo"] in {"local", "meta"}:
continue
for name, version in hook_pins(repo["repo"], repo["rev"], repo.get("hooks", [])):
cutoff = upload_time(repo["repo"], name, version).replace(microsecond=0) + datetime.timedelta(seconds=1)
if cutoff > seven_days_ago:
pins[name] = max(pins.get(name, cutoff), cutoff)
for pin, uploaded in read_pin_manifest().items():
name, _, _ = pin.partition("==")
cutoff = datetime.datetime.fromisoformat(uploaded).replace(microsecond=0) + datetime.timedelta(seconds=1)
if cutoff > seven_days_ago:
pins[name] = max(pins.get(name, cutoff), cutoff)
for name, cutoff in pins.items():
if (current := as_datetime(cutoffs.get(name))) is None or current < cutoff:
cutoffs[name] = cutoff
Expand Down
Loading