// ===================================================================== // ANTOOZA Frontend Jenkinsfile (PROD) // - Branches: main only (dev 는 Frontend/Jenkinsfile 사용) // - Agent: PROD-Window-Server (Node v24) // - Deploy path: C:\workspace\IIS\Front // - Process: WinSW service 'antooza-frontend' (node server.js on :3000) // - IIS: ANTOOZA-Front site reverse-proxies HTTPS:443 -> 127.0.0.1:3000 // ===================================================================== pipeline { agent { label 'PROD-Window-Server' } options { timestamps() disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '20')) } environment { DEPLOY_ROOT = 'C:\\workspace\\IIS\\Front' RELEASES = 'C:\\workspace\\IIS\\_releases' SVC_NAME = 'antooza-frontend' } stages { stage('Install') { steps { bat 'chcp 65001 >NUL && node -v && npm -v' script { def lockChanged = bat(returnStatus: true, script: '@git diff --quiet HEAD~1 HEAD -- package-lock.json 2>nul') != 0 def hasModules = fileExists('node_modules/.package-lock.json') if (!hasModules || lockChanged) { echo "Installing (lockChanged=${lockChanged}, hasModules=${hasModules})" bat 'chcp 65001 >NUL && npm ci --no-audit --no-fund --prefer-offline' } else { echo 'package-lock.json unchanged + node_modules present — skip npm ci' } } } } stage('Inject Env (Secret File)') { steps { script { // PROD 전용 — 항상 PROD-ANTOOZA-FRONTEND-ENV 사용 def credId = 'PROD-ANTOOZA-FRONTEND-ENV' echo "[env] Using Jenkins Secret File: ${credId}" withCredentials([file(credentialsId: credId, variable: 'CFG')]) { bat 'chcp 65001 >NUL && copy /Y "%CFG%" .env.production >NUL && for %%I in (.env.production) do @echo [env] .env.production injected (%%~zI bytes)' } } } } stage('Build') { steps { bat 'chcp 65001 >NUL && npm run build' } } stage('Deploy') { steps { script { deployFrontend() } } } } post { success { echo "PROD Frontend build & deploy completed" } failure { echo 'PROD Frontend build/deploy failed - check stage logs' } } } def deployFrontend() { powershell """ \$ErrorActionPreference = 'Stop' \$ts = Get-Date -Format 'yyyyMMdd-HHmmss' \$dest = '${env.DEPLOY_ROOT}' \$backup = Join-Path '${env.RELEASES}' ('${env.SVC_NAME}-' + \$ts) if (-not (Test-Path '${env.RELEASES}')) { New-Item -ItemType Directory -Path '${env.RELEASES}' -Force | Out-Null } Write-Host '[${env.SVC_NAME}] Stopping service...' Stop-Service ${env.SVC_NAME} -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 # Preserve infrastructure files (WinSW + IIS web.config + logs) \$keep = @('${env.SVC_NAME}.exe', '${env.SVC_NAME}.xml', 'web.config', '${env.SVC_NAME}.err.log', '${env.SVC_NAME}.out.log', '${env.SVC_NAME}.wrapper.log') if ((Test-Path \$dest) -and ((Get-ChildItem \$dest -Force | Measure-Object).Count -gt 0)) { Write-Host ('[${env.SVC_NAME}] Backing up current to ' + \$backup) New-Item -ItemType Directory -Path \$backup -Force | Out-Null Get-ChildItem \$dest -Force | Where-Object { \$_.Name -notin \$keep } | Move-Item -Destination \$backup -Force } elseif (-not (Test-Path \$dest)) { New-Item -ItemType Directory -Path \$dest -Force | Out-Null } # Next.js standalone layout Write-Host '[${env.SVC_NAME}] Copying standalone server...' Copy-Item '.next\\standalone\\*' \$dest -Recurse -Force Write-Host '[${env.SVC_NAME}] Copying static assets...' New-Item -ItemType Directory -Path (Join-Path \$dest '.next\\static') -Force | Out-Null Copy-Item '.next\\static\\*' (Join-Path \$dest '.next\\static') -Recurse -Force Write-Host '[${env.SVC_NAME}] Copying public assets...' New-Item -ItemType Directory -Path (Join-Path \$dest 'public') -Force | Out-Null if (Test-Path 'public') { Copy-Item 'public\\*' (Join-Path \$dest 'public') -Recurse -Force } if (Test-Path '.env.production') { Write-Host '[${env.SVC_NAME}] Copying .env.production...' Copy-Item '.env.production' (Join-Path \$dest '.env.production') -Force } else { Write-Warning '[${env.SVC_NAME}] .env.production missing in workspace' } # First-deploy auto-install WinSW if (-not (Get-Service ${env.SVC_NAME} -ErrorAction SilentlyContinue)) { Write-Host '[${env.SVC_NAME}] Service not registered - installing' \$installer = Join-Path \$dest '${env.SVC_NAME}.exe' if (-not (Test-Path \$installer)) { throw ('[${env.SVC_NAME}] WinSW binary missing at ' + \$installer) } & \$installer install if (\$LASTEXITCODE -ne 0) { throw ('[${env.SVC_NAME}] install failed (exit ' + \$LASTEXITCODE + ')') } Start-Sleep -Seconds 2 } Write-Host '[${env.SVC_NAME}] Starting service...' Start-Service ${env.SVC_NAME} # Health check \$ok = \$false 1..15 | ForEach-Object { Start-Sleep -Seconds 3 try { \$r = Invoke-WebRequest -Uri 'http://127.0.0.1:3000/' -UseBasicParsing -TimeoutSec 5 -MaximumRedirection 0 if (\$r.StatusCode -ge 200 -and \$r.StatusCode -lt 400) { Write-Host ('[${env.SVC_NAME}] Healthy (HTTP ' + \$r.StatusCode + ') on attempt ' + \$_) \$ok = \$true; return } } catch { \$resp = \$_.Exception.Response if (\$resp) { \$code = [int]\$resp.StatusCode if (\$code -ge 200 -and \$code -lt 400) { Write-Host ('[${env.SVC_NAME}] Healthy (HTTP ' + \$code + ') on attempt ' + \$_) \$ok = \$true; return } } Write-Host ('[${env.SVC_NAME}] attempt ' + \$_ + ' failed: ' + \$_.Exception.Message) } } if (-not \$ok) { Write-Warning '[${env.SVC_NAME}] Verification failed - rolling back' Stop-Service ${env.SVC_NAME} -Force -ErrorAction SilentlyContinue if (Test-Path \$backup) { Get-ChildItem \$dest -Force | Where-Object { \$_.Name -notin \$keep } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue Copy-Item (Join-Path \$backup '*') \$dest -Recurse -Force Start-Service ${env.SVC_NAME} } throw '${env.SVC_NAME} deployment failed; rolled back.' } Get-ChildItem '${env.RELEASES}' -Directory -ErrorAction SilentlyContinue | Where-Object { \$_.Name -like '${env.SVC_NAME}-*' } | Sort-Object LastWriteTime -Descending | Select-Object -Skip 5 | Remove-Item -Recurse -Force """ }