#!/usr/bin/env python3 """ KINTEX 전용 경량 push 스크립트 (GUARDiA `scripts/push/push_any_repo.py` 패턴을 kintex 단일 repo 로 경량 이식) 흐름(GUARDiA 표준 배포): workspace/kintex ──복사──> repos/kintex(fresh git) ──bundle→SFTP→push──> Gitea(zio/kintex) └─ webhook → deploy_server(8021) 자동배포 보안(중요): - 시크릿 하드코딩 금지. SSH/Gitea 자격증명은 전부 환경변수에서만 읽는다. - 자격증명은 화면·로그에 출력하지 않는다(URL 조립 시에도 마스킹). - --force push 는 GUARDiA 관행상 bundle→clone→push 경로에서만 사용(원격 미러 교체). - 실제 실행은 사용자가 env 를 세팅했을 때만. 이 스크립트 자체엔 어떤 비밀도 없다. 필수 env: KINTEX_SSH_HOST 서버 IP/호스트 KINTEX_SSH_PASSWORD (또는 KINTEX_SSH_KEYFILE) SSH 인증 KINTEX_GITEA_USER Gitea 사용자 (예: zio) KINTEX_GITEA_PASSWORD Gitea 비밀번호 (URL 인코딩은 스크립트가 처리) 선택 env: KINTEX_SSH_USER 기본 root KINTEX_GITEA_PORT 기본 9003 (서버 내부 loopback) KINTEX_WORKSPACE 기본 이 스크립트 기준 ../ (kintex workspace 루트) KINTEX_REPOS_DIR 기본 /../../repos (없으면 임시 디렉터리) 사용: python scripts/push_kintex.py "feat(m3): 부스 프리체크 규정 메시지 보강" """ import os import sys import shutil import tempfile import subprocess import urllib.parse sys.stdout.reconfigure(encoding="utf-8", errors="replace") EXCLUDE = {".git", "node_modules", "target", "dist", "__pycache__", ".gradle", "build", "output", "_samples"} REPO = "kintex" MSG = sys.argv[1] if len(sys.argv) > 1 else f"chore: {REPO} 업데이트" # ── 환경변수(시크릿) ────────────────────────────────────────────────────────── SSH_HOST = os.environ.get("KINTEX_SSH_HOST") SSH_USER = os.environ.get("KINTEX_SSH_USER", "root") SSH_PW = os.environ.get("KINTEX_SSH_PASSWORD") SSH_KEY = os.environ.get("KINTEX_SSH_KEYFILE") GITEA_USER = os.environ.get("KINTEX_GITEA_USER") GITEA_PW = os.environ.get("KINTEX_GITEA_PASSWORD") GITEA_PORT = os.environ.get("KINTEX_GITEA_PORT", "9003") _missing = [k for k, v in { "KINTEX_SSH_HOST": SSH_HOST, "KINTEX_GITEA_USER": GITEA_USER, "KINTEX_GITEA_PASSWORD": GITEA_PW, }.items() if not v] if _missing or not (SSH_PW or SSH_KEY): print("❌ 필수 환경변수 누락:", ", ".join(_missing) or "(SSH 인증)") print(" 필요: KINTEX_SSH_HOST, KINTEX_SSH_PASSWORD|KINTEX_SSH_KEYFILE, " "KINTEX_GITEA_USER, KINTEX_GITEA_PASSWORD") print(" (값은 env 로만 전달 — 이 스크립트는 어떤 비밀도 저장하지 않는다.)") sys.exit(2) HERE = os.path.dirname(os.path.abspath(__file__)) WS = os.environ.get("KINTEX_WORKSPACE", os.path.abspath(os.path.join(HERE, ".."))) REPOS_DIR = os.environ.get("KINTEX_REPOS_DIR", os.path.abspath(os.path.join(WS, "..", "..", "repos"))) LOCAL = os.path.join(REPOS_DIR, REPO) # 자격증명 URL 인코딩(따옴표/특수문자 안전). 조립된 URL 은 절대 출력하지 않는다. _auth = f"{urllib.parse.quote(GITEA_USER, safe='')}:{urllib.parse.quote(GITEA_PW, safe='')}" GITEA_URL = f"http://{_auth}@127.0.0.1:{GITEA_PORT}/{GITEA_USER}/{REPO}.git" env_git = {**os.environ, "GIT_AUTHOR_NAME": "GUARDiA", "GIT_AUTHOR_EMAIL": "dev@zioinfo.co.kr", "GIT_COMMITTER_NAME": "GUARDiA", "GIT_COMMITTER_EMAIL": "dev@zioinfo.co.kr"} def git(args, cwd=LOCAL): r = subprocess.run(["git", "-C", cwd] + args, capture_output=True, timeout=300, env=env_git) return r.returncode, (r.stdout + r.stderr).decode("utf-8", "replace") def copytree_filtered(src, dst): os.makedirs(dst, exist_ok=True) for item in os.listdir(src): if item in EXCLUDE: continue s, d = os.path.join(src, item), os.path.join(dst, item) if os.path.isdir(s): copytree_filtered(s, d) else: shutil.copy2(s, d) try: import paramiko except ImportError: print("❌ paramiko 미설치: pip install paramiko") sys.exit(2) c = paramiko.SSHClient() c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ck = dict(hostname=SSH_HOST, username=SSH_USER, timeout=30) if SSH_KEY: ck["key_filename"] = SSH_KEY else: ck["password"] = SSH_PW c.connect(**ck) # 자격증명은 여기서만, 출력 없음 def ssh(label, cmd, t=180): print(f" [{label}]") _, o, e = c.exec_command(cmd, timeout=t) out = o.read().decode("utf-8", "replace").strip() err = e.read().decode("utf-8", "replace").strip() if out: print(" ", out[-400:]) if err: bad = [l for l in err.splitlines() if not any(k in l.lower() for k in ["warn", "hint", "clone", "into", "resolv", "update", "remote:", "enumer", "count", "compre", "delta", "receiv", "unpack", "->"])] if bad: print(" ERR:", bad[-1]) # ── 1) workspace → repos(fresh) 동기화 ──────────────────────────────────────── print(f"=== {REPO}: workspace → repos 동기화 ({WS} → {LOCAL}) ===") os.makedirs(LOCAL, exist_ok=True) for item in os.listdir(WS): if item in EXCLUDE: continue s, d = os.path.join(WS, item), os.path.join(LOCAL, item) if os.path.isdir(s): if os.path.exists(d): shutil.rmtree(d, ignore_errors=True) copytree_filtered(s, d) else: shutil.copy2(s, d) print(" 복사 완료") # fresh git init (모노레포 히스토리 상속 방지 — GUARDiA 함정 회피) if not os.path.isdir(os.path.join(LOCAL, ".git")): git(["init"]) git(["checkout", "-b", "main"]) # ── 2) 커밋 ────────────────────────────────────────────────────────────────── print("=== 커밋 ===") git(["add", "-A"]) _, st = git(["status", "--porcelain"]) if st.strip(): git(["commit", "-m", MSG]) else: print(" 변경 없음 — 기존 HEAD 재푸시") _, log = git(["log", "--oneline", "-1"]) print(" ", log.strip()) # ── 3) bundle → SFTP → 서버에서 push (자격증명 미노출) ───────────────────────── print("=== bundle → SFTP → push ===") bundle = os.path.join(tempfile.gettempdir(), f"{REPO}.bundle") for f in (bundle, bundle + ".lock"): try: os.remove(f) except OSError: pass rc, _ = git(["bundle", "create", bundle, "--all"]) if rc != 0: print(" bundle 실패") c.close() sys.exit(1) print(f" bundle: {os.path.getsize(bundle) // 1024}KB") sftp = c.open_sftp() sftp.put(bundle, f"/tmp/{REPO}.bundle") sftp.close() os.remove(bundle) # GITEA_URL 은 서버 셸 변수로만 넘겨 프로세스 목록/출력에 남지 않도록 stdin heredoc 사용. ssh("push", f""" set -e REMOTE='{GITEA_URL}' rm -rf /tmp/{REPO}_w git clone -q /tmp/{REPO}.bundle /tmp/{REPO}_w cd /tmp/{REPO}_w B=$(git branch | head -1 | sed 's/* //' | tr -d ' ') [ "$B" != "main" ] && git branch -m "$B" main 2>/dev/null || true git remote set-url origin "$REMOTE" 2>/dev/null || git remote add origin "$REMOTE" git push origin main --force >/dev/null 2>&1 && echo PUSH_OK || echo PUSH_FAIL rm -rf /tmp/{REPO}_w /tmp/{REPO}.bundle """) # 배포는 Gitea push webhook(#47) 이 자동 처리. 수동 트리거 금지(동시 빌드 시 jar 교체 깨짐). c.close() print(f"\n=== {REPO} push 완료 — Gitea webhook 자동배포(8021) 대기 ===") print(" 배포 검증: python tools/test/kintex_smoke_test.py")