Move directory

This commit is contained in:
wbsong111
2026-03-06 17:08:31 +09:00
parent 4d99258344
commit 21addb6e88
73 changed files with 0 additions and 0 deletions
@@ -0,0 +1,41 @@
---
name: breaking_change_check
description: Determine breaking changes from Structured Diff JSON using deterministic rules.
user-invokable: false
---
# breaking_change_check (Skill)
Rule-engine based breaking change detector.
## Input schema
```json
{
"diff_json": "object",
"custom_values": "object (optional)"
}
```
`custom_values`가 제공되면 `values_key_removed` 판단 시 custom-values.yaml에서 실제 사용 중인 key만 breaking으로 분류하고, 나머지는 warnings로 기록한다. 제공되지 않으면 제거된 모든 key를 breaking으로 판단한다.
## Breaking 판단 기준
| rule | severity | breaking 조건 |
|------|----------|--------------|
| `values_key_removed` | high | custom-values.yaml에 해당 key가 존재하는 경우 |
| `values_type_changed` | high | 항상 |
| `resource_removed` | high | 항상 |
| `service_port_changed` | high | 항상 |
| `dependency_major_changed` | high | 항상 (sub-chart 내부 breaking 가능) |
`breaking = severity in ("high", "critical")`
## Output schema
```json
{
"breaking": "boolean",
"severity": "critical|high|medium|warning",
"reasons": "array",
"warnings": "array"
}
```
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import argparse
import json
import os
import sys
from pathlib import Path
DEFAULT_REPO = None
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--diff-file", required=True)
parser.add_argument("--custom-values", default=None, help="custom-values.yaml 경로 (없으면 전체 key 검사)")
args = parser.parse_args()
repo_root = os.environ.get("UPDATE_CATALOG_ROOT")
if repo_root:
sys.path.insert(0, str(Path(repo_root) / "src"))
else:
sys.path.insert(0, str(Path(__file__).resolve().parent))
from update_catalog.breaking_change_check import breaking_change_check # type: ignore
with open(args.diff_file, "r", encoding="utf-8") as f:
diff_json = json.load(f)
custom_values = None
if args.custom_values:
try:
import yaml # type: ignore
with open(args.custom_values, "r", encoding="utf-8") as f:
custom_values = yaml.safe_load(f) or {}
except Exception as e:
print(f"WARNING: custom-values.yaml 로드 실패, 전체 key 검사로 대체: {e}", file=sys.stderr)
out = breaking_change_check({"diff_json": diff_json, "custom_values": custom_values})
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
__all__ = ["skill_interface"]
@@ -0,0 +1,143 @@
"""breaking_change_check skill implementation (deterministic rules).
Breaking 정의: custom-values.yaml을 수정해야 하는 상황.
카탈로그는 신규 배포용 차트 보관소이며, 운영 클러스터 직접 변경과 무관하다.
판단 기준:
- values_key_removed / type_changed: custom-values.yaml에 해당 key가 있을 때만 breaking
- service_port_changed, resource_removed: warning (custom-values.yaml 수정 불필요)
- dependency_major_changed: custom-values.yaml에 subchart prefix key가 있으면 breaking, 없으면 warning
"""
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Set
from .skill_interface import BreakingCheckInput, BreakingCheckOutput, BreakingReason, Severity
def _severity_max(current: Severity, candidate: Severity) -> Severity:
order = ["warning", "medium", "high", "critical"]
return order[max(order.index(current), order.index(candidate))] # type: ignore
def _flatten_keys(d: Any, prefix: str = "") -> Set[str]:
"""YAML dict를 dotted key path 집합으로 평탄화한다.
예: {"webserver": {"defaultUser": {"enabled": true}}}
{"webserver", "webserver.defaultUser", "webserver.defaultUser.enabled"}
"""
keys: Set[str] = set()
if not isinstance(d, dict):
return keys
for k, v in d.items():
full_key = f"{prefix}.{k}" if prefix else k
keys.add(full_key)
if isinstance(v, dict):
keys.update(_flatten_keys(v, full_key))
return keys
def breaking_change_check(payload: Dict[str, Any]) -> Dict[str, Any]:
inp = BreakingCheckInput(**payload)
diff = inp.diff_json
# custom-values.yaml key 집합 (없으면 None → 모든 key가 "미사용"으로 처리)
custom_keys: Optional[Set[str]] = (
_flatten_keys(inp.custom_values) if inp.custom_values else None
)
reasons: List[BreakingReason] = []
warnings: List[BreakingReason] = []
severity: Severity = "warning"
# values rules: custom-values.yaml에 있는 key 변경만 breaking
values = diff.get("values", {})
for k in values.get("removed", []):
if custom_keys is not None and k in custom_keys:
# 실제 사용 중인 key 삭제 → breaking (custom-values.yaml 수정 필요)
reasons.append(BreakingReason(
type="values_key_removed",
key=k,
detail="custom-values.yaml에서 사용 중인 key 삭제 — 수정 필요",
))
severity = _severity_max(severity, "high")
else:
# 미사용 key 삭제 → warning (수정 불필요)
warnings.append(BreakingReason(
type="values_key_removed",
key=k,
detail="custom-values.yaml에서 사용하지 않는 key 삭제 — 수정 불필요",
))
for item in values.get("type_changed", []):
k = item.get("key", "")
detail = f"{item.get('old_type')}{item.get('new_type')}"
if custom_keys is not None and k in custom_keys:
reasons.append(BreakingReason(
type="values_type_changed",
key=k,
detail=f"custom-values.yaml에서 사용 중인 key 타입 변경 ({detail}) — 수정 필요",
))
severity = _severity_max(severity, "high")
else:
warnings.append(BreakingReason(
type="values_type_changed",
key=k,
detail=f"미사용 key 타입 변경 ({detail}) — 수정 불필요",
))
# template rules: 카탈로그 맥락에서 custom-values.yaml 수정 요인 아님 → warning
templates = diff.get("templates", {})
for rid, info in templates.items():
if info.get("removed"):
warnings.append(BreakingReason(
type="resource_removed",
resource=rid,
detail="리소스 삭제 — 신규 배포 시 참고",
))
if rid.startswith("Service/") and info.get("port_changed"):
warnings.append(BreakingReason(
type="service_port_changed",
resource=rid,
detail="Service port 변경 — 신규 배포 시 Ingress/LB 설정 확인",
))
# dependency rules: custom-values.yaml에 subchart prefix key가 있으면 breaking
deps = diff.get("dependencies", {})
for dep, change in deps.get("version_changed", {}).items():
old = change.get("old") or ""
new = change.get("new") or ""
try:
old_major = int(str(old).split(".")[0])
new_major = int(str(new).split(".")[0])
if new_major > old_major:
dep_prefix = f"{dep}."
dep_in_custom = (
custom_keys is not None
and any(k.startswith(dep_prefix) for k in custom_keys)
)
if dep_in_custom:
reasons.append(BreakingReason(
type="dependency_major_changed",
key=dep,
detail=f"{old}{new} — custom-values.yaml에 해당 subchart 설정 있음, 수정 검토 필요",
))
severity = _severity_max(severity, "medium")
else:
warnings.append(BreakingReason(
type="dependency_major_changed",
key=dep,
detail=f"{old}{new} — custom-values.yaml에 해당 subchart 설정 없음",
))
except Exception:
pass
breaking = len(reasons) > 0
out = BreakingCheckOutput(
breaking=breaking,
severity=severity,
reasons=reasons,
warnings=warnings,
)
return json.loads(out.model_dump_json())
@@ -0,0 +1,147 @@
"""Skill Interface v1.0 for Helm upgrade automation."""
from __future__ import annotations
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, Field
SKILL_INTERFACE_VERSION = "1.0"
# -------------------------
# Common Error Schema
# -------------------------
class SkillError(BaseModel):
code: str
message: str
retryable: bool = False
details: Optional[Dict[str, Any]] = None
class ErrorResponse(BaseModel):
error: SkillError
# -------------------------
# Shared Types
# -------------------------
Severity = Literal["critical", "high", "medium", "warning"]
class BreakingReason(BaseModel):
type: str
resource: Optional[str] = None
key: Optional[str] = None
detail: Optional[str] = None
# -------------------------
# helm_diff
# -------------------------
class HelmDiffInput(BaseModel):
chart: str
repo: Optional[str] = None
chart_path: Optional[str] = None
from_version: str
to_version: str
values_override: Optional[Dict[str, Any]] = None
class HelmDiffOutput(BaseModel):
chart: str
from_version: str
to_version: str
generated_at: str
values: Dict[str, Any]
templates: Dict[str, Any]
crd: Dict[str, Any]
dependencies: Dict[str, Any]
errors: List[Dict[str, Any]] = Field(default_factory=list)
# -------------------------
# breaking_change_check
# -------------------------
class BreakingCheckInput(BaseModel):
diff_json: Dict[str, Any]
custom_values: Optional[Dict[str, Any]] = None # custom-values.yaml 내용 (없으면 전체 key 검사)
class BreakingCheckOutput(BaseModel):
breaking: bool
severity: Severity
reasons: List[BreakingReason] = Field(default_factory=list)
warnings: List[BreakingReason] = Field(default_factory=list)
# -------------------------
# generate_upgrade_doc
# -------------------------
class GenerateDocInput(BaseModel):
diff_json: Dict[str, Any]
breaking_result: Dict[str, Any]
docs_context: Optional[Dict[str, str]] = None
max_tokens: int = 50000
class GenerateDocOutput(BaseModel):
markdown: str
truncated: bool = False
# -------------------------
# update_docs_file
# -------------------------
class UpdateDocsInput(BaseModel):
repo_path: str
docs_file: str = "docs/upgrade.md"
version: str
content: str
overwrite: bool = False
class UpdateDocsOutput(BaseModel):
success: bool
file_path: str
already_existed: bool = False
# -------------------------
# create_pr
# -------------------------
class CreatePRInput(BaseModel):
chart: str
from_version: str
to_version: str
repo_path: str
breaking: bool
severity: Severity
reasons: List[BreakingReason] = Field(default_factory=list)
warnings: List[BreakingReason] = Field(default_factory=list)
class CreatePROutput(BaseModel):
pr_url: str
branch_name: str
labels: List[str]
committed: bool = False
# -------------------------
# deploy_validate
# -------------------------
class DeployValidateInput(BaseModel):
chart: str
repo: Optional[str] = None
version: str
values_override: Optional[Dict[str, Any]] = None
namespace: str
timeout: int = 300
class DeployValidateOutput(BaseModel):
success: bool
dry_run_passed: bool
pod_status: Dict[str, int]
events: List[Dict[str, Any]] = Field(default_factory=list)
logs: Optional[str] = None