Move directory
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: generate_upgrade_doc
|
||||
description: Generate upgrade Markdown from structured diff and breaking results.
|
||||
user-invokable: false
|
||||
---
|
||||
|
||||
# generate_upgrade_doc
|
||||
|
||||
Produce Markdown upgrade notes from Structured Diff JSON and breaking analysis.
|
||||
|
||||
## Input schema
|
||||
```json
|
||||
{
|
||||
"diff_json": "object",
|
||||
"breaking_result": "object",
|
||||
"docs_context": "object | null",
|
||||
"max_tokens": "number (default 50000)"
|
||||
}
|
||||
```
|
||||
|
||||
## Output schema
|
||||
```json
|
||||
{
|
||||
"markdown": "string",
|
||||
"truncated": "boolean"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/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("--breaking-file", required=True)
|
||||
parser.add_argument("--docs-context", default=None)
|
||||
parser.add_argument("--max-tokens", type=int, default=50000)
|
||||
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.generate_upgrade_doc import generate_upgrade_doc # type: ignore
|
||||
|
||||
with open(args.diff_file, "r", encoding="utf-8") as f:
|
||||
diff_json = json.load(f)
|
||||
with open(args.breaking_file, "r", encoding="utf-8") as f:
|
||||
breaking = json.load(f)
|
||||
|
||||
docs_context = None
|
||||
if args.docs_context:
|
||||
with open(args.docs_context, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
docs_context = {"CUSTOM-README.md": text}
|
||||
|
||||
out = generate_upgrade_doc({
|
||||
"diff_json": diff_json,
|
||||
"breaking_result": breaking,
|
||||
"docs_context": docs_context,
|
||||
"max_tokens": args.max_tokens,
|
||||
})
|
||||
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.
+150
@@ -0,0 +1,150 @@
|
||||
"""generate_upgrade_doc skill implementation.
|
||||
|
||||
항상 실행된다 (breaking 여부 무관).
|
||||
- breaking=true + USE_CLAUDE_CLI=1: LLM으로 custom-values.yaml 수정 방법 포함 상세 가이드 생성
|
||||
- 그 외: 템플릿 기반 간단 요약 생성
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from .skill_interface import GenerateDocInput, GenerateDocOutput
|
||||
|
||||
|
||||
def _fallback_summary(diff: Dict[str, Any], breaking: Dict[str, Any]) -> str:
|
||||
chart = diff.get("chart")
|
||||
to_version = diff.get("to_version")
|
||||
from_version = diff.get("from_version")
|
||||
|
||||
values = diff.get("values", {})
|
||||
templates = diff.get("templates", {})
|
||||
deps = diff.get("dependencies", {})
|
||||
|
||||
values_added = len(values.get("added", []))
|
||||
values_removed = len(values.get("removed", []))
|
||||
values_changed = len(values.get("changed", {}))
|
||||
values_type_changed = len(values.get("type_changed", []))
|
||||
|
||||
template_added = sum(1 for v in templates.values() if isinstance(v, dict) and v.get("added"))
|
||||
template_removed = sum(1 for v in templates.values() if isinstance(v, dict) and v.get("removed"))
|
||||
|
||||
breaking_flag = breaking.get("breaking", False)
|
||||
severity = breaking.get("severity", "warning")
|
||||
reasons = breaking.get("reasons", [])
|
||||
warnings_list = breaking.get("warnings", [])
|
||||
|
||||
lines: List[str] = []
|
||||
|
||||
# update_docs_file이 ## {version} 헤더를 별도 추가하므로 여기서는 생략
|
||||
lines.append("### 변경 요약")
|
||||
if from_version:
|
||||
lines.append(f"- from_version: {from_version}")
|
||||
if to_version:
|
||||
lines.append(f"- to_version: {to_version}")
|
||||
if chart and from_version and to_version:
|
||||
lines.append(f"- Chart `{chart}` {from_version} → {to_version} 업데이트")
|
||||
lines.append(f"- Values: +{values_added} / -{values_removed} / ~{values_changed} / type~{values_type_changed}")
|
||||
lines.append(f"- Templates: +{template_added} / -{template_removed}")
|
||||
if deps:
|
||||
added = len(deps.get("added", []))
|
||||
removed = len(deps.get("removed", []))
|
||||
changed = len(deps.get("version_changed", {}))
|
||||
lines.append(f"- Dependencies: +{added} / -{removed} / ~{changed}")
|
||||
|
||||
lines.append("\n### custom-values.yaml 수정 필요 항목")
|
||||
if breaking_flag:
|
||||
for r in reasons:
|
||||
key = r.get("key") or r.get("resource") or ""
|
||||
detail = r.get("detail") or ""
|
||||
lines.append(f"- **`{key}`**: {detail}".rstrip())
|
||||
else:
|
||||
lines.append("없음")
|
||||
|
||||
if warnings_list:
|
||||
lines.append("\n### 배포 시 주의사항")
|
||||
for w in warnings_list:
|
||||
resource = w.get("resource") or w.get("key") or ""
|
||||
detail = w.get("detail") or ""
|
||||
wtype = w.get("type", "")
|
||||
lines.append(f"- **{wtype}** {resource}: {detail}".rstrip())
|
||||
|
||||
lines.append("\n### 참고")
|
||||
lines.append(f"- severity: {severity}")
|
||||
lines.append(f"- breaking: {str(breaking_flag).lower()}")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _build_prompt(diff: Dict[str, Any], breaking: Dict[str, Any], docs_context: Dict[str, Any] | None) -> str:
|
||||
prompt = (
|
||||
"당신은 Kubernetes Helm 업그레이드 문서를 작성하는 전문가입니다.\n\n"
|
||||
"두 Helm 차트 버전 간 변경점을 담은 Structured Diff JSON이 제공됩니다.\n\n"
|
||||
"## 작업\n"
|
||||
"1. 핵심 변경 사항을 평문으로 요약합니다.\n"
|
||||
"2. 각 변경의 운영 영향(예: 롤링 업데이트, 재시작 등)을 설명합니다.\n"
|
||||
"3. Breaking Change를 강조하고 구체적인 마이그레이션 절차를 제공합니다.\n"
|
||||
"4. 아래 형식의 간결한 Markdown 문서를 생성합니다.\n\n"
|
||||
"## 규칙\n"
|
||||
"- 입력 JSON에 없는 내용은 절대 추가하지 마세요.\n"
|
||||
"- 추측하지 마세요.\n"
|
||||
"- 값이 비어 있거나 null인 필드는 언급하지 마세요.\n"
|
||||
"- docs_context는 보조 설명에만 사용하고, diff에 없는 변경을 추가하지 마세요.\n"
|
||||
"- errors가 있으면 분석이 불완전할 수 있음을 명시하세요.\n"
|
||||
"- SRE/DevOps 엔지니어가 바로 실행할 수 있도록 명확하게 작성하세요.\n\n"
|
||||
"## 출력 형식\n"
|
||||
"아래 Markdown 구조를 정확히 지키세요 (## {to_version} 헤더는 포함하지 마세요):\n\n"
|
||||
"### 변경 요약\n"
|
||||
"- from_version: <from_version>\n"
|
||||
"- to_version: <to_version>\n"
|
||||
"- <핵심 변경 사항 요약>\n\n"
|
||||
"### custom-values.yaml 수정 필요 항목\n"
|
||||
"<breaking=true면 항목별 key와 수정 방법>\n"
|
||||
"<breaking=false면 \"없음\">\n\n"
|
||||
"### 배포 시 주의사항\n"
|
||||
"<warnings가 있으면 나열>\n"
|
||||
"<없으면 섹션 생략>\n\n"
|
||||
"### 참고\n"
|
||||
"- severity: <severity>\n"
|
||||
"- breaking: <true/false>\n\n"
|
||||
"---\n\n"
|
||||
"Input:\n"
|
||||
)
|
||||
payload = {
|
||||
**diff,
|
||||
**breaking,
|
||||
}
|
||||
if docs_context:
|
||||
payload["docs_context"] = docs_context
|
||||
prompt += json.dumps(payload, ensure_ascii=False)
|
||||
return prompt
|
||||
|
||||
|
||||
def _run_claude(prompt: str) -> str:
|
||||
res = subprocess.run(["claude", "-p", prompt], capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(res.stderr.strip() or "claude CLI failed")
|
||||
return res.stdout.strip()
|
||||
|
||||
|
||||
def generate_upgrade_doc(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
inp = GenerateDocInput(**payload)
|
||||
|
||||
use_claude = os.environ.get("USE_CLAUDE_CLI", "0") == "1"
|
||||
breaking = inp.breaking_result.get("breaking", False)
|
||||
markdown: str
|
||||
|
||||
# LLM 호출 조건: USE_CLAUDE_CLI=1 AND breaking=true
|
||||
if use_claude and breaking:
|
||||
try:
|
||||
prompt = _build_prompt(inp.diff_json, inp.breaking_result, inp.docs_context)
|
||||
markdown = _run_claude(prompt)
|
||||
except Exception:
|
||||
markdown = _fallback_summary(inp.diff_json, inp.breaking_result)
|
||||
else:
|
||||
markdown = _fallback_summary(inp.diff_json, inp.breaking_result)
|
||||
|
||||
out = GenerateDocOutput(markdown=markdown, truncated=False)
|
||||
return json.loads(out.model_dump_json())
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
"""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]
|
||||
|
||||
|
||||
class BreakingCheckOutput(BaseModel):
|
||||
breaking: bool
|
||||
severity: Severity
|
||||
breaking_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 = "BUILD-README.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
|
||||
doc_content: str
|
||||
breaking: bool
|
||||
severity: Severity
|
||||
|
||||
|
||||
class CreatePROutput(BaseModel):
|
||||
pr_url: str
|
||||
branch_name: str
|
||||
labels: List[str]
|
||||
|
||||
|
||||
# -------------------------
|
||||
# deploy_validate
|
||||
# -------------------------
|
||||
class DeployValidateInput(BaseModel):
|
||||
chart: str
|
||||
repo: Optional[str] = None
|
||||
chart_path: 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