Files
framework-ignition-docker/tools/publish_docs.py
2026-03-17 13:23:56 -05:00

201 lines
6.8 KiB
Python

# 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())