Andrey Epifancev fd13770427 Настройки: ключи v3 base_jql и board_id удаляются при открытии хаба (#129, #64)
OBSOLETE расширен ключами base_jql и board_id: ensure_defaults убирает их при каждом открытии, миграция v7 -> v8 тоже.
settings_version не растёт. PUT /settings для этих ключей по-прежнему отвечает 422 с пояснением. Тест миграции v3
обновлён: после миграции ключей в настройках нет (проекты уже созданы из них).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 03:02:11 +04:00

136 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Настройки модуля jira: умолчания, ключи для агента, проверка значений, перенос из config.example.json."""
from ...core import settings as core_settings
from . import projects
DEFAULT_PACING = {"ensure_issue": 120, "ensure_fields": 120, "ensure_comment": 30, "ensure_status": 30,
"ensure_link": 30, "ensure_sprint": 30, "ensure_attachment": 30, "fetch_attachment": 2, "refresh": 2,
"ensure_sprint_meta": 30, "ensure_sprint_state": 30, "ensure_rank": 30}
DEFAULTS = {
"min_agent_version": 1,
"project_key": "AURORA",
"pacing": DEFAULT_PACING,
"auto_refresh_minutes": 60,
"issue_types": {},
"epic_name_field_id": None,
"field_ids": {},
"epic_link": {},
"priority_map": {},
"priority_value_key": "name",
"labels": [],
"kind_labels": {},
"comment_marker": "",
"link_types": {},
"closed_statuses": ["Готово", "Снято"], # статусы Jira закрытых задач: список и доска приложения (hide-closed)
}
# Ключи прежних версий (fact-status-removal; base_jql и board_id — v3, #129): их удаляет миграция v8 и каждый ensure_defaults — процесс со старым кодом
# (MCP-сессия до reconnect) вставил бы их снова своими умолчаниями
OBSOLETE = ("status_labels", "transitions_map", "base_jql", "board_id")
# Что получает агент от модуля (без токенов и служебных ключей хаба); settings_version добавляет ядро
AGENT_KEYS = ["project_key", "issue_types", "epic_name_field_id", "field_ids", "epic_link",
"priority_map", "priority_value_key", "labels", "kind_labels", "comment_marker",
"link_types", "pacing", "min_agent_version"]
def _is_int(v) -> bool:
return isinstance(v, int) and not isinstance(v, bool)
def validate_setting(key, value) -> bool:
"""True — ключ принадлежит модулю и проверен; ValueError — значение плохое (422 в API).
base_jql и board_id не пишутся: JQL собирается из проектов, доска — свойство проекта;
pacing — объект {тип команды: целое >= 0} только с известными типами команд;
auto_refresh_minutes и min_agent_version — целые >= 0."""
if key == "base_jql":
raise ValueError("базовая JQL собирается из проектов (GET /projects)")
if key == "board_id":
raise ValueError("доска Jira — свойство проекта (POST /projects)")
if key == "pacing":
if not isinstance(value, dict):
raise ValueError("pacing — объект {тип команды: пауза в секундах}")
for ctype, pause in value.items():
if ctype not in DEFAULT_PACING:
raise ValueError(f"pacing: неизвестный тип команды «{ctype}»")
if not _is_int(pause) or pause < 0:
raise ValueError(f"pacing.{ctype}: ожидается целое >= 0")
return True
if key in ("auto_refresh_minutes", "min_agent_version"):
if not _is_int(value) or value < 0:
raise ValueError(f"{key} — целое >= 0")
return True
return False
def settings_view(conn) -> dict:
"""Вычисляемые поля GET/PUT /settings."""
return {"base_jql": projects.base_jql(conn)}
def pacing(conn) -> dict:
return {**DEFAULT_PACING, **(core_settings.get(conn, "pacing") or {})}
def pacing_from_config(p: dict) -> dict:
"""Старая форма pacing (issue_seconds / comment_seconds) → паузы по типам команд."""
issue, comment = p.get("issue_seconds", 120), p.get("comment_seconds", 30)
return {"ensure_issue": issue, "ensure_fields": issue, "ensure_comment": comment, "ensure_status": comment,
"ensure_link": comment, "ensure_sprint": comment, "ensure_attachment": comment, "fetch_attachment": 2, "refresh": 2,
"ensure_sprint_meta": comment, "ensure_sprint_state": comment, "ensure_rank": comment}
def _issue_types(cfg: dict) -> dict:
"""issue_types из ключей конфига issue_type_<тип>[_id]: task и epic — всегда, bug — только если в конфиге есть
issue_type_bug или issue_type_bug_id (bug-and-attachments, data-model «Настройки»)."""
out = {t: {"name": cfg.get(f"issue_type_{t}"), "id": cfg.get(f"issue_type_{t}_id")} for t in ("task", "epic")}
if "issue_type_bug" in cfg or "issue_type_bug_id" in cfg:
out["bug"] = {"name": cfg.get("issue_type_bug"), "id": cfg.get("issue_type_bug_id")}
return out
def migrate_v7(conn) -> None:
"""Шаг миграции v7 → v8 (fact-status-removal): колонка issue_note.fact_status и ключи status_labels, transitions_map
удаляются безвозвратно; settings_version поднимается, если удалён хотя бы один ключ и строка версии есть."""
if "fact_status" in {r["name"] for r in conn.execute("PRAGMA table_info(issue_note)")}:
conn.execute("ALTER TABLE issue_note DROP COLUMN fact_status")
gone = conn.execute(f"DELETE FROM setting WHERE key IN ({', '.join('?' * len(OBSOLETE))})", OBSOLETE).rowcount
if gone:
conn.execute("UPDATE setting SET value = CAST(value AS INTEGER) + 1 WHERE key = 'settings_version'")
def init_from_config(conn, cfg: dict, force=False) -> list[str]:
"""Перенести из config.example.json всё, что относится к проекту Jira. Учётные данные и адрес Jira
(auth, base_url, verify_ssl, ca_bundle) остаются у агента и сюда не попадают; status_labels и transitions — тоже
(статуса по факту в хабе нет, fact-status-removal)."""
if core_settings.get(conn, "initialized") and not force:
raise RuntimeError("хаб уже инициализирован; повторный init — с --force")
project = cfg["project_key"]
values = {
"project_key": project,
"issue_types": _issue_types(cfg),
"epic_name_field_id": cfg.get("epic_name_field_id"),
"field_ids": cfg.get("fields", {}),
"epic_link": cfg.get("epic_link", {}),
"priority_map": cfg.get("priority_map", {}),
"priority_value_key": cfg.get("priority_value_key", "name"),
"labels": cfg.get("labels", []),
"kind_labels": cfg.get("kind_labels", {}),
"comment_marker": cfg.get("comment_marker", ""),
"link_types": cfg.get("link_types", {}),
"pacing": pacing_from_config(cfg.get("pacing", {})),
}
for key, value in values.items():
core_settings.set_(conn, key, value, bump=False)
core_settings.set_(conn, "initialized", True)
return sorted(values)
def on_init(conn, cfg: dict, force: bool) -> list[str]:
"""init --config: настройки Jira и проект AWPCA; строки для вывода."""
lines = ["перенесено: " + ", ".join(init_from_config(conn, cfg, force=force))]
if projects.ensure_legacy(conn, cfg.get("sprints", {}).get("board_id")):
board = projects.board_of(conn, projects.LEGACY_LABEL)
lines.append(f"проект {projects.LEGACY_LABEL} «{projects.LEGACY_NAME}», доска {board}")
return lines