115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate a registry tool submission.
|
|
|
|
Usage: python scripts/validate_tool.py path/to/tool
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
import yaml
|
|
|
|
TOOL_NAME_RE = re.compile(r"^[A-Za-z0-9-]{1,64}$")
|
|
SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+.+)?$")
|
|
|
|
REQUIRED_README_SECTIONS = ["## Usage", "## Examples"]
|
|
|
|
|
|
def find_repo_root(start: Path) -> Path | None:
|
|
current = start.resolve()
|
|
while current != current.parent:
|
|
if (current / "categories" / "categories.yaml").exists():
|
|
return current
|
|
current = current.parent
|
|
if (current / "categories" / "categories.yaml").exists():
|
|
return current
|
|
return None
|
|
|
|
|
|
def load_categories(repo_root: Path) -> List[str]:
|
|
categories_path = repo_root / "categories" / "categories.yaml"
|
|
data = yaml.safe_load(categories_path.read_text(encoding="utf-8")) or {}
|
|
categories = data.get("categories", [])
|
|
return [c.get("name") for c in categories if c.get("name")]
|
|
|
|
|
|
def validate_tool(tool_path: Path) -> List[str]:
|
|
errors: List[str] = []
|
|
if tool_path.is_dir():
|
|
config_path = tool_path / "config.yaml"
|
|
readme_path = tool_path / "README.md"
|
|
else:
|
|
config_path = tool_path
|
|
readme_path = tool_path.parent / "README.md"
|
|
|
|
if not config_path.exists():
|
|
return [f"Missing config.yaml at {config_path}"]
|
|
|
|
try:
|
|
config_text = config_path.read_text(encoding="utf-8")
|
|
data = yaml.safe_load(config_text) or {}
|
|
except Exception as exc:
|
|
return [f"Invalid YAML in config.yaml: {exc}"]
|
|
|
|
name = (data.get("name") or "").strip()
|
|
version = (data.get("version") or "").strip()
|
|
description = (data.get("description") or "").strip()
|
|
category = (data.get("category") or "").strip()
|
|
|
|
if not name:
|
|
errors.append("Missing required field: name")
|
|
elif not TOOL_NAME_RE.match(name):
|
|
errors.append("Tool name must match ^[A-Za-z0-9-]{1,64}$")
|
|
|
|
if not version:
|
|
errors.append("Missing required field: version")
|
|
elif not SEMVER_RE.match(version):
|
|
errors.append("Version must be valid semver (MAJOR.MINOR.PATCH)")
|
|
|
|
if not description:
|
|
errors.append("Missing required field: description")
|
|
|
|
repo_root = find_repo_root(tool_path)
|
|
if repo_root:
|
|
categories = load_categories(repo_root)
|
|
if category and category not in categories:
|
|
errors.append(f"Unknown category '{category}' (not in categories.yaml)")
|
|
else:
|
|
if category:
|
|
errors.append("Cannot validate category (categories.yaml not found)")
|
|
|
|
if not readme_path.exists():
|
|
errors.append(f"Missing README.md at {readme_path}")
|
|
else:
|
|
readme_text = readme_path.read_text(encoding="utf-8")
|
|
for section in REQUIRED_README_SECTIONS:
|
|
if section not in readme_text:
|
|
errors.append(f"README.md missing section: {section}")
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python scripts/validate_tool.py path/to/tool")
|
|
return 1
|
|
|
|
tool_path = Path(sys.argv[1])
|
|
errors = validate_tool(tool_path)
|
|
if errors:
|
|
print("Validation failed:")
|
|
for err in errors:
|
|
print(f"- {err}")
|
|
return 1
|
|
|
|
print("Validation passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|