Files
sam-react-prod/Jenkinsfile
권혁성 c3266e5d3f deploy: 2026-03-12 배포
- feat: [QMS] 점검표 템플릿 Mock→API 연동 + 로트심사 UI 개선
- feat: [견적] 제어기 타입 변경 + 가이드레일 제품연동 + 수식보기 개선
- feat: [생산/출하] 작업자 화면 step 서버 토글 + 출하 수주 조인 연동
- feat: [배포] Jenkinsfile 롤백 기능 추가
- feat: [입고] 성적서 파일 백엔드 연동 + CSP 도메인 허용
- feat: ESLint 정리 및 전체 코드 품질 개선
- fix: [QMS] 제품검사 성적서 렌더링 개선 + 빌드 타입 에러 수정
- fix: [품질검사] LegacyPhotoUpload images undefined 에러 수정
- fix: middleware publicRoutes 타입 에러 수정
2026-03-12 15:21:20 +09:00

235 lines
11 KiB
Groovy

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}>"
}
}
}
}
}