Move directory
This commit is contained in:
@@ -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"]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+143
@@ -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())
|
||||
+147
@@ -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
|
||||
Reference in New Issue
Block a user