name: Docs on: pull_request: paths: - "**/*.md" - ".github/ISSUE_TEMPLATE/**" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/workflows/docs.yml" push: branches: [main] paths: - "**/*.md" - ".github/ISSUE_TEMPLATE/**" - ".github/PULL_REQUEST_TEMPLATE.md" - ".github/workflows/docs.yml" permissions: contents: read concurrency: group: docs-${{ github.ref }} cancel-in-progress: true jobs: links: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Validate active relative Markdown links run: | python3 - <<'PY' from pathlib import Path import re import sys # Validate every Markdown file in the documentation surface. Globbing # (rather than a hand-maintained list) means new docs are covered # automatically and phantom links cannot slip in. Skip vendored trees. skip_dirs = {".git", "node_modules", ".venv", ".uv-cache"} files = sorted( p for p in Path(".").rglob("*.md") if not any(part in skip_dirs for part in p.parts) ) missing = [] for path in files: if not path.exists(): continue text = path.read_text() active = re.sub(r"", "", text, flags=re.S) for raw in re.findall(r"\[[^\]]*\]\(([^)]+)\)", active): link = raw.split("#", 1)[0] if not link or link.startswith(("http://", "https://", "mailto:")): continue target = (path.parent / link).resolve() try: target.relative_to(Path.cwd().resolve()) except ValueError: missing.append((path, raw, "outside repository")) continue if not target.exists(): missing.append((path, raw, "missing")) if missing: for path, raw, reason in missing: print(f"{path}: {raw} -> {reason}") sys.exit(1) print("Active relative Markdown links resolve.") PY - name: Validate use-case banner links run: | python3 - <<'PY' from pathlib import Path import re import sys files = { Path("README.md"): "## Use Cases", Path("use-cases/README.md"): "## Use Case Catalogue", } failures = [] warnings = [] primary_link_pattern = re.compile( r"^\[(?:Code|Plugin|Live Demo|Learn more)\]\(([^)]+)\)", flags=re.M, ) for path, heading in files.items(): text = path.read_text() start = text.find(heading) if start == -1: failures.append(f"{path}: missing {heading}") continue table_start = text.find("