pipeline { agent any environment { REGISTRY = 'git.gihyeon.com' IMAGE_NAME = 'gihyeon/cointrader' IMAGE_TAG = "${env.BUILD_NUMBER}" FULL_IMAGE = "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" LATEST_IMAGE = "${REGISTRY}/${IMAGE_NAME}:latest" DASH_API_IMAGE = "${REGISTRY}/gihyeon/cointrader-dashboard-api" DASH_UI_IMAGE = "${REGISTRY}/gihyeon/cointrader-dashboard-ui" DISCORD_WEBHOOK = credentials('discord-webhook') } stages { stage('Notify Build Start') { steps { sh """ curl -H "Content-Type: application/json" \ -X POST \ -d '{"content": "๐Ÿš€ **[๋นŒ๋“œ ์‹œ์ž‘]** `cointrader` (Build #${env.BUILD_NUMBER}) ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ๊ฐ€๋™"}' \ ${DISCORD_WEBHOOK} """ } } stage('Git Clone from Gitea') { steps { git branch: 'main', credentialsId: 'gitea-cred', url: 'https://git.gihyeon.com/gihyeon/cointrader.git' } } stage('Detect Changes') { steps { script { def changes = sh(script: 'git diff --name-only HEAD~1 || echo "ALL"', returnStdout: true).trim() echo "Changed files:\n${changes}" if (changes == 'ALL') { // ์ฒซ ๋นŒ๋“œ์ด๊ฑฐ๋‚˜ diff ์‹คํŒจ ์‹œ ์ „์ฒด ๋นŒ๋“œ env.BOT_CHANGED = 'true' env.DASH_API_CHANGED = 'true' env.DASH_UI_CHANGED = 'true' } else { env.BOT_CHANGED = (changes =~ /(?m)^(src\/|main\.py|requirements\.txt|Dockerfile)/).find() ? 'true' : 'false' env.DASH_API_CHANGED = (changes =~ /(?m)^dashboard\/api\//).find() ? 'true' : 'false' env.DASH_UI_CHANGED = (changes =~ /(?m)^dashboard\/ui\//).find() ? 'true' : 'false' } // docker-compose.yml ๋ณ€๊ฒฝ ์‹œ์—๋„ ๋ฐฐํฌ ํ•„์š” if (changes.contains('docker-compose.yml') || changes.contains('Jenkinsfile')) { env.COMPOSE_CHANGED = 'true' } else { env.COMPOSE_CHANGED = 'false' } echo "BOT_CHANGED=${env.BOT_CHANGED}, DASH_API_CHANGED=${env.DASH_API_CHANGED}, DASH_UI_CHANGED=${env.DASH_UI_CHANGED}, COMPOSE_CHANGED=${env.COMPOSE_CHANGED}" } } } stage('Build Docker Images') { parallel { stage('Bot') { when { expression { env.BOT_CHANGED == 'true' } } steps { sh "docker build -t ${FULL_IMAGE} -t ${LATEST_IMAGE} ." } } stage('Dashboard API') { when { expression { env.DASH_API_CHANGED == 'true' } } steps { sh "docker build -t ${DASH_API_IMAGE}:${IMAGE_TAG} -t ${DASH_API_IMAGE}:latest ./dashboard/api" } } stage('Dashboard UI') { when { expression { env.DASH_UI_CHANGED == 'true' } } steps { sh "docker build -t ${DASH_UI_IMAGE}:${IMAGE_TAG} -t ${DASH_UI_IMAGE}:latest ./dashboard/ui" } } } } stage('Push to Gitea Registry') { steps { withCredentials([usernamePassword(credentialsId: 'gitea-registry-cred', passwordVariable: 'GITEA_TOKEN', usernameVariable: 'GITEA_USER')]) { sh "echo \$GITEA_TOKEN | docker login ${REGISTRY} -u \$GITEA_USER --password-stdin" script { if (env.BOT_CHANGED == 'true') { sh "docker push ${FULL_IMAGE}" sh "docker push ${LATEST_IMAGE}" } if (env.DASH_API_CHANGED == 'true') { sh "docker push ${DASH_API_IMAGE}:${IMAGE_TAG}" sh "docker push ${DASH_API_IMAGE}:latest" } if (env.DASH_UI_CHANGED == 'true') { sh "docker push ${DASH_UI_IMAGE}:${IMAGE_TAG}" sh "docker push ${DASH_UI_IMAGE}:latest" } } } } } stage('Deploy to Prod LXC') { steps { script { // docker-compose.yml์ด ๋ณ€๊ฒฝ๋˜์—ˆ์œผ๋ฉด ํ•ญ์ƒ ์ „์†ก if (env.COMPOSE_CHANGED == 'true') { sh 'ssh root@10.1.10.24 "mkdir -p /root/cointrader"' sh 'scp docker-compose.yml root@10.1.10.24:/root/cointrader/' } // ๋ณ€๊ฒฝ๋œ ์„œ๋น„์Šค๋งŒ pull & recreate (๋‚˜๋จธ์ง€๋Š” ์ค‘๋‹จ ์—†์Œ) def services = [] if (env.BOT_CHANGED == 'true') services.add('cointrader') if (env.DASH_API_CHANGED == 'true') services.add('dashboard-api') if (env.DASH_UI_CHANGED == 'true') services.add('dashboard-ui') if (env.COMPOSE_CHANGED == 'true' && services.isEmpty()) { // compose๋งŒ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ ์ „์ฒด ์žฌ์‹œ์ž‘ sh 'ssh root@10.1.10.24 "cd /root/cointrader/ && docker compose up -d"' } else if (!services.isEmpty()) { def svcList = services.join(' ') sh "ssh root@10.1.10.24 \"cd /root/cointrader/ && docker compose pull ${svcList} && docker compose up -d ${svcList}\"" } } } } stage('Cleanup') { steps { script { if (env.BOT_CHANGED == 'true') { sh "docker rmi ${FULL_IMAGE} || true" sh "docker rmi ${LATEST_IMAGE} || true" } if (env.DASH_API_CHANGED == 'true') { sh "docker rmi ${DASH_API_IMAGE}:${IMAGE_TAG} || true" sh "docker rmi ${DASH_API_IMAGE}:latest || true" } if (env.DASH_UI_CHANGED == 'true') { sh "docker rmi ${DASH_UI_IMAGE}:${IMAGE_TAG} || true" sh "docker rmi ${DASH_UI_IMAGE}:latest || true" } } } } } post { success { echo "Build #${env.BUILD_NUMBER} ์„ฑ๊ณต" sh """ curl -H "Content-Type: application/json" \ -X POST \ -d '{"content": "โœ… **[๋ฐฐํฌ ์„ฑ๊ณต]** `cointrader` (Build #${env.BUILD_NUMBER}) ์šด์˜ ์„œ๋ฒ„(10.1.10.24) ๋ฐฐํฌ ์™„๋ฃŒ!\\n- ๐Ÿค– ๋ด‡: ${env.BOT_CHANGED}\\n- ๐Ÿ“Š API: ${env.DASH_API_CHANGED}\\n- ๐Ÿ–ฅ๏ธ UI: ${env.DASH_UI_CHANGED}"}' \ ${DISCORD_WEBHOOK} """ } failure { echo "Build #${env.BUILD_NUMBER} ์‹คํŒจ" sh """ curl -H "Content-Type: application/json" \ -X POST \ -d '{"content": "โŒ **[๋ฐฐํฌ ์‹คํŒจ]** `cointrader` (Build #${env.BUILD_NUMBER}) ํŒŒ์ดํ”„๋ผ์ธ ์—๋Ÿฌ ๋ฐœ์ƒ. ์  ํ‚จ์Šค ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด ์ฃผ์„ธ์š”!"}' \ ${DISCORD_WEBHOOK} """ } } }