# env: python3.10+ (external tooling — NOT Ignition) """ publish_docs.py — sync selected markdown files to WikiJS via GraphQL API. Reads WIKIJS_API_KEY and WIKIJS_URL from the environment (or docker/.env via python-dotenv). Run manually or via the git post-push hook installed by scripts/install-hooks.sh. Usage: python3 tools/publish_docs.py # sync all files python3 tools/publish_docs.py --dry-run # preview only, no API calls """ import argparse import os import sys from pathlib import Path import requests from dotenv import load_dotenv # ── Config ──────────────────────────────────────────────────────────────────── REPO_ROOT = Path(__file__).parent.parent # Load secrets from docker/.env (symlink to ~/.config/ignition-dev/secrets.env) load_dotenv(REPO_ROOT / "docker" / ".env") WIKIJS_URL = os.environ.get("WIKIJS_URL", "https://wikijs.primecontrols-dev.com") WIKIJS_API_KEY = os.environ["WIKIJS_API_KEY"] # loud failure if missing GRAPHQL_ENDPOINT = f"{WIKIJS_URL.rstrip('/')}/graphql" LOCALE = "en" # Maps repo-relative file path → WikiJS page path (no leading slash) MANIFEST: dict[str, str] = { "README.md": "en/engineering/AI-Framework/home", "CLAUDE.md": "en/engineering/AI-Framework/CLAUDE", "docker/CLAUDE.md": "en/engineering/AI-Framework/docker", "ignition/CLAUDE.md": "en/engineering/AI-Framework/ignition", "testing/CLAUDE.md": "en/engineering/AI-Framework/testing", "webdev/CLAUDE.md": "en/engineering/AI-Framework/webdev", "ignition/ignition-api.md": "en/engineering/AI-Framework/ignition/ignition-api", "docker/config/traefik/dynamic/README.md": "en/engineering/AI-Framework/docker/traefik", } # ── Helpers ─────────────────────────────────────────────────────────────────── def gql(query: str, variables: dict | None = None) -> dict: resp = requests.post( GRAPHQL_ENDPOINT, json={"query": query, "variables": variables or {}}, headers={ "Authorization": f"Bearer {WIKIJS_API_KEY}", "Content-Type": "application/json", }, timeout=15, ) resp.raise_for_status() data = resp.json() if "errors" in data: raise RuntimeError(f"GraphQL errors: {data['errors']}") return data def page_id_for_path(wiki_path: str) -> int | None: """Return the page ID for an existing WikiJS page, or None if not found.""" query = """ query ($path: String!, $locale: String!) { pages { singleByPath(path: $path, locale: $locale) { id } } } """ try: data = gql(query, {"path": wiki_path, "locale": LOCALE}) page = data["data"]["pages"]["singleByPath"] return page["id"] if page else None except RuntimeError: return None def derive_title(local_path: str) -> str: stem = Path(local_path).stem # e.g. "ignition-api" if stem.upper() == "README": # Use parent directory name for README files parent = Path(local_path).parent.name if parent == ".": return "Home" return parent.replace("-", " ").replace("_", " ").title() return stem.replace("-", " ").replace("_", " ").title() def create_page(wiki_path: str, title: str, content: str) -> None: mutation = """ mutation ($path: String!, $title: String!, $content: String!, $locale: String!) { pages { create( path: $path title: $title content: $content locale: $locale editor: "markdown" isPublished: true isPrivate: false tags: [] description: "" ) { responseResult { succeeded errorCode message } } } } """ data = gql(mutation, {"path": wiki_path, "title": title, "content": content, "locale": LOCALE}) result = data["data"]["pages"]["create"]["responseResult"] if not result["succeeded"]: raise RuntimeError(f"Create failed [{result['errorCode']}]: {result['message']}") def update_page(page_id: int, title: str, content: str) -> None: mutation = """ mutation ($id: Int!, $title: String!, $content: String!) { pages { update( id: $id title: $title content: $content editor: "markdown" isPublished: true isPrivate: false tags: [] description: "" ) { responseResult { succeeded errorCode message } } } } """ data = gql(mutation, {"id": page_id, "title": title, "content": content}) result = data["data"]["pages"]["update"]["responseResult"] if not result["succeeded"]: raise RuntimeError(f"Update failed [{result['errorCode']}]: {result['message']}") # ── Main ────────────────────────────────────────────────────────────────────── def main() -> int: parser = argparse.ArgumentParser(description="Sync markdown files to WikiJS") parser.add_argument("--dry-run", action="store_true", help="Preview only, no API calls") args = parser.parse_args() if args.dry_run: print("Dry run — no changes will be made.\n") errors = 0 for local_rel, wiki_path in MANIFEST.items(): local_file = REPO_ROOT / local_rel title = derive_title(local_rel) if not local_file.exists(): print(f"[SKIP] {local_rel} (file not found)") continue if args.dry_run: print(f"[DRY-RUN] {local_rel} → {WIKIJS_URL}/{wiki_path} (title: {title!r})") continue content = local_file.read_text(encoding="utf-8") try: page_id = page_id_for_path(wiki_path) if page_id is None: create_page(wiki_path, title, content) print(f"[CREATED] {local_rel} → /{wiki_path}") else: update_page(page_id, title, content) print(f"[OK] {local_rel} → /{wiki_path}") except Exception as exc: print(f"[ERROR] {local_rel}: {exc}", file=sys.stderr) errors += 1 if errors: print(f"\n{errors} file(s) failed.", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())