202 lines
6.9 KiB
Python
202 lines
6.9 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 — try docker/.env symlink first, then the canonical secrets file
|
|
load_dotenv(REPO_ROOT / "docker" / ".env")
|
|
load_dotenv(Path.home() / ".config" / "ignition-dev" / "secrets.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())
|