198 lines
7.8 KiB
Python
198 lines
7.8 KiB
Python
#!/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 기본 <workspace>/../../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")
|