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>
136 lines
7.5 KiB
Python
136 lines
7.5 KiB
Python
"""Настройки модуля 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
|