DEVX-115: fix: make wiki sync resilient to API timeouts and stale page lists
Post-merge / detect-type (push) Successful in 12s
Post-merge / validate-commit-msg (push) Successful in 9s
Post-merge / vikunja (push) Successful in 16s
Post-merge / release (push) Successful in 39s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / sync-wiki (push) Successful in 45s
Post-merge / badges (push) Successful in 47s
Post-merge / publish (push) Successful in 18s

This commit was merged in pull request #174.
This commit is contained in:
2026-07-06 04:55:06 +00:00
parent 268a4e7988
commit d623a64344
4 changed files with 99 additions and 34 deletions
+44 -17
View File
@@ -122,6 +122,9 @@ def sync_page(
"""Create or update a single wiki page.
Returns "created", "updated", or "skipped" (if dry-run).
If a create fails with HTTP 400 "already exists" (the page list was
stale), re-lists the wiki and falls back to an update.
"""
if dry_run:
click.echo(_("[dry-run] Would sync page: {title} ({chars} chars)", title=page_title, chars=len(content)))
@@ -144,16 +147,36 @@ def sync_page(
return "updated"
# Create new page via POST /wiki/new
client._request(
"POST",
"/wiki/new",
json={
"title": page_title,
"content_base64": content_b64,
"message": f"Sync from docs/ — create {page_title}",
},
)
return "created"
try:
client._request(
"POST",
"/wiki/new",
json={
"title": page_title,
"content_base64": content_b64,
"message": f"Sync from docs/ — create {page_title}",
},
)
return "created"
except APIError as e:
if e.status == 400 and "already exists" in e.message.lower():
# The page list was stale (e.g. after a timeout-retry returned
# incomplete data). Re-list and fall back to update.
click.echo(_(" Page '{title}' already exists (stale list). Re-listing and updating...", title=page_title))
fresh_pages = _list_wiki_pages_with_retry(client)
if page_title in fresh_pages:
sub_url = fresh_pages[page_title]
client._request(
"PATCH",
f"/wiki/page/{sub_url}",
json={
"title": page_title,
"content_base64": content_b64,
"message": f"Sync from docs/ — update {page_title} (create→update fallback)",
},
)
return "updated"
raise
def verify_wiki_page(
@@ -173,15 +196,15 @@ def verify_wiki_page(
def _list_wiki_pages_with_retry(client: GiteaClient) -> dict[str, str]:
"""List wiki pages with tenacity retry on APIError.
The Gitea API can be briefly unavailable right after a batch of wiki
page updates. Uses the same tenacity pattern as ``api_clients`` for
exponential backoff.
The Gitea wiki API can be slow (it renders pages on each request)
and may time out. Uses 5 attempts with exponential backoff to handle
transient slowness.
"""
_logger = logging.getLogger("sync_wiki")
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=2, min=2, max=8),
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=2, min=2, max=16),
retry=retry_if_exception_type(APIError),
before_sleep=before_sleep_log(_logger, logging.WARNING),
reraise=True,
@@ -291,10 +314,14 @@ def main(dry_run: bool, repo: str | None, verify: bool, strict: bool) -> None:
click.echo(_("Syncing {count} documentation pages to wiki...", count=len(mapping)))
try:
existing_pages = list_wiki_pages(client)
existing_pages = _list_wiki_pages_with_retry(client)
except APIError as e:
raise click.ClickException(
_("Failed to list existing wiki pages: {error}. Aborting to avoid creating duplicate pages.", error=e)
_(
"Failed to list existing wiki pages after retries: {error}. "
"Aborting to avoid creating duplicate pages.",
error=e,
)
) from e
if existing_pages:
click.echo(_("Found {count} existing wiki pages.", count=len(existing_pages)))
+15 -7
View File
@@ -3343,12 +3343,20 @@
"ru": "Директория для сканирования (по умолчанию: tests/integration). Можно повторять.",
"zh": "要扫描的目录(默认:tests/integration)。可重复。"
},
"Failed to list existing wiki pages: {error}. Aborting to avoid creating duplicate pages.": {
"bg": "Неуспешно извличане на съществуващи wiki страници: {error}. Прекратяване, за да се избегне създаване на дублирани страници.",
"de": "Abrufen bestehender Wiki-Seiten fehlgeschlagen: {error}. Abbruch, um doppelte Seiten zu vermeiden.",
"en": "Failed to list existing wiki pages: {error}. Aborting to avoid creating duplicate pages.",
"pl": "Nie udało się wylistować istniejących stron wiki: {error}. Przerywanie, aby uniknąć tworzenia zduplikowanych stron.",
"ru": "Не удалось получить список существующих wiki-страниц: {error}. Прерывание, чтобы избежать создания дубликатов страниц.",
"zh": "列出现有 wiki 页面失败:{error}。正在中止以避免创建重复页面。"
"Failed to list existing wiki pages after retries: {error}. Aborting to avoid creating duplicate pages.": {
"bg": "Неуспешно извличане на съществуващи wiki страници след повторни опити: {error}. Прекратяване, за да се избегне създаване на дублирани страници.",
"de": "Abrufen bestehender Wiki-Seiten nach Wiederholungen fehlgeschlagen: {error}. Abbruch, um doppelte Seiten zu vermeiden.",
"en": "Failed to list existing wiki pages after retries: {error}. Aborting to avoid creating duplicate pages.",
"pl": "Nie udało się wylistować istniejących stron wiki po ponownych próbach: {error}. Przerywanie, aby uniknąć tworzenia zduplikowanych stron.",
"ru": "Не удалось получить список существующих wiki-страниц после повторных попыток: {error}. Прерывание, чтобы избежать создания дубликатов страниц.",
"zh": "重试后列出现有 wiki 页面失败:{error}。正在中止以避免创建重复页面。"
},
" Page '{title}' already exists (stale list). Re-listing and updating...": {
"bg": " Страницата '{title}' вече съществува (остарял списък). Пресписване и обновяване...",
"de": " Seite '{title}' existiert bereits (veraltete Liste). Neu auflisten und aktualisieren...",
"en": " Page '{title}' already exists (stale list). Re-listing and updating...",
"pl": " Strona '{title}' już istnieje (nieaktualna lista). Ponowne listowanie i aktualizacja...",
"ru": " Страница '{title}' уже существует (устаревший список). Повторное получение списка и обновление...",
"zh": " 页面 '{title}' 已存在(列表过期)。重新列出并更新..."
}
}