#!/usr/bin/env python3 """Generate the template index in README.md from manifest.yaml files.""" from __future__ import annotations import argparse import difflib import sys from pathlib import Path import yaml ROOT = Path(__file__).resolve().parent.parent TEMPLATES_DIR = ROOT / "templates" README = ROOT / "README.md" START = "" END = "" def load_manifests() -> list[dict]: rows: list[dict] = [] for path in sorted(TEMPLATES_DIR.rglob("manifest.yaml")): try: with path.open(encoding="utf-8") as fh: data = yaml.safe_load(fh) or {} except yaml.YAMLError as exc: print(f"WARN {path}: Could not parse YAML: {exc}", file=sys.stderr) continue if not isinstance(data, dict): continue zabbix = data.get("zabbix") if not isinstance(zabbix, dict): zabbix = {} rows.append( { "id": str(data.get("id") or ""), "name": str(data.get("name") or ""), "version": str(data.get("version") or ""), "description": " ".join( str(data.get("description") or "").split() ), "zabbix": zabbix, "path": path.parent.relative_to(ROOT).as_posix(), } ) rows.sort(key=lambda row: row["id"]) return rows def zabbix_range(zabbix: dict) -> str: minimum = str(zabbix.get("min") or "") maximum = zabbix.get("max") if not minimum: return "-" if maximum: return f"{minimum} - {maximum}" return f"{minimum}+" def tested(zabbix: dict) -> str: values = zabbix.get("tested") or [] if not isinstance(values, list): return "-" return ", ".join(str(value) for value in values) or "-" def escape_cell(value: str) -> str: return value.replace("|", "\\|") def render(rows: list[dict]) -> str: if not rows: return "_No templates yet. Add one with `make new`._\n" lines = [ "| ID | Template | Version | Zabbix | Tested | Description |", "|---|---|---|---|---|---|", ] for row in rows: lines.append( "| [{id}]({path}/) | {name} | {version} | {zabbix} | {tested} | {description} |".format( id=escape_cell(row["id"]), path=row["path"], name=escape_cell(row["name"]), version=escape_cell(row["version"]), zabbix=escape_cell(zabbix_range(row["zabbix"])), tested=escape_cell(tested(row["zabbix"])), description=escape_cell(row["description"]) or "-", ) ) return "\n".join(lines) + "\n" def replace_section(readme: str, body: str) -> str: start = readme.find(START) end = readme.find(END) if start == -1 or end == -1 or end < start: raise ValueError(f"README.md is missing {START} and/or {END}") return readme[: start + len(START)] + "\n" + body + readme[end:] def main() -> int: parser = argparse.ArgumentParser( description="Generate the template index in README.md from manifest.yaml files." ) parser.add_argument( "--check", action="store_true", help="Check whether README.md is up to date without modifying it", ) args = parser.parse_args() if not README.is_file(): print(f"ERROR README.md not found: {README}", file=sys.stderr) return 1 original = README.read_text(encoding="utf-8") rows = load_manifests() body = render(rows) try: updated = replace_section(original, body) except ValueError as exc: print(f"ERROR {exc}", file=sys.stderr) return 1 if args.check: if original != updated: print( "ERROR The template index in README.md is not up to date. " "Run `make index`.", file=sys.stderr, ) diff = difflib.unified_diff( original.splitlines(), updated.splitlines(), fromfile="README.md", tofile="README.md (generated)", lineterm="", ) print("\n".join(diff), file=sys.stderr) return 1 print("The template index in README.md is up to date.") return 0 README.write_text(updated, encoding="utf-8") print(f"Updated README.md ({len(rows)} package(s)).") return 0 if __name__ == "__main__": sys.exit(main())