GRM-20: feat: user-friendly click errors with i18n in configure_repo
This commit is contained in:
+54
-20
@@ -5,10 +5,12 @@ Usage:
|
||||
GITEA_ADMIN_TOKEN=<token> python3 scripts/configure_repo.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
import click
|
||||
import requests
|
||||
|
||||
from gitea_runner_manager.i18n import _
|
||||
|
||||
|
||||
GITEA_API = "https://git.oblachno.oblachno.fyi/api/v1"
|
||||
OWNER = "oblachno-oss"
|
||||
@@ -98,33 +100,65 @@ class GiteaRepoConfig:
|
||||
return self.create_label(name, color, description)
|
||||
|
||||
|
||||
def _handle_http_error(e: requests.HTTPError) -> None:
|
||||
"""Raise a user-friendly Click exception for HTTP errors."""
|
||||
response = e.response
|
||||
status = response.status_code if response else 0
|
||||
if status == 403:
|
||||
raise click.ClickException(
|
||||
_(
|
||||
"HTTP {status} Forbidden — your token lacks admin rights.\n"
|
||||
"Make sure the token belongs to a repo owner or organisation admin.\n"
|
||||
"Alternatively, configure branch protection manually in Settings → Branches.",
|
||||
status=status,
|
||||
)
|
||||
)
|
||||
try:
|
||||
body = response.json() if response else {}
|
||||
message = body.get("message", str(e))
|
||||
except Exception:
|
||||
message = str(e)
|
||||
raise click.ClickException(
|
||||
_("HTTP error: {status} — {message}", status=status, message=message)
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
token = os.environ.get("GITEA_ADMIN_TOKEN", "")
|
||||
if not token:
|
||||
print("ERROR: GITEA_ADMIN_TOKEN is not set.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise click.ClickException(_("ERROR: GITEA_ADMIN_TOKEN is not set."))
|
||||
|
||||
cfg = GiteaRepoConfig(GITEA_API, token, OWNER, REPO)
|
||||
|
||||
print("Configuring branch protection for master...")
|
||||
cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
|
||||
print(" - Direct pushes: BLOCKED (require PR)")
|
||||
print(" - Required approvals: 1")
|
||||
print(" - Dismiss stale approvals: yes")
|
||||
print(" - Block outdated branches: yes")
|
||||
print(" - Block rejected reviews: yes")
|
||||
print(" - Required status checks: lint, unit-tests, molecule-tests")
|
||||
try:
|
||||
click.echo(_("Configuring branch protection for {branch}...", branch="master"))
|
||||
cfg.ensure_branch_protection("master", BRANCH_PROTECTION_CONFIG)
|
||||
click.echo(_(" - Direct pushes: BLOCKED (require PR)"))
|
||||
click.echo(
|
||||
_(
|
||||
" - Required approvals: {count}",
|
||||
count=BRANCH_PROTECTION_CONFIG["required_approvals"],
|
||||
)
|
||||
)
|
||||
click.echo(_(" - Dismiss stale approvals: yes"))
|
||||
click.echo(_(" - Block outdated branches: yes"))
|
||||
click.echo(_(" - Block rejected reviews: yes"))
|
||||
checks = ", ".join(BRANCH_PROTECTION_CONFIG["status_check_contexts"])
|
||||
click.echo(_(" - Required status checks: {checks}", checks=checks))
|
||||
|
||||
print("")
|
||||
print("Creating ready-to-merge label...")
|
||||
result = cfg.ensure_label(**LABEL_CONFIG)
|
||||
if result is None:
|
||||
print(" Label 'ready-to-merge' already exists.")
|
||||
else:
|
||||
print(" Label 'ready-to-merge' created.")
|
||||
click.echo("")
|
||||
label_name = LABEL_CONFIG["name"]
|
||||
click.echo(_("Creating {label} label...", label=label_name))
|
||||
result = cfg.ensure_label(**LABEL_CONFIG)
|
||||
if result is None:
|
||||
click.echo(_(" Label '{label}' already exists.", label=label_name))
|
||||
else:
|
||||
click.echo(_(" Label '{label}' created.", label=label_name))
|
||||
|
||||
print("")
|
||||
print("Repository configuration complete.")
|
||||
click.echo("")
|
||||
click.echo(_("Repository configuration complete."))
|
||||
except requests.HTTPError as e:
|
||||
_handle_http_error(e)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
@@ -502,6 +502,104 @@ TRANSLATIONS: dict[str, dict[str, str]] = {
|
||||
"ru": "Ad-hoc команда не удалась на {host}: {stderr}",
|
||||
"zh": "Ad-hoc 命令在 {host} 上失败: {stderr}",
|
||||
},
|
||||
"ERROR: GITEA_ADMIN_TOKEN is not set.": {
|
||||
"en": "ERROR: GITEA_ADMIN_TOKEN is not set.",
|
||||
"bg": "ГРЕШКА: GITEA_ADMIN_TOKEN не е зададен.",
|
||||
"de": "FEHLER: GITEA_ADMIN_TOKEN ist nicht gesetzt.",
|
||||
"ru": "ОШИБКА: GITEA_ADMIN_TOKEN не задан.",
|
||||
"zh": "错误:未设置 GITEA_ADMIN_TOKEN。",
|
||||
},
|
||||
"Configuring branch protection for {branch}...": {
|
||||
"en": "Configuring branch protection for {branch}...",
|
||||
"bg": "Конфигуриране на защита на клона {branch}...",
|
||||
"de": "Konfiguriere Branch-Schutz für {branch}...",
|
||||
"ru": "Настройка защиты ветки {branch}...",
|
||||
"zh": "正在配置 {branch} 的分支保护...",
|
||||
},
|
||||
" - Direct pushes: BLOCKED (require PR)": {
|
||||
"en": " - Direct pushes: BLOCKED (require PR)",
|
||||
"bg": " - Директни push-ове: БЛОКИРАНИ (изисква PR)",
|
||||
"de": " - Direkte Pushes: BLOCKIERT (PR erforderlich)",
|
||||
"ru": " - Прямые push: ЗАБЛОКИРОВАНЫ (требуется PR)",
|
||||
"zh": " - 直接推送:已阻止(需要 PR)",
|
||||
},
|
||||
" - Required approvals: {count}": {
|
||||
"en": " - Required approvals: {count}",
|
||||
"bg": " - Необходими одобрения: {count}",
|
||||
"de": " - Erforderliche Genehmigungen: {count}",
|
||||
"ru": " - Требуемые одобрения: {count}",
|
||||
"zh": " - 必需审批数: {count}",
|
||||
},
|
||||
" - Dismiss stale approvals: yes": {
|
||||
"en": " - Dismiss stale approvals: yes",
|
||||
"bg": " - Анулиране на остарели одобрения: да",
|
||||
"de": " - Veraltete Genehmigungen ablehnen: ja",
|
||||
"ru": " - Отклонять устаревшие одобрения: да",
|
||||
"zh": " - 忽略过时审批: 是",
|
||||
},
|
||||
" - Block outdated branches: yes": {
|
||||
"en": " - Block outdated branches: yes",
|
||||
"bg": " - Блокиране на остарели клонове: да",
|
||||
"de": " - Veraltete Branches blockieren: ja",
|
||||
"ru": " - Блокировать устаревшие ветки: да",
|
||||
"zh": " - 阻止过时分支: 是",
|
||||
},
|
||||
" - Block rejected reviews: yes": {
|
||||
"en": " - Block rejected reviews: yes",
|
||||
"bg": " - Блокиране на отхвърлени рецензии: да",
|
||||
"de": " - Abgelehnte Reviews blockieren: ja",
|
||||
"ru": " - Блокировать отклонённые ревью: да",
|
||||
"zh": " - 阻止被拒绝的审查: 是",
|
||||
},
|
||||
" - Required status checks: {checks}": {
|
||||
"en": " - Required status checks: {checks}",
|
||||
"bg": " - Необходими проверки на състоянието: {checks}",
|
||||
"de": " - Erforderliche Status-Checks: {checks}",
|
||||
"ru": " - Требуемые проверки статуса: {checks}",
|
||||
"zh": " - 必需状态检查: {checks}",
|
||||
},
|
||||
"Creating {label} label...": {
|
||||
"en": "Creating {label} label...",
|
||||
"bg": "Създаване на етикет {label}...",
|
||||
"de": "Erstelle Label {label}...",
|
||||
"ru": "Создание метки {label}...",
|
||||
"zh": "正在创建标签 {label}...",
|
||||
},
|
||||
" Label '{label}' already exists.": {
|
||||
"en": " Label '{label}' already exists.",
|
||||
"bg": " Етикетът '{label}' вече съществува.",
|
||||
"de": " Label '{label}' existiert bereits.",
|
||||
"ru": " Метка '{label}' уже существует.",
|
||||
"zh": " 标签 '{label}' 已存在。",
|
||||
},
|
||||
" Label '{label}' created.": {
|
||||
"en": " Label '{label}' created.",
|
||||
"bg": " Етикетът '{label}' е създаден.",
|
||||
"de": " Label '{label}' erstellt.",
|
||||
"ru": " Метка '{label}' создана.",
|
||||
"zh": " 标签 '{label}' 已创建。",
|
||||
},
|
||||
"Repository configuration complete.": {
|
||||
"en": "Repository configuration complete.",
|
||||
"bg": "Конфигурирането на хранилището е завършено.",
|
||||
"de": "Repository-Konfiguration abgeschlossen.",
|
||||
"ru": "Конфигурация репозитория завершена.",
|
||||
"zh": "仓库配置完成。",
|
||||
},
|
||||
"HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.": { # noqa: E501
|
||||
"en": "HTTP {status} Forbidden — your token lacks admin rights.\nMake sure the token belongs to a repo owner or organisation admin.\nAlternatively, configure branch protection manually in Settings → Branches.", # noqa: E501
|
||||
"bg": "HTTP {status} Забранено — вашият токен няма администраторски права.\nУверете се, че токенът принадлежи на собственик на хранилище или администратор на организация.\nАлтернативно, конфигурирайте защитата на клона ръчно в Настройки → Клонове.", # noqa: E501
|
||||
"de": "HTTP {status} Verboten — Ihr Token hat keine Admin-Rechte.\nStellen Sie sicher, dass das Token einem Repository-Besitzer oder Organisations-Admin gehört.\nAlternativ können Sie den Branch-Schutz manuell unter Einstellungen → Branches konfigurieren.", # noqa: E501
|
||||
"ru": "HTTP {status} Запрещено — у вашего токена нет прав администратора.\nУбедитесь, что токен принадлежит владельцу репозитория или администратору организации.\nЛибо настройте защиту ветки вручную в разделе Настройки → Ветки.", # noqa: E501
|
||||
"zh": "HTTP {status} 禁止访问 — 您的令牌缺少管理员权限。\n请确保令牌属于仓库所有者或组织管理员。\n或者,您可以在 设置 → 分支 中手动配置分支保护。", # noqa: E501
|
||||
},
|
||||
"HTTP error: {status} — {message}": {
|
||||
"en": "HTTP error: {status} — {message}",
|
||||
"bg": "HTTP грешка: {status} — {message}",
|
||||
"de": "HTTP-Fehler: {status} — {message}",
|
||||
"ru": "Ошибка HTTP: {status} — {message}",
|
||||
"zh": "HTTP 错误: {status} — {message}",
|
||||
},
|
||||
"exit code {code}": {
|
||||
"en": "exit code {code}",
|
||||
"bg": "код за изход {code}",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import click
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
@@ -9,6 +10,7 @@ from scripts.configure_repo import (
|
||||
BRANCH_PROTECTION_CONFIG,
|
||||
LABEL_CONFIG,
|
||||
GiteaRepoConfig,
|
||||
_handle_http_error,
|
||||
main,
|
||||
)
|
||||
|
||||
@@ -203,9 +205,9 @@ class TestGiteaRepoConfig:
|
||||
class TestMain:
|
||||
def test_main_missing_token(self) -> None:
|
||||
with patch.dict("os.environ", {}, clear=True):
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
main()
|
||||
assert exc.value.code == 1
|
||||
assert "GITEA_ADMIN_TOKEN" in str(exc.value)
|
||||
|
||||
def test_main_success(self) -> None:
|
||||
with patch.dict("os.environ", {"GITEA_ADMIN_TOKEN": "tok"}, clear=True):
|
||||
@@ -237,8 +239,38 @@ class TestMain:
|
||||
mock_cfg.ensure_branch_protection.side_effect = requests.HTTPError("403")
|
||||
mock_cfg_class.return_value = mock_cfg
|
||||
|
||||
with pytest.raises(requests.HTTPError):
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
main()
|
||||
assert "HTTP" in str(exc.value)
|
||||
|
||||
def test_handle_http_error_403(self) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 403
|
||||
mock_response.json.return_value = {"message": "Forbidden"}
|
||||
err = requests.HTTPError("403", response=mock_response)
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
_handle_http_error(err)
|
||||
msg = str(exc.value)
|
||||
assert "admin rights" in msg
|
||||
assert "Settings → Branches" in msg
|
||||
|
||||
def test_handle_http_error_other(self) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.json.return_value = {"message": "Internal Server Error"}
|
||||
err = requests.HTTPError("500", response=mock_response)
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
_handle_http_error(err)
|
||||
assert "500" in str(exc.value)
|
||||
|
||||
def test_handle_http_error_json_parse_fails(self) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 502
|
||||
mock_response.json.side_effect = ValueError("not json")
|
||||
err = requests.HTTPError("502", response=mock_response)
|
||||
with pytest.raises(click.ClickException) as exc:
|
||||
_handle_http_error(err)
|
||||
assert "502" in str(exc.value)
|
||||
|
||||
|
||||
def test_main_module_block() -> None:
|
||||
|
||||
Reference in New Issue
Block a user