feat: initial zioinfo-mail webmail system

This commit is contained in:
DESKTOP-TKLFCPR\ython 2026-06-01 21:55:48 +09:00
parent 3d5a125b04
commit 8ab660cd38
22 changed files with 1543 additions and 0 deletions

View File

@ -0,0 +1,9 @@
{
"timestamp": "2026-06-01T21:10:57.257172",
"results": {
"guardia-itsm": "⚠️ ",
"guardia-manager": "⚠️ Modify: 2026-05-31 21:33:16.307249938 +0900",
"zioinfo-web-stash": "⚠️ stash 보존 (중요 파일 포함) - 수동 확인 필요",
"zioinfo-web": "✅ 28개 파일 반영"
}
}

View File

@ -0,0 +1,11 @@
{
"port_8025": "free",
"port_8026": "free",
"imap_login": "ok",
"smtp_login": "ok",
"gitea_repo": "not_found",
"ssl_cert": "/etc/ssl/guardia/server.crt\nexists",
"lets_encrypt": "/etc/letsencrypt/live/zioinfo.co.kr/fullchain.pem\nexists",
"aioimaplib": "",
"aiosmtplib": ""
}

View File

@ -0,0 +1,52 @@
"""Phase 1: IMAP/SMTP/포트 사전 검증"""
import paramiko, sys, json, socket, ssl, imaplib, smtplib, base64
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
def run(cmd, timeout=10):
_, o, _ = c.exec_command(cmd, timeout=timeout)
return o.read().decode('utf-8','replace').strip()
result = {}
# 1. 포트 가용성
result['port_8025'] = 'free' if '8025' not in run('ss -tlnp') else 'in_use'
result['port_8026'] = 'free' if '8026' not in run('ss -tlnp') else 'in_use'
# 2. IMAP 테스트
imap_test = run('python3 -c "'
'import imaplib, ssl; ctx=ssl.create_default_context(); '
'ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; '
'M=imaplib.IMAP4_SSL(\'localhost\', 993, ssl_context=ctx); '
'print(M.login(\'ythong\', \'1q2w3e!Q\')); M.logout()"')
result['imap_login'] = 'ok' if 'OK' in imap_test else f'fail:{imap_test[:80]}'
# 3. SMTP 테스트
smtp_test = run('python3 -c "'
'import smtplib; s=smtplib.SMTP(\'localhost\', 587); '
's.ehlo(); s.starttls(); s.login(\'ythong\', \'1q2w3e!Q\'); '
'print(\'ok\'); s.quit()"')
result['smtp_login'] = 'ok' if 'ok' in smtp_test else f'fail:{smtp_test[:80]}'
# 4. Gitea repo 확인
gitea = run(f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail" '
f'-H "Authorization: Basic {G}" 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get(\'full_name\',\'?\'))" 2>/dev/null || echo "not_found"')
result['gitea_repo'] = gitea
# 5. SSL cert
cert = run('ls /etc/ssl/guardia/server.crt 2>/dev/null && echo exists || echo missing')
result['ssl_cert'] = cert
result['lets_encrypt'] = run('ls /etc/letsencrypt/live/zioinfo.co.kr/fullchain.pem 2>/dev/null && echo exists || echo missing')
# 6. Python 패키지 확인
result['aioimaplib'] = run('pip3 show aioimaplib 2>/dev/null | head -1 || echo missing')
result['aiosmtplib'] = run('pip3 show aiosmtplib 2>/dev/null | head -1 || echo missing')
c.close()
print(json.dumps(result, ensure_ascii=False, indent=2))
import os; os.makedirs('C:/GUARDiA/.claude/agents/_workspace', exist_ok=True)
with open('C:/GUARDiA/.claude/agents/_workspace/infra-check.json','w') as f:
json.dump(result, f, ensure_ascii=False, indent=2)

View File

@ -0,0 +1,55 @@
{
"timestamp": "2026-06-01T21:14:25.665805",
"systems": {
"guardia-itsm": {
"issues": [
"APP_SRC_DRIFT:Only in /opt/guardia/app: rpa_rules.json"
],
"status": {
"service": "active",
"server_commit": "5e987833",
"gitea_commit": "5e987833"
}
},
"zioinfo-web": {
"issues": [
"STASH_EXISTS:stash@{0}: WIP on main: ed276b6 fix(ui): 로고 필터 제거, 메뉴 네비게이션 오류 수정"
],
"status": {
"service": "active",
"server_commit": "9b203455",
"gitea_commit": "9b203455",
"www_date": "2026-06-01 20:48:44.033930458"
}
},
"guardia-manager": {
"issues": [],
"status": {
"service": "active",
"server_commit": "none",
"gitea_commit": "fa2657a2",
"www_date": "2026-06-01 21:12:50.599828319"
}
},
"guardia-messenger": {
"issues": [],
"status": {}
},
"guardia-docs": {
"issues": [],
"status": {
"www_date": ""
}
}
},
"action_required": [
"guardia-itsm",
"zioinfo-web"
],
"critical": [
"guardia-itsm: app/src drift"
],
"warnings": [
"zioinfo-web: stash exists"
]
}

View File

@ -0,0 +1,23 @@
import paramiko, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
def run(label, cmd, timeout=15):
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(f'\n[{label}]\n' + o.read().decode('utf-8','replace').strip()[:1000])
# 현재 Company.jsx (stash에서 복원된 468줄) vs git HEAD의 Company.jsx
run('현재 vs HEAD diff (추가된 부분)',
'git -C /opt/zioinfo/src diff HEAD -- frontend/src/pages/Company.jsx 2>/dev/null | '
"grep '^+' | grep -v '^+++' | head -50")
# index.html이 참조하는 JS 파일
run('index.html 스크립트 태그',
"grep -o 'src=\"/assets/[^\"]*\"\\|href=\"/assets/[^\"]*\"' /var/www/zioinfo/index.html | head -10")
# 브라우저에서 실제로 보이는 것: curl로 홈페이지 확인
run('실제 서빙 확인 (200 OK)',
'curl -sf -o /dev/null -w "%{http_code}" https://zioinfo.co.kr/company/greeting 2>/dev/null || '
'curl -sf -o /dev/null -w "%{http_code}" http://localhost:8082/company/greeting 2>/dev/null')
c.close()

View File

