#!/usr/bin/env python3 """Sync documentation from /docs/ to the Gitea wiki via API. Reads markdown files from the ``docs/`` directory, uses ``mapping.json`` to map file paths to wiki page titles, and creates/updates wiki pages via the Gitea API. Pages that exist in the wiki but not in the mapping are left untouched (not deleted). Gitea 1.26 wiki API endpoints: - Create: POST /repos/{owner}/{repo}/wiki/new {title, content, message} - Update: PATCH /repos/{owner}/{repo}/wiki/page/{sub_url} {title, content, message} - List: GET /repos/{owner}/{repo}/wiki/pages → [{title, sub_url, ...}] - Delete: DELETE /repos/{owner}/{repo}/wiki/page/{sub_url} Usage: REPO_TOKEN= python3 scripts/sync_wiki.py [--dry-run] [--repo owner/repo] """ from __future__ import annotations import json import os from pathlib import Path import click from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType] from gitea_runner_manager.api_clients import GiteaClient from gitea_runner_manager.config import GITEA_API_URL from gitea_runner_manager.exceptions import APIError from gitea_runner_manager.i18n import _ load_dotenv(override=True) DOCS_DIR = Path(__file__).resolve().parent.parent / "docs" MAPPING_FILE = DOCS_DIR / "mapping.json" def load_mapping() -> dict[str, str]: """Load the file-to-wiki-page mapping from mapping.json.""" with open(MAPPING_FILE) as f: return json.load(f) def read_doc_content(file_path: str) -> str: """Read markdown content from a docs file.""" full_path = DOCS_DIR / file_path with open(full_path) as f: return f.read() def list_wiki_pages(client: GiteaClient) -> dict[str, str]: """List existing wiki pages, returning {title: sub_url}.""" try: pages = client._request("GET", "/wiki/pages").json() except APIError: return {} return {page.get("title", ""): page.get("sub_url", page.get("title", "")) for page in pages} def sync_page( client: GiteaClient, page_title: str, content: str, existing_pages: dict[str, str], dry_run: bool, ) -> str: """Create or update a single wiki page. Returns "created", "updated", or "skipped" (if dry-run). """ if dry_run: click.echo(_("[dry-run] Would sync page: {title} ({chars} chars)", title=page_title, chars=len(content))) return "skipped" if page_title in existing_pages: # Update existing page via PATCH sub_url = existing_pages[page_title] client._request( "PATCH", f"/wiki/page/{sub_url}", json={"title": page_title, "content": content, "message": f"Sync from docs/ — update {page_title}"}, ) return "updated" # Create new page via POST /wiki/new client._request( "POST", "/wiki/new", json={"title": page_title, "content": content, "message": f"Sync from docs/ — create {page_title}"}, ) return "created" @click.command() @click.option("--dry-run", is_flag=True, default=False, help="Show what would happen without making changes.") @click.option("--repo", default=None, help="Repository in owner/name format (auto-detected if omitted).") def main(dry_run: bool, repo: str | None) -> None: token = os.environ.get("REPO_TOKEN", "") if not token: raise click.ClickException(_("ERROR: REPO_TOKEN is not set.")) if repo is None: owner = os.environ.get("GRM_REPO_OWNER", "oblachno-oss") repo_name = os.environ.get("GRM_REPO_NAME", "grm") else: owner, repo_name = repo.split("/") if not MAPPING_FILE.exists(): raise click.ClickException(_("ERROR: mapping.json not found at {path}", path=MAPPING_FILE)) mapping = load_mapping() client = GiteaClient(GITEA_API_URL, token, owner, repo_name) click.echo(_("Syncing {count} documentation pages to wiki...", count=len(mapping))) existing_pages = list_wiki_pages(client) if existing_pages: click.echo(_("Found {count} existing wiki pages.", count=len(existing_pages))) created = 0 updated = 0 skipped = 0 for file_path, page_title in sorted(mapping.items()): try: content = read_doc_content(file_path) except FileNotFoundError: click.echo(_("WARNING: File {file} not found — skipping.", file=file_path)) skipped += 1 continue result = sync_page(client, page_title, content, existing_pages, dry_run) if result == "created": created += 1 click.echo(_(" Created: {title}", title=page_title)) elif result == "updated": updated += 1 click.echo(_(" Updated: {title}", title=page_title)) else: skipped += 1 click.echo( _( "\nDone! Created: {created}, Updated: {updated}, Skipped: {skipped}", created=created, updated=updated, skipped=skipped, ) ) if __name__ == "__main__": # pragma: no cover main()