日常更新
This commit is contained in:
119
ansible/tools/check_docs_no_parent_links.py
Normal file
119
ansible/tools/check_docs_no_parent_links.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline gate: forbid '../' in docs/*.md (R3).
|
||||
|
||||
Default: any '../' occurrence in docs markdown fails.
|
||||
Exception: allowlist entries in scripts/offline-check-whitelist.json with expiry.
|
||||
Expired allowlist entries fail the gate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
DOCS_DIR = ROOT / "docs"
|
||||
ALLOWLIST_PATH = ROOT / "scripts" / "offline-check-whitelist.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AllowItem:
|
||||
file: str
|
||||
rule: str
|
||||
reason: str
|
||||
expires: str
|
||||
|
||||
|
||||
def _load_allowlist() -> list[AllowItem]:
|
||||
if not ALLOWLIST_PATH.exists():
|
||||
return []
|
||||
try:
|
||||
raw = json.loads(ALLOWLIST_PATH.read_text(encoding="utf-8"))
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise RuntimeError(f"allowlist json parse error: {ALLOWLIST_PATH}: {e}") from e
|
||||
|
||||
if not isinstance(raw, list):
|
||||
raise RuntimeError("allowlist must be a JSON list")
|
||||
|
||||
items: list[AllowItem] = []
|
||||
for i, obj in enumerate(raw):
|
||||
if not isinstance(obj, dict):
|
||||
raise RuntimeError(f"allowlist item {i} must be object")
|
||||
items.append(
|
||||
AllowItem(
|
||||
file=str(obj.get("file", "")),
|
||||
rule=str(obj.get("rule", "")),
|
||||
reason=str(obj.get("reason", "")),
|
||||
expires=str(obj.get("expires", "")),
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def _parse_date(s: str) -> date:
|
||||
try:
|
||||
return date.fromisoformat(s)
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise RuntimeError(f"invalid date (expected YYYY-MM-DD): {s}") from e
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not DOCS_DIR.is_dir():
|
||||
print("ERR: docs 目录缺失", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
allow_items = _load_allowlist()
|
||||
allow_by_file: dict[str, list[AllowItem]] = {}
|
||||
for it in allow_items:
|
||||
if not it.file or not it.rule or not it.reason or not it.expires:
|
||||
print(f"ERR: allowlist item missing fields: {it}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
allow_by_file.setdefault(it.file, []).append(it)
|
||||
|
||||
today = date.today()
|
||||
expired: list[str] = []
|
||||
for it in allow_items:
|
||||
if _parse_date(it.expires) < today:
|
||||
expired.append(f"{it.file} ({it.rule}) expired {it.expires}: {it.reason}")
|
||||
if expired:
|
||||
print("ERR: offline-check allowlist expired:", file=sys.stderr)
|
||||
for line in expired:
|
||||
print(f" - {line}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
violations: list[str] = []
|
||||
for md in sorted(DOCS_DIR.glob("*.md")):
|
||||
rel = md.relative_to(ROOT).as_posix()
|
||||
content = md.read_text(encoding="utf-8", errors="ignore")
|
||||
if "../" not in content:
|
||||
continue
|
||||
|
||||
allowed = False
|
||||
for it in allow_by_file.get(rel, []):
|
||||
if it.rule == "docs_no_parent_links":
|
||||
allowed = True
|
||||
break
|
||||
|
||||
if not allowed:
|
||||
violations.append(rel)
|
||||
|
||||
if violations:
|
||||
print("ERR: docs contains '../' (R3 forbids parent links):", file=sys.stderr)
|
||||
for rel in violations:
|
||||
print(f" - {rel}", file=sys.stderr)
|
||||
print(
|
||||
f"HINT: add temporary allowlist item in {ALLOWLIST_PATH} with expires+reason.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
print("[OK] docs R3 parent-link check passed (no '../' found)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user