@ -0,0 +1,64 @@
"""E2E 파이프라인 최종 검증: 실제 deploy trigger → 배포+Jenkins 빌드 확인"""
import paramiko, sys, json, time, base64
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
J = 'http://127.0.0.1:9080'
A = 'admin:Admin@2026!'
GITEA_B64 = base64.b64encode(b'zio:Zio@Admin2026!').decode()
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
out = o.read().decode('utf-8','replace').strip()
if out: print(out[:600])
return out
# 1. 현재 build 번호 기록
_, o, _ = c.exec_command(f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null', timeout=10)
d = json.loads(o.read().decode('utf-8','replace'))
build_before = d.get('lastBuild', {}).get('number', 0)
print(f'빌드 전 lastBuild: #{build_before}')
# 2. Gitea push 시뮬레이션 (webhook 직접 호출)
run('push 시뮬레이션 → 배포+Jenkins',
"curl -sf -X POST http://localhost:9999 "
"-H 'Content-Type: application/json' "
"-H 'X-Gitea-Event: push' "
"-d '{\"repository\":{\"name\":\"guardia-itsm\"},\"ref\":\"refs/heads/main\"}' 2>/dev/null")
# 3. 배포 완료 대기
print('\n배포 + Jenkins 빌드 대기...')
for i in range(15):
time.sleep(4)
_, o, _ = c.exec_command(
f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null', timeout=10)
try:
d = json.loads(o.read().decode('utf-8','replace'))
lb = d.get('lastBuild', {}).get('number', 0)
building = d.get('lastBuild', {}).get('building', False) if lb > build_before else None
if lb > build_before:
if building == False:
_, o2, _ = c.exec_command(
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/api/json 2>/dev/null', timeout=10)
bd = json.loads(o2.read().decode('utf-8','replace'))
print(f' ✅ 새 빌드 #{lb}: {bd.get("result")}')
break
else:
print(f' build #{lb} 진행중...')
else:
_, o3, _ = c.exec_command('tail -3 /var/log/zioinfo/deploy.log 2>/dev/null', timeout=5)
status = o3.read().decode('utf-8','replace').strip().splitlines()[-1] if o3.read else '...'
except Exception as e:
print(f' 오류: {e}')
# 4. 최종 결과
run('배포 로그 마지막',
'tail -8 /var/log/zioinfo/deploy.log 2>/dev/null')
run('Jenkins 빌드 콘솔 (마지막 20줄)',
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/consoleText 2>/dev/null | tail -20')
run('guardia 서비스 상태',
'systemctl is-active guardia')
c.close()

View File

@ -0,0 +1,7 @@
import paramiko, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=10)
_, o, _ = c.exec_command('cat /var/log/zioinfo/mail.log 2>/dev/null | grep -A 30 "500\\|Error\\|Exception\\|Traceback" | tail -50', timeout=10)
print(o.read().decode('utf-8','replace'))
c.close()

View File

@ -0,0 +1,54 @@
"""Jenkins job 빌드 트리거 + 결과 확인"""
import paramiko, sys, time, json
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
J = 'http://127.0.0.1:9080'
A = 'admin:Admin@2026!'
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
out = o.read().decode('utf-8','replace').strip()
if out: print(out[:600])
return out
# crumb 가져오기
_, o, _ = c.exec_command(f'curl -sf -u "{A}" {J}/crumbIssuer/api/json 2>/dev/null', timeout=10)
try:
cd = json.loads(o.read().decode('utf-8','replace').strip())
CRUMB = f'-H "{cd["crumbRequestField"]}: {cd["crumb"]}"'
except:
CRUMB = ''
# guardia-itsm 빌드 트리거
run('guardia-itsm 빌드 트리거',
f'curl -sf -X POST -u "{A}" {CRUMB} {J}/job/guardia-itsm/build 2>/dev/null && echo "트리거됨"')
time.sleep(5)
# 빌드 번호 확인
run('guardia-itsm 빌드 번호',
f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); '
'print(\'nextBuildNumber:\', d.get(\'nextBuildNumber\'), \'lastBuild:\', d.get(\'lastBuild\',{}).get(\'number\',\'없음\'))" 2>/dev/null')
time.sleep(8)
# 빌드 로그 확인
run('guardia-itsm 빌드 콘솔 로그',
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/consoleText 2>/dev/null | head -40')
# zioinfo-web 빌드 트리거
run('zioinfo-web 빌드 트리거',
f'curl -sf -X POST -u "{A}" {CRUMB} {J}/job/zioinfo-web/build 2>/dev/null && echo "트리거됨"')
time.sleep(3)
# 전체 job 상태
run('전체 job 상태',
f'curl -sf -u "{A}" {J}/api/json 2>/dev/null | '
'python3 -c "import sys,json; [print(j[\'name\'], j[\'color\']) for j in json.load(sys.stdin)[\'jobs\']]" 2>/dev/null')
c.close()

View File

@ -0,0 +1,133 @@
"""5개 시스템 전체 배포 상태 검증"""
import paramiko, sys, base64, json
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
def run(cmd, timeout=15):
_, o, _ = c.exec_command(cmd, timeout=timeout)
return o.read().decode('utf-8','replace').strip()
def gitea_commit(repo):
out = run(f"curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/{repo}/commits?limit=1' "
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
"python3 -c \"import sys,json; d=json.load(sys.stdin); "
"print(d[0]['sha'][:8], d[0]['commit']['message'][:50])\" 2>/dev/null")
return out or '(조회 실패)'
print('=' * 60)
print('GUARDiA 5개 시스템 배포 상태 검증')
print('=' * 60)
# ── 1. guardia-itsm ──────────────────────────────────────────
print('\n━━ 1. GUARDiA ITSM (guardia-itsm) ━━')
svc = run('systemctl is-active guardia 2>/dev/null')
port = run('ss -tlnp | grep 9001 2>/dev/null | head -1')
src_commit = run('git -C /opt/guardia/src log -1 --oneline 2>/dev/null')
app_routers = run('ls /opt/guardia/app/routers/*.py 2>/dev/null | wc -l')
app_vs_src = run('diff -rq /opt/guardia/src /opt/guardia/app '
'--exclude="*.pyc" --exclude="__pycache__" --exclude=".git" '
'--exclude="*.db" --exclude="uploads" 2>/dev/null | head -5 || echo "동일"')
stash = run('git -C /opt/guardia/src stash list 2>/dev/null')
uncommit = run('git -C /opt/guardia/src status --short 2>/dev/null | grep -v "static/assets" | head -5')
gitea = gitea_commit('guardia-itsm')
print(f' 서비스: {svc}')
print(f' 포트 9001: {"✅ LISTEN" if "9001" in port else "❌ 없음"}')
print(f' src 커밋: {src_commit}')
print(f' app routers: {app_routers}')
print(f' Gitea: {gitea}')
print(f' app vs src: {app_vs_src[:100]}')
print(f' stash: {stash or "없음"}')
print(f' uncommit: {uncommit or "없음"}')
# ── 2. zioinfo-web ───────────────────────────────────────────
print('\n━━ 2. 지오정보기술 홈페이지 (zioinfo-web) ━━')
svc = run('systemctl is-active zioinfo 2>/dev/null')
port = run('ss -tlnp | grep 8082 2>/dev/null | head -1')
src_commit = run('git -C /opt/zioinfo/src log -1 --oneline 2>/dev/null')
jar_date = run('ls -la /opt/zioinfo/app/app.jar 2>/dev/null | awk \'{print $6,$7,$8}\'')
www_date = run('ls -la /var/www/zioinfo/index.html 2>/dev/null | awk \'{print $6,$7,$8}\'')
stash = run('git -C /opt/zioinfo/src stash list 2>/dev/null')
uncommit = run('git -C /opt/zioinfo/src status --short 2>/dev/null | grep -v "static/assets" | head -5')
gitea = gitea_commit('zioinfo-web')
guardia_detail = run('ls /opt/zioinfo/src/frontend/src/pages/GuardiaDetail.jsx 2>/dev/null && '
'wc -l < /opt/zioinfo/src/frontend/src/pages/GuardiaDetail.jsx 2>/dev/null')
print(f' 서비스: {svc}')
print(f' 포트 8082: {"✅ LISTEN" if "8082" in port else "❌ 없음"}')
print(f' src 커밋: {src_commit}')
print(f' jar 날짜: {jar_date}')
print(f' /var/www 날짜: {www_date}')
print(f' GuardiaDetail: {guardia_detail}')
print(f' Gitea: {gitea}')
print(f' stash: {stash or "없음"}')
print(f' uncommit: {uncommit or "없음"}')
# ── 3. guardia-manager ───────────────────────────────────────
print('\n━━ 3. GUARDiA Manager (guardia-manager) ━━')
svc = run('systemctl is-active guardia-manager 2>/dev/null')
port = run('ss -tlnp | grep 8090 2>/dev/null | head -1')
www_date = run('ls -la /var/www/manager/index.html 2>/dev/null | awk \'{print $6,$7,$8}\' || echo "없음"')
be_routers = run('ls /opt/manager/backend/routers/*.py 2>/dev/null | wc -l || echo 0')
gitea = gitea_commit('guardia-manager')
print(f' 서비스: {svc}')
print(f' 포트 8090: {"✅ LISTEN" if "8090" in port else "❌ 없음"}')
print(f' /var/www/manager: {www_date}')
print(f' backend routers: {be_routers}')
print(f' Gitea: {gitea}')
# ── 4. guardia-messenger ─────────────────────────────────────
print('\n━━ 4. GUARDiA Messenger (guardia-messenger) ━━')
gitea = gitea_commit('guardia-messenger')
app_json = run("curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-messenger/contents/app.json' "
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); "
"c=base64.b64decode(d['content']).decode(); "
"import json as j2; a=j2.loads(c); "
"print('v'+a.get('expo',{}).get('version','?'))\" 2>/dev/null")
eas_json = run("curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-messenger/contents/eas.json' "
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
"python3 -c \"import sys,json,base64; d=json.load(sys.stdin); "
"c=base64.b64decode(d['content']).decode(); print(c[:100])\" 2>/dev/null")
print(f' Gitea: {gitea}')
print(f' 앱 버전: {app_json or "(조회 실패)"}')
print(f' eas.json: {eas_json[:80] if eas_json else "(없음)"}')
print(f' 배포 방식: EAS Build (expo.dev) — 서버 배포 없음')
# ── 5. guardia-docs ──────────────────────────────────────────
print('\n━━ 5. GUARDiA Docs (guardia-docs) ━━')
gitea = gitea_commit('guardia-docs')
doc_count = run("curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-docs/git/trees/main?recursive=true' "
f"-H 'Authorization: Basic {G}' 2>/dev/null | "
"python3 -c \"import sys,json; d=json.load(sys.stdin); "
"mds=[t for t in d.get('tree',[]) if t['path'].endswith('.md')]; "
"print(len(mds), '개 md 문서')\" 2>/dev/null")
print(f' Gitea: {gitea}')
print(f' 문서: {doc_count or "(조회 실패)"}')
# ── 종합 ─────────────────────────────────────────────────────
print('\n' + '=' * 60)
print('종합 요약')
print('=' * 60)
services = {
'guardia (ITSM)': run('systemctl is-active guardia 2>/dev/null'),
'zioinfo (홈페이지)': run('systemctl is-active zioinfo 2>/dev/null'),
'guardia-manager': run('systemctl is-active guardia-manager 2>/dev/null'),
'zioinfo-deploy': run('systemctl is-active zioinfo-deploy 2>/dev/null'),
'jenkins': run('systemctl is-active jenkins 2>/dev/null'),
}
for name, status in services.items():
icon = '' if status == 'active' else ''
print(f' {icon} {name}: {status}')
c.close()

View File

@ -0,0 +1,50 @@
"""GUARDiA ITSM 서버 배포 상태 전체 검증"""
import paramiko, sys, json, base64
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
def run(label, cmd, timeout=20):
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(f'\n[{label}]\n' + o.read().decode('utf-8','replace').strip()[:800])
# 1. 서버 git 최신 커밋
run('서버 /opt/guardia/app git 커밋',
'git -C /opt/guardia/app log -3 --oneline 2>/dev/null || echo "git 없음"')
run('서버 /opt/guardia/src git 커밋',
'git -C /opt/guardia/src log -3 --oneline 2>/dev/null || echo "git 없음"')
# 2. Gitea 최신 커밋
GITEA_B64 = base64.b64encode(b'zio:Zio@Admin2026!').decode()
run('Gitea guardia-itsm 최신 커밋',
f"curl -sf 'http://127.0.0.1:9003/api/v1/repos/zio/guardia-itsm/commits?limit=3' "
f"-H 'Authorization: Basic {GITEA_B64}' 2>/dev/null | "
"python3 -c \"import sys,json; "
"[print(c['sha'][:8], c['commit']['message'][:60]) for c in json.load(sys.stdin)]\" 2>/dev/null")
# 3. workspace vs 서버 핵심 파일 비교
run('서버 routers 목록',
'ls /opt/guardia/app/routers/*.py 2>/dev/null | xargs -I{} basename {} | sort')
run('서버 routers 개수', 'ls /opt/guardia/app/routers/*.py 2>/dev/null | wc -l')
# 4. 핵심 최신 기능 파일 존재 확인 (DR, CSAP, network, RPA, scraping)
run('최신 기능 파일 존재 여부',
'''for f in dr.py network_devices.py compliance.py rpa.py scraping.py ai_cmd.py; do
[ -f "/opt/guardia/app/routers/$f" ] && echo "✅ $f" || echo "❌ $f 없음"
done''')
# 5. 서버 서비스 상태 및 API 응답
run('guardia 서비스 상태',
'systemctl is-active guardia && systemctl status guardia --no-pager | grep -E "Active|Main PID" | head -3')
run('ITSM API 응답 확인',
'curl -sf http://localhost:8001/health 2>/dev/null || '
'curl -sf http://localhost:8001/docs 2>/dev/null | head -3 || '
'curl -sf http://localhost:9001/health 2>/dev/null || echo "API 확인 필요"')
# 6. 서버 stash 여부 (혹시 못 올린 작업 있는지)
run('서버 /opt/guardia/src stash',
'git -C /opt/guardia/src stash list 2>/dev/null || echo "stash 없음"')
run('서버 uncommitted 변경',
'git -C /opt/guardia/src status --short 2>/dev/null | head -10 || echo "없음"')
c.close()

View File

@ -0,0 +1,31 @@
"""동기화 후 Gitea 최신 커밋 확인 + 배포 트리거"""
import paramiko, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
def run(label, cmd, timeout=20):
print(f'\n[{label}]')
_, o, e = client.exec_command(cmd, timeout=timeout)
print(o.read().decode('utf-8','replace').strip()[:500])
REPOS = ['guardia-itsm', 'guardia-manager', 'guardia-docs', 'zioinfo-web']
B64 = 'Authorization: Basic ' + __import__('base64').b64encode(b'zio:Zio@Admin2026!').decode()
for repo in REPOS:
run(f'Gitea {repo} 최신 커밋',
f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/{repo}/commits?limit=1" '
f'--header "{B64}" 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); c=d[0]; print(c[\'sha\'][:8], c[\'commit\'][\'message\'][:50])" 2>/dev/null || echo FAIL')
# guardia-itsm 배포 트리거로 최신 코드 반영
run('guardia-itsm 배포 트리거',
'curl -sf -X POST http://localhost:9999 -H "Content-Type: application/json" '
'-H "X-Gitea-Event: push" '
'-d \'{"repository":{"name":"guardia-itsm"},"ref":"refs/heads/main"}\' 2>/dev/null')
time.sleep(10)
run('배포 결과', 'tail -8 /var/log/zioinfo/deploy.log 2>/dev/null')
client.close()

View File

@ -0,0 +1,21 @@
import paramiko, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
def run(label, cmd, timeout=15):
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(f'\n[{label}]\n' + o.read().decode('utf-8','replace').strip()[:600])
run('새 Company 파일 존재 확인',
'ls /var/www/zioinfo/assets/ | grep -i Company')
run('/var/www/zioinfo index.html 내용',
'cat /var/www/zioinfo/index.html | grep -o "Company[^\"]*" | head -5')
run('/var/www/zioinfo 최신 시간 파일',
'find /var/www/zioinfo/assets -newer /var/www/zioinfo/assets/Home-BC38QtTl.js 2>/dev/null | wc -l && '
'find /var/www/zioinfo/assets -newer /var/www/zioinfo/assets/Home-BC38QtTl.js 2>/dev/null | head -5')
run('backend static의 Company',
'ls /opt/zioinfo/src/backend/src/main/resources/static/assets/ | grep Company')
run('zioinfo 서비스 상태', 'systemctl is-active zioinfo')
run('실제 사이트 응답 (greeting)',
'curl -sf -L http://localhost:8082/company/greeting 2>/dev/null | grep -o "Company[^\"]*" | head -3')
c.close()

View File

@ -0,0 +1,69 @@
"""Jenkins 빌드 완료 대기 + 전체 파이프라인 최종 검증"""
import paramiko, sys, json, time, base64
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
J = 'http://127.0.0.1:9080'
A = 'admin:Admin@2026!'
def run(label, cmd, timeout=20):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(o.read().decode('utf-8','replace').strip()[:600])
# 빌드 완료 대기
print('Jenkins 빌드 #4 대기...')
for i in range(15):
time.sleep(5)
_, o, _ = c.exec_command(
f'curl -sf -u "{A}" {J}/job/guardia-itsm/api/json 2>/dev/null', timeout=10)
try:
d = json.loads(o.read().decode('utf-8','replace'))
lb = d.get('lastBuild', {}).get('number', 0)
nb = d.get('nextBuildNumber', 0)
print(f' lastBuild=#{lb} nextBuild=#{nb}', end='')
if lb >= 4:
_, o2, _ = c.exec_command(
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/api/json 2>/dev/null', timeout=10)
bd = json.loads(o2.read().decode('utf-8','replace'))
result = bd.get('result')
building = bd.get('building')
print(f'{result} building={building}')
if not building:
print(f'\n✅ build #{lb}: {result}')
break
else:
print()
except:
print()
# 최종 결과
run('build #4 콘솔 로그',
f'curl -sf -u "{A}" {J}/job/guardia-itsm/lastBuild/consoleText 2>/dev/null | tail -15')
# 전체 E2E 파이프라인 최종 상태
print('\n' + '='*55)
print('GUARDiA CI/CD 파이프라인 최종 현황')
print('='*55)
print('\n📋 구성 요소:')
print(' - Gitea repos (5개): webhook 2개씩 (port 9999 + Jenkins)')
print(' - deploy_server.py (port 9999): 즉시 배포 + Jenkins 트리거')
print(' - Jenkins (port 9080): Build + Test + ITSM 메신저 알림')
run('webhook 서버', 'systemctl is-active zioinfo-deploy && echo "port 9999 ✅"')
run('5개 job 상태',
f'curl -sf -u "{A}" {J}/api/json 2>/dev/null | '
"python3 -c \"import sys,json; d=json.load(sys.stdin); "
"[print(' ',j['name'].ljust(22), j['color'], '' if j['color']=='blue' else '⚠️') for j in d['jobs']]\" 2>/dev/null")
run('guardia + zioinfo 서비스',
'echo guardia: $(systemctl is-active guardia); echo zioinfo: $(systemctl is-active zioinfo)')
print('\n📌 자동 배포 흐름:')
print(' 1. 로컬 수정 → repos/에 commit')
print(' 2. sync_workspace_to_repos.py 실행 → Gitea push')
print(' 3. Gitea webhook → port 9999 → 즉시 배포 ✅')
print(' 4. deploy_server.py → Jenkins 트리거 ✅')
print(' 5. Jenkins: Build + Test → ITSM 메신저 알림 ✅')
c.close()

View File

@ -0,0 +1,74 @@
"""수정된 backend 파일 서버 패치 + E2E 재검증"""
import paramiko, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(o.read().decode('utf-8','replace').strip()[:600])
# 파일 전송
for fn in ['imap_client.py', 'smtp_client.py', 'main.py']:
sftp.put(f'C:/GUARDiA/workspace/zioinfo-mail/backend/{fn}', f'/opt/mail/backend/{fn}')
print(f'{fn}')
run('서비스 재기동', 'systemctl restart zioinfo-mail && sleep 3 && systemctl is-active zioinfo-mail')
time.sleep(2)
# E2E: 로그인 → 폴더 → 메일 발송 → Sent 확인
test = """
import urllib.request, json, time
BASE = "http://localhost:8026"
def req(url, data=None, headers={}):
r = urllib.request.Request(BASE + url, data=data, headers=headers)
return json.loads(urllib.request.urlopen(r, timeout=15).read())
# 1. ythong 로그인
t = req("/api/auth/login",
data=json.dumps({"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
headers={"Content-Type":"application/json"})
token_y = t["access_token"]
Hy = {"Authorization": "Bearer " + token_y}
print("1. ythong 로그인 OK")
# 2. 폴더 목록
folders = req("/api/mail/folders", headers=Hy)
for f in folders:
print(f" {f['display']}({f['name']}) 총:{f['total']} 미읽음:{f['unread']}")
# 3. 메일 발송 (ythong → info)
r = req("/api/mail/send",
data=json.dumps({
"to": "info@zioinfo.co.kr",
"subject": "테스트 발송",
"body": "웹메일 테스트 메시지입니다."
}).encode(),
headers={**Hy, "Content-Type":"application/json"})
print("3. 발송:", r)
time.sleep(2)
# 4. ythong Sent 폴더 확인
sent = req("/api/mail/messages?folder=Sent", headers=Hy)
print(f"4. Sent 폴더 총:{sent.get('total')}")
for m in sent.get("messages", [])[:2]:
print(f" [{m['uid']}] {m['subject']}{m['sender_addr']}")
# 5. info 로그인 → INBOX 확인
t2 = req("/api/auth/login",
data=json.dumps({"username":"info@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
headers={"Content-Type":"application/json"})
Hi = {"Authorization": "Bearer " + t2["access_token"]}
inbox = req("/api/mail/messages?folder=INBOX", headers=Hi)
print(f"5. info INBOX 총:{inbox.get('total')}")
for m in inbox.get("messages", [])[:3]:
print(f" [{m['uid']}] {m['subject']} - from:{m['sender']}")
"""
with sftp.open('/tmp/e2e.py', 'w') as f: f.write(test)
run('E2E 테스트', 'python3 /tmp/e2e.py 2>&1; rm /tmp/e2e.py', timeout=30)
sftp.close(); c.close()

View File

@ -0,0 +1,90 @@
"""최종 패치: mail_parser + imap_client + frontend 재빌드"""
import paramiko, sys, subprocess, os
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(o.read().decode('utf-8','replace').strip()[:400])
# 1. Backend 파일 전송
for fn in ['mail_parser.py', 'imap_client.py', 'smtp_client.py', 'main.py']:
sftp.put(f'C:/GUARDiA/workspace/zioinfo-mail/backend/{fn}', f'/opt/mail/backend/{fn}')
print(f'{fn}')
run('서비스 재기동', 'systemctl restart zioinfo-mail && sleep 3 && systemctl is-active zioinfo-mail')
# 2. Frontend 재빌드
print('\n[Frontend 재빌드]')
FE = 'C:/GUARDiA/workspace/zioinfo-mail/frontend'
NPM = 'npm.cmd' if sys.platform == 'win32' else 'npm'
r = subprocess.run([NPM, 'run', 'build'], cwd=FE, capture_output=True, text=True,
encoding='utf-8', errors='replace', timeout=180)
if r.returncode == 0:
print(' ✅ 빌드 성공')
else:
print(f' ❌ 빌드 실패: {r.stderr[:200]}')
import sys as _s; _s.exit(1)
# dist 전송
dist = 'C:/GUARDiA/workspace/zioinfo-mail/dist'
import tarfile, io
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w:gz') as tar:
for root, dirs, files in os.walk(dist):
for fn2 in files:
fp = os.path.join(root, fn2)
tar.add(fp, arcname=os.path.relpath(fp, dist))
buf.seek(0)
sftp.putfo(buf, '/tmp/mail_dist.tar.gz')
run('dist 배포', 'cd /var/www/mail && tar -xzf /tmp/mail_dist.tar.gz && rm /tmp/mail_dist.tar.gz && echo "files: $(ls | wc -l)"')
import time; time.sleep(2)
# 3. E2E 재검증
test = """
import urllib.request, json
BASE = "http://localhost:8026"
def req(url, data=None, headers={}):
r = urllib.request.Request(BASE + url, data=data, headers=headers)
try:
return json.loads(urllib.request.urlopen(r, timeout=15).read())
except Exception as e:
print("ERROR", url, ":", e)
return None
# info 로그인 → INBOX
t = req("/api/auth/login",
data=json.dumps({"username":"info@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
headers={"Content-Type":"application/json"})
if t:
Hi = {"Authorization": "Bearer " + t["access_token"]}
inbox = req("/api/mail/messages?folder=INBOX", headers=Hi)
if inbox:
print(f"info INBOX total:{inbox['total']}")
for m in inbox.get("messages", [])[:3]:
print(f" [{m['uid']}] {m['subject']!r} from:{m['sender']!r}")
else:
print("inbox 오류")
# ythong → Sent 확인
t2 = req("/api/auth/login",
data=json.dumps({"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}).encode(),
headers={"Content-Type":"application/json"})
if t2:
Hy = {"Authorization": "Bearer " + t2["access_token"]}
sent = req("/api/mail/messages?folder=Sent", headers=Hy)
if sent:
print(f"ythong Sent total:{sent['total']}")
for m in sent.get("messages", [])[:2]:
print(f" [{m['uid']}] {m['subject']!r} to:{m.get('to','')!r}")
"""
with sftp.open('/tmp/e2e2.py', 'w') as f: f.write(test)
run('E2E 재검증', 'python3 /tmp/e2e2.py 2>&1; rm /tmp/e2e2.py', timeout=30)
sftp.close(); c.close()
print('\n=== 완료 ===')

View File

@ -0,0 +1,50 @@
"""최종 검증: 로그인 → 폴더 → 메일 목록"""
import paramiko, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(o.read().decode('utf-8','replace').strip()[:600])
# 수정 파일 재전송
sftp.put('C:/GUARDiA/workspace/zioinfo-mail/backend/imap_client.py', '/opt/mail/backend/imap_client.py')
print('✅ imap_client.py 재전송')
run('서비스 재기동', 'systemctl restart zioinfo-mail && sleep 3 && systemctl is-active zioinfo-mail')
time.sleep(2)
# E2E 테스트
run('E2E: 로그인 → 폴더 → 메일목록', """
TOKEN=$(curl -sf -X POST http://localhost:8026/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"ythong@zioinfo.co.kr","password":"1q2w3e!Q"}' 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" 2>/dev/null)
echo "=폴더목록="
curl -sf http://localhost:8026/api/mail/folders \
-H "Authorization: Bearer $TOKEN" 2>/dev/null | \
python3 -c "
import sys,json
d=json.load(sys.stdin)
for f in d:
print(f' {f[\"display\"]} ({f[\"name\"]}) - 읽음:{f[\"unread\"]} / 전체:{f[\"total\"]}')
" 2>/dev/null
echo "=메일목록(INBOX)="
curl -sf "http://localhost:8026/api/mail/messages?folder=INBOX&page=1" \
-H "Authorization: Bearer $TOKEN" 2>/dev/null | \
python3 -c "
import sys,json
d=json.load(sys.stdin)
print(f'{d.get(\"total\",0)}개 메일')
for m in d.get('messages',[])[:3]:
print(f' [{m[\"uid\"]}] {m[\"subject\"][:40]} - {m[\"sender\"]}')
" 2>/dev/null
""", timeout=20)
run('nginx URL 응답', 'curl -sf -o /dev/null -w "HTTP %{http_code}" https://localhost:8025/ -k 2>/dev/null')
run('최종 서비스 상태', 'systemctl is-active zioinfo-mail && systemctl is-active nginx')
sftp.close(); c.close()

View File

@ -0,0 +1,179 @@
"""system-sync-orchestrator: 검증 결과 기반 수정 실행"""
import paramiko, sys, time, subprocess, shutil, os, json, base64
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
def run(label, cmd, timeout=120):
print(f'\n [{label}]')
_, o, e = c.exec_command(cmd, timeout=timeout)
out = o.read().decode('utf-8','replace').strip()
if out: print(' ' + out[:400])
return out
results = {}
# ── 1. guardia-itsm: APP_SRC_DRIFT 수정 ────────────────────
print('\n━━ FIX 1: guardia-itsm APP_SRC_DRIFT ━━')
# rpa_rules.json, embed_codebase.py는 app에서만 생성되는 런타임 파일 → src에서도 생성 경로 동기화
run('rpa 폴더 확인', 'ls /opt/guardia/src/routers/rpa.py 2>/dev/null && echo exists || echo missing')
run('rsync src→app (런타임 파일 제외)',
'rsync -a --delete '
'--exclude=__pycache__ --exclude=.git --exclude="*.db" '
'--exclude="uploads" --exclude=".env" --exclude=".pytest_cache" '
'--exclude="rpa_rules.json" --exclude="rpa/" '
'/opt/guardia/src/ /opt/guardia/app/ 2>&1 | tail -3')
run('guardia 재기동', 'systemctl restart guardia && sleep 4 && systemctl is-active guardia')
# 재검증
diff = run('app vs src 재검증',
'diff -rq /opt/guardia/src /opt/guardia/app '
'--exclude="*.pyc" --exclude="__pycache__" --exclude=".git" '
'--exclude="*.db" --exclude="uploads" --exclude=".env" '
'--exclude=".pytest_cache" --exclude="rpa_rules.json" --exclude="rpa" '
'2>/dev/null | head -5 || echo "동일"')
results['guardia-itsm'] = '✅ 동기화 완료' if '동일' in diff else f'⚠️ {diff[:80]}'
print(f' 결과: {results["guardia-itsm"]}')
# ── 2. guardia-manager: STALE_WWW 수정 ─────────────────────
print('\n━━ FIX 2: guardia-manager STALE_WWW 재빌드 ━━')
# workspace/guardia-manager의 최신 frontend를 서버에 업로드
# repos/guardia-manager에서 bundle → server → git pull → npm build
run('Manager src 확인',
'ls /opt/manager/backend/main.py 2>/dev/null && echo "backend OK" || echo "backend MISSING"')
run('Manager frontend 확인',
'ls /opt/manager/frontend/package.json 2>/dev/null && echo "frontend OK" || echo "frontend MISSING"')
# repos/guardia-manager에서 최신 코드 가져와 서버에 업로드
# 이미 Gitea에 올라가 있으므로 서버에서 직접 클론
GITEA_URL = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-manager.git'
run('Manager 소스 clone/pull',
f'''if [ -d /opt/manager/frontend/.git ]; then
git -C /opt/manager/frontend fetch origin main && git -C /opt/manager/frontend reset --hard origin/main
elif [ -d /tmp/mgr_src ]; then
rm -rf /tmp/mgr_src
fi
[ -d /opt/manager/frontend/src ] && echo "frontend src OK" || echo "frontend src MISSING"
''')
# workspace의 guardia-manager frontend를 bundle로 서버에 전송
print('\n workspace/guardia-manager bundle 서버 전송 중...')
bundle_path = 'C:/GUARDiA/repos/guardia-manager.bundle'
result = subprocess.run(
['git', '-C', 'C:/GUARDiA/repos/guardia-manager', 'bundle', 'create', bundle_path, '--all'],
capture_output=True)
if result.returncode == 0:
size = os.path.getsize(bundle_path) // 1024
print(f' bundle {size}KB → 서버 전송...')
sftp.put(bundle_path, '/tmp/mgr.bundle')
os.remove(bundle_path)
run('서버에서 Manager 클론',
'rm -rf /tmp/mgr_work && '
'git clone /tmp/mgr.bundle /tmp/mgr_work 2>/dev/null && '
'echo "clone OK" && ls /tmp/mgr_work/ | head -5')
# frontend 빌드
run('Manager frontend 빌드',
'cd /tmp/mgr_work/frontend && '
'npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && '
'npm run build 2>&1 | tail -5', timeout=300)
# /var/www/manager 복사
run('www/manager 복사',
'mkdir -p /var/www/manager && '
'cp -r /tmp/mgr_work/frontend/dist/. /var/www/manager/ && '
f'stat /var/www/manager/index.html | grep Modify')
# backend도 업데이트
run('Manager backend 업데이트',
'cp -r /tmp/mgr_work/backend/. /opt/manager/backend/ 2>/dev/null || true && '
'rm -rf /tmp/mgr_work /tmp/mgr.bundle')
run('Manager 서비스 재기동',
'systemctl restart guardia-manager && sleep 4 && systemctl is-active guardia-manager')
www_date = run('www/manager 날짜 확인',
'stat /var/www/manager/index.html 2>/dev/null | grep Modify')
results['guardia-manager'] = f'✅ www 갱신: {www_date[:30]}' if 'Jun 1' in www_date or 'Jun 1' in www_date else f'⚠️ {www_date}'
else:
results['guardia-manager'] = '❌ bundle 생성 실패'
print(f' ❌ bundle 오류: {result.stderr.decode()[:100]}')
print(f' 결과: {results["guardia-manager"]}')
# ── 3. zioinfo-web: stash + uncommitted 정리 ───────────────
print('\n━━ FIX 3: zioinfo-web stash 정리 + 스크린샷 반영 ━━')
# uncommitted 파일 목록
uncommit = run('uncommitted 파일 목록',
"git -C /opt/zioinfo/src status --short 2>/dev/null | "
"grep -v 'static/assets/' | grep -v '.pyc'")
# logo_bottom.png, 스크린샷 → workspace로 다운로드 후 커밋
new_files = [l.strip().split()[-1] for l in uncommit.splitlines()
if '??' in l or 'M ' in l]
print(f' 새 파일: {new_files}')
downloaded = []
for f in new_files:
if not f: continue
remote = f'/opt/zioinfo/src/{f}'
local_ws = f'C:/GUARDiA/workspace/zioinfo-web/{f}'
local_repo = f'C:/GUARDiA/repos/zioinfo-web/{f}'
try:
os.makedirs(os.path.dirname(local_ws), exist_ok=True)
os.makedirs(os.path.dirname(local_repo), exist_ok=True)
sftp.get(remote, local_ws)
shutil.copy2(local_ws, local_repo)
downloaded.append(f)
print(f' 다운로드: {f}')
except Exception as e:
print(f' SKIP {f}: {e}')
# stash 처리: 빌드 산출물만 있으면 drop
stash_stat = run('stash 내용', 'git -C /opt/zioinfo/src stash show --stat 2>/dev/null | head -10')
only_assets = all('static/assets' in l or 'Jenkinsfile' in l
for l in stash_stat.splitlines() if '|' in l)
if only_assets:
run('stash drop (빌드 산출물만)', 'git -C /opt/zioinfo/src stash drop 2>/dev/null && echo "dropped"')
results['zioinfo-web-stash'] = '✅ stash drop (빌드산출물)'
else:
# 중요 파일이 있으면 보존
results['zioinfo-web-stash'] = '⚠️ stash 보존 (중요 파일 포함) - 수동 확인 필요'
print(f' stash 보존: {stash_stat[:200]}')
# 서버 uncommitted 파일 add & commit
if new_files:
run('uncommitted 파일 commit',
'cd /opt/zioinfo/src && '
'git add backend/src/main/resources/static/logo_bottom.png '
'backend/src/main/resources/static/screenshots/ 2>/dev/null; '
'git config user.email "ci@zioinfo.co.kr"; '
'git config user.name "CI Bot"; '
'git diff --cached --stat 2>/dev/null; '
'git commit -m "chore: add screenshots and logo assets" 2>/dev/null || echo "nothing to commit"')
results['zioinfo-web'] = f'{len(downloaded)}개 파일 반영'
print(f' 결과: {results["zioinfo-web"]}')
# ── 최종 요약 ────────────────────────────────────────────────
print('\n' + '='*55)
print('수정 결과 요약')
print('='*55)
for sys_name, result in results.items():
print(f' {result} [{sys_name}]')
# fix_report 저장
os.makedirs('C:/GUARDiA/.claude/agents/_workspace', exist_ok=True)
with open('C:/GUARDiA/.claude/agents/_workspace/fix_report.json', 'w', encoding='utf-8') as f:
json.dump({"timestamp": __import__('datetime').datetime.now().isoformat(),
"results": results}, f, ensure_ascii=False, indent=2)
sftp.close()
c.close()
print('\n=== 수정 완료 ===')

View File

@ -0,0 +1,60 @@
"""guardia-manager /var/www/manager 수정 + guardia-itsm 재검증"""
import paramiko, sys, time, subprocess, os
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
def run(label, cmd, timeout=120):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
out = o.read().decode('utf-8','replace').strip()
if out: print(out[:500])
return out
# 1. 빌드 산출물 경로 확인
run('빌드 산출물 위치 확인',
'find /tmp -name "index.html" -path "*/dist/*" 2>/dev/null | head -5 || echo "없음"')
# 이미 삭제됐으면 재빌드
run('Manager 재빌드',
'rm -rf /tmp/mgr2 && '
'git clone /tmp/mgr.bundle /tmp/mgr2 2>/dev/null || '
'git clone http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/guardia-manager.git /tmp/mgr2 2>/dev/null && '
'echo "clone OK"', timeout=60)
run('vite 빌드 (outDir 확인)',
'cat /tmp/mgr2/frontend/vite.config.ts 2>/dev/null || cat /tmp/mgr2/frontend/vite.config.js 2>/dev/null | grep -A3 "build\|outDir"')
run('npm 빌드',
'cd /tmp/mgr2/frontend && '
'npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && '
'npm run build 2>&1 | tail -5', timeout=300)
run('dist 위치 탐색',
'find /tmp/mgr2 -name "index.html" -not -path "*/node_modules/*" 2>/dev/null')
# 실제 dist 폴더를 /var/www/manager에 복사
run('www/manager 복사',
'''DIST=$(find /tmp/mgr2 -name "index.html" -not -path "*/node_modules/*" 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
if [ -n "$DIST" ]; then
echo "dist 폴더: $DIST"
mkdir -p /var/www/manager
cp -r $DIST/. /var/www/manager/
echo "복사 완료: $(ls /var/www/manager/ | wc -l) 파일"
stat /var/www/manager/index.html | grep Modify
else
echo "dist 폴더 없음"
fi''')
run('Manager 서비스 재기동', 'systemctl restart guardia-manager && sleep 3 && systemctl is-active guardia-manager')
run('정리', 'rm -rf /tmp/mgr2 /tmp/mgr.bundle 2>/dev/null; echo "정리 완료"')
# 2. guardia-itsm diff 재확인
run('ITSM app vs src 최종 확인',
'diff -rq /opt/guardia/src /opt/guardia/app '
'--exclude="*.pyc" --exclude="__pycache__" --exclude=".git" '
'--exclude="*.db" --exclude="uploads" --exclude=".env" '
'--exclude=".pytest_cache" --exclude="rpa_rules.json" --exclude="rpa" '
'--exclude="scripts" 2>/dev/null || echo "차이 없음"')
c.close()

View File

@ -0,0 +1,54 @@
"""zioinfo-web 서버 커밋(스크린샷 등) → Gitea push + 나머지 uncommitted 처리"""
import paramiko, sys, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
def run(label, cmd, timeout=60):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
out = o.read().decode('utf-8','replace').strip()
if out: print(out[:500])
return out
GITEA = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-web.git'
# 1. 서버 남은 uncommitted 처리
run('uncommitted 상태',
'git -C /opt/zioinfo/src status --short 2>/dev/null | '
"grep -v 'static/assets/' | grep -v '.pyc'")
run('남은 파일 추가 commit',
'''cd /opt/zioinfo/src
git config user.email "ci@zioinfo.co.kr" 2>/dev/null
git config user.name "CI Bot" 2>/dev/null
# 모든 static 파일 추가 (빌드 산출물 제외)
git add backend/src/main/resources/static/zioinfo-building.png 2>/dev/null
git add backend/src/main/resources/static/zioinfo-logo*.png 2>/dev/null
git add backend/src/main/resources/static/*.png 2>/dev/null
git add backend/src/main/resources/static/index.html 2>/dev/null
git diff --cached --stat 2>/dev/null | head -10
git commit -m "chore: add static assets (logos, building photo)" 2>/dev/null || echo "nothing to commit"
''')
# 2. 서버 커밋들을 Gitea에 push
run('서버 현재 커밋 로그',
'git -C /opt/zioinfo/src log --oneline -3 2>/dev/null')
run('Gitea push (서버 → Gitea)',
f'git -C /opt/zioinfo/src remote set-url origin "{GITEA}" && '
'git -C /opt/zioinfo/src push origin main --force 2>&1 | tail -5')
# 3. 최신 Gitea 커밋 확인
run('Gitea 최신 커밋 확인',
'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-web/commits?limit=2" '
'-H "Authorization: Basic emhvOlppb0BBZG1pbjIwMjYh" 2>/dev/null | '
'python3 -c "import sys,json; [print(c[\'sha\'][:8], c[\'commit\'][\'message\'][:50]) for c in json.load(sys.stdin)]" 2>/dev/null')
# 4. /var/www/zioinfo 최신 상태 확인
run('www/zioinfo 날짜', 'stat /var/www/zioinfo/index.html 2>/dev/null | grep Modify')
sftp.close()
c.close()
print('\n=== 완료 ===')

View File

@ -0,0 +1,214 @@
"""zioinfo-mail CI/CD 전체 구축:
1. deploy_server.py에 zioinfo-mail 배포 함수 추가
2. Jenkins job 생성
3. Gitea webhook 등록 (port 9999 + Jenkins)
4. Jenkinsfile Gitea push
"""
import paramiko, sys, json, base64, subprocess, os, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
J = 'http://127.0.0.1:9080'
A = 'admin:Admin@2026!'
TOK = 'gitea-build-2026'
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
out = o.read().decode('utf-8','replace').strip()
if out: print(out[:500])
return out
# crumb
_, o, _ = c.exec_command(f'curl -sf -u "{A}" {J}/crumbIssuer/api/json 2>/dev/null', timeout=10)
try:
cd = json.loads(o.read().decode('utf-8','replace').strip())
CH = f'{cd["crumbRequestField"]}: {cd["crumb"]}'
except:
CH = 'Jenkins-Crumb: x'
# ── 1. deploy_server.py 업데이트 ─────────────────────────────
print('\n━━ 1. deploy_server.py zioinfo-mail 추가 ━━')
update_script = r"""
import re
with open('/opt/zioinfo/deploy_server.py', 'r') as f:
content = f.read()
if 'zioinfo-mail' in content:
print('이미 있음')
else:
# guardia-docs 블록 이후에 zioinfo-mail 추가
# 또는 elif repo == "guardia-docs": 다음에 추가
MAIL_BLOCK = '''
elif repo == "zioinfo-mail":
SRC = "/opt/mail"
ok = run_steps(repo, [
("git pull", ["bash", "-c",
f"[ -d {SRC}/src/.git ] && git -C {SRC}/src fetch origin main && git -C {SRC}/src reset --hard origin/main"
f" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git' {SRC}/src"]),
("npm build", ["bash", "-c",
f"cd {SRC}/src/frontend && npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && npm run build"]),
("copy dist", ["bash", "-c", f"cp -r {SRC}/src/dist/. /var/www/mail/"]),
("pip install", ["bash", "-c",
f"{SRC}/venv/bin/pip install -r {SRC}/src/backend/requirements.txt -q"]),
("rsync", ["bash", "-c",
f"rsync -a --exclude=__pycache__ --exclude=.git --exclude='*.pyc' --exclude='.env' {SRC}/src/backend/ {SRC}/backend/"]),
("restart", ["systemctl", "restart", "zioinfo-mail"]),
("health check", ["bash", "-c", "sleep 4 && curl -sf http://localhost:8026/health"]),
])
if ok:
notify_itsm(True, "\\u2705 zioinfo-mail \\ubc30\\ud3ec \\uc644\\ub8cc")
else:
notify_itsm(False, "\\u274c zioinfo-mail \\ube4c\\ub4dc \\uc2e4\\ud328")
'''
# guardia-docs 블록 직전에 삽입
content = content.replace(
' elif repo == "guardia-docs":',
MAIL_BLOCK + ' elif repo == "guardia-docs":'
)
with open('/opt/zioinfo/deploy_server.py', 'w') as f:
f.write(content)
print('추가 완료')
"""
with sftp.open('/tmp/upd.py', 'w') as f: f.write(update_script)
run('deploy_server 업데이트', 'python3 /tmp/upd.py 2>&1; rm /tmp/upd.py')
run('zioinfo-mail 추가 확인', "grep -n 'zioinfo-mail' /opt/zioinfo/deploy_server.py")
run('webhook 서버 재시작', 'systemctl restart zioinfo-deploy && sleep 2 && systemctl is-active zioinfo-deploy')
# ── 2. Jenkins job 생성 ──────────────────────────────────────
print('\n━━ 2. Jenkins job 생성 (zioinfo-mail) ━━')
job_config = f"""<?xml version='1.1' encoding='UTF-8'?>
<flow-definition plugin="workflow-job">
<description>GUARDiA zioinfo-mail Webmail CI/CD</description>
<keepDependencies>false</keepDependencies>
<properties>
<org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty/>
<authToken>{TOK}</authToken>
</properties>
<definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="workflow-cps">
<scm class="hudson.plugins.git.GitSCM" plugin="git">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>http://127.0.0.1:9003/zio/zioinfo-mail.git</url>
<credentialsId>gitea-zio</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec><name>*/main</name></hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="empty-list"/>
<extensions/>
</scm>
<scriptPath>Jenkinsfile</scriptPath>
<lightweight>true</lightweight>
</definition>
<triggers>
<com.cloudbees.jenkins.gitea.GiteaWebHookTrigger plugin="gitea">
<properties/>
</com.cloudbees.jenkins.gitea.GiteaWebHookTrigger>
</triggers>
</flow-definition>"""
with sftp.open('/tmp/job_mail.xml', 'w') as f: f.write(job_config)
run('Jenkins job 생성',
f'curl -sf -X POST "{J}/createItem?name=zioinfo-mail" '
f'-u "{A}" -H "{CH}" '
f'-H "Content-Type: text/xml" '
f'--data-binary @/tmp/job_mail.xml 2>/dev/null; echo $?')
# authToken 설정
run('authToken config.xml 설정',
f'curl -sf -u "{A}" {J}/job/zioinfo-mail/config.xml 2>/dev/null | '
f'grep -c "authToken" || echo "없음"')
# ── 3. Gitea webhook 등록 ────────────────────────────────────
print('\n━━ 3. Gitea webhook 등록 ━━')
# 3-1. port 9999 webhook
payload9999 = json.dumps({
"type": "gitea",
"config": {"url": "http://127.0.0.1:9999", "content_type": "json",
"secret": "zioinfo-deploy-2026"},
"events": ["push"], "active": True
})
with sftp.open('/tmp/h1.json', 'w') as f: f.write(payload9999)
run('webhook 9999 등록',
f'curl -sf -X POST "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/hooks" '
f'-H "Authorization: Basic {G}" -H "Content-Type: application/json" '
f'--data @/tmp/h1.json 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); print(\'id:\',d.get(\'id\'),d.get(\'config\',{}).get(\'url\',\'\'))" 2>/dev/null')
# 3-2. Jenkins webhook
payload_jenkins = json.dumps({
"type": "gitea",
"config": {"url": f"http://127.0.0.1:9080/job/zioinfo-mail/build?token={TOK}",
"content_type": "json"},
"events": ["push"], "active": True
})
with sftp.open('/tmp/h2.json', 'w') as f: f.write(payload_jenkins)
run('webhook Jenkins 등록',
f'curl -sf -X POST "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/hooks" '
f'-H "Authorization: Basic {G}" -H "Content-Type: application/json" '
f'--data @/tmp/h2.json 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); print(\'id:\',d.get(\'id\'),d.get(\'config\',{}).get(\'url\',\'\'))" 2>/dev/null')
# ── 4. Jenkinsfile Gitea push ─────────────────────────────────
print('\n━━ 4. Jenkinsfile → Gitea push ━━')
jf_content = open('C:/GUARDiA/workspace/zioinfo-mail/Jenkinsfile', encoding='utf-8').read()
jf_b64 = base64.b64encode(jf_content.encode('utf-8')).decode()
# 기존 파일 SHA 확인
_, o, _ = c.exec_command(
f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/contents/Jenkinsfile" '
f'-H "Authorization: Basic {G}" 2>/dev/null | '
'python3 -c "import sys,json; print(json.load(sys.stdin).get(\'sha\',\'\'))" 2>/dev/null', timeout=10)
sha = o.read().decode('utf-8','replace').strip()
if sha:
payload_jf = json.dumps({"message":"ci: add Jenkinsfile","content":jf_b64,"sha":sha,"branch":"main"})
method = "PUT"
else:
payload_jf = json.dumps({"message":"ci: add Jenkinsfile","content":jf_b64,"branch":"main"})
method = "POST"
with sftp.open('/tmp/jf.json', 'w') as f: f.write(payload_jf)
run('Jenkinsfile Gitea 업로드',
f'curl -sf -X {method} "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/contents/Jenkinsfile" '
f'-H "Authorization: Basic {G}" -H "Content-Type: application/json" '
f'--data @/tmp/jf.json 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); print(\'OK:\', d.get(\'content\',{}).get(\'name\',\'?\'))" 2>/dev/null')
# ── 5. 검증 ─────────────────────────────────────────────────
print('\n━━ 5. 최종 검증 ━━')
time.sleep(3)
run('Jenkins jobs 전체',
f'curl -sf -u "{A}" {J}/api/json 2>/dev/null | '
'python3 -c "import sys,json; [print(j[\'name\'].ljust(22),j[\'color\']) for j in json.load(sys.stdin)[\'jobs\']]" 2>/dev/null')
run('zioinfo-mail hooks 확인',
f'curl -sf "http://127.0.0.1:9003/api/v1/repos/zio/zioinfo-mail/hooks" '
f'-H "Authorization: Basic {G}" 2>/dev/null | '
'python3 -c "import sys,json; [print(\' hook\',h[\'id\'],h[\'config\'].get(\'url\',\'\')[:60],\'active:\',h[\'active\']) for h in json.load(sys.stdin)]" 2>/dev/null')
# 첫 빌드 트리거
run('첫 빌드 트리거',
f'curl -sf -X POST -u "{A}" -H "{CH}" {J}/job/zioinfo-mail/build 2>/dev/null && echo "트리거됨"')
time.sleep(10)
run('빌드 상태',
f'curl -sf -u "{A}" {J}/job/zioinfo-mail/api/json 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); '
'lb=d.get(\'lastBuild\',{}); print(\'build #\'+str(lb.get(\'number\',\'?\')))" 2>/dev/null')
sftp.close(); c.close()
print('\n=== zioinfo-mail CI/CD 구축 완료 ===')

View File

@ -0,0 +1,175 @@
"""zioinfo-mail repos 생성 + workspace 소스 동기화 + Gitea push + deploy_server 수정"""
import paramiko, subprocess, sys, os, shutil, base64, json, time
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
c.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15)
sftp = c.open_sftp()
G = base64.b64encode(b'zio:Zio@Admin2026!').decode()
def run(label, cmd, timeout=30):
print(f'\n[{label}]')
_, o, _ = c.exec_command(cmd, timeout=timeout)
print(o.read().decode('utf-8','replace').strip()[:500])
REPO_PATH = 'C:/GUARDiA/repos/zioinfo-mail'
WS_PATH = 'C:/GUARDiA/workspace/zioinfo-mail'
# ── 1. repos/zioinfo-mail git 초기화 ────────────────────────
print('\n━━ 1. repos/zioinfo-mail git 초기화 ━━')
os.makedirs(REPO_PATH, exist_ok=True)
# workspace → repos 동기화 (node_modules, dist, __pycache__ 제외)
EXCLUDE = {'node_modules', 'dist', '__pycache__', '.git', '*.pyc', '.pytest_cache'}
def sync_dir(src, dst):
for root, dirs, files in os.walk(src):
dirs[:] = [d for d in dirs if d not in EXCLUDE and not d.endswith('.egg-info')]
rel = os.path.relpath(root, src)
dst_dir = os.path.join(dst, rel) if rel != '.' else dst
os.makedirs(dst_dir, exist_ok=True)
for fn in files:
if fn.endswith(('.pyc',)):
continue
s = os.path.join(root, fn)
d = os.path.join(dst_dir, fn)
shutil.copy2(s, d)
sync_dir(WS_PATH, REPO_PATH)
print(f' 동기화 완료')
# git init + commit
r = subprocess.run(['git', '-C', REPO_PATH, 'rev-parse', '--git-dir'], capture_output=True)
if r.returncode != 0:
subprocess.run(['git', '-C', REPO_PATH, 'init'], capture_output=True)
subprocess.run(['git', '-C', REPO_PATH, 'config', 'user.email', 'ci@zioinfo.co.kr'], capture_output=True)
subprocess.run(['git', '-C', REPO_PATH, 'config', 'user.name', 'CI Bot'], capture_output=True)
# .gitignore 생성
with open(f'{REPO_PATH}/.gitignore', 'w') as f:
f.write('node_modules/\ndist/\n__pycache__/\n*.pyc\n.env\n*.db\n.pytest_cache/\n')
subprocess.run(['git', '-C', REPO_PATH, 'add', '-A'], capture_output=True)
r2 = subprocess.run(['git', '-C', REPO_PATH, 'status', '--short'],
capture_output=True, text=True)
if r2.stdout.strip():
subprocess.run(['git', '-C', REPO_PATH, 'commit', '-m', 'feat: initial zioinfo-mail webmail system'],
capture_output=True)
print(' 커밋 완료')
else:
print(' 변경 없음')
r3 = subprocess.run(['git', '-C', REPO_PATH, 'log', '--oneline', '-2'],
capture_output=True, text=True)
print(f' git log: {r3.stdout.strip()}')
# ── 2. bundle → 서버 → Gitea push ───────────────────────────
print('\n━━ 2. Gitea push ━━')
bundle = f'{REPO_PATH}.bundle'
subprocess.run(['git', '-C', REPO_PATH, 'bundle', 'create', bundle, '--all'],
capture_output=True, timeout=120)
size = os.path.getsize(bundle) // 1024
print(f' bundle: {size}KB → 서버 전송...')
sftp.put(bundle, '/tmp/mail.bundle')
os.remove(bundle)
GITEA_URL = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git'
run('Gitea push',
f'rm -rf /tmp/mail_push && '
f'git clone /tmp/mail.bundle /tmp/mail_push 2>/dev/null && '
f'cd /tmp/mail_push && '
f'git remote set-url origin "{GITEA_URL}" && '
f'git push origin main --force 2>&1 | tail -3 && '
f'rm -rf /tmp/mail_push /tmp/mail.bundle && echo "pushed"', timeout=120)
# ── 3. deploy_server.py 수정 (직접 편집) ─────────────────────
print('\n━━ 3. deploy_server.py zioinfo-mail 추가 ━━')
# 현재 deploy_server.py 내용 확인
_, o, _ = c.exec_command('cat /opt/zioinfo/deploy_server.py', timeout=15)
content = o.read().decode('utf-8','replace')
if 'zioinfo-mail' in content:
print(' 이미 있음')
else:
# 마지막 elif 블록 찾기 (guardia-docs 또는 guardia-messenger)
mail_block = '''
elif repo == "zioinfo-mail":
SRC = "/opt/mail"
ok = run_steps(repo, [
("git pull", ["bash", "-c",
f"[ -d {SRC}/src/.git ] && git -C {SRC}/src fetch origin main && git -C {SRC}/src reset --hard origin/main"
" || git clone 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/zioinfo-mail.git' " + f"{SRC}/src"]),
("npm build", ["bash", "-c",
f"cd {SRC}/src/frontend && npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps && npm run build"]),
("copy dist", ["bash", "-c", f"mkdir -p /var/www/mail && cp -r {SRC}/src/dist/. /var/www/mail/"]),
("pip install", ["bash", "-c",
f"{SRC}/venv/bin/pip install -r {SRC}/src/backend/requirements.txt -q"]),
("rsync", ["bash", "-c",
f"rsync -a --exclude=__pycache__ --exclude=.git --exclude='*.pyc' --exclude='.env' {SRC}/src/backend/ {SRC}/backend/"]),
("restart", ["systemctl", "restart", "zioinfo-mail"]),
("health check", ["bash", "-c", "sleep 4 && curl -sf http://localhost:8026/health"]),
])
if ok:
notify_itsm(True, "\\u2705 zioinfo-mail \\ubc30\\ud3ec \\uc644\\ub8cc")
else:
notify_itsm(False, "\\u274c zioinfo-mail \\ube4c\\ub4dc \\uc2e4\\ud328")
'''
# def notify_itsm 함수 앞에 삽입하거나, guardia-docs 블록 찾기
if 'guardia-docs' in content:
# guardia-docs 블록 찾아서 그 다음에 삽입
import re
# 마지막 elif 블록 끝(다음 elif 또는 else 직전) 찾기
lines = content.split('\n')
insert_idx = None
for i, line in enumerate(lines):
if 'elif repo == "guardia-docs"' in line:
# 이 블록의 끝 찾기
j = i + 1
while j < len(lines) and (lines[j].startswith(' ') or not lines[j].strip()):
j += 1
insert_idx = j
break
if insert_idx:
lines.insert(insert_idx, mail_block)
new_content = '\n'.join(lines)
else:
new_content = content + mail_block
else:
new_content = content + mail_block
with sftp.open('/opt/zioinfo/deploy_server.py', 'w') as f:
f.write(new_content)
print(' 추가 완료')
run('zioinfo-mail 확인',
"grep -n 'zioinfo-mail' /opt/zioinfo/deploy_server.py | head -5")
run('webhook 재시작',
'systemctl restart zioinfo-deploy && sleep 2 && systemctl is-active zioinfo-deploy')
# ── 4. Jenkins 재빌드 ─────────────────────────────────────────
print('\n━━ 4. Jenkins 재빌드 ━━')
J_A = 'admin:Admin@2026!'
_, o, _ = c.exec_command(f'curl -sf -u "{J_A}" http://127.0.0.1:9080/crumbIssuer/api/json 2>/dev/null', timeout=10)
try:
cd = json.loads(o.read().decode('utf-8','replace').strip())
CH = f'{cd["crumbRequestField"]}: {cd["crumb"]}'
except:
CH = 'Jenkins-Crumb: x'
run('재빌드 트리거',
f'curl -sf -X POST -u "{J_A}" -H "{CH}" http://127.0.0.1:9080/job/zioinfo-mail/build 2>/dev/null && echo "트리거됨"')
print('\n빌드 대기 (60초)...')
time.sleep(60)
run('빌드 #2 결과',
f'curl -sf -u "{J_A}" http://127.0.0.1:9080/job/zioinfo-mail/lastBuild/api/json 2>/dev/null | '
'python3 -c "import sys,json; d=json.load(sys.stdin); '
'print(\'build #\'+str(d[\'number\']),d.get(\'result\',\'진행중\'),\'building:\',d.get(\'building\'))" 2>/dev/null')
run('빌드 콘솔 (마지막)',
f'curl -sf -u "{J_A}" http://127.0.0.1:9080/job/zioinfo-mail/lastBuild/consoleText 2>/dev/null | tail -15')
sftp.close(); c.close()
print('\n=== 완료 ===')

68
workspace/zioinfo-mail/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,68 @@
pipeline {
agent any
environment {
SRC = '/opt/mail'
STATIC = '/var/www/mail'
NOTIFY = "http://127.0.0.1:9001/api/messenger/webhook"
}
options {
buildDiscarder(logRotator(numToKeepStr: '5'))
timeout(time: 20, unit: 'MINUTES')
timestamps()
}
stages {
stage('Checkout') { steps { checkout scm } }
stage('Frontend Build') {
steps {
dir('frontend') {
sh 'npm ci --legacy-peer-deps 2>/dev/null || npm install --legacy-peer-deps'
sh 'npm run build'
}
}
}
stage('Deploy') {
when {
expression { env.GIT_BRANCH ==~ /.*main/ || env.BRANCH_NAME == 'main' }
}
steps {
sh """
# Frontend 정적 파일
mkdir -p ${STATIC}
cp -r dist/. ${STATIC}/
# Backend 소스
rsync -a --exclude=__pycache__ --exclude=.git \
--exclude='*.pyc' --exclude='.env' \
backend/ ${SRC}/backend/
# 패키지 설치
${SRC}/venv/bin/pip install -r backend/requirements.txt -q
# 서비스 재기동
systemctl restart zioinfo-mail
sleep 4
systemctl is-active zioinfo-mail || exit 1
# 헬스체크
curl -sf http://localhost:8026/health || exit 1
"""
}
}
}
post {
success {
sh """curl -sf -X POST ${NOTIFY} \
-H 'Content-Type:application/json' \
-d '{"event":"build_result","room":"ops","success":true,"result_summary":"✅ zioinfo-mail 배포 완료 #${BUILD_NUMBER}"}' \
2>/dev/null || true"""
}
failure {
sh """curl -sf -X POST ${NOTIFY} \
-H 'Content-Type:application/json' \
-d '{"event":"build_result","room":"ops","success":false,"result_summary":"❌ zioinfo-mail 빌드 실패 #${BUILD_NUMBER}"}' \
2>/dev/null || true"""
}
}
}