feat(bot): CI/CD 파이프라인 봇 명령어 4개 추가
## 신규 명령어 - /cicd [project] Jenkins + Gitea 전체 CI/CD 현황 - /jenkins <job> [build|status|log] Jenkins 빌드 트리거·상태·로그 조회 - /git <repo> [log|pr|branch] Gitea 저장소 커밋·PR·브랜치 조회 - /release <project> [version] 릴리즈 배포 파이프라인 트리거 ## 백엔드 헬퍼 함수 - _cmd_cicd_status() Jenkins + Gitea 통합 현황 - _cmd_jenkins_status() Jenkins 잡 상태/로그 - _cmd_jenkins_trigger() Jenkins 빌드 트리거 (백그라운드) - _cmd_gitea_status() Gitea 커밋/PR/브랜치 조회 - _cmd_release() 릴리즈 파이프라인 실행 ## 환경변수 - JENKINS_API_TOKEN: Jenkins API 토큰 (Jenkins 초기 설정 후 발급) - GITEA_API_TOKEN: 발급 완료 (ce25405940c3...) ## 테스트 결과 - /git guardia-itsm log ✅ 최근 커밋 조회 - /git guardia-itsm branch ✅ 브랜치 목록 - /cicd ✅ Gitea 정상, Jenkins 토큰 대기 - /jenkins ⏳ Jenkins 초기 설정 완료 후 정상화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
75d92f2b90
commit
16e063b8ed
@ -446,6 +446,52 @@ async def handle_bot_command(
|
|||||||
bg.add_task(_cmd_vuln_scan, cmd.room, cmd.user, target)
|
bg.add_task(_cmd_vuln_scan, cmd.room, cmd.user, target)
|
||||||
return BotReply(room=cmd.room, text=f"[취약점 스캔] {target} 스캔 시작 (약 30초 소요)...")
|
return BotReply(room=cmd.room, text=f"[취약점 스캔] {target} 스캔 시작 (약 30초 소요)...")
|
||||||
|
|
||||||
|
# ── /cicd [project] ─── CI/CD 전체 현황 ─────────────────────────────────
|
||||||
|
elif keyword in ("/cicd", "!cicd"):
|
||||||
|
project = parts[1] if len(parts) > 1 else None
|
||||||
|
reply = await _cmd_cicd_status(project)
|
||||||
|
return BotReply(room=cmd.room, text=reply)
|
||||||
|
|
||||||
|
# ── /jenkins <job> [build|status|log] ─── Jenkins 제어 ──────────────────
|
||||||
|
elif keyword in ("/jenkins", "!jenkins"):
|
||||||
|
if len(parts) < 2:
|
||||||
|
return BotReply(room=cmd.room,
|
||||||
|
text="사용법: /jenkins <job명> [build|status|log]\n"
|
||||||
|
"예) /jenkins guardia-itsm build\n"
|
||||||
|
"예) /jenkins guardia-itsm status")
|
||||||
|
job = parts[1]
|
||||||
|
action = parts[2].lower() if len(parts) > 2 else "status"
|
||||||
|
if action == "build":
|
||||||
|
bg.add_task(_cmd_jenkins_trigger, cmd.room, cmd.user, job)
|
||||||
|
return BotReply(room=cmd.room, text=f"[Jenkins] {job} 빌드 트리거 요청 중...")
|
||||||
|
else:
|
||||||
|
reply = await _cmd_jenkins_status(job, action)
|
||||||
|
return BotReply(room=cmd.room, text=reply)
|
||||||
|
|
||||||
|
# ── /git <repo> [branch|pr|log] ─── Gitea 저장소 상태 ───────────────────
|
||||||
|
elif keyword in ("/git", "!git"):
|
||||||
|
if len(parts) < 2:
|
||||||
|
return BotReply(room=cmd.room,
|
||||||
|
text="사용법: /git <저장소> [branch|pr|log]\n"
|
||||||
|
"예) /git guardia-itsm log\n"
|
||||||
|
"예) /git zioinfo-web pr")
|
||||||
|
repo = parts[1]
|
||||||
|
action = parts[2].lower() if len(parts) > 2 else "log"
|
||||||
|
reply = await _cmd_gitea_status(repo, action)
|
||||||
|
return BotReply(room=cmd.room, text=reply)
|
||||||
|
|
||||||
|
# ── /release <project> [version] ─── 릴리즈 배포 트리거 ─────────────────
|
||||||
|
elif keyword in ("/release", "!release"):
|
||||||
|
if len(parts) < 2:
|
||||||
|
return BotReply(room=cmd.room,
|
||||||
|
text="사용법: /release <프로젝트명> [버전]\n"
|
||||||
|
"예) /release guardia-itsm v2.1.0")
|
||||||
|
project = parts[1]
|
||||||
|
version = parts[2] if len(parts) > 2 else "latest"
|
||||||
|
bg.add_task(_cmd_release, cmd.room, cmd.user, project, version)
|
||||||
|
return BotReply(room=cmd.room,
|
||||||
|
text=f"[릴리즈] {project} {version} 배포 파이프라인 시작...")
|
||||||
|
|
||||||
# ── /help ─────────────────────────────────────────────────────────────────
|
# ── /help ─────────────────────────────────────────────────────────────────
|
||||||
elif keyword == "/help":
|
elif keyword == "/help":
|
||||||
return BotReply(room=cmd.room, text=_help_text())
|
return BotReply(room=cmd.room, text=_help_text())
|
||||||
@ -1371,6 +1417,241 @@ def _get_internal_token() -> str:
|
|||||||
return os.environ.get("INTERNAL_API_TOKEN", "")
|
return os.environ.get("INTERNAL_API_TOKEN", "")
|
||||||
|
|
||||||
|
|
||||||
|
# ── CI/CD 봇 명령어 헬퍼 함수 ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
JENKINS_URL = "http://localhost:9080" # Nginx 뒤 내부 포트
|
||||||
|
GITEA_URL = "http://localhost:9003" # Nginx 뒤 내부 포트
|
||||||
|
JENKINS_USER = "admin"
|
||||||
|
JENKINS_TOKEN_ENV = "JENKINS_API_TOKEN" # 환경변수에서 읽기
|
||||||
|
GITEA_USER = "zio"
|
||||||
|
GITEA_TOKEN_ENV = "GITEA_API_TOKEN" # 환경변수에서 읽기
|
||||||
|
|
||||||
|
import os as _os
|
||||||
|
|
||||||
|
|
||||||
|
def _jenkins_auth():
|
||||||
|
token = _os.environ.get(JENKINS_TOKEN_ENV, "")
|
||||||
|
return (JENKINS_USER, token) if token else (JENKINS_USER, "")
|
||||||
|
|
||||||
|
|
||||||
|
def _gitea_headers():
|
||||||
|
token = _os.environ.get(GITEA_TOKEN_ENV, "")
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"token {token}"
|
||||||
|
else:
|
||||||
|
# 토큰 없으면 Basic Auth (Gitea 기본)
|
||||||
|
import base64
|
||||||
|
cred = base64.b64encode(b"zio:Zio@Admin2026!").decode()
|
||||||
|
headers["Authorization"] = f"Basic {cred}"
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
async def _cmd_cicd_status(project: Optional[str]) -> str:
|
||||||
|
"""CI/CD 전체 현황: Jenkins 최근 빌드 + Gitea 최근 커밋."""
|
||||||
|
lines = ["[CI/CD 현황]"]
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as c:
|
||||||
|
# Jenkins: 모든 Job 목록
|
||||||
|
r = await c.get(
|
||||||
|
f"{JENKINS_URL}/api/json?tree=jobs[name,color,lastBuild[number,result,timestamp,duration]]",
|
||||||
|
auth=_jenkins_auth(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
jobs = r.json().get("jobs", [])
|
||||||
|
if project:
|
||||||
|
jobs = [j for j in jobs if project.lower() in j["name"].lower()]
|
||||||
|
lines.append("\n■ Jenkins 빌드")
|
||||||
|
for j in jobs[:8]:
|
||||||
|
lb = j.get("lastBuild") or {}
|
||||||
|
result = lb.get("result", "N/A") or "진행중"
|
||||||
|
num = lb.get("number", "-")
|
||||||
|
icon = {"SUCCESS":"✅","FAILURE":"❌","UNSTABLE":"⚠️",
|
||||||
|
"ABORTED":"⛔","진행중":"🔄"}.get(result, "❓")
|
||||||
|
lines.append(f" {icon} {j['name']} #{num} — {result}")
|
||||||
|
else:
|
||||||
|
lines.append(f"\n■ Jenkins 연결 실패 (HTTP {r.status_code})")
|
||||||
|
|
||||||
|
# Gitea: 저장소 목록
|
||||||
|
r2 = await c.get(
|
||||||
|
f"{GITEA_URL}/api/v1/repos/search?limit=5",
|
||||||
|
headers=_gitea_headers(),
|
||||||
|
)
|
||||||
|
if r2.status_code == 200:
|
||||||
|
repos = r2.json().get("data", [])
|
||||||
|
if project:
|
||||||
|
repos = [rep for rep in repos if project.lower() in rep["name"].lower()]
|
||||||
|
lines.append("\n■ Gitea 저장소")
|
||||||
|
for rep in repos[:5]:
|
||||||
|
updated = (rep.get("updated_at") or "")[:10]
|
||||||
|
lines.append(f" 📁 {rep['full_name']} — 최근: {updated}")
|
||||||
|
else:
|
||||||
|
lines.append(f"\n■ Gitea 연결 실패 (HTTP {r2.status_code})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
lines.append(f"\n연결 오류: {str(e)[:80]}")
|
||||||
|
lines.append("Jenkins/Gitea JENKINS_API_TOKEN, GITEA_API_TOKEN 환경변수를 확인하세요.")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
async def _cmd_jenkins_status(job: str, action: str) -> str:
|
||||||
|
"""Jenkins 잡 상태/로그 조회."""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as c:
|
||||||
|
if action == "log":
|
||||||
|
r = await c.get(
|
||||||
|
f"{JENKINS_URL}/job/{job}/lastBuild/consoleText",
|
||||||
|
auth=_jenkins_auth(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
log = r.text[-1200:] # 마지막 1200자
|
||||||
|
return f"[Jenkins] {job} 최근 빌드 로그:\n```\n{log}\n```"
|
||||||
|
return f"[Jenkins] {job} 로그 조회 실패 (HTTP {r.status_code})"
|
||||||
|
else:
|
||||||
|
# status
|
||||||
|
r = await c.get(
|
||||||
|
f"{JENKINS_URL}/job/{job}/api/json?tree=name,color,lastBuild[number,result,timestamp,duration,url]",
|
||||||
|
auth=_jenkins_auth(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
d = r.json()
|
||||||
|
lb = d.get("lastBuild") or {}
|
||||||
|
result = lb.get("result", "진행중") or "진행중"
|
||||||
|
num = lb.get("number", "N/A")
|
||||||
|
dur_sec = (lb.get("duration", 0) or 0) // 1000
|
||||||
|
icon = {"SUCCESS":"✅","FAILURE":"❌","UNSTABLE":"⚠️",
|
||||||
|
"ABORTED":"⛔","진행중":"🔄"}.get(result, "❓")
|
||||||
|
return (
|
||||||
|
f"[Jenkins] {job}\n"
|
||||||
|
f" 최근 빌드: #{num}\n"
|
||||||
|
f" 결과: {icon} {result}\n"
|
||||||
|
f" 소요시간: {dur_sec}초\n"
|
||||||
|
f" 빌드 URL: {JENKINS_URL}/job/{job}/{num}/"
|
||||||
|
)
|
||||||
|
return f"[Jenkins] {job} 조회 실패 (HTTP {r.status_code})\n잡 이름을 확인하세요."
|
||||||
|
except Exception as e:
|
||||||
|
return f"[Jenkins] 연결 오류: {str(e)[:100]}"
|
||||||
|
|
||||||
|
|
||||||
|
async def _cmd_jenkins_trigger(room: str, actor: str, job: str):
|
||||||
|
"""Jenkins 빌드 트리거 (백그라운드)."""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as c:
|
||||||
|
r = await c.post(
|
||||||
|
f"{JENKINS_URL}/job/{job}/build",
|
||||||
|
auth=_jenkins_auth(),
|
||||||
|
)
|
||||||
|
if r.status_code in (200, 201):
|
||||||
|
await _send_to_room(room, f"[Jenkins] ✅ {job} 빌드 트리거 완료 by {actor}\n빌드 상태 확인: /jenkins {job} status")
|
||||||
|
else:
|
||||||
|
await _send_to_room(room, f"[Jenkins] ❌ {job} 빌드 트리거 실패 (HTTP {r.status_code})")
|
||||||
|
except Exception as e:
|
||||||
|
await _send_to_room(room, f"[Jenkins] 연결 오류: {str(e)[:100]}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _cmd_gitea_status(repo: str, action: str) -> str:
|
||||||
|
"""Gitea 저장소 상태 조회."""
|
||||||
|
# repo 형식: 'guardia-itsm' → 'zio/guardia-itsm' 자동 보완
|
||||||
|
full_repo = repo if "/" in repo else f"zio/{repo}"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as c:
|
||||||
|
if action == "pr":
|
||||||
|
r = await c.get(
|
||||||
|
f"{GITEA_URL}/api/v1/repos/{full_repo}/pulls?state=open&limit=5",
|
||||||
|
headers=_gitea_headers(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
prs = r.json()
|
||||||
|
if not prs:
|
||||||
|
return f"[Gitea] {full_repo} — 오픈 PR 없음"
|
||||||
|
lines = [f"[Gitea] {full_repo} 오픈 PR {len(prs)}건"]
|
||||||
|
for pr in prs:
|
||||||
|
lines.append(f" #{pr['number']} {pr['title']} — {pr['user']['login']}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
return f"[Gitea] PR 조회 실패 (HTTP {r.status_code})"
|
||||||
|
|
||||||
|
elif action == "branch":
|
||||||
|
r = await c.get(
|
||||||
|
f"{GITEA_URL}/api/v1/repos/{full_repo}/branches?limit=5",
|
||||||
|
headers=_gitea_headers(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
branches = r.json()
|
||||||
|
lines = [f"[Gitea] {full_repo} 브랜치 목록"]
|
||||||
|
for b in branches:
|
||||||
|
commit = b.get("commit", {}).get("id", "")[:7]
|
||||||
|
lines.append(f" 🌿 {b['name']} — {commit}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
return f"[Gitea] 브랜치 조회 실패 (HTTP {r.status_code})"
|
||||||
|
|
||||||
|
else: # log (최근 커밋)
|
||||||
|
r = await c.get(
|
||||||
|
f"{GITEA_URL}/api/v1/repos/{full_repo}/commits?limit=5",
|
||||||
|
headers=_gitea_headers(),
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
commits = r.json()
|
||||||
|
lines = [f"[Gitea] {full_repo} 최근 커밋"]
|
||||||
|
for cm in commits:
|
||||||
|
sha = cm.get("sha", "")[:7]
|
||||||
|
msg = (cm.get("commit", {}).get("message") or "")[:50].split("\n")[0]
|
||||||
|
auth = cm.get("commit", {}).get("author", {}).get("name", "")
|
||||||
|
lines.append(f" {sha} {msg} — {auth}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
return f"[Gitea] 커밋 조회 실패 (HTTP {r.status_code})"
|
||||||
|
except Exception as e:
|
||||||
|
return f"[Gitea] 연결 오류: {str(e)[:100]}"
|
||||||
|
|
||||||
|
|
||||||
|
async def _cmd_release(room: str, actor: str, project: str, version: str):
|
||||||
|
"""릴리즈 배포 파이프라인 (Jenkins + 배포 서버)."""
|
||||||
|
# 저장소명 → Jenkins Job 이름 매핑
|
||||||
|
job_map = {
|
||||||
|
"guardia-itsm": "guardia-itsm",
|
||||||
|
"guardia-manager": "guardia-manager",
|
||||||
|
"zioinfo-web": "zioinfo-web",
|
||||||
|
"guardia-messenger":"guardia-messenger",
|
||||||
|
}
|
||||||
|
job = job_map.get(project.lower(), project)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _send_to_room(room,
|
||||||
|
f"[릴리즈] 🚀 {project} {version} 배포 파이프라인 시작 by {actor}")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as c:
|
||||||
|
# 1단계: Jenkins 빌드 트리거
|
||||||
|
r = await c.post(
|
||||||
|
f"{JENKINS_URL}/job/{job}/buildWithParameters",
|
||||||
|
auth=_jenkins_auth(),
|
||||||
|
params={"VERSION": version, "ACTOR": actor},
|
||||||
|
)
|
||||||
|
if r.status_code in (200, 201):
|
||||||
|
await _send_to_room(room, f"[릴리즈] ✅ 1단계 — Jenkins 빌드 트리거 완료 ({job})")
|
||||||
|
else:
|
||||||
|
# 파라미터 없는 빌드 시도
|
||||||
|
r2 = await c.post(
|
||||||
|
f"{JENKINS_URL}/job/{job}/build",
|
||||||
|
auth=_jenkins_auth(),
|
||||||
|
)
|
||||||
|
if r2.status_code in (200, 201):
|
||||||
|
await _send_to_room(room, f"[릴리즈] ✅ 1단계 — Jenkins 빌드 트리거 완료")
|
||||||
|
else:
|
||||||
|
await _send_to_room(room,
|
||||||
|
f"[릴리즈] ⚠️ Jenkins 빌드 트리거 실패 (HTTP {r.status_code})\n"
|
||||||
|
f"수동 빌드: {JENKINS_URL}/job/{job}/")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2단계: 완료 후 상태 확인 안내
|
||||||
|
await _send_to_room(room,
|
||||||
|
f"[릴리즈] ℹ️ 빌드 상태 확인:\n"
|
||||||
|
f" /jenkins {job} status\n"
|
||||||
|
f" /jenkins {job} log")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await _send_to_room(room, f"[릴리즈] 연결 오류: {str(e)[:100]}")
|
||||||
|
|
||||||
|
|
||||||
def _help_text() -> str:
|
def _help_text() -> str:
|
||||||
return """GUARDiA ITSM 봇 명령어
|
return """GUARDiA ITSM 봇 명령어
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━
|
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
@ -1404,6 +1685,12 @@ def _help_text() -> str:
|
|||||||
/checklist → 공공기관 이행 현황
|
/checklist → 공공기관 이행 현황
|
||||||
/perf [url] → 성능 테스트
|
/perf [url] → 성능 테스트
|
||||||
|
|
||||||
|
[CI/CD 파이프라인]
|
||||||
|
/cicd [project] → CI/CD 전체 현황 (Jenkins + Gitea)
|
||||||
|
/jenkins <job> [build|status|log] → Jenkins 빌드 트리거·상태·로그
|
||||||
|
/git <repo> [log|pr|branch] → Gitea 저장소 커밋·PR·브랜치
|
||||||
|
/release <project> [version] → 릴리즈 배포 파이프라인 실행
|
||||||
|
|
||||||
[배포 제어]
|
[배포 제어]
|
||||||
!vibe <sr_id> [project_id] → 바이브 코딩 세션
|
!vibe <sr_id> [project_id] → 바이브 코딩 세션
|
||||||
!build <session_id> → 빌드 실행
|
!build <session_id> → 빌드 실행
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user