172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Check documentation coverage for CLI commands and major modules.
|
|
|
|
Parses Click commands from the CLI source code and checks if each command
|
|
has corresponding documentation in the wiki/docs. Reports missing
|
|
documentation as warnings and exits with non-zero if coverage is below 100%.
|
|
|
|
Usage:
|
|
python3 scripts/ci/doc_coverage.py [--docs-dir docs/] [--fail-on-missing]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import click
|
|
|
|
from gitea_runner_manager.i18n import _
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
DOCS_DIR = REPO_ROOT / "docs"
|
|
CLI_FILE = REPO_ROOT / "src" / "gitea_runner_manager" / "cli.py"
|
|
|
|
# Major modules that should be documented in tech/architecture.md
|
|
REQUIRED_MODULES = [
|
|
"cli.py",
|
|
"runner_manager.py",
|
|
"executor.py",
|
|
"registry.py",
|
|
"i18n.py",
|
|
"exceptions.py",
|
|
"api_clients.py",
|
|
"config.py",
|
|
]
|
|
|
|
# CI scripts that should be documented in tech/ci-cd-workflow.md
|
|
REQUIRED_SCRIPTS = [
|
|
"auto_merge.py",
|
|
"release.py",
|
|
"publish.py",
|
|
"review_pr.py",
|
|
"notify_failure.py",
|
|
"post_merge.py",
|
|
"classify_changes.py",
|
|
"discover_runners.py",
|
|
"detect_release_commit.py",
|
|
"push_badges.py",
|
|
"distribute_molecule.py",
|
|
"molecule_ci_guard.py",
|
|
"validate_commit_msg.py",
|
|
]
|
|
|
|
|
|
def extract_cli_commands() -> list[str]:
|
|
"""Extract command names from the CLI source file."""
|
|
content = CLI_FILE.read_text()
|
|
commands: list[str] = []
|
|
# Find all @cli.command(...) occurrences, then the next def statement
|
|
for match in re.finditer(r"@cli\.command\b", content):
|
|
# Check for explicit name="..." in the decorator arguments
|
|
decorator_end = content.find(")", match.start())
|
|
decorator_text = content[match.start() : decorator_end + 1]
|
|
name_match = re.search(r'name\s*=\s*"([^"]+)"', decorator_text)
|
|
if name_match:
|
|
commands.append(name_match.group(1))
|
|
continue
|
|
# Find the next def statement after this decorator
|
|
after = content[decorator_end:]
|
|
def_match = re.search(r"def\s+(\w+)\s*\(", after)
|
|
if def_match:
|
|
commands.append(def_match.group(1))
|
|
return commands
|
|
|
|
|
|
def check_command_documented(command: str, docs_content: str) -> bool:
|
|
"""Check if a CLI command is documented in the docs content."""
|
|
# Look for the command name as a heading or in code blocks
|
|
patterns = [
|
|
rf"##.*\b{re.escape(command)}\b",
|
|
rf"`grm\s+{re.escape(command)}\b",
|
|
rf"\bgrm\s+{re.escape(command)}\b",
|
|
rf"###.*\b{re.escape(command)}\b",
|
|
]
|
|
return any(re.search(p, docs_content, re.IGNORECASE) for p in patterns)
|
|
|
|
|
|
def check_module_documented(module: str, docs_content: str) -> bool:
|
|
"""Check if a module is mentioned in the docs content."""
|
|
return module in docs_content
|
|
|
|
|
|
@click.command()
|
|
@click.option("--docs-dir", default=str(DOCS_DIR), help="Path to the docs directory.")
|
|
@click.option(
|
|
"--fail-on-missing",
|
|
is_flag=True,
|
|
default=False,
|
|
help="Exit with non-zero status if any documentation is missing.",
|
|
)
|
|
def main(docs_dir: str, fail_on_missing: bool) -> None:
|
|
docs_path = Path(docs_dir)
|
|
cli_commands_file = docs_path / "user" / "cli-commands.md"
|
|
architecture_file = docs_path / "tech" / "architecture.md"
|
|
ci_cd_file = docs_path / "tech" / "ci-cd-workflow.md"
|
|
|
|
missing: list[str] = []
|
|
total = 0
|
|
|
|
# Check CLI commands
|
|
click.echo(_("Checking CLI command documentation..."))
|
|
commands = extract_cli_commands()
|
|
total += len(commands)
|
|
cli_docs = cli_commands_file.read_text() if cli_commands_file.exists() else ""
|
|
for cmd in commands:
|
|
if check_command_documented(cmd, cli_docs):
|
|
click.echo(_(" OK: grm {cmd}", cmd=cmd))
|
|
else:
|
|
click.echo(_(" MISSING: grm {cmd}", cmd=cmd))
|
|
missing.append(f"CLI command: grm {cmd}")
|
|
|
|
# Check modules in architecture.md
|
|
click.echo(_("\nChecking module documentation in architecture.md..."))
|
|
total += len(REQUIRED_MODULES)
|
|
arch_docs = architecture_file.read_text() if architecture_file.exists() else ""
|
|
for module in REQUIRED_MODULES:
|
|
if check_module_documented(module, arch_docs):
|
|
click.echo(_(" OK: {module}", module=module))
|
|
else:
|
|
click.echo(_(" MISSING: {module}", module=module))
|
|
missing.append(f"Module: {module}")
|
|
|
|
# Check CI scripts in ci-cd-workflow.md
|
|
click.echo(_("\nChecking CI script documentation in ci-cd-workflow.md..."))
|
|
total += len(REQUIRED_SCRIPTS)
|
|
ci_docs = ci_cd_file.read_text() if ci_cd_file.exists() else ""
|
|
for script in REQUIRED_SCRIPTS:
|
|
if check_module_documented(script, ci_docs):
|
|
click.echo(_(" OK: {script}", script=script))
|
|
else:
|
|
click.echo(_(" MISSING: {script}", script=script))
|
|
missing.append(f"CI script: {script}")
|
|
|
|
# Report
|
|
covered = total - len(missing)
|
|
percentage = (covered / total * 100) if total > 0 else 100.0
|
|
click.echo(
|
|
_(
|
|
"\nDoc coverage: {covered}/{total} ({pct}%)",
|
|
covered=covered,
|
|
total=total,
|
|
pct=f"{percentage:.0f}",
|
|
)
|
|
)
|
|
|
|
if missing:
|
|
click.echo(_("\nMissing documentation:"))
|
|
for item in missing:
|
|
click.echo(f" - {item}")
|
|
|
|
if missing and fail_on_missing:
|
|
click.echo(_("\nERROR: Documentation coverage is not 100%. Use --fail-on-missing to enforce."))
|
|
sys.exit(1)
|
|
|
|
if not missing:
|
|
click.echo(_("\nAll documentation coverage checks passed!"))
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
main()
|