Skip to content

Commit cc6fa67

Browse files
committed
ci: exercise Lychee pre-commit on Linux and Windows
Run the actual configured hook against generated local Markdown and reST fixtures. Verify existing files and directories, reject deleted targets despite cached prior successes, and preserve source bytes. Keep the Python unit-test schedule unchanged. Signed-off-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
1 parent f67206d commit cc6fa67

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

‎.github/workflows/pre-commit.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
env:
4444
# Full-CI docs builds and nightly check links. The installation reminder
4545
# is for developer machines; CI does not install a Git commit hook.
46+
# A separate bounded check below exercises Lychee with local fixtures.
4647
SKIP: lychee,check-precommit-installed
4748
# Minor version used by setup-python and as the cache restore-key prefix.
4849
PYTHON_VERSION: "3.14"
@@ -88,6 +89,12 @@ jobs:
8889
- name: Run pre-commit
8990
run: pre-commit run --all-files --show-diff-on-failure
9091

92+
- name: Exercise the configured Lychee hook with local fixtures
93+
env:
94+
SKIP: check-precommit-installed
95+
PYTHONUTF8: "1"
96+
run: python ci/tools/test_lychee_precommit.py
97+
9198
- name: Save pre-commit cache
9299
# Only the default branch saves, so per-PR merge refs don't store their
93100
# own duplicate copies. !cancelled() keeps a push that fails lint from

‎ci/tools/test_lychee_precommit.py‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Exercise the configured Lychee pre-commit hook without scanning external links."""
5+
6+
from __future__ import annotations
7+
8+
import os
9+
import subprocess
10+
import sys
11+
from pathlib import Path
12+
from tempfile import TemporaryDirectory
13+
from urllib.parse import quote
14+
15+
ROOT = Path(__file__).resolve().parents[2]
16+
17+
18+
def run_hook(sources: dict[Path, bytes], missing: Path | None = None) -> None:
19+
result = subprocess.run( # noqa: S603
20+
[
21+
sys.executable,
22+
"-m",
23+
"pre_commit",
24+
"run",
25+
"lychee",
26+
"--verbose",
27+
"--color",
28+
"never",
29+
"--files",
30+
*(source.relative_to(ROOT).as_posix() for source in sources),
31+
],
32+
cwd=ROOT,
33+
env=dict(os.environ, SKIP=""),
34+
capture_output=True,
35+
encoding="utf-8",
36+
timeout=120,
37+
)
38+
output = result.stdout + result.stderr
39+
print(output, end="", flush=True)
40+
for source, original in sources.items():
41+
if source.read_bytes() != original:
42+
raise RuntimeError(f"The Lychee hook changed {source}")
43+
if "- hook id: lychee" not in output or "Skipped" in output:
44+
raise RuntimeError("The configured Lychee hook did not run")
45+
if missing is None:
46+
if result.returncode != 0:
47+
raise RuntimeError("The Lychee hook rejected existing checkout targets")
48+
elif (
49+
result.returncode == 0
50+
or "File not found" not in output
51+
or missing.as_uri() not in output
52+
or "- exit code: 2" not in output
53+
):
54+
raise RuntimeError(f"Expected Lychee to report the missing local target {missing}")
55+
56+
57+
def main() -> None:
58+
# Generate sources inside the checkout so pre-commit applies the real file filters.
59+
with TemporaryDirectory(prefix="lychee hook smoke ", dir=ROOT) as temporary:
60+
fixture = Path(temporary)
61+
directory = fixture / "new directory"
62+
directory.mkdir()
63+
target = fixture / "new source #$.py"
64+
target.write_text("value = 1\n", encoding="utf-8")
65+
relative = fixture / "relative guide.rst"
66+
relative.write_text("Relative target\n", encoding="utf-8")
67+
prefix = "https://github.com/NVIDIA/cuda-python"
68+
file_url = f"{prefix}/blob/main/{quote(target.relative_to(ROOT).as_posix())}"
69+
directory_url = f"{prefix}/tree/main/{quote(directory.relative_to(ROOT).as_posix())}/"
70+
sources = {
71+
fixture / "source guide.md": (
72+
f"[File]({file_url})\r\n[Directory]({directory_url})\r\n[Relative](relative%20guide.rst)\r\n"
73+
).encode(),
74+
fixture / "source guide.rst": (
75+
f"`File <{file_url}>`_\n`Directory <{directory_url}>`_\n`Relative <relative%20guide.rst>`_\n"
76+
).encode(),
77+
}
78+
for source, content in sources.items():
79+
source.write_bytes(content)
80+
81+
print("Checking existing local file, directory, and relative targets", flush=True)
82+
run_hook(sources)
83+
target.unlink()
84+
print("Checking that a previously valid file is rejected after deletion", flush=True)
85+
run_hook(sources, missing=target)
86+
target.write_text("value = 1\n", encoding="utf-8")
87+
directory.rmdir()
88+
print("Checking that a previously valid directory is rejected after deletion", flush=True)
89+
run_hook(sources, missing=directory)
90+
91+
print("Configured Lychee hook checks passed; documentation sources were unchanged.")
92+
93+
94+
if __name__ == "__main__":
95+
main()

0 commit comments

Comments
 (0)