Public Access
DEVX-164: fix(ci): retry Vikunja lookups and surface self-approval merge failures
This commit was merged in pull request #325.
This commit is contained in:
@@ -21,10 +21,12 @@ Usage:
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
import requests
|
||||
from dotenv import load_dotenv # pyright: ignore[reportMissingImports,reportUnknownVariableType]
|
||||
|
||||
from devx.api_clients import GiteaClient, VikunjaClient
|
||||
@@ -110,6 +112,27 @@ def validate_pr_title(pr_title: str, task_id: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
_VIKUNJA_LOOKUP_ATTEMPTS = 4
|
||||
_VIKUNJA_LOOKUP_BACKOFF = 5.0
|
||||
|
||||
|
||||
def _list_project_tasks(client: VikunjaClient, page: int) -> list[dict[str, Any]]:
|
||||
"""List Vikunja tasks with bounded retries for transient outages.
|
||||
|
||||
Implements REQ-1: during a Vikunja restart the tasks endpoint can briefly
|
||||
return 404/502; retry a few times so title validation rides out the window
|
||||
instead of stranding an otherwise-valid PR.
|
||||
"""
|
||||
for attempt in range(1, _VIKUNJA_LOOKUP_ATTEMPTS + 1):
|
||||
try:
|
||||
return list(client.list_project_tasks(VIKUNJA_PROJECT_ID, page=page, per_page=DEFAULT_PER_PAGE))
|
||||
except (APIError, requests.RequestException):
|
||||
if attempt == _VIKUNJA_LOOKUP_ATTEMPTS:
|
||||
raise
|
||||
time.sleep(_VIKUNJA_LOOKUP_BACKOFF * attempt)
|
||||
raise AssertionError("unreachable") # pragma: no cover
|
||||
|
||||
|
||||
def get_vikunja_task_title(task_id: str) -> str:
|
||||
"""Fetch the Vikunja task title for the given DEVX-N identifier.
|
||||
|
||||
@@ -124,7 +147,7 @@ def get_vikunja_task_title(task_id: str) -> str:
|
||||
client = VikunjaClient(VIKUNJA_API_URL, token)
|
||||
page = 1
|
||||
while True:
|
||||
tasks = client.list_project_tasks(VIKUNJA_PROJECT_ID, page=page, per_page=DEFAULT_PER_PAGE)
|
||||
tasks = _list_project_tasks(client, page)
|
||||
if not tasks:
|
||||
break
|
||||
matches = [t for t in tasks if t.get("identifier") == task_id]
|
||||
@@ -272,14 +295,28 @@ def main(branch: str, pr_title: str, repo: str, pr_number: str) -> None:
|
||||
# Exit cleanly — the rebase triggers a new CI run that will retry.
|
||||
return
|
||||
else:
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"Merge failed with HTTP {status}: {message}\n"
|
||||
"Please check the PR is ready and you have merge rights.",
|
||||
status=e.status,
|
||||
message=e.message,
|
||||
)
|
||||
) from None
|
||||
hint = ""
|
||||
if e.status == 405:
|
||||
# Implements: REQ-2 — surface approval state. When the PR author
|
||||
# and the CI reviewer token map to the same Gitea user,
|
||||
# self-approval is rejected and the merge fails 405.
|
||||
try:
|
||||
reviews = client.get_pr_reviews(pr_num)
|
||||
if not any(r.get("state") == "APPROVED" for r in reviews):
|
||||
hint = _(
|
||||
"\nNo APPROVED review found on the PR. If the PR author and the"
|
||||
" CI reviewer token map to the same Gitea user, self-approval is"
|
||||
" rejected — approve the PR via a non-author account, then"
|
||||
" re-run the auto-merge job."
|
||||
)
|
||||
except APIError:
|
||||
pass
|
||||
msg = _(
|
||||
"Merge failed with HTTP {status}: {message}\nPlease check the PR is ready and you have merge rights.",
|
||||
status=e.status,
|
||||
message=e.message,
|
||||
)
|
||||
raise click.ClickException(msg + hint) from None
|
||||
|
||||
click.echo(
|
||||
_(
|
||||
|
||||
@@ -127,6 +127,14 @@
|
||||
"ru": "\nОтсутствующая документация:",
|
||||
"zh": "\n缺失的文档:"
|
||||
},
|
||||
"\nNo APPROVED review found on the PR. If the PR author and the CI reviewer token map to the same Gitea user, self-approval is rejected — approve the PR via a non-author account, then re-run the auto-merge job.": {
|
||||
"bg": "\nВ PR не е намерено ревю APPROVED. Ако авторът на PR и токенът на CI рецензента са един и същ потребител в Gitea, самоодобрението се отхвърля — одобрете PR чрез друг акаунт и стартирайте отново задачата за автоматично сливане.",
|
||||
"de": "\nKein APPROVED-Review im PR gefunden. Wenn der PR-Autor und das CI-Reviewer-Token demselben Gitea-Benutzer entsprechen, wird die Selbstgenehmigung abgelehnt — genehmigen Sie den PR über ein anderes Konto und führen Sie den Auto-Merge-Job erneut aus.",
|
||||
"en": "\nNo APPROVED review found on the PR. If the PR author and the CI reviewer token map to the same Gitea user, self-approval is rejected — approve the PR via a non-author account, then re-run the auto-merge job.",
|
||||
"pl": "\nNie znaleziono recenzji APPROVED w PR. Jeśli autor PR i token recenzenta CI mapują na tego samego użytkownika Gitea, samoakceptacja jest odrzucana — zatwierdź PR za pomocą innego konta, a następnie ponownie uruchom zadanie automatycznego scalania.",
|
||||
"ru": "\nВ PR не найдено ревью APPROVED. Если автор PR и токен CI-ревьюера принадлежат одному и тому же пользователю Gitea, самоодобрение отклоняется — одобрите PR через другой аккаунт, затем повторно запустите задачу автоматического слияния.",
|
||||
"zh": "\n在 PR 中未找到 APPROVED 评审。如果 PR 作者和 CI 评审者令牌映射到同一个 Gitea 用户,自我批准将被拒绝 — 请通过非作者账户批准该 PR,然后重新运行自动合并任务。"
|
||||
},
|
||||
"\nNo stale version references found.": {
|
||||
"bg": "",
|
||||
"de": "",
|
||||
|
||||
Reference in New Issue
Block a user