Add catalog update agent
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: chart_version_detector
|
||||
description: Detect current chart version from dip-catalog and resolve latest version from helm repo.
|
||||
---
|
||||
|
||||
# chart_version_detector
|
||||
|
||||
Determine current_version from manifests/helm/<chart>/ directories and parse repo info from BUILD-README.md.
|
||||
Then resolve latest_version from helm repo.
|
||||
|
||||
## Input schema
|
||||
```json
|
||||
{
|
||||
"catalog_root": "string",
|
||||
"chart": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## Output schema
|
||||
```json
|
||||
{
|
||||
"chart": "string",
|
||||
"catalog_root": "string",
|
||||
"current_version": "string",
|
||||
"repo": "string | null",
|
||||
"repo_url": "string | null",
|
||||
"latest_version": "string | null"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--catalog-root", required=True)
|
||||
parser.add_argument("--chart", required=True)
|
||||
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.chart_version_detector import chart_version_detector # type: ignore
|
||||
|
||||
out = chart_version_detector({
|
||||
"catalog_root": args.catalog_root,
|
||||
"chart": args.chart,
|
||||
})
|
||||
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.
+131
@@ -0,0 +1,131 @@
|
||||
"""chart_version_detector skill implementation.
|
||||
|
||||
Determine current_version from dip-catalog directory, parse repo info from BUILD-README,
|
||||
then resolve latest_version from helm repo.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def _run(cmd: List[str]) -> str:
|
||||
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(res.stderr.strip() or f"command failed: {' '.join(cmd)}")
|
||||
return res.stdout
|
||||
|
||||
|
||||
def _parse_version(v: str) -> Tuple[int, ...]:
|
||||
# basic semver compare: take numeric parts only
|
||||
v = v.strip()
|
||||
v = re.split(r"[+-]", v)[0]
|
||||
parts = v.split(".")
|
||||
out = []
|
||||
for p in parts:
|
||||
try:
|
||||
out.append(int(p))
|
||||
except ValueError:
|
||||
out.append(0)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def _current_version_from_dirs(chart_dir: Path) -> Optional[str]:
|
||||
versions = [p.name for p in chart_dir.iterdir() if p.is_dir()]
|
||||
if not versions:
|
||||
return None
|
||||
versions_sorted = sorted(versions, key=_parse_version)
|
||||
return versions_sorted[-1]
|
||||
|
||||
|
||||
def _current_version_from_chart_yaml(chart_dir: Path) -> Optional[str]:
|
||||
chart_yaml = chart_dir / "Chart.yaml"
|
||||
if not chart_yaml.exists():
|
||||
return None
|
||||
with chart_yaml.open("r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return data.get("version")
|
||||
|
||||
|
||||
def _parse_repo_from_build_readme(path: Path) -> Tuple[Optional[str], Optional[str]]:
|
||||
if not path.exists():
|
||||
return None, None
|
||||
text = path.read_text(encoding="utf-8")
|
||||
m = re.search(r"helm\s+repo\s+add\s+(\S+)\s+(\S+)", text)
|
||||
if not m:
|
||||
return None, None
|
||||
return m.group(1), m.group(2)
|
||||
|
||||
|
||||
def _ensure_repo(repo: str, url: str) -> None:
|
||||
try:
|
||||
out = _run(["helm", "repo", "list"])
|
||||
if repo in out:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
_run(["helm", "repo", "add", repo, url])
|
||||
_run(["helm", "repo", "update"])
|
||||
|
||||
|
||||
def _latest_version(repo: str, chart: str) -> Optional[str]:
|
||||
out = _run(["helm", "search", "repo", f"{repo}/{chart}", "--versions"])
|
||||
lines = [l for l in out.splitlines() if l.strip()]
|
||||
if len(lines) < 2:
|
||||
return None
|
||||
# header at line 0; next line is latest
|
||||
parts = re.split(r"\s+", lines[1].strip())
|
||||
if len(parts) >= 2:
|
||||
return parts[1]
|
||||
return None
|
||||
|
||||
|
||||
def chart_version_detector(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
catalog_root = Path(payload["catalog_root"])
|
||||
chart = payload["chart"]
|
||||
chart_dir = catalog_root / "manifests" / "helm" / chart
|
||||
|
||||
if not chart_dir.exists():
|
||||
return {
|
||||
"chart": chart,
|
||||
"catalog_root": str(catalog_root),
|
||||
"current_version": None,
|
||||
"repo": None,
|
||||
"repo_url": None,
|
||||
"latest_version": None,
|
||||
"error": {"code": "CHART_NOT_FOUND", "message": f"chart path not found: {chart_dir}"},
|
||||
}
|
||||
|
||||
current = _current_version_from_dirs(chart_dir)
|
||||
if current is None:
|
||||
return {
|
||||
"chart": chart,
|
||||
"catalog_root": str(catalog_root),
|
||||
"current_version": None,
|
||||
"repo": None,
|
||||
"repo_url": None,
|
||||
"latest_version": None,
|
||||
"error": {"code": "VERSION_NOT_FOUND", "message": f"no version directories under {chart_dir}"},
|
||||
}
|
||||
|
||||
build_readme = chart_dir / current / "BUILD-README.md"
|
||||
repo, url = _parse_repo_from_build_readme(build_readme)
|
||||
|
||||
latest = None
|
||||
if repo and url:
|
||||
_ensure_repo(repo, url)
|
||||
latest = _latest_version(repo, chart)
|
||||
|
||||
return {
|
||||
"chart": chart,
|
||||
"catalog_root": str(catalog_root),
|
||||
"current_version": current,
|
||||
"repo": repo,
|
||||
"repo_url": url,
|
||||
"latest_version": latest,
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
"""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
|
||||
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
|
||||
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
|
||||
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