Skip to content

Commit 2d0a217

Browse files
committed
Add documentation validation script
1 parent e5f3882 commit 2d0a217

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

.github/scripts/check_docs.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Validate required documentation and local Markdown links."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
import sys
7+
from pathlib import Path
8+
from urllib.parse import unquote
9+
10+
ROOT = Path.cwd().resolve()
11+
REQUIRED = (
12+
Path("README.md"),
13+
Path("SECURITY.md"),
14+
Path("CONTRIBUTING.md"),
15+
Path("docs/seo-metadata.md"),
16+
)
17+
LINK_PATTERN = re.compile(r"!?[[^]]*](([^)]+))")
18+
19+
20+
def link_target(raw: str) -> str:
21+
value = raw.strip()
22+
if value.startswith("<") and ">" in value:
23+
value = value[1 : value.index(">")]
24+
else:
25+
value = value.split(maxsplit=1)[0]
26+
return unquote(value.split("#", 1)[0])
27+
28+
29+
def main() -> int:
30+
errors: list[str] = []
31+
32+
for relative in REQUIRED:
33+
path = ROOT / relative
34+
if not path.is_file():
35+
errors.append(f"missing required file: {relative}")
36+
37+
markdown_files = sorted(
38+
path for path in ROOT.rglob("*.md") if ".git" not in path.parts
39+
)
40+
if not markdown_files:
41+
errors.append("no Markdown files found")
42+
43+
for path in markdown_files:
44+
relative = path.relative_to(ROOT)
45+
text = path.read_text(encoding="utf-8")
46+
47+
if not text.strip():
48+
errors.append(f"empty Markdown file: {relative}")
49+
continue
50+
51+
for line_number, line in enumerate(text.splitlines(), start=1):
52+
for match in LINK_PATTERN.finditer(line):
53+
raw = match.group(1).strip()
54+
if raw.startswith(("http://", "https://", "mailto:", "tel:", "#")):
55+
continue
56+
57+
target = link_target(raw)
58+
if not target:
59+
continue
60+
61+
resolved = (path.parent / target).resolve()
62+
try:
63+
resolved.relative_to(ROOT)
64+
except ValueError:
65+
errors.append(
66+
f"{relative}:{line_number}: link escapes repository: {raw}"
67+
)
68+
continue
69+
70+
if not resolved.exists():
71+
errors.append(
72+
f"{relative}:{line_number}: broken relative link: {raw}"
73+
)
74+
75+
if errors:
76+
print("Documentation checks failed:")
77+
for error in errors:
78+
print(f"- {error}")
79+
return 1
80+
81+
print(
82+
f"Documentation checks passed for {len(markdown_files)} Markdown files."
83+
)
84+
return 0
85+
86+
87+
if __name__ == "__main__":
88+
sys.exit(main())

0 commit comments

Comments
 (0)