pipeline { agent any parameters { choice(name: 'ACTION', choices: ['deploy', 'rollback'], description: '배포 또는 롤백') choice(name: 'ROLLBACK_TARGET', choices: ['production', 'stage'], description: '롤백 대상 환경') string(name: 'ROLLBACK_RELEASE', defaultValue: '', description: '롤백할 릴리스 ID (예: 20260310_120000). 비워두면 직전 릴리스로 롤백') } options { disableConcurrentBuilds() } environment { DEPLOY_USER = 'hskwon' RELEASE_ID = new Date().format('yyyyMMdd_HHmmss') PROD_SERVER = '211.117.60.189' } stages { // ── 롤백: 릴리스 목록 조회 ── stage('Rollback: List Releases') { when { expression { params.ACTION == 'rollback' } } steps { script { def basePath = params.ROLLBACK_TARGET == 'production' ? '/home/webservice/react' : '/home/webservice/react-stage' def pmName = params.ROLLBACK_TARGET == 'production' ? 'sam-front' : 'sam-front-stage' sshagent(credentials: ['deploy-ssh-key']) { def releases = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'ls -1dt ${basePath}/releases/*/ | head -6 | xargs -I{} basename {}'", returnStdout: true).trim() def current = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'basename \$(readlink -f ${basePath}/current)'", returnStdout: true).trim() echo "=== ${params.ROLLBACK_TARGET} 릴리스 목록 ===" echo "현재 활성: ${current}" echo "사용 가능:\n${releases}" } } } } // ── 롤백: symlink 전환 ── stage('Rollback: Switch Release') { when { expression { params.ACTION == 'rollback' } } steps { script { def basePath = params.ROLLBACK_TARGET == 'production' ? '/home/webservice/react' : '/home/webservice/react-stage' def pmName = params.ROLLBACK_TARGET == 'production' ? 'sam-front' : 'sam-front-stage' sshagent(credentials: ['deploy-ssh-key']) { def targetRelease = params.ROLLBACK_RELEASE if (!targetRelease?.trim()) { targetRelease = sh(script: "ssh ${DEPLOY_USER}@${PROD_SERVER} 'ls -1dt ${basePath}/releases/*/ | sed -n 2p | xargs basename'", returnStdout: true).trim() } // 릴리스 존재 여부 확인 sh "ssh ${DEPLOY_USER}@${PROD_SERVER} 'test -d ${basePath}/releases/${targetRelease}'" slackSend channel: '#deploy_react', color: '#FF9800', tokenCredentialId: 'slack-token', message: "🔄 *react* ${params.ROLLBACK_TARGET} 롤백 시작 → ${targetRelease}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" sh """ ssh ${DEPLOY_USER}@${PROD_SERVER} ' ln -sfn ${basePath}/releases/${targetRelease} ${basePath}/current && cd /home/webservice && pm2 reload ${pmName} ' """ slackSend channel: '#deploy_react', color: 'good', tokenCredentialId: 'slack-token', message: "✅ *react* ${params.ROLLBACK_TARGET} 롤백 완료 → ${targetRelease}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } } } // ── 일반 배포: Checkout ── stage('Checkout') { when { expression { params.ACTION == 'deploy' } } steps { checkout scm script { env.GIT_COMMIT_MSG = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim() } slackSend channel: '#deploy_react', color: '#439FE0', tokenCredentialId: 'slack-token', message: "🚀 *react* 빌드 시작 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } stage('Prepare Env') { when { expression { params.ACTION == 'deploy' } } steps { script { if (env.BRANCH_NAME == 'main') { // main: Stage 빌드 먼저 → Production 재빌드 sh "cp /var/lib/jenkins/env-files/react/.env.stage .env.production" } else { def envFile = "/var/lib/jenkins/env-files/react/.env.${env.BRANCH_NAME}" sh "cp ${envFile} .env.production" } } } } stage('Install') { when { expression { params.ACTION == 'deploy' } } steps { sh 'npm install --prefer-offline' } } stage('Build') { when { expression { params.ACTION == 'deploy' } } steps { sh 'npm run build' } } // ── develop → 개발서버 배포 ── stage('Deploy Development') { when { allOf { branch 'develop' expression { params.ACTION == 'deploy' } } } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ rsync -az --delete \ --exclude='.git' --exclude='.env*' --exclude='ecosystem.config.*' \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/ scp .env.production ${DEPLOY_USER}@114.203.209.83:/home/webservice/react/.env.production ssh ${DEPLOY_USER}@114.203.209.83 'cd /home/webservice/react && pm2 restart sam-react' """ } } } // ── main → 운영서버 Stage 배포 ── stage('Deploy Stage') { when { allOf { branch 'main' expression { params.ACTION == 'deploy' } } } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@${PROD_SERVER} 'mkdir -p /home/webservice/react-stage/releases/${RELEASE_ID}' rsync -az --delete \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/react-stage/releases/${RELEASE_ID}/ scp .env.production ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/react-stage/releases/${RELEASE_ID}/.env.production ssh ${DEPLOY_USER}@${PROD_SERVER} ' ln -sfn /home/webservice/react-stage/releases/${RELEASE_ID} /home/webservice/react-stage/current && cd /home/webservice && pm2 reload sam-front-stage 2>/dev/null || pm2 start react-stage/current/node_modules/.bin/next --name sam-front-stage -- start -p 3100 && cd /home/webservice/react-stage/releases && ls -1dt */ | tail -n +4 | xargs rm -rf 2>/dev/null || true ' """ } } } // ── 운영 배포 승인 (런칭 후 활성화) ── // stage('Production Approval') { // when { branch 'main' } // steps { // slackSend channel: '#product_deploy', color: '#FF9800', tokenCredentialId: 'slack-token', // message: "🔔 *react* 운영 배포 승인 대기 중\n${env.GIT_COMMIT_MSG}\nStage: https://stage.sam.it.kr\n<${env.BUILD_URL}input|승인하러 가기>" // timeout(time: 24, unit: 'HOURS') { // input message: 'Stage 확인 후 운영 배포를 진행하시겠습니까?\nStage: https://stage.sam.it.kr', // ok: '운영 배포 진행' // } // } // } // ── main → Production 재빌드 (운영 환경변수) ── stage('Rebuild for Production') { when { allOf { branch 'main' expression { params.ACTION == 'deploy' } } } steps { sh "cp /var/lib/jenkins/env-files/react/.env.main .env.production" sh 'npm run build' } } // ── main → 운영서버 Production 배포 ── stage('Deploy Production') { when { allOf { branch 'main' expression { params.ACTION == 'deploy' } } } steps { sshagent(credentials: ['deploy-ssh-key']) { sh """ ssh ${DEPLOY_USER}@${PROD_SERVER} 'mkdir -p /home/webservice/react/releases/${RELEASE_ID}' rsync -az --delete \ .next package.json next.config.ts public node_modules \ ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/react/releases/${RELEASE_ID}/ scp .env.production ${DEPLOY_USER}@${PROD_SERVER}:/home/webservice/react/releases/${RELEASE_ID}/.env.production ssh ${DEPLOY_USER}@${PROD_SERVER} ' ln -sfn /home/webservice/react/releases/${RELEASE_ID} /home/webservice/react/current && cd /home/webservice && pm2 reload sam-front && cd /home/webservice/react/releases && ls -1dt */ | tail -n +6 | xargs rm -rf 2>/dev/null || true ' """ } } } } post { success { script { if (params.ACTION == 'deploy') { slackSend channel: '#deploy_react', color: 'good', tokenCredentialId: 'slack-token', message: "✅ *react* 배포 성공 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } } failure { script { if (params.ACTION == 'deploy') { slackSend channel: '#deploy_react', color: 'danger', tokenCredentialId: 'slack-token', message: "❌ *react* 배포 실패 (`${env.BRANCH_NAME}`)\n${env.GIT_COMMIT_MSG}\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } else { slackSend channel: '#deploy_react', color: 'danger', tokenCredentialId: 'slack-token', message: "❌ *react* ${params.ROLLBACK_TARGET} 롤백 실패\n<${env.BUILD_URL}|빌드 #${env.BUILD_NUMBER}>" } } } } }