Move directory
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: create_pr
|
||||
description: Create a GitHub PR for the chart upgrade.
|
||||
user-invokable: false
|
||||
---
|
||||
|
||||
# create_pr (Skill)
|
||||
|
||||
브랜치 생성 → 커밋 → 푸시 → GitHub PR 생성을 수행한다.
|
||||
breaking=true이면 `needs-review` 레이블, 아니면 `auto-update` 레이블을 붙인다.
|
||||
실행 후 `main` 브랜치로 복귀하여 다음 cronjob 실행을 위한 클린 상태를 유지한다.
|
||||
|
||||
## 브랜치 명명 규칙
|
||||
|
||||
`update-{chart}/{to_version}` — 예: `update-airflow/1.19.0`
|
||||
|
||||
## 재실행 안전성 (Idempotent)
|
||||
|
||||
- 브랜치가 이미 존재하면 체크아웃 후 추가 커밋
|
||||
- PR이 이미 존재하면 URL만 반환 (중복 PR 생성 안 함)
|
||||
|
||||
## Input schema
|
||||
```json
|
||||
{
|
||||
"repo_path": "string",
|
||||
"chart": "string",
|
||||
"from_version": "string",
|
||||
"to_version": "string",
|
||||
"breaking": "boolean",
|
||||
"severity": "critical|high|medium|warning",
|
||||
"reasons": "array (breaking_change_check.reasons)",
|
||||
"warnings": "array (breaking_change_check.warnings)"
|
||||
}
|
||||
```
|
||||
|
||||
## Output schema
|
||||
```json
|
||||
{
|
||||
"pr_url": "string",
|
||||
"branch_name": "string",
|
||||
"labels": ["string"],
|
||||
"committed": "boolean"
|
||||
}
|
||||
```
|
||||
|
||||
## 의존 도구
|
||||
|
||||
- `git` — 브랜치/커밋/푸시
|
||||
- `gh` (GitHub CLI) — PR 생성 및 레이블 관리. `gh auth login` 완료 필요.
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--repo-path", required=True, help="dip-catalog git repo 경로")
|
||||
parser.add_argument("--chart", required=True)
|
||||
parser.add_argument("--from-version", required=True)
|
||||
parser.add_argument("--to-version", required=True)
|
||||
parser.add_argument("--breaking-file", required=True, help="breaking_change_check 출력 JSON 경로")
|
||||
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.create_pr import create_pr # type: ignore
|
||||
|
||||
with open(args.breaking_file, "r", encoding="utf-8") as f:
|
||||
breaking_data = json.load(f)
|
||||
|
||||
out = create_pr({
|
||||
"repo_path": args.repo_path,
|
||||
"chart": args.chart,
|
||||
"from_version": args.from_version,
|
||||
"to_version": args.to_version,
|
||||
"breaking": breaking_data.get("breaking", False),
|
||||
"severity": breaking_data.get("severity", "warning"),
|
||||
"reasons": breaking_data.get("reasons", []),
|
||||
"warnings": breaking_data.get("warnings", []),
|
||||
})
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,164 @@
|
||||
"""create_pr skill implementation.
|
||||
|
||||
Branch → Commit → Push → GitHub PR 생성.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .skill_interface import CreatePRInput, CreatePROutput, BreakingReason # type: ignore
|
||||
|
||||
|
||||
def _git(args: List[str], cwd: Path) -> str:
|
||||
res = subprocess.run(["git"] + args, cwd=cwd, capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args)} failed: {res.stderr.strip()}")
|
||||
return res.stdout.strip()
|
||||
|
||||
|
||||
def _gh(args: List[str], cwd: Path) -> str:
|
||||
res = subprocess.run(["gh"] + args, cwd=cwd, capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
raise RuntimeError(f"gh {' '.join(args)} failed: {res.stderr.strip()}")
|
||||
return res.stdout.strip()
|
||||
|
||||
|
||||
def _get_existing_pr_url(branch_name: str, cwd: Path) -> Optional[str]:
|
||||
try:
|
||||
url = _gh(
|
||||
["pr", "list", "--head", branch_name, "--json", "url", "--jq", ".[0].url"],
|
||||
cwd=cwd,
|
||||
)
|
||||
return url.strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _ensure_label(label: str, cwd: Path) -> None:
|
||||
"""레이블이 없으면 생성한다."""
|
||||
label_colors = {
|
||||
"needs-review": "e11d48", # 빨강
|
||||
"auto-update": "16a34a", # 초록
|
||||
}
|
||||
try:
|
||||
_gh(["label", "list", "--json", "name", "--jq", f'.[] | select(.name == "{label}") | .name'], cwd=cwd)
|
||||
except Exception:
|
||||
pass
|
||||
color = label_colors.get(label, "0075ca")
|
||||
try:
|
||||
_gh(["label", "create", label, "--color", color, "--force"], cwd=cwd)
|
||||
except Exception:
|
||||
pass # 레이블 생성 실패해도 PR 생성은 계속
|
||||
|
||||
|
||||
def _build_pr_body(
|
||||
chart: str,
|
||||
from_version: str,
|
||||
to_version: str,
|
||||
breaking: bool,
|
||||
severity: str,
|
||||
reasons: List[BreakingReason],
|
||||
warnings: List[BreakingReason],
|
||||
) -> str:
|
||||
lines = [
|
||||
f"## Helm Chart Update: {chart} `{from_version}` → `{to_version}`",
|
||||
"",
|
||||
f"**Severity**: `{severity}` ",
|
||||
f"**Breaking**: {'✅ Human review required before merge' if breaking else '❌ No breaking changes'}",
|
||||
"",
|
||||
]
|
||||
if reasons:
|
||||
lines.append("### Breaking Changes")
|
||||
for r in reasons:
|
||||
key = r.get("key") or r.get("resource") or ""
|
||||
lines.append(f"- `[{r['type']}]` {key} — {r.get('detail', '')}")
|
||||
lines.append("")
|
||||
if warnings:
|
||||
lines.append(f"### Warnings ({len(warnings)} removed keys not used in custom-values.yaml)")
|
||||
for w in warnings:
|
||||
lines.append(f"- `[{w['type']}]` {w.get('key', '')} — {w.get('detail', '')}")
|
||||
lines.append("")
|
||||
if not reasons:
|
||||
lines += ["No breaking changes detected.", ""]
|
||||
lines.append("---")
|
||||
lines.append("*Generated by update-catalog automation*")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def create_pr(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
inp = CreatePRInput(**payload)
|
||||
repo_path = Path(inp.repo_path)
|
||||
chart = inp.chart
|
||||
from_version = inp.from_version
|
||||
to_version = inp.to_version
|
||||
breaking = inp.breaking
|
||||
severity = inp.severity
|
||||
reasons = inp.reasons
|
||||
warnings = inp.warnings
|
||||
|
||||
branch_name = f"update-{chart}/{to_version}"
|
||||
chart_dir = f"manifests/helm/{chart}/{to_version}"
|
||||
|
||||
# 1. main으로 이동 후 최신화
|
||||
_git(["checkout", "main"], cwd=repo_path)
|
||||
_git(["pull"], cwd=repo_path)
|
||||
|
||||
# 2. 브랜치 생성 또는 기존 브랜치 체크아웃
|
||||
local_exists = bool(_git(["branch", "--list", branch_name], cwd=repo_path).strip())
|
||||
remote_exists = bool(_git(["branch", "-r", "--list", f"origin/{branch_name}"], cwd=repo_path).strip())
|
||||
|
||||
if local_exists:
|
||||
_git(["checkout", branch_name], cwd=repo_path)
|
||||
elif remote_exists:
|
||||
_git(["checkout", "-b", branch_name, f"origin/{branch_name}"], cwd=repo_path)
|
||||
else:
|
||||
_git(["checkout", "-b", branch_name], cwd=repo_path)
|
||||
|
||||
# 3. 변경 파일 스테이징
|
||||
_git(["add", chart_dir], cwd=repo_path)
|
||||
|
||||
# 4. 변경사항이 있으면 커밋
|
||||
status = _git(["status", "--porcelain", chart_dir], cwd=repo_path)
|
||||
committed = False
|
||||
if status.strip():
|
||||
breaking_tag = " [BREAKING]" if breaking else ""
|
||||
commit_msg = f"update {chart}/{to_version}{breaking_tag}"
|
||||
_git(["commit", "-m", commit_msg], cwd=repo_path)
|
||||
committed = True
|
||||
|
||||
# 5. 원격 브랜치에 푸시
|
||||
_git(["push", "-u", "origin", branch_name], cwd=repo_path)
|
||||
|
||||
# 6. 레이블 준비
|
||||
labels = ["needs-review"] if breaking else ["auto-update"]
|
||||
for label in labels:
|
||||
_ensure_label(label, cwd=repo_path)
|
||||
|
||||
# 7. PR 생성 (이미 존재하면 URL만 반환)
|
||||
pr_url = _get_existing_pr_url(branch_name, repo_path)
|
||||
if not pr_url:
|
||||
breaking_tag = " [BREAKING]" if breaking else ""
|
||||
title = f"update {chart}: {from_version} → {to_version}{breaking_tag}"
|
||||
body = _build_pr_body(chart, from_version, to_version, breaking, severity, reasons, warnings)
|
||||
label_args: List[str] = []
|
||||
for label in labels:
|
||||
label_args += ["--label", label]
|
||||
pr_url = _gh(
|
||||
["pr", "create", "--title", title, "--body", body, "--base", "main"] + label_args,
|
||||
cwd=repo_path,
|
||||
)
|
||||
|
||||
# 8. main으로 복귀 (다음 cronjob 실행을 위해 클린 상태 유지)
|
||||
_git(["checkout", "main"], cwd=repo_path)
|
||||
|
||||
return json.loads(
|
||||
CreatePROutput(
|
||||
pr_url=pr_url,
|
||||
branch_name=branch_name,
|
||||
labels=labels,
|
||||
committed=committed,
|
||||
).model_dump_json()
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Skill Interface for create_pr."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
Severity = Literal["critical", "high", "medium", "warning"]
|
||||
|
||||
# BreakingReason은 dict로 처리 (breaking_change_check 출력 그대로 수신)
|
||||
BreakingReason = Dict[str, Any]
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user