- Move backend/frontend/messenger/ old paths to _archive/ - Reorganize scripts into scripts/deploy, check, push, setup, misc - Move docs (pptx, docx) to docs/ - Add .claude agents and skills for fullstack/folder-cleanup harness - workspace/ projects remain intact Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
345 lines
13 KiB
Python
345 lines
13 KiB
Python
"""CI/CD 전체 구축: 플러그인 → Credential → Jenkinsfile → Job 생성"""
|
|
import paramiko, sys, time, json
|
|
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=30)
|
|
sftp = client.open_sftp()
|
|
|
|
JENKINS = 'http://127.0.0.1:9080'
|
|
AUTH = 'admin:Admin@2026!'
|
|
|
|
def run(label, cmd, timeout=120):
|
|
print(f'\n[{label}]')
|
|
_, o, e = client.exec_command(cmd, timeout=timeout)
|
|
out = o.read().decode('utf-8', errors='replace').strip()
|
|
if out: print(out[:400])
|
|
return out
|
|
|
|
def jenkins_script(label, script, timeout=30):
|
|
"""Jenkins Script Console 실행"""
|
|
print(f'\n[Script: {label}]')
|
|
# crumb 가져오기
|
|
_, o, _ = client.exec_command(
|
|
f'curl -sf "{JENKINS}/crumbIssuer/api/json" -u {AUTH} 2>/dev/null', timeout=10)
|
|
crumb_data = o.read().decode('utf-8', errors='replace').strip()
|
|
try:
|
|
crumb = json.loads(crumb_data)['crumb']
|
|
crumb_field = json.loads(crumb_data)['crumbRequestField']
|
|
except:
|
|
crumb = ''
|
|
crumb_field = 'Jenkins-Crumb'
|
|
|
|
escaped = script.replace("'", "'\\''")
|
|
cmd = f"""curl -sf -X POST "{JENKINS}/scriptText" -u {AUTH} \
|
|
-H "{crumb_field}: {crumb}" \
|
|
--data-urlencode 'script={escaped}' 2>/dev/null"""
|
|
_, o, _ = client.exec_command(cmd, timeout=timeout)
|
|
out = o.read().decode('utf-8', errors='replace').strip()
|
|
if out: print(f' {out[:200]}')
|
|
return out
|
|
|
|
# ── 1. 플러그인 설치 ──────────────────────────────────────────────────────────
|
|
run('필수 플러그인 설치', f"""
|
|
java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} install-plugin \
|
|
git workflow-aggregator pipeline-stage-view credentials-binding \
|
|
ssh-agent nodejs timestamper ansicolor http_request junit \
|
|
2>&1 | tail -3
|
|
echo "플러그인 설치 요청"
|
|
""")
|
|
|
|
# ── 2. Credential 등록 ────────────────────────────────────────────────────────
|
|
jenkins_script('Gitea Credential 등록', """
|
|
import jenkins.model.*
|
|
import com.cloudbees.plugins.credentials.*
|
|
import com.cloudbees.plugins.credentials.domains.*
|
|
import com.cloudbees.plugins.credentials.impl.*
|
|
|
|
def store = Jenkins.instance.getExtensionList(
|
|
"com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0]?.getStore()
|
|
|
|
if (store) {
|
|
// 기존 삭제
|
|
def existing = store.getCredentials(Domain.global()).find { it.id == "gitea-zio" }
|
|
if (existing) store.removeCredentials(Domain.global(), existing)
|
|
|
|
def cred = new UsernamePasswordCredentialsImpl(
|
|
CredentialsScope.GLOBAL, "gitea-zio", "Gitea zio account",
|
|
"zio", "Zio@Admin2026!"
|
|
)
|
|
store.addCredentials(Domain.global(), cred)
|
|
println "gitea-zio credential 등록 완료"
|
|
} else {
|
|
println "Store 없음 - credentials 플러그인 미설치"
|
|
}
|
|
""")
|
|
|
|
# ── 3. 글로벌 환경변수 설정 ───────────────────────────────────────────────────
|
|
jenkins_script('글로벌 환경변수', """
|
|
import jenkins.model.*
|
|
import hudson.slaves.*
|
|
|
|
def instance = Jenkins.instance
|
|
def globalProps = instance.globalNodeProperties
|
|
def existing = globalProps.getAll(EnvironmentVariablesNodeProperty.class)
|
|
|
|
EnvironmentVariablesNodeProperty prop
|
|
if (existing) {
|
|
prop = existing[0]
|
|
} else {
|
|
prop = new EnvironmentVariablesNodeProperty()
|
|
globalProps.add(prop)
|
|
}
|
|
|
|
def env = prop.getEnvVars()
|
|
env.put("ITSM_BASE_URL", "http://127.0.0.1:9001")
|
|
env.put("GITEA_URL", "http://127.0.0.1:9003")
|
|
env.put("SERVER_HOST", "101.79.17.164")
|
|
instance.save()
|
|
println "환경변수 설정 완료"
|
|
""")
|
|
|
|
# ── 4. Jenkinsfile 작성 ───────────────────────────────────────────────────────
|
|
print('\n[Jenkinsfile 작성]')
|
|
|
|
# zioinfo-web Jenkinsfile (이미 있으면 업데이트)
|
|
ZIOINFO_JF = '''pipeline {
|
|
agent any
|
|
environment {
|
|
SRC = '/opt/zioinfo/src'
|
|
JAR_DIR = '/opt/zioinfo/app'
|
|
STATIC = '/var/www/zioinfo'
|
|
MVN = '/usr/bin/mvn'
|
|
NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook"
|
|
}
|
|
options {
|
|
buildDiscarder(logRotator(numToKeepStr: '5'))
|
|
timeout(time: 20, unit: 'MINUTES')
|
|
timestamps()
|
|
}
|
|
stages {
|
|
stage('Checkout') {
|
|
steps {
|
|
checkout([
|
|
$class: 'GitSCM', branches: scm.branches,
|
|
userRemoteConfigs: scm.userRemoteConfigs,
|
|
extensions: [[$class: 'SparseCheckoutPaths',
|
|
sparseCheckoutPaths: [[path: 'frontend'], [path: 'backend']]
|
|
]]
|
|
])
|
|
}
|
|
}
|
|
stage('Frontend Build') {
|
|
steps {
|
|
dir('frontend') {
|
|
sh 'npm ci --legacy-peer-deps --prefer-offline 2>/dev/null || npm install --legacy-peer-deps'
|
|
sh 'npm run build'
|
|
}
|
|
}
|
|
}
|
|
stage('Backend Build') {
|
|
steps {
|
|
dir('backend') {
|
|
sh "${MVN} clean package -DskipTests -q"
|
|
}
|
|
}
|
|
}
|
|
stage('Deploy') {
|
|
when { branch 'main' }
|
|
steps {
|
|
sh """
|
|
cp backend/target/*.jar ${JAR_DIR}/app.jar
|
|
cp -r backend/src/main/resources/static/. ${STATIC}/
|
|
systemctl restart zioinfo
|
|
sleep 4
|
|
systemctl is-active zioinfo || 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-web 배포 완료 #${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-web 빌드 실패 #${BUILD_NUMBER}\\"}' 2>/dev/null || true" }
|
|
}
|
|
}'''
|
|
|
|
ITSM_JF = '''pipeline {
|
|
agent any
|
|
environment {
|
|
APP = '/opt/guardia/app'
|
|
VENV = '/opt/guardia/venv'
|
|
NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook"
|
|
}
|
|
options {
|
|
buildDiscarder(logRotator(numToKeepStr: '5'))
|
|
timeout(time: 15, unit: 'MINUTES')
|
|
timestamps()
|
|
}
|
|
stages {
|
|
stage('Checkout') { steps { checkout scm } }
|
|
stage('Install') {
|
|
steps { sh "${VENV}/bin/pip install -r requirements.txt -q" }
|
|
}
|
|
stage('Test') {
|
|
when { expression { fileExists('tests/') } }
|
|
steps {
|
|
sh "${VENV}/bin/pytest tests/ -q --tb=short --junitxml=test-results.xml || true"
|
|
junit allowEmptyResults: true, testResults: 'test-results.xml'
|
|
}
|
|
}
|
|
stage('Deploy') {
|
|
when { branch 'main' }
|
|
steps {
|
|
sh """
|
|
rsync -a --exclude=__pycache__ --exclude=.git \\
|
|
--exclude=rpa_rules.json --exclude='*.pyc' \\
|
|
. ${APP}/
|
|
systemctl restart guardia
|
|
sleep 4
|
|
systemctl is-active guardia || 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\\":\\"✅ guardia-itsm 배포 완료 #${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\\":\\"❌ guardia-itsm 빌드 실패 #${BUILD_NUMBER}\\"}' 2>/dev/null || true" }
|
|
}
|
|
}'''
|
|
|
|
MANAGER_JF = '''pipeline {
|
|
agent any
|
|
environment {
|
|
STATIC = '/var/www/manager'
|
|
NOTIFY = "${ITSM_BASE_URL}/api/messenger/webhook"
|
|
}
|
|
options { buildDiscarder(logRotator(numToKeepStr: '5')); timeout(time: 15, unit: 'MINUTES') }
|
|
stages {
|
|
stage('Checkout') { steps { checkout scm } }
|
|
stage('Build') {
|
|
steps {
|
|
dir('frontend') {
|
|
sh 'npm ci 2>/dev/null || npm install'
|
|
sh 'npm run build'
|
|
}
|
|
}
|
|
}
|
|
stage('Deploy') {
|
|
when { branch 'main' }
|
|
steps {
|
|
sh """
|
|
cp -r frontend/dist/. ${STATIC}/
|
|
systemctl restart guardia-manager 2>/dev/null || true
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
post {
|
|
success { sh "curl -sf -X POST ${NOTIFY} -H 'Content-Type:application/json' -d '{\\"event\\":\\"build_result\\",\\"room\\":\\"ops\\",\\"success\\":true,\\"result_summary\\":\\"✅ guardia-manager 배포 완료\\"}' 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\\":\\"❌ guardia-manager 빌드 실패\\"}' 2>/dev/null || true" }
|
|
}
|
|
}'''
|
|
|
|
DOCS_JF = '''pipeline {
|
|
agent any
|
|
environment { DOCS = '/var/www/docs' }
|
|
options { buildDiscarder(logRotator(numToKeepStr: '3')) }
|
|
stages {
|
|
stage('Checkout') { steps { checkout scm } }
|
|
stage('Deploy') {
|
|
when { branch 'main' }
|
|
steps { sh 'mkdir -p ${DOCS} && cp -r . ${DOCS}/' }
|
|
}
|
|
}
|
|
}'''
|
|
|
|
# 서버의 독립 repo에 Jenkinsfile 작성
|
|
REPO_FILES = [
|
|
('/opt/zioinfo/src/Jenkinsfile', ZIOINFO_JF),
|
|
('/opt/guardia/app/Jenkinsfile', ITSM_JF),
|
|
('/opt/manager/Jenkinsfile', MANAGER_JF),
|
|
('/opt/docs/Jenkinsfile', DOCS_JF),
|
|
]
|
|
|
|
for path, content in REPO_FILES:
|
|
dir_path = '/'.join(path.split('/')[:-1])
|
|
client.exec_command(f'mkdir -p {dir_path}')
|
|
time.sleep(0.05)
|
|
try:
|
|
with sftp.open(path, 'w') as f:
|
|
f.write(content)
|
|
print(f' OK: {path}')
|
|
except Exception as ex:
|
|
print(f' SKIP {path}: {ex}')
|
|
|
|
# 로컬 workspace에도 Jenkinsfile 저장
|
|
import os
|
|
JF_MAP = {
|
|
r'C:\GUARDiA\workspace\zioinfo-web\Jenkinsfile': ZIOINFO_JF,
|
|
r'C:\GUARDiA\workspace\guardia-itsm\Jenkinsfile': ITSM_JF,
|
|
r'C:\GUARDiA\workspace\guardia-manager\Jenkinsfile': MANAGER_JF,
|
|
r'C:\GUARDiA\workspace\guardia-docs\Jenkinsfile': DOCS_JF,
|
|
}
|
|
for path, content in JF_MAP.items():
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
print(f' local OK: {path.split(chr(92))[-2]}/Jenkinsfile')
|
|
|
|
# ── 5. Jenkins Job 생성 ───────────────────────────────────────────────────────
|
|
REPOS = {
|
|
'zioinfo-web': ('http://127.0.0.1:9003/zio/zioinfo-web.git', 'Jenkinsfile'),
|
|
'guardia-itsm': ('http://127.0.0.1:9003/zio/guardia-itsm.git', 'Jenkinsfile'),
|
|
'guardia-manager': ('http://127.0.0.1:9003/zio/guardia-manager.git', 'Jenkinsfile'),
|
|
'guardia-docs': ('http://127.0.0.1:9003/zio/guardia-docs.git', 'Jenkinsfile'),
|
|
}
|
|
|
|
for job_name, (git_url, jf_path) in REPOS.items():
|
|
job_xml = f'''<?xml version='1.1' encoding='UTF-8'?>
|
|
<flow-definition plugin="workflow-job">
|
|
<description>{job_name} CI/CD Pipeline</description>
|
|
<keepDependencies>false</keepDependencies>
|
|
<definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition">
|
|
<scm class="hudson.plugins.git.GitSCM">
|
|
<userRemoteConfigs>
|
|
<hudson.plugins.git.UserRemoteConfig>
|
|
<url>{git_url}</url>
|
|
<credentialsId>gitea-zio</credentialsId>
|
|
</hudson.plugins.git.UserRemoteConfig>
|
|
</userRemoteConfigs>
|
|
<branches>
|
|
<hudson.plugins.git.BranchSpec>
|
|
<name>*/main</name>
|
|
</hudson.plugins.git.BranchSpec>
|
|
</branches>
|
|
</scm>
|
|
<scriptPath>{jf_path}</scriptPath>
|
|
<lightweight>true</lightweight>
|
|
</definition>
|
|
<triggers>
|
|
<com.cloudbees.jenkins.GitHubPushTrigger plugin="github">
|
|
<spec></spec>
|
|
</com.cloudbees.jenkins.GitHubPushTrigger>
|
|
</triggers>
|
|
</flow-definition>'''
|
|
|
|
# Job 생성
|
|
r = run(f'Job 생성: {job_name}', f"""
|
|
echo '{job_xml}' | java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} \
|
|
create-job {job_name} 2>/dev/null \
|
|
|| java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} \
|
|
update-job {job_name} << 'EOF'
|
|
{job_xml}
|
|
EOF
|
|
""")
|
|
|
|
run('Jenkins 플러그인 재시작', f"""
|
|
java -jar /tmp/jenkins-cli.jar -s {JENKINS} -auth {AUTH} restart 2>/dev/null || true
|
|
sleep 5
|
|
systemctl is-active jenkins
|
|
""")
|
|
|
|
sftp.close()
|
|
client.close()
|
|
print('\n=== CI/CD 파이프라인 구축 완료 ===')
|