Jenkinsfile 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // =====================================================================
  2. // ANTOOZA Frontend Jenkinsfile (Next.js 15 standalone + WinSW + IIS reverse proxy)
  3. // - Branches: main / dev -> deploy
  4. // others -> build/test only
  5. // - Agent: CTL-Window-Server (Node v24)
  6. // - Deploy path: F:\IIS\ANTOOZA\Front
  7. // - Process: WinSW service 'antooza-frontend' (node server.js on :3000)
  8. // - IIS: ANTOOZA-Front site reverse-proxies HTTPS:443 -> 127.0.0.1:3000
  9. // =====================================================================
  10. pipeline {
  11. agent { label 'CTL-Window-Server' }
  12. options {
  13. timestamps()
  14. disableConcurrentBuilds()
  15. buildDiscarder(logRotator(numToKeepStr: '20'))
  16. }
  17. environment {
  18. DEPLOY_ROOT = 'F:\\IIS\\ANTOOZA\\Front'
  19. RELEASES = 'F:\\IIS\\ANTOOZA\\_releases'
  20. SVC_NAME = 'antooza-frontend'
  21. }
  22. stages {
  23. stage('Install') {
  24. steps {
  25. bat 'chcp 65001 >NUL && node -v && npm -v'
  26. script {
  27. // package-lock.json 변경 + node_modules 존재 여부 조합으로 npm ci 스킵.
  28. // 첫 빌드 또는 lock 변경 시에만 full install (캐시 활용 위해 --prefer-offline).
  29. def lockChanged = bat(returnStatus: true,
  30. script: '@git diff --quiet HEAD~1 HEAD -- package-lock.json 2>nul') != 0
  31. def hasModules = fileExists('node_modules/.package-lock.json')
  32. if (!hasModules || lockChanged) {
  33. echo "Installing (lockChanged=${lockChanged}, hasModules=${hasModules})"
  34. bat 'chcp 65001 >NUL && npm ci --no-audit --no-fund --prefer-offline'
  35. } else {
  36. echo 'package-lock.json unchanged + node_modules present — skip npm ci'
  37. }
  38. }
  39. }
  40. }
  41. stage('Inject Env (Secret File)') {
  42. when { anyOf { branch 'main'; branch 'dev' } }
  43. steps {
  44. script {
  45. // Backend 패턴과 동일: 브랜치별로 Jenkins Secret File 주입
  46. // dev → DEV-ANTOOZA-FRONTEND-ENV (dev-api.antooza.com)
  47. // main → PROD-ANTOOZA-FRONTEND-ENV (api.antooza.com)
  48. // .env.production 은 .gitignore 됨 (Backend 의 appsettings.Production.json 처럼 빌드 시점에만 존재).
  49. def credId = (env.BRANCH_NAME == 'main') ? 'PROD-ANTOOZA-FRONTEND-ENV' : 'DEV-ANTOOZA-FRONTEND-ENV'
  50. echo "[env] Using Jenkins Secret File: ${credId}"
  51. withCredentials([file(credentialsId: credId, variable: 'CFG')]) {
  52. bat 'chcp 65001 >NUL && copy /Y "%CFG%" .env.production >NUL && for %%I in (.env.production) do @echo [env] .env.production injected (%%~zI bytes)'
  53. }
  54. }
  55. }
  56. }
  57. stage('Build') {
  58. steps {
  59. bat 'chcp 65001 >NUL && npm run build'
  60. }
  61. }
  62. stage('Deploy') {
  63. when { anyOf { branch 'main'; branch 'dev' } }
  64. steps {
  65. script { deployFrontend() }
  66. }
  67. }
  68. }
  69. post {
  70. success { echo "Frontend build & deploy completed (branch=${env.BRANCH_NAME ?: 'unknown'})" }
  71. failure { echo 'Frontend build/deploy failed - check stage logs' }
  72. }
  73. }
  74. def deployFrontend() {
  75. powershell """
  76. \$ErrorActionPreference = 'Stop'
  77. \$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
  78. \$dest = '${env.DEPLOY_ROOT}'
  79. \$backup = Join-Path '${env.RELEASES}' ('${env.SVC_NAME}-' + \$ts)
  80. if (-not (Test-Path '${env.RELEASES}')) { New-Item -ItemType Directory -Path '${env.RELEASES}' -Force | Out-Null }
  81. Write-Host '[${env.SVC_NAME}] Stopping service...'
  82. Stop-Service ${env.SVC_NAME} -Force -ErrorAction SilentlyContinue
  83. Start-Sleep -Seconds 2
  84. # Preserve infrastructure files (WinSW + web.config + logs)
  85. \$keep = @('${env.SVC_NAME}.exe', '${env.SVC_NAME}.xml', 'web.config',
  86. '${env.SVC_NAME}.err.log', '${env.SVC_NAME}.out.log', '${env.SVC_NAME}.wrapper.log')
  87. if ((Test-Path \$dest) -and ((Get-ChildItem \$dest -Force | Measure-Object).Count -gt 0)) {
  88. Write-Host ('[${env.SVC_NAME}] Backing up current to ' + \$backup)
  89. New-Item -ItemType Directory -Path \$backup -Force | Out-Null
  90. Get-ChildItem \$dest -Force | Where-Object { \$_.Name -notin \$keep } |
  91. Move-Item -Destination \$backup -Force
  92. } elseif (-not (Test-Path \$dest)) {
  93. New-Item -ItemType Directory -Path \$dest -Force | Out-Null
  94. }
  95. # Next.js standalone layout:
  96. # .next/standalone/ -> server.js + minimal node_modules + .next folder (server bundles)
  97. # .next/static/ -> static assets (must be copied to <dest>/.next/static)
  98. # public/ -> public assets (must be copied to <dest>/public)
  99. Write-Host '[${env.SVC_NAME}] Copying standalone server...'
  100. Copy-Item '.next\\standalone\\*' \$dest -Recurse -Force
  101. Write-Host '[${env.SVC_NAME}] Copying static assets...'
  102. New-Item -ItemType Directory -Path (Join-Path \$dest '.next\\static') -Force | Out-Null
  103. Copy-Item '.next\\static\\*' (Join-Path \$dest '.next\\static') -Recurse -Force
  104. Write-Host '[${env.SVC_NAME}] Copying public assets...'
  105. New-Item -ItemType Directory -Path (Join-Path \$dest 'public') -Force | Out-Null
  106. if (Test-Path 'public') {
  107. Copy-Item 'public\\*' (Join-Path \$dest 'public') -Recurse -Force
  108. }
  109. # Server-side env (.env.production) — Next.js standalone runtime reads from cwd.
  110. # NEXT_PUBLIC_* 은 빌드 시 inline 되지만, server-side (API_URL, SIGNALR_CHAT_URL 등) 는 런타임 로드.
  111. if (Test-Path '.env.production') {
  112. Write-Host '[${env.SVC_NAME}] Copying .env.production...'
  113. Copy-Item '.env.production' (Join-Path \$dest '.env.production') -Force
  114. } else {
  115. Write-Warning '[${env.SVC_NAME}] .env.production missing in workspace - server-side env will fall back to OS/WinSW env'
  116. }
  117. # Server-side env (.env.production) — Next.js standalone runtime reads from cwd
  118. # NEXT_PUBLIC_* 은 빌드 시 inline 되지만, server-side (API_URL, SIGNALR_CHAT_URL 등) 는 런타임 로드
  119. if (Test-Path '.env.production') {
  120. Write-Host '[${env.SVC_NAME}] Copying .env.production...'
  121. Copy-Item '.env.production' (Join-Path \$dest '.env.production') -Force
  122. } else {
  123. Write-Warning '[${env.SVC_NAME}] .env.production missing in workspace - server-side env will fall back to OS/WinSW env'
  124. }
  125. # First-deploy auto-install WinSW
  126. if (-not (Get-Service ${env.SVC_NAME} -ErrorAction SilentlyContinue)) {
  127. Write-Host '[${env.SVC_NAME}] Service not registered - installing'
  128. \$installer = Join-Path \$dest '${env.SVC_NAME}.exe'
  129. if (-not (Test-Path \$installer)) {
  130. throw ('[${env.SVC_NAME}] WinSW binary missing at ' + \$installer + ' — place ${env.SVC_NAME}.exe (renamed WinSW) + ${env.SVC_NAME}.xml in ' + \$dest)
  131. }
  132. & \$installer install
  133. if (\$LASTEXITCODE -ne 0) { throw ('[${env.SVC_NAME}] install failed (exit ' + \$LASTEXITCODE + ')') }
  134. Start-Sleep -Seconds 2
  135. }
  136. Write-Host '[${env.SVC_NAME}] Starting service...'
  137. Start-Service ${env.SVC_NAME}
  138. # Health check: HTTP 200/3xx on http://127.0.0.1:3000/
  139. \$ok = \$false
  140. 1..15 | ForEach-Object {
  141. Start-Sleep -Seconds 3
  142. try {
  143. \$r = Invoke-WebRequest -Uri 'http://127.0.0.1:3000/' -UseBasicParsing -TimeoutSec 5 -MaximumRedirection 0
  144. if (\$r.StatusCode -ge 200 -and \$r.StatusCode -lt 400) {
  145. Write-Host ('[${env.SVC_NAME}] Healthy (HTTP ' + \$r.StatusCode + ') on attempt ' + \$_)
  146. \$ok = \$true; return
  147. }
  148. } catch {
  149. \$resp = \$_.Exception.Response
  150. if (\$resp) {
  151. \$code = [int]\$resp.StatusCode
  152. if (\$code -ge 200 -and \$code -lt 400) {
  153. Write-Host ('[${env.SVC_NAME}] Healthy (HTTP ' + \$code + ') on attempt ' + \$_)
  154. \$ok = \$true; return
  155. }
  156. }
  157. Write-Host ('[${env.SVC_NAME}] attempt ' + \$_ + ' failed: ' + \$_.Exception.Message)
  158. }
  159. }
  160. if (-not \$ok) {
  161. Write-Warning '[${env.SVC_NAME}] Verification failed - rolling back'
  162. Stop-Service ${env.SVC_NAME} -Force -ErrorAction SilentlyContinue
  163. if (Test-Path \$backup) {
  164. Get-ChildItem \$dest -Force | Where-Object { \$_.Name -notin \$keep } |
  165. Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
  166. Copy-Item (Join-Path \$backup '*') \$dest -Recurse -Force
  167. Start-Service ${env.SVC_NAME}
  168. }
  169. throw '${env.SVC_NAME} deployment failed; rolled back.'
  170. }
  171. # Retain only 5 recent backups
  172. Get-ChildItem '${env.RELEASES}' -Directory -ErrorAction SilentlyContinue |
  173. Where-Object { \$_.Name -like '${env.SVC_NAME}-*' } |
  174. Sort-Object LastWriteTime -Descending |
  175. Select-Object -Skip 5 |
  176. Remove-Item -Recurse -Force
  177. """
  178. }