155 lines
5.0 KiB
Python
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()
|