Merge pull request #109 from abhi1693/ci/docs-quality-gates
CI: docs quality gates (broken link check)
This commit is contained in:
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -87,6 +87,11 @@ jobs:
|
|||||||
make frontend-test
|
make frontend-test
|
||||||
make frontend-build
|
make frontend-build
|
||||||
|
|
||||||
|
|
||||||
|
- name: Docs quality gates (lint + relative link check)
|
||||||
|
run: |
|
||||||
|
make docs-check
|
||||||
|
|
||||||
- name: Upload coverage artifacts
|
- name: Upload coverage artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
23
.markdownlint-cli2.yaml
Normal file
23
.markdownlint-cli2.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# markdownlint-cli2 config
|
||||||
|
# Keep the ruleset intentionally tiny to avoid noisy churn.
|
||||||
|
|
||||||
|
config:
|
||||||
|
default: false
|
||||||
|
MD009: true # no trailing spaces
|
||||||
|
MD010: true # no hard tabs
|
||||||
|
MD012: true # no multiple consecutive blank lines
|
||||||
|
MD047: true # single trailing newline
|
||||||
|
|
||||||
|
globs:
|
||||||
|
- "**/*.md"
|
||||||
|
|
||||||
|
ignores:
|
||||||
|
- "**/node_modules/**"
|
||||||
|
- "**/.next/**"
|
||||||
|
- "**/dist/**"
|
||||||
|
- "**/build/**"
|
||||||
|
- "**/.venv/**"
|
||||||
|
- "**/__pycache__/**"
|
||||||
|
- "**/.pytest_cache/**"
|
||||||
|
- "**/.mypy_cache/**"
|
||||||
|
- "**/coverage/**"
|
||||||
@@ -38,7 +38,6 @@ git checkout -b <branch-name>
|
|||||||
|
|
||||||
If you accidentally based your branch off another feature branch, fix it by cherry-picking the intended commits onto a clean branch and force-pushing the corrected branch (or opening a new PR).
|
If you accidentally based your branch off another feature branch, fix it by cherry-picking the intended commits onto a clean branch and force-pushing the corrected branch (or opening a new PR).
|
||||||
|
|
||||||
|
|
||||||
### Expectations
|
### Expectations
|
||||||
|
|
||||||
- Keep PRs **small and focused** when possible.
|
- Keep PRs **small and focused** when possible.
|
||||||
|
|||||||
12
Makefile
12
Makefile
@@ -122,3 +122,15 @@ backend-templates-sync: ## Sync templates to existing gateway agents (usage: mak
|
|||||||
|
|
||||||
.PHONY: check
|
.PHONY: check
|
||||||
check: lint typecheck backend-coverage frontend-test build ## Run lint + typecheck + tests + coverage + build
|
check: lint typecheck backend-coverage frontend-test build ## Run lint + typecheck + tests + coverage + build
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: docs-lint
|
||||||
|
docs-lint: frontend-tooling ## Lint markdown files (tiny ruleset; avoids noisy churn)
|
||||||
|
$(NODE_WRAP) npx markdownlint-cli2@0.15.0 --config .markdownlint-cli2.yaml "**/*.md"
|
||||||
|
|
||||||
|
.PHONY: docs-link-check
|
||||||
|
docs-link-check: ## Check for broken relative links in markdown docs
|
||||||
|
python scripts/check_markdown_links.py
|
||||||
|
|
||||||
|
.PHONY: docs-check
|
||||||
|
docs-check: docs-lint docs-link-check ## Run all docs quality gates
|
||||||
|
|||||||
@@ -31,4 +31,3 @@ This file defines how you decide when to act vs when to ask.
|
|||||||
## Collaboration defaults
|
## Collaboration defaults
|
||||||
- If you are idle/unassigned: pick 1 in-progress/review task owned by someone else and leave a concrete, helpful comment (context gaps, quality risks, validation ideas, edge cases, handoff clarity).
|
- If you are idle/unassigned: pick 1 in-progress/review task owned by someone else and leave a concrete, helpful comment (context gaps, quality risks, validation ideas, edge cases, handoff clarity).
|
||||||
- If you notice duplicate work: flag it and propose a merge/split so there is one clear DRI per deliverable.
|
- If you notice duplicate work: flag it and propose a merge/split so there is one clear DRI per deliverable.
|
||||||
|
|
||||||
|
|||||||
@@ -66,4 +66,3 @@ Notes:
|
|||||||
| Date | Change |
|
| Date | Change |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| | |
|
| | |
|
||||||
|
|
||||||
|
|||||||
3
docs/03-development.md
Normal file
3
docs/03-development.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Development workflow
|
||||||
|
|
||||||
|
Placeholder: see root `README.md` for current setup steps.
|
||||||
18
docs/README.md
Normal file
18
docs/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Mission Control docs
|
||||||
|
|
||||||
|
This folder is the starting point for Mission Control documentation.
|
||||||
|
|
||||||
|
## Sections
|
||||||
|
|
||||||
|
- [Development workflow](./03-development.md)
|
||||||
|
- [Testing guide](./testing/README.md)
|
||||||
|
- [Coverage policy](./coverage-policy.md)
|
||||||
|
- [Deployment](./deployment/README.md)
|
||||||
|
- [Production notes](./production/README.md)
|
||||||
|
- [Troubleshooting](./troubleshooting/README.md)
|
||||||
|
- [Gateway WebSocket protocol](./openclaw_gateway_ws.md)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
These pages are minimal placeholders so repo-relative links stay healthy. The actual docs
|
||||||
|
information architecture will be defined in the Docs overhaul tasks.
|
||||||
3
docs/coverage-policy.md
Normal file
3
docs/coverage-policy.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Coverage policy
|
||||||
|
|
||||||
|
Placeholder: coverage policy is currently documented in the root `Makefile` (`backend-coverage`).
|
||||||
3
docs/deployment/README.md
Normal file
3
docs/deployment/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Deployment guide
|
||||||
|
|
||||||
|
Placeholder.
|
||||||
3
docs/openclaw_gateway_ws.md
Normal file
3
docs/openclaw_gateway_ws.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Gateway WebSocket protocol
|
||||||
|
|
||||||
|
Placeholder.
|
||||||
3
docs/production/README.md
Normal file
3
docs/production/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Production notes
|
||||||
|
|
||||||
|
Placeholder.
|
||||||
3
docs/testing/README.md
Normal file
3
docs/testing/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Testing guide
|
||||||
|
|
||||||
|
Placeholder: see root `README.md` and `CONTRIBUTING.md` for current commands.
|
||||||
3
docs/troubleshooting/README.md
Normal file
3
docs/troubleshooting/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Placeholder.
|
||||||
103
scripts/check_markdown_links.py
Executable file
103
scripts/check_markdown_links.py
Executable file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Lightweight markdown link checker for repo docs.
|
||||||
|
|
||||||
|
Checks *relative* links inside markdown files and fails CI if any targets are missing.
|
||||||
|
|
||||||
|
Design goals:
|
||||||
|
- No external deps.
|
||||||
|
- Ignore http(s)/mailto links.
|
||||||
|
- Ignore pure anchors (#foo).
|
||||||
|
- Support links with anchors (./path.md#section) by checking only the path part.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Does not validate that anchors exist inside target files.
|
||||||
|
- Does not validate links generated dynamically or via HTML.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
||||||
|
|
||||||
|
|
||||||
|
def iter_md_files(root: Path) -> list[Path]:
|
||||||
|
"""Return markdown files to check.
|
||||||
|
|
||||||
|
Policy:
|
||||||
|
- Always check root contributor-facing markdown (`README.md`, `CONTRIBUTING.md`).
|
||||||
|
- If `docs/` exists, also check `docs/**/*.md`.
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
- We want fast feedback on broken *relative* links in the most important entrypoints.
|
||||||
|
- We intentionally do **not** crawl external URLs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
files: list[Path] = []
|
||||||
|
|
||||||
|
for p in (root / "README.md", root / "CONTRIBUTING.md"):
|
||||||
|
if p.exists():
|
||||||
|
files.append(p)
|
||||||
|
|
||||||
|
docs = root / "docs"
|
||||||
|
if docs.exists():
|
||||||
|
files.extend(sorted(docs.rglob("*.md")))
|
||||||
|
|
||||||
|
# de-dupe + stable order
|
||||||
|
return sorted({p.resolve() for p in files})
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_target(raw: str) -> str | None:
|
||||||
|
raw = raw.strip()
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
if raw.startswith("http://") or raw.startswith("https://") or raw.startswith("mailto:"):
|
||||||
|
return None
|
||||||
|
if raw.startswith("#"):
|
||||||
|
return None
|
||||||
|
# strip query/fragment
|
||||||
|
raw = raw.split("#", 1)[0].split("?", 1)[0]
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
md_files = iter_md_files(root)
|
||||||
|
|
||||||
|
missing: list[tuple[Path, str]] = []
|
||||||
|
|
||||||
|
for md in md_files:
|
||||||
|
text = md.read_text(encoding="utf-8")
|
||||||
|
for m in LINK_RE.finditer(text):
|
||||||
|
target_raw = m.group(1)
|
||||||
|
target = normalize_target(target_raw)
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip common markdown reference-style quirks.
|
||||||
|
if target.startswith("<") and target.endswith(">"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Resolve relative to current file.
|
||||||
|
resolved = (md.parent / target).resolve()
|
||||||
|
if not resolved.exists():
|
||||||
|
missing.append((md, target_raw))
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print("Broken relative links detected:\n")
|
||||||
|
for md, target in missing:
|
||||||
|
print(f"- {md.relative_to(root)} -> {target}")
|
||||||
|
print(f"\nTotal: {len(missing)}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"OK: checked {len(md_files)} markdown files")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user