diff --git a/changelog.d/+cooled-prek-run-env.bugfix.md b/changelog.d/+cooled-prek-run-env.bugfix.md new file mode 100644 index 0000000..a78f406 --- /dev/null +++ b/changelog.d/+cooled-prek-run-env.bugfix.md @@ -0,0 +1,2 @@ +`cooled-prek` builds hook environments under the seven-day cutoff, then runs the hooks with the caller's environment, so +a system hook that calls `uv run` resolves the project from its own lock instead of under the cutoff. diff --git a/tests/cooled_prek/test_cooled_prek.py b/tests/cooled_prek/test_cooled_prek.py index 72a8dac..ae2bb30 100644 --- a/tests/cooled_prek/test_cooled_prek.py +++ b/tests/cooled_prek/test_cooled_prek.py @@ -68,27 +68,49 @@ def test_cooled_prek_action_passes_each_argument(tmp_path: Path, action_scripts: ] -def test_cooled_prek_isolates_hook_installs(tmp_path: Path) -> None: +@pytest.mark.parametrize( + ("arguments", "install"), + [ + pytest.param(["run", "--all-files"], ["install-hooks"], id="every-hook"), + pytest.param(["run", "mdformat", "--files", "CHANGELOG.md"], ["install-hooks", "mdformat"], id="named-hook"), + ], +) +def test_cooled_prek_builds_hooks_cooled_and_runs_them_with_the_caller_environment( + tmp_path: Path, arguments: list[str], install: list[str] +) -> None: fake = tmp_path / "prek" - fake.write_text('#!/bin/sh\nprintf "%s\\n" "$@" >args\nenv >env\n') + fake.write_text( + '#!/bin/sh\nn=$(ls calls 2>/dev/null | wc -l)\nmkdir -p calls/$n\nprintf "%s\\n" "$@" >calls/$n/args\nenv >calls/$n/env\n' + ) fake.chmod(0o755) leaked = dict.fromkeys(("UV_INDEX_URL", "UV_EXCLUDE_NEWER_PACKAGE", "UV_SYSTEM_CERTS", "SSL_CERT_FILE"), "leak") subprocess.run( - [str(Path(__file__).resolve().parents[2] / "tools/cooled_prek.sh"), "run", "--all-files"], + [str(Path(__file__).resolve().parents[2] / "tools/cooled_prek.sh"), *arguments], cwd=tmp_path, env=os.environ | leaked | {"PATH": f"{tmp_path}:{os.environ['PATH']}", "UV_EXCLUDE_NEWER": "1 day"}, check=True, ) - assert (tmp_path / "args").read_text().splitlines() == ["--config", ".pre-commit-config.yaml", "run", "--all-files"] - environment = dict(line.split("=", 1) for line in (tmp_path / "env").read_text().splitlines() if "=" in line) - assert {name: environment.get(name) for name in ("UV_EXCLUDE_NEWER", "UV_NO_CONFIG", "UV_NO_SOURCES")} == { + calls = [ + ( + (call / "args").read_text().splitlines(), + dict(line.split("=", 1) for line in (call / "env").read_text().splitlines() if "=" in line), + ) + for call in sorted((tmp_path / "calls").iterdir(), key=lambda path: int(path.name)) + ] + (install_args, install_env), (run_args, run_env) = calls + assert install_args == ["--config", ".pre-commit-config.yaml", *install] + assert {name: install_env.get(name) for name in ("UV_EXCLUDE_NEWER", "UV_NO_CONFIG", "UV_NO_SOURCES")} == { "UV_EXCLUDE_NEWER": "7 days", "UV_NO_CONFIG": "1", "UV_NO_SOURCES": "1", } - assert not leaked.keys() & environment.keys() + assert not leaked.keys() & install_env.keys() + assert run_args == ["--config", ".pre-commit-config.yaml", *arguments] + assert run_env.get("UV_EXCLUDE_NEWER") == "1 day" + assert "UV_NO_CONFIG" not in run_env + assert leaked.items() <= run_env.items() def test_cooled_prek_admits_nab_cutoff_exceptions(tmp_path: Path) -> None: diff --git a/tools/cooled_prek.sh b/tools/cooled_prek.sh index 0627d7b..f2bac0b 100755 --- a/tools/cooled_prek.sh +++ b/tools/cooled_prek.sh @@ -1,14 +1,16 @@ #!/usr/bin/env bash -# Hook environments build from PyPI under the seven-day cutoff, ignoring the repository's uv settings. +# Hook environments build from PyPI under the seven-day cutoff, ignoring the repository's uv settings. A run then +# executes the hooks with the caller's environment, so a system hook that calls uv resolves the project as usual. set -euo pipefail -unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL -unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE -unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR -export UV_EXCLUDE_NEWER="7 days" UV_NO_CONFIG=1 UV_NO_SOURCES=1 -# uv pip, which prek calls, reads per-package cutoffs only from a config file, so the [tool.nab] exceptions the -# release lock already admits are written to one that holds nothing else. -exceptions=$( - python3 -I - <<'PY' +cooled() ( + unset UV_CONFIG_FILE UV_INDEX UV_INDEX_URL UV_DEFAULT_INDEX UV_EXTRA_INDEX_URL + unset UV_FIND_LINKS UV_EXCLUDE_NEWER_PACKAGE + unset UV_INSECURE_HOST UV_SYSTEM_CERTS SSL_CERT_FILE SSL_CERT_DIR + export UV_EXCLUDE_NEWER="7 days" UV_NO_CONFIG=1 UV_NO_SOURCES=1 + # uv pip, which prek calls, reads per-package cutoffs only from a config file, so the [tool.nab] exceptions the + # release lock already admits are written to one that holds nothing else. + exceptions=$( + python3 -I - <<'PY' import pathlib import tomllib @@ -19,14 +21,28 @@ for name, settings in sorted(packages.items()): value = cutoff.isoformat().replace("+00:00", "Z") if hasattr(cutoff, "isoformat") else str(cutoff) print(f'"{name}" = "{value}"') PY + ) + if [[ -n "$exceptions" ]]; then + config=$(mktemp) + printf '[exclude-newer-package]\n%s\n' "$exceptions" >"$config" + export UV_CONFIG_FILE="$config" + echo "Running prek $* with hook environments built under the 7 days cutoff, repository uv settings ignored," \ + "[tool.nab] cutoff exceptions admitted: $(tr '\n' ' ' <<<"$exceptions")" + else + echo "Running prek $* with hook environments built under the 7 days cutoff, repository uv settings ignored" + fi + exec prek --config .pre-commit-config.yaml "$@" ) -if [[ -n "$exceptions" ]]; then - config=$(mktemp) - printf '[exclude-newer-package]\n%s\n' "$exceptions" >"$config" - export UV_CONFIG_FILE="$config" - echo "Running prek $* with hook environments built under the 7 days cutoff, repository uv settings ignored," \ - "[tool.nab] cutoff exceptions admitted: $(tr '\n' ' ' <<<"$exceptions")" -else - echo "Running prek $* with hook environments built under the 7 days cutoff, repository uv settings ignored" +if [[ "${1:-}" != run ]]; then + cooled "$@" + exit fi +# The hook IDs a run names come before its first option; none means every hook. +hooks=() +for argument in "${@:2}"; do + [[ "$argument" == -* ]] && break + hooks+=("$argument") +done +cooled install-hooks ${hooks[@]+"${hooks[@]}"} +echo "Running prek $* with the caller's environment" exec prek --config .pre-commit-config.yaml "$@"