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,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"]
@@ -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())
@@ -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