From f4f5abd65b16ba77eafc74988648b51ca80180f5 Mon Sep 17 00:00:00 2001 From: "DESKTOP-TKLFCPR\\ython" Date: Mon, 1 Jun 2026 20:02:21 +0900 Subject: [PATCH] feat(cicd): sync workspace to repos, fix git ownership and pull strategy Co-Authored-By: Claude Sonnet 4.6 --- scripts/push/sync_workspace_to_repos.py | 116 ++++++++++++++++++++++ scripts/setup/fix_deploy_pull_strategy.py | 71 +++++++++++++ scripts/setup/fix_git_ownership.py | 48 +++++++++ 3 files changed, 235 insertions(+) create mode 100644 scripts/push/sync_workspace_to_repos.py create mode 100644 scripts/setup/fix_deploy_pull_strategy.py create mode 100644 scripts/setup/fix_git_ownership.py diff --git a/scripts/push/sync_workspace_to_repos.py b/scripts/push/sync_workspace_to_repos.py new file mode 100644 index 00000000..32b4be42 --- /dev/null +++ b/scripts/push/sync_workspace_to_repos.py @@ -0,0 +1,116 @@ +"""workspace/ → repos/ 동기화 후 Gitea push""" +import subprocess, shutil, os, sys, paramiko, json, base64 +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +EXCLUDE = {'.git', '__pycache__', 'node_modules', '.expo', + 'dist', 'build', 'target', '*.db', '*.pyc', '.env'} + +def should_skip(name): + if name.startswith('.'): return False # .env 등 포함 + for pat in EXCLUDE: + if pat.startswith('*'): + if name.endswith(pat[1:]): return True + elif name == pat: + return True + return False + +def sync_dir(src, dst): + """src → dst 파일 동기화 (dst의 .git 보존)""" + added, updated = 0, 0 + # 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_root = os.path.relpath(root, src) + dst_root = os.path.join(dst, rel_root) if rel_root != '.' else dst + os.makedirs(dst_root, exist_ok=True) + for f in files: + if f.endswith(('.pyc', '.db', '.db-journal')): continue + s = os.path.join(root, f) + d = os.path.join(dst_root, f) + try: + if not os.path.exists(d): + shutil.copy2(s, d); added += 1 + elif os.path.getmtime(s) > os.path.getmtime(d): + shutil.copy2(s, d); updated += 1 + except: pass + return added, updated + +def git(path, *args, check=False): + r = subprocess.run(['git', '-C', path] + list(args), + capture_output=True, text=True, + encoding='utf-8', errors='replace') + return r.stdout.strip(), r.returncode + +REPOS = ['guardia-itsm', 'guardia-manager', 'guardia-docs', 'zioinfo-web', 'guardia-messenger'] +BASE_W = 'C:/GUARDiA/workspace' +BASE_R = 'C:/GUARDiA/repos' + +# Gitea API 업로드용 클라이언트 +ssh = paramiko.SSHClient() +ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +ssh.connect('101.79.17.164', username='root', password='1q2w3e!Q', timeout=15) + +def gitea_push_bundle(repo): + """bundle → 서버 → Gitea push""" + local = f'{BASE_R}/{repo}' + bundle = f'{BASE_R}/{repo}.bundle' + sftp = ssh.open_sftp() + print(f' bundle 생성 중...') + subprocess.run(['git', '-C', local, 'bundle', 'create', bundle, '--all'], + capture_output=True, timeout=120) + size = os.path.getsize(bundle) // 1024 + print(f' 크기: {size}KB → 서버 전송...') + sftp.put(bundle, f'/tmp/{repo}.bundle') + sftp.close() + os.remove(bundle) + + def srv(cmd, t=60): + _, o, e = ssh.exec_command(cmd, timeout=t) + return o.read().decode('utf-8','replace').strip() + + result = srv(f""" + rm -rf /tmp/{repo}_push + git clone /tmp/{repo}.bundle /tmp/{repo}_push 2>/dev/null + cd /tmp/{repo}_push + git remote set-url origin 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio/{repo}.git' + git push origin main --force 2>&1 | tail -3 + rm -rf /tmp/{repo}_push /tmp/{repo}.bundle + echo "DONE" + """, t=120) + return 'DONE' in result, result + +print('=== workspace → repos 동기화 ===\n') +for repo in REPOS: + src = f'{BASE_W}/{repo}' + dst = f'{BASE_R}/{repo}' + if not os.path.isdir(src): + print(f'[{repo}] workspace 없음, skip') + continue + + print(f'[{repo}]') + added, updated = sync_dir(src, dst) + print(f' sync: +{added} 추가, {updated} 갱신') + + # git 상태 확인 + out, _ = git(dst, 'status', '--short') + if not out: + print(' 변경 없음') + continue + + changed = len(out.splitlines()) + print(f' 변경 파일: {changed}개') + + git(dst, 'add', '-A') + msg = f'sync: update from workspace (latest ITSM/CICD/DR changes)' + git(dst, 'commit', '-m', msg) + + print(' Gitea push 중...') + ok, log = gitea_push_bundle(repo) + if ok: + print(f' ✅ push 완료') + else: + print(f' ❌ push 실패: {log[:200]}') + +ssh.close() +print('\n=== 동기화 완료 ===') diff --git a/scripts/setup/fix_deploy_pull_strategy.py b/scripts/setup/fix_deploy_pull_strategy.py new file mode 100644 index 00000000..69747892 --- /dev/null +++ b/scripts/setup/fix_deploy_pull_strategy.py @@ -0,0 +1,71 @@ +"""deploy_server.py git pull → fetch+reset 방식으로 변경 + 서버 클론 강제 리셋""" +import paramiko, sys +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=40): + print(f'\n[{label}]') + _, o, e = client.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' + +# 1. 서버 src 디렉토리 강제 리셋 (diverge 해결) +for path, repo in [('/opt/guardia/src', 'guardia-itsm'), ('/opt/zioinfo/src', 'zioinfo-web')]: + run(f'{repo} 강제 리셋', f""" + git config --global --add safe.directory {path} 2>/dev/null + git -C {path} remote set-url origin '{GITEA}/{repo}.git' + git -C {path} fetch origin main + git -C {path} reset --hard origin/main + echo "리셋 완료: $(git -C {path} log -1 --oneline)" + """) + +# 2. deploy_server.py git pull → fetch+reset 방식으로 교체 +run('deploy_server.py 수정 확인', "grep -n 'git pull\|git -C\|pull origin' /opt/zioinfo/deploy_server.py | head -15") + +run('git pull 명령 교체', r""" +# ["git", "-C", SRC, "pull", "origin", "main"] → fetch + reset +python3 -c " +import re +with open('/opt/zioinfo/deploy_server.py', 'r') as f: + content = f.read() + +# git pull 단순 명령 교체 +content = content.replace( + '[\"git\", \"-C\", SRC, \"pull\", \"origin\", \"main\"]', + '[\"bash\", \"-c\", f\"git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main\"]' +) + +# bash git pull 교체 +content = content.replace( + '\"[ -d {SRC}/.git ] && git -C {SRC} pull origin main\"', + '\"[ -d {SRC}/.git ] && git -C {SRC} fetch origin main && git -C {SRC} reset --hard origin/main\"' +) + +with open('/opt/zioinfo/deploy_server.py', 'w') as f: + f.write(content) +print('수정 완료') +" +""") + +run('수정 결과 확인', "grep -n 'git.*pull\|fetch\|reset' /opt/zioinfo/deploy_server.py | head -10") + +# 3. 서비스 재시작 +run('서비스 재시작', 'systemctl restart zioinfo-deploy && sleep 2 && systemctl is-active zioinfo-deploy') + +# 4. 배포 재트리거 +import time +for repo in ['guardia-itsm', 'zioinfo-web']: + run(f'{repo} 배포 트리거', + f'curl -sf -X POST http://localhost:9999 ' + f'-H "Content-Type: application/json" -H "X-Gitea-Event: push" ' + f'-d \'{{"repository":{{"name":"{repo}"}},"ref":"refs/heads/main"}}\' 2>/dev/null') + +time.sleep(12) +run('최종 배포 로그', 'tail -20 /var/log/zioinfo/deploy.log 2>/dev/null') + +client.close() diff --git a/scripts/setup/fix_git_ownership.py b/scripts/setup/fix_git_ownership.py new file mode 100644 index 00000000..fc6d6b31 --- /dev/null +++ b/scripts/setup/fix_git_ownership.py @@ -0,0 +1,48 @@ +"""git safe.directory + remote URL 전체 수정""" +import paramiko, sys +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=30): + print(f'\n[{label}]') + _, o, e = client.exec_command(cmd, timeout=timeout) + out = o.read().decode('utf-8','replace').strip() + if out: print(out[:400]) + return out + +GITEA = 'http://zio:Zio%40Admin2026%21@127.0.0.1:9003/zio' + +SRC_DIRS = { + '/opt/guardia/src': f'{GITEA}/guardia-itsm.git', + '/opt/zioinfo/src': f'{GITEA}/zioinfo-web.git', + '/opt/manager/src': f'{GITEA}/guardia-manager.git', + '/opt/guardia-docs/src': f'{GITEA}/guardia-docs.git', +} + +# safe.directory 등록 + ownership 수정 + remote update +for path, url in SRC_DIRS.items(): + run(f'fix {path}', f""" + git config --global --add safe.directory {path} 2>/dev/null + chown -R root:root {path} 2>/dev/null + if [ -d {path}/.git ]; then + git -C {path} remote set-url origin '{url}' + git -C {path} pull origin main 2>&1 | tail -3 + else + echo "no clone yet" + fi + """, timeout=30) + +# 배포 트리거 +import time +for repo in ['guardia-itsm', 'zioinfo-web']: + run(f'{repo} 배포 트리거', + f'curl -sf -X POST http://localhost:9999 ' + f'-H "Content-Type: application/json" ' + f'-H "X-Gitea-Event: push" ' + f'-d \'{{"repository":{{"name":"{repo}"}},"ref":"refs/heads/main"}}\' 2>/dev/null') + +time.sleep(12) +run('최종 배포 로그', 'tail -20 /var/log/zioinfo/deploy.log 2>/dev/null') +client.close()