pipeline { agent any // 환경 변수 설정 (실제 환경 정보 반영) environment { // ACR 정보 ACR_REGISTRY = 'acrhealthsync01.azurecr.io' ACR_REPOSITORY = 'healthsync-frontend' RESOURCE_GROUP = 'rg-digitalgarage-01' // SonarQube 정보 SONARQUBE_SERVER = 'http://20.249.161.128:9000' SONAR_PROJECT_KEY = 'Healthsync-front' // Docker 이미지 관련 IMAGE_TAG = "${BUILD_NUMBER}-${GIT_COMMIT.take(7)}" IMAGE_NAME = "${ACR_REGISTRY}/${ACR_REPOSITORY}:${IMAGE_TAG}" LATEST_IMAGE = "${ACR_REGISTRY}/${ACR_REPOSITORY}:latest" // Node.js 환경 설정 NODE_VERSION = '18' NODE_OPTIONS = '--max-old-space-size=4096' // 빌드 환경 설정 CI = 'true' GENERATE_SOURCEMAP = 'false' INLINE_RUNTIME_CHUNK = 'false' // 타임스탬프 BUILD_TIMESTAMP = new Date().format("yyyy-MM-dd_HH-mm-ss") // 프로젝트 정보 PROJECT_NAME = 'HealthSync Frontend' BUILD_USER = "${env.BUILD_USER ?: 'Jenkins'}" } options { // 빌드 기록 보관 (최근 15개) buildDiscarder(logRotator(numToKeepStr: '15', daysToKeepStr: '30')) // 타임아웃 설정 (30분) timeout(time: 30, unit: 'MINUTES') // 동시 실행 방지 disableConcurrentBuilds() // 기본 체크아웃 건너뛰기 (수동으로 처리) skipDefaultCheckout() } stages { stage('🔍 Checkout & Validation') { steps { script { echo "🚀 === ${PROJECT_NAME} CI/CD 파이프라인 시작 ===" echo "📋 빌드 정보:" echo " • 빌드 번호: #${BUILD_NUMBER}" echo " • 브랜치: ${env.BRANCH_NAME ?: 'N/A'}" echo " • 시작 시간: ${BUILD_TIMESTAMP}" echo " • 트리거: ${BUILD_USER}" } // 소스코드 체크아웃 checkout scm script { // Git 정보 확인 env.GIT_COMMIT = sh(returnStdout: true, script: 'git rev-parse HEAD').trim() env.GIT_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() env.GIT_AUTHOR = sh(returnStdout: true, script: 'git log -1 --pretty=format:"%an"').trim() echo "📝 Git 정보:" echo " • Commit: ${env.GIT_COMMIT}" echo " • Branch: ${env.GIT_BRANCH}" echo " • Author: ${env.GIT_AUTHOR}" // 필수 파일 존재 확인 def requiredFiles = ['package.json', 'src/index.js', 'public/index.html'] requiredFiles.each { file -> if (!fileExists(file)) { error "❌ 필수 파일이 없습니다: ${file}" } } echo "✅ 프로젝트 구조 검증 완료" } } } stage('⚙️ Setup Environment') { steps { script { echo "🔧 === Node.js 환경 설정 ===" // Node.js 버전 확인 def nodeVersion = sh(returnStdout: true, script: 'node --version').trim() def npmVersion = sh(returnStdout: true, script: 'npm --version').trim() echo "📋 환경 정보:" echo " • Node.js: ${nodeVersion}" echo " • npm: ${npmVersion}" echo " • 작업 디렉토리: ${WORKSPACE}" echo " • NODE_OPTIONS: ${NODE_OPTIONS}" // Node.js 버전 검증 (v18 이상 필요) def nodeVersionNumber = nodeVersion.replaceAll(/^v/, '').split('\\.')[0] as Integer if (nodeVersionNumber < 18) { error "❌ Node.js 18 이상이 필요합니다. 현재 버전: ${nodeVersion}" } echo "✅ Node.js 버전 검증 완료" // 프로젝트 정보 확인 def packageJson = readJSON file: 'package.json' echo "📦 프로젝트 정보:" echo " • 이름: ${packageJson.name}" echo " • 버전: ${packageJson.version}" echo " • React: ${packageJson.dependencies?.react ?: 'N/A'}" echo " • Scripts: ${packageJson.scripts?.keySet()?.join(', ')}" } } } stage('📦 Install Dependencies') { steps { script { echo "📥 === 종속성 설치 시작 ===" // npm 캐시 설정 sh ''' echo "🔧 npm 캐시 설정..." npm config set cache ${WORKSPACE}/.npm-cache npm config set prefer-offline true npm config set audit-level moderate ''' // package-lock.json 존재 여부에 따른 설치 방법 선택 def packageLockExists = fileExists('package-lock.json') if (packageLockExists) { echo "📋 package-lock.json 발견 - npm ci 실행" sh ''' echo "⏱️ 설치 시작 시간: $(date)" npm ci --prefer-offline --no-audit --no-fund echo "✅ 설치 완료 시간: $(date)" ''' } else { echo "📋 package-lock.json 없음 - npm install 실행" sh ''' echo "⏱️ 설치 시작 시간: $(date)" npm install --prefer-offline --no-audit --no-fund echo "✅ 설치 완료 시간: $(date)" ''' } // 설치된 패키지 정보 확인 sh ''' echo "📊 설치 완료 통계:" if [ -d node_modules ]; then echo " • node_modules 크기: $(du -sh node_modules 2>/dev/null || echo 'N/A')" echo " • 설치된 패키지 수: $(ls node_modules 2>/dev/null | wc -l || echo '0')" else echo " • node_modules 디렉토리 없음" fi ''' } } post { success { echo "✅ 종속성 설치 성공" } failure { echo "❌ 종속성 설치 실패" } } } stage('🔍 Code Quality Analysis') { parallel { stage('ESLint') { steps { script { echo "🔍 === ESLint 정적 분석 시작 ===" try { // package.json에서 lint 스크립트 확인 def packageJson = readJSON file: 'package.json' def hasLintScript = packageJson.scripts?.containsKey('lint') if (hasLintScript) { echo "📋 package.json에 lint 스크립트 발견" sh 'npm run lint 2>&1 || echo "ESLint 검사 완료 (경고 포함)"' } else { echo "📋 package.json에 lint 스크립트 없음 - 직접 ESLint 실행" // ESLint가 설치되어 있는지 확인 후 실행 sh ''' if [ -f node_modules/.bin/eslint ]; then echo "🔍 직접 ESLint 실행 중..." npx eslint src --ext .js,.jsx,.ts,.tsx --format table 2>&1 || echo "ESLint 검사 완료 (경고 포함)" else echo "⚠️ ESLint가 설치되지 않았습니다. React App의 기본 설정을 사용합니다." fi ''' } echo "✅ ESLint 분석 완료" } catch (Exception e) { echo "⚠️ ESLint 실행 중 오류: ${e.getMessage()}" echo "ℹ️ 빌드를 계속 진행합니다" } } } } stage('Security Audit') { steps { script { echo "🔒 === npm 보안 감사 시작 ===" try { // 보안 취약점 검사 def auditResult = sh( returnStatus: true, script: ''' echo "🔍 보안 취약점 검사 실행 중..." npm audit --audit-level=moderate --json > audit-results.json 2>/dev/null || true npm audit --audit-level=moderate ''' ) if (auditResult == 0) { echo "✅ 보안 취약점 없음" } else { echo "⚠️ 보안 취약점 발견 - 검토 필요" echo "ℹ️ 자세한 내용은 'npm audit' 결과를 확인하세요" } // 감사 결과 아카이브 (파일이 존재하는 경우에만) if (fileExists('audit-results.json')) { archiveArtifacts artifacts: 'audit-results.json', allowEmptyArchive: true } } catch (Exception e) { echo "⚠️ 보안 감사 중 오류: ${e.getMessage()}" } } } } stage('Dependency Check') { steps { script { echo "📋 === 종속성 검사 시작 ===" sh ''' echo "📊 종속성 분석..." npm ls --depth=0 2>/dev/null || echo "일부 종속성 문제 발견" echo "" echo "📋 주요 종속성 버전:" npm list react react-dom react-scripts 2>/dev/null || echo "React 정보 확인 불가" echo "" echo "🔍 outdated 패키지 확인:" npm outdated 2>/dev/null || echo "모든 패키지가 최신 상태이거나 확인 불가" ''' } } } } } stage('🧪 Run Tests') { steps { script { echo "🧪 === 유닛 테스트 실행 시작 ===" try { // React 테스트 실행 sh ''' echo "🔧 테스트 환경 설정..." export CI=true export NODE_ENV=test export WATCHMAN_DISABLE_RECRAWL=true echo "🧪 테스트 실행 중..." npm test -- --coverage --watchAll=false --verbose --testResultsProcessor=jest-junit --coverageReporters=text-lcov --coverageReporters=html --testTimeout=30000 2>&1 ''' // 테스트 결과 분석 sh ''' echo "📊 테스트 결과 분석..." if [ -d coverage ]; then echo "✅ 커버리지 리포트 생성 완료" echo "📋 커버리지 요약:" ls -la coverage/ # 커버리지 요약 정보 출력 if [ -f coverage/lcov.info ]; then echo "📊 LCOV 리포트 생성됨" fi if [ -f coverage/lcov-report/index.html ]; then echo "📊 HTML 리포트 생성됨" fi else echo "⚠️ 커버리지 리포트 생성 실패" fi ''' echo "✅ 테스트 실행 완료" } catch (Exception e) { echo "❌ 테스트 실행 실패: ${e.getMessage()}" currentBuild.result = 'UNSTABLE' echo "ℹ️ 빌드는 계속 진행됩니다 (UNSTABLE)" } } } post { always { script { // 테스트 결과 발행 if (fileExists('coverage/lcov-report/index.html')) { publishHTML([ allowMissing: false, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'coverage/lcov-report', reportFiles: 'index.html', reportName: 'Test Coverage Report', reportTitles: 'Coverage Report' ]) echo "📊 커버리지 리포트 발행 완료" } // JUnit 테스트 결과 발행 (파일 존재 시) if (fileExists('junit.xml')) { publishTestResults testResultsPattern: 'junit.xml' echo "📋 테스트 결과 발행 완료" } else if (fileExists('test-results.xml')) { publishTestResults testResultsPattern: 'test-results.xml' echo "📋 테스트 결과 발행 완료" } } } } } stage('📊 SonarQube Analysis') { when { anyOf { branch 'main' branch 'develop' branch 'master' } } steps { script { echo "📊 === SonarQube 코드 품질 분석 시작 ===" // SonarQube 토큰을 Jenkins Secret으로 사용 (수정된 크리덴셜 ID) withCredentials([string(credentialsId: 'sonarqube_access_token', variable: 'SONAR_TOKEN')]) { sh ''' echo "🔧 SonarQube 분석 설정..." echo " • 프로젝트 키: ${SONAR_PROJECT_KEY}" echo " • 서버: ${SONARQUBE_SERVER}" echo " • 브랜치: ${GIT_BRANCH}" # SonarQube Scanner 실행 가능 여부 확인 if command -v sonar-scanner &> /dev/null; then echo "📊 SonarQube Scanner 발견 - 분석 시작..." sonar-scanner \\ -Dsonar.projectKey=${SONAR_PROJECT_KEY} \\ -Dsonar.projectName="HealthSync Frontend" \\ -Dsonar.projectVersion=${BUILD_NUMBER} \\ -Dsonar.sources=src \\ -Dsonar.tests=src \\ -Dsonar.test.inclusions="**/*.test.js,**/*.test.jsx,**/*.spec.js,**/*.spec.jsx" \\ -Dsonar.host.url=${SONARQUBE_SERVER} \\ -Dsonar.login=${SONAR_TOKEN} \\ -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \\ -Dsonar.coverage.exclusions="**/*.test.js,**/*.test.jsx,**/*.spec.js,**/*.spec.jsx,**/node_modules/**,**/public/**,**/build/**" \\ -Dsonar.cpd.exclusions="**/node_modules/**,**/build/**,**/coverage/**" \\ -Dsonar.exclusions="**/node_modules/**,**/build/**,**/coverage/**,**/*.min.js,**/serviceWorker.js,**/reportWebVitals.js" \\ -Dsonar.sourceEncoding=UTF-8 \\ -Dsonar.scm.provider=git \\ -Dsonar.working.directory=${WORKSPACE}/.scannerwork \\ -Dsonar.qualitygate.wait=true echo "✅ SonarQube 분석 완료" else echo "❌ SonarQube Scanner를 찾을 수 없습니다" echo "Jenkins Global Tool Configuration에서 SonarQube Scanner를 설정해주세요" error "SonarQube Scanner 설정 필요" fi ''' } } } post { success { echo "✅ SonarQube 분석 성공" } failure { echo "❌ SonarQube 분석 실패" } } } stage('🚪 Quality Gate') { when { anyOf { branch 'main' branch 'develop' branch 'master' } } steps { script { echo "🚪 === SonarQube 품질 게이트 확인 시작 ===" timeout(time: 10, unit: 'MINUTES') { echo "⏳ 품질 게이트 결과 대기 중..." def qg = waitForQualityGate() echo "📋 품질 게이트 결과: ${qg.status}" if (qg.status != 'OK') { echo "❌ 품질 게이트 실패!" echo "📄 실패 상세:" qg.conditions?.each { condition -> echo " • ${condition.metricKey}: ${condition.actualValue} (기준: ${condition.operator} ${condition.threshold})" } error "SonarQube 품질 게이트를 통과하지 못했습니다: ${qg.status}" } else { echo "✅ 품질 게이트 통과!" echo "🎉 코드 품질 기준을 만족합니다" } } } } } stage('🏗️ Build Application') { steps { script { echo "🏗️ === React 애플리케이션 빌드 시작 ===" try { // 빌드 환경 설정 sh ''' echo "🔧 빌드 환경 설정..." export NODE_ENV=production export GENERATE_SOURCEMAP=false export INLINE_RUNTIME_CHUNK=false export BUILD_PATH=build echo "📋 빌드 설정:" echo " • NODE_ENV: $NODE_ENV" echo " • 소스맵 생성: $GENERATE_SOURCEMAP" echo " • 런타임 청크 인라인: $INLINE_RUNTIME_CHUNK" echo " • 빌드 경로: $BUILD_PATH" ''' // React 프로덕션 빌드 sh ''' echo "🏗️ 프로덕션 빌드 시작..." echo "⏱️ 빌드 시작 시간: $(date)" npm run build echo "⏱️ 빌드 완료 시간: $(date)" echo "✅ 빌드 성공!" ''' // 빌드 결과 분석 sh ''' echo "📊 빌드 결과 분석..." if [ -d build ]; then echo "📁 빌드 디렉토리 내용:" ls -la build/ echo "" echo "📏 빌드 크기 분석:" du -sh build/ if [ -d build/static ]; then echo " • 정적 파일: $(du -sh build/static 2>/dev/null || echo 'N/A')" if [ -d build/static/js ]; then echo " • JS 파일: $(find build/static/js -name '*.js' -exec du -ch {} + 2>/dev/null | tail -1 || echo 'N/A')" fi if [ -d build/static/css ]; then echo " • CSS 파일: $(find build/static/css -name '*.css' -exec du -ch {} + 2>/dev/null | tail -1 || echo 'N/A')" fi fi echo "" echo "📋 주요 파일 목록:" find build -name '*.js' -o -name '*.css' -o -name '*.html' | head -10 else echo "❌ 빌드 디렉토리가 생성되지 않았습니다" exit 1 fi ''' // 빌드 아티팩트 아카이브 archiveArtifacts artifacts: 'build/**/*', fingerprint: true, allowEmptyArchive: false echo "📦 빌드 아티팩트 아카이브 완료" } catch (Exception e) { error "❌ React 빌드 실패: ${e.getMessage()}" } } } post { success { echo "✅ 애플리케이션 빌드 성공" } failure { echo "❌ 애플리케이션 빌드 실패" } } } stage('🐳 Docker Build') { steps { script { echo "🐳 === Docker 이미지 빌드 시작 ===" // 최적화된 Dockerfile 생성 writeFile file: 'Dockerfile', text: ''' # 멀티스테이지 빌드: 프로덕션 스테이지 (빌드는 Jenkins에서 완료) FROM nginx:1.25-alpine # 메타데이터 추가 LABEL maintainer="HealthSync Team" \\ version="1.0" \\ description="HealthSync Frontend Application" # 보안을 위한 사용자 생성 RUN addgroup -g 1001 -S healthsync && \\ adduser -S healthsync -u 1001 -G healthsync # 빌드된 React 애플리케이션 복사 COPY --chown=healthsync:healthsync build/ /usr/share/nginx/html/ # 최적화된 Nginx 설정 복사 COPY --chown=healthsync:healthsync nginx.conf /etc/nginx/nginx.conf # 헬스체크용 curl 설치 및 권한 설정 RUN apk add --no-cache curl && \\ mkdir -p /var/log/nginx /var/cache/nginx /var/run && \\ chown -R healthsync:healthsync /usr/share/nginx/html && \\ chown -R healthsync:healthsync /var/cache/nginx && \\ chown -R healthsync:healthsync /var/log/nginx && \\ chown -R healthsync:healthsync /etc/nginx/conf.d && \\ touch /var/run/nginx.pid && \\ chown healthsync:healthsync /var/run/nginx.pid # 포트 노출 EXPOSE 80 # 헬스체크 추가 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\ CMD curl -f http://localhost:80/ || exit 1 # 사용자 변경 USER healthsync # Nginx 실행 CMD ["nginx", "-g", "daemon off;"] ''' // 최적화된 nginx.conf 생성 writeFile file: 'nginx.conf', text: ''' # Nginx 설정 - HealthSync Frontend 최적화 user healthsync; worker_processes auto; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; use epoll; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; # 로그 포맷 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'rt=$request_time uct="$upstream_connect_time" ' 'uht="$upstream_header_time" urt="$upstream_response_time"'; access_log /var/log/nginx/access.log main; # 성능 최적화 sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 10m; # Gzip 압축 설정 gzip on; gzip_vary on; gzip_min_length 1024; gzip_comp_level 6; gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; # 서버 설정 server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html index.htm; # 보안 헤더 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:" always; # React Router 지원 (SPA) location / { try_files $uri $uri/ /index.html; # 캐시 설정 (HTML은 캐시 안함) location ~* \\.html$ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; } } # 정적 파일 캐싱 location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # API 프록시 (필요시) location /api/ { # API Gateway로 프록시 설정 (실제 환경에 맞게 수정) proxy_pass http://healthsync-api-gateway:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # 헬스체크 엔드포인트 location /health { access_log off; return 200 "healthy\\n"; add_header Content-Type text/plain; } # 에러 페이지 error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } } ''' // Docker 이미지 빌드 sh ''' echo "🔧 Docker 이미지 빌드 준비..." echo " • 이미지 이름: ${IMAGE_NAME}" echo " • Latest 이미지: ${LATEST_IMAGE}" echo " • 빌드 컨텍스트: $(pwd)" echo "🐳 Docker 이미지 빌드 중..." docker build \\ --tag ${IMAGE_NAME} \\ --tag ${LATEST_IMAGE} \\ --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \\ --build-arg VCS_REF=${GIT_COMMIT} \\ --build-arg BUILD_VERSION=${BUILD_NUMBER} \\ . echo "📊 이미지 정보 확인..." docker images | grep ${ACR_REPOSITORY} | head -5 echo "📏 이미지 크기:" docker image inspect ${IMAGE_NAME} --format='Size: {{.Size}} bytes ({{div .Size 1048576}} MB)' echo "✅ Docker 이미지 빌드 완료" ''' } } } stage('🔒 Security Scan') { steps { script { echo "🔒 === Docker 이미지 보안 스캔 시작 ===" try { // Docker 이미지 보안 스캔 sh ''' echo "🔍 Docker 이미지 취약점 스캔..." # Trivy 보안 스캔 (설치되어 있는 경우) if command -v trivy &> /dev/null; then echo "🔍 Trivy로 취약점 스캔 중..." trivy image --exit-code 0 --severity HIGH,CRITICAL --format table ${IMAGE_NAME} trivy image --exit-code 0 --severity HIGH,CRITICAL --format json -o trivy-report.json ${IMAGE_NAME} || true else echo "⚠️ Trivy가 설치되지 않아 보안 스캔을 건너뜁니다" fi # Docker Scout 스캔 (Docker Desktop에 포함) if command -v docker && docker scout version &> /dev/null; then echo "🔍 Docker Scout로 취약점 스캔 중..." docker scout cves ${IMAGE_NAME} || echo "Docker Scout 스캔 완료 (경고 포함)" else echo "ℹ️ Docker Scout를 사용할 수 없습니다" fi echo "✅ 보안 스캔 완료" ''' // 스캔 결과 아카이브 if (fileExists('trivy-report.json')) { archiveArtifacts artifacts: 'trivy-report.json', allowEmptyArchive: true echo "📋 보안 스캔 리포트 아카이브 완료" } } catch (Exception e) { echo "⚠️ 보안 스캔 중 오류: ${e.getMessage()}" echo "ℹ️ 빌드는 계속 진행됩니다" } } } } stage('🚀 Push to ACR') { steps { script { echo "🚀 === Azure Container Registry에 이미지 푸시 시작 ===" // Azure 크리덴셜로 로그인 및 푸시 withCredentials([azureServicePrincipal('Azure-Credential')]) { sh ''' echo "🔑 Azure 서비스 프린시펄로 로그인..." az login --service-principal \\ -u ${AZURE_CLIENT_ID} \\ -p ${AZURE_CLIENT_SECRET} \\ --tenant ${AZURE_TENANT_ID} echo "🔑 ACR 로그인..." az acr login --name acrhealthsync01 echo "📤 이미지 푸시 중..." echo " • 태그별 이미지: ${IMAGE_NAME}" docker push ${IMAGE_NAME} echo " • Latest 이미지: ${LATEST_IMAGE}" docker push ${LATEST_IMAGE} echo "📊 푸시된 이미지 확인..." az acr repository show-tags --name acrhealthsync01 --repository ${ACR_REPOSITORY} --top 5 --orderby time_desc echo "✅ 이미지 푸시 완료!" echo "🎯 푸시된 이미지:" echo " • ${IMAGE_NAME}" echo " • ${LATEST_IMAGE}" ''' } } } post { success { echo "✅ ACR 푸시 성공" } failure { echo "❌ ACR 푸시 실패" } } } stage('📋 Update Deployment') { when { branch 'main' } steps { script { echo "📋 === Kubernetes 배포 매니페스트 업데이트 ===" // GitOps를 위한 매니페스트 업데이트 또는 직접 배포 sh ''' echo "🔄 배포 매니페스트 업데이트..." echo " • 새로운 이미지: ${IMAGE_NAME}" echo " • 빌드 번호: ${BUILD_NUMBER}" echo " • Git 커밋: ${GIT_COMMIT}" # ArgoCD 또는 GitOps를 위한 배포 정보 생성 echo "📝 배포 정보 생성..." cat > deployment-info.yaml << EOF apiVersion: apps/v1 kind: Deployment metadata: name: healthsync-frontend annotations: deployment.jenkins.build: '${BUILD_NUMBER}' deployment.jenkins.commit: '${GIT_COMMIT}' deployment.jenkins.timestamp: '${BUILD_TIMESTAMP}' deployment.jenkins.image: '${IMAGE_NAME}' spec: template: spec: containers: - name: healthsync-frontend image: ${IMAGE_NAME} imagePullPolicy: Always EOF echo "📄 생성된 배포 정보:" cat deployment-info.yaml ''' // 배포 정보 아카이브 archiveArtifacts artifacts: 'deployment-info.yaml', allowEmptyArchive: true } } } } post { always { script { echo "🧹 === 파이프라인 정리 작업 시작 ===" // 빌드 통계 출력 def buildDuration = currentBuild.duration ? "${(currentBuild.duration / 1000).intValue()}초" : "계산 중" echo "📊 빌드 통계:" echo " • 총 소요 시간: ${buildDuration}" echo " • 최종 상태: ${currentBuild.result ?: 'SUCCESS'}" echo " • 빌드 URL: ${env.BUILD_URL}" echo " • 워크스페이스: ${WORKSPACE}" // Docker 이미지 정리 (디스크 공간 확보) sh ''' echo "🐳 Docker 정리..." docker system prune -f --volumes || echo "Docker 정리 완료" echo "📊 디스크 사용량:" df -h ${WORKSPACE} || echo "디스크 정보 확인 불가" ''' // 워크스페이스 정리 try { cleanWs( cleanWhenNotBuilt: false, deleteDirs: true, disableDeferredWipeout: true, notFailBuild: true, patterns: [ [pattern: 'node_modules', type: 'INCLUDE'], [pattern: '.npm-cache', type: 'INCLUDE'], [pattern: 'coverage', type: 'INCLUDE'], [pattern: '.scannerwork', type: 'INCLUDE'], [pattern: 'build', type: 'INCLUDE'] ] ) echo "✅ 워크스페이스 정리 완료" } catch (Exception e) { echo "⚠️ 워크스페이스 정리 중 오류: ${e.getMessage()}" } } } success { script { echo "🎉 === 파이프라인 성공적으로 완료! ===" echo "✅ 모든 단계가 성공했습니다" echo "🚀 배포 준비 완료: ${IMAGE_NAME}" def buildDurationMinutes = currentBuild.duration ? "${(currentBuild.duration / 60000).intValue()}분 ${((currentBuild.duration % 60000) / 1000).intValue()}초" : "계산 중" // 성공 알림 (Slack, Teams 등) try { def message = """ 🎉 *HealthSync Frontend 빌드 성공!* 📋 *빌드 정보:* • 브랜치: `${env.BRANCH_NAME ?: 'N/A'}` • 빌드: `#${env.BUILD_NUMBER}` • 커밋: `${env.GIT_COMMIT?.take(8) ?: 'N/A'}` • 작성자: `${env.GIT_AUTHOR ?: 'N/A'}` 🐳 *이미지 정보:* • 태그: `${IMAGE_TAG}` • 레지스트리: `${ACR_REGISTRY}` ⏱️ *소요 시간:* ${buildDurationMinutes} 🔗 *빌드 링크:* ${env.BUILD_URL} ✅ 프로덕션 배포 준비 완료! """ // Slack 알림 (환경 변수 설정 시) if (env.SLACK_CHANNEL) { slackSend( channel: env.SLACK_CHANNEL, color: 'good', message: message ) } else { echo "📢 Slack 채널이 설정되지 않아 알림을 건너뜁니다" } } catch (Exception e) { echo "📢 알림 전송 실패: ${e.getMessage()}" } } } failure { script { echo "❌ === 파이프라인 실패! ===" echo "💥 빌드가 실패했습니다" echo "🔍 실패 단계: ${env.STAGE_NAME ?: '알 수 없음'}" def buildDurationMinutes = currentBuild.duration ? "${(currentBuild.duration / 60000).intValue()}분 ${((currentBuild.duration % 60000) / 1000).intValue()}초" : "계산 중" // 실패 알림 try { def message = """ ❌ *HealthSync Frontend 빌드 실패!* 📋 *빌드 정보:* • 브랜치: `${env.BRANCH_NAME ?: 'N/A'}` • 빌드: `#${env.BUILD_NUMBER}` • 실패 단계: `${env.STAGE_NAME ?: '알 수 없음'}` • 커밋: `${env.GIT_COMMIT?.take(8) ?: 'N/A'}` ⏱️ *소요 시간:* ${buildDurationMinutes} 🔗 *로그 확인:* ${env.BUILD_URL}console 🛠️ 개발팀 확인 필요! """ // Slack 알림 (환경 변수 설정 시) if (env.SLACK_CHANNEL) { slackSend( channel: env.SLACK_CHANNEL, color: 'danger', message: message ) } else { echo "📢 Slack 채널이 설정되지 않아 알림을 건너뜁니다" } } catch (Exception e) { echo "📢 실패 알림 전송 실패: ${e.getMessage()}" } } } unstable { script { echo "⚠️ === 파이프라인이 불안정한 상태로 완료 ===" echo "🔶 일부 테스트 실패 또는 경고 발생" def buildDurationMinutes = currentBuild.duration ? "${(currentBuild.duration / 60000).intValue()}분 ${((currentBuild.duration % 60000) / 1000).intValue()}초" : "계산 중" // 불안정 상태 알림 try { def message = """ ⚠️ *HealthSync Frontend 빌드 불안정* 📋 *빌드 정보:* • 브랜치: `${env.BRANCH_NAME ?: 'N/A'}` • 빌드: `#${env.BUILD_NUMBER}` • 상태: `UNSTABLE` 💡 *가능한 원인:* • 일부 테스트 실패 • 코드 품질 경고 • 보안 스캔 경고 ⏱️ *소요 시간:* ${buildDurationMinutes} 🔗 *상세 확인:* ${env.BUILD_URL} 🔍 검토 후 조치 필요 """ // Slack 알림 (환경 변수 설정 시) if (env.SLACK_CHANNEL) { slackSend( channel: env.SLACK_CHANNEL, color: 'warning', message: message ) } else { echo "📢 Slack 채널이 설정되지 않아 알림을 건너뜁니다" } } catch (Exception e) { echo "📢 불안정 알림 전송 실패: ${e.getMessage()}" } } } } }