Files
grm/scripts/ci/sync_wiki.py
T

155 lines
5.0 KiB
Python

#!/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=<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.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()