dp-zp-agent/tools/sync/wiki_git.py

103 lines
3.9 KiB
Python

"""Non-interactive, branch-specific Git updates of the configured wiki."""
from __future__ import annotations
import os
import re
import subprocess
from pathlib import Path
from urllib.parse import urlsplit
GIT_TIMEOUT_SECONDS = 120
def expected_branch() -> str:
branch = os.getenv("EXPECTED_GITEA_BRANCH", "master").strip()
if (
not branch
or branch.startswith(("-", "/"))
or branch.endswith(("/", "."))
or ".." in branch
or "@{" in branch
or branch == "@"
or re.search(r"[\s\x00-\x1f\x7f~^:?*\[\\]", branch)
or any(
not part or part.startswith(".") or part.endswith(".lock") for part in branch.split("/")
)
):
raise RuntimeError("EXPECTED_GITEA_BRANCH nie je platný názov Git vetvy")
return branch
def repository_from_url(url: str) -> str | None:
if "://" in url:
parsed = urlsplit(url)
if parsed.scheme not in {"ssh", "https", "http", "git"}:
return None
path = parsed.path
elif re.match(r"[^/\s]+:[^\s]+", url):
path = url.split(":", 1)[1]
else:
return None
path = path.strip("/").removesuffix(".git")
return path if len(path.split("/")) == 2 else None
def update_wiki(repo_path: Path) -> None:
if not repo_path.exists():
raise RuntimeError(f"ZPWIKI_ROOT neexistuje: {repo_path}")
if not (repo_path / ".git").exists():
raise RuntimeError(f"Nie je to git repozitár: {repo_path}")
environment = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
def git(*args):
try:
result = subprocess.run(
["git", *args],
cwd=repo_path,
env=environment,
text=True,
capture_output=True,
timeout=GIT_TIMEOUT_SECONDS,
check=False,
)
except subprocess.TimeoutExpired as error:
raise RuntimeError(
f"Git aktualizácia prekročila limit {GIT_TIMEOUT_SECONDS} sekúnd"
) from error
if result.returncode:
# Remote URLs and credentials can occur in Git stderr; do not place
# them in API responses or ordinary application logs.
raise RuntimeError(
f"Git operácia {args[0]} zlyhala s návratovým kódom {result.returncode}"
)
return result.stdout.strip()
expected = os.getenv("EXPECTED_GITEA_REPOSITORY", "").strip()
origin = repository_from_url(git("remote", "get-url", "origin"))
if not expected or origin is None or origin.casefold() != expected.casefold():
raise RuntimeError("Git origin nezodpovedá EXPECTED_GITEA_REPOSITORY")
branch = expected_branch()
if git("branch", "--show-current") != branch:
raise RuntimeError("Aktívna Git vetva nezodpovedá EXPECTED_GITEA_BRANCH")
if git("status", "--porcelain", "--untracked-files=normal"):
raise RuntimeError("Wiki obsahuje lokálne zmeny; automatická aktualizácia ich neprepíše")
if os.getenv("SYNC_UPSTREAM_FORK", "0") == "1":
if expected != "jp170na/zpwiki" or branch != "master":
raise RuntimeError("Upstream fork synchronization is restricted to jp170na/zpwiki master")
key_file = os.getenv("ZPWIKI_SYNC_KEY_FILE", "/run/secrets/zpwiki_ssh_key")
script = Path(__file__).resolve().parents[1] / "git_runner/sync_fork.sh"
result = subprocess.run(["bash", str(script), "--key-file", key_file],
env=environment, capture_output=True, text=True, timeout=GIT_TIMEOUT_SECONDS,
check=False)
if result.returncode:
raise RuntimeError("Fork synchronization failed; local indexes were not changed")
git(
"fetch",
"--no-tags",
"--no-recurse-submodules",
"origin",
f"refs/heads/{branch}:refs/remotes/origin/{branch}",
)
git("merge", "--ff-only", f"refs/remotes/origin/{branch}")