Jenkinsfile 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. // =====================================================================
  2. // BITFORUM 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: C:\workspace\IIS\bitforum-front (?쒕쾭 愿€濡€)
  7. // - Process: WinSW service 'bitforum-frontend' (node server.js on :3001)
  8. // - IIS: bitforum-front site (URL Rewrite) reverse-proxies :80 -> 127.0.0.1:3001
  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 = 'C:\\workspace\\IIS\\bitforum-front'
  19. RELEASES = 'C:\\workspace\\IIS\\_releases'
  20. SVC_NAME = 'bitforum-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 { expression { !env.BRANCH_NAME || env.BRANCH_NAME in ['main', 'dev'] } }
  43. steps {
  44. script {
  45. // Backend ?⑦꽩怨??숈씪: 釉뚮옖移섎퀎濡?Jenkins Secret File 二쇱엯
  46. // dev ??DEV-BITFORUM-FRONTEND-ENV (api.bitforum.io ??dev ?쒕쾭 ?댁쁺 ?꾨찓??
  47. // main ??PROD-BITFORUM-FRONTEND-ENV (api.bitforum.io)
  48. // .env.production ?€ .gitignore ??(Backend ??appsettings.Production.json 泥섎읆 鍮뚮뱶 ?쒖젏?먮쭔 議댁옱).
  49. def credId = (env.BRANCH_NAME == 'main') ? 'PROD-BITFORUM-FRONTEND-ENV' : 'DEV-BITFORUM-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 { expression { !env.BRANCH_NAME || env.BRANCH_NAME in ['main', '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. # Infra files (IIS reverse-proxy web.config + WinSW service xml) - carried in repo, placed into dest.
  126. Write-Host '[${env.SVC_NAME}] Copying infra files (web.config + WinSW xml)...'
  127. if (Test-Path 'web.config') { Copy-Item 'web.config' (Join-Path \$dest 'web.config') -Force }
  128. if (Test-Path '${env.SVC_NAME}.xml') { Copy-Item '${env.SVC_NAME}.xml' (Join-Path \$dest '${env.SVC_NAME}.xml') -Force }
  129. # Auto-provision WinSW binary from antooza-front (same WinSW exe, renamed) if missing.
  130. \$svcExe = Join-Path \$dest '${env.SVC_NAME}.exe'
  131. if (-not (Test-Path \$svcExe)) {
  132. \$srcExe = 'C:\\workspace\\IIS\\antooza-front\\antooza-frontend.exe'
  133. if (Test-Path \$srcExe) { Write-Host '[${env.SVC_NAME}] Provisioning WinSW binary from antooza-front'; Copy-Item \$srcExe \$svcExe -Force }
  134. }
  135. # First-deploy auto-install WinSW
  136. if (-not (Get-Service ${env.SVC_NAME} -ErrorAction SilentlyContinue)) {
  137. Write-Host '[${env.SVC_NAME}] Service not registered - installing'
  138. \$installer = Join-Path \$dest '${env.SVC_NAME}.exe'
  139. if (-not (Test-Path \$installer)) {
  140. throw ('[${env.SVC_NAME}] WinSW binary missing at ' + \$installer + ' ??place ${env.SVC_NAME}.exe (renamed WinSW) + ${env.SVC_NAME}.xml in ' + \$dest)
  141. }
  142. & \$installer install
  143. if (\$LASTEXITCODE -ne 0) { throw ('[${env.SVC_NAME}] install failed (exit ' + \$LASTEXITCODE + ')') }
  144. Start-Sleep -Seconds 2
  145. }
  146. Write-Host '[${env.SVC_NAME}] Starting service...'
  147. Start-Service ${env.SVC_NAME}
  148. # Health check: HTTP 200/3xx on http://127.0.0.1:3001/
  149. \$ok = \$false
  150. 1..15 | ForEach-Object {
  151. Start-Sleep -Seconds 3
  152. try {
  153. \$r = Invoke-WebRequest -Uri 'http://127.0.0.1:3001/' -UseBasicParsing -TimeoutSec 5 -MaximumRedirection 0
  154. if (\$r.StatusCode -ge 200 -and \$r.StatusCode -lt 400) {
  155. Write-Host ('[${env.SVC_NAME}] Healthy (HTTP ' + \$r.StatusCode + ') on attempt ' + \$_)
  156. \$ok = \$true; return
  157. }
  158. } catch {
  159. \$resp = \$_.Exception.Response
  160. if (\$resp) {
  161. \$code = [int]\$resp.StatusCode
  162. if (\$code -ge 200 -and \$code -lt 400) {
  163. Write-Host ('[${env.SVC_NAME}] Healthy (HTTP ' + \$code + ') on attempt ' + \$_)
  164. \$ok = \$true; return
  165. }
  166. }
  167. Write-Host ('[${env.SVC_NAME}] attempt ' + \$_ + ' failed: ' + \$_.Exception.Message)
  168. }
  169. }
  170. if (-not \$ok) {
  171. Write-Warning '[${env.SVC_NAME}] Verification failed - rolling back'
  172. Stop-Service ${env.SVC_NAME} -Force -ErrorAction SilentlyContinue
  173. if (Test-Path \$backup) {
  174. Get-ChildItem \$dest -Force | Where-Object { \$_.Name -notin \$keep } |
  175. Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
  176. Copy-Item (Join-Path \$backup '*') \$dest -Recurse -Force
  177. Start-Service ${env.SVC_NAME}
  178. }
  179. throw '${env.SVC_NAME} deployment failed; rolled back.'
  180. }
  181. # Retain only 5 recent backups
  182. Get-ChildItem '${env.RELEASES}' -Directory -ErrorAction SilentlyContinue |
  183. Where-Object { \$_.Name -like '${env.SVC_NAME}-*' } |
  184. Sort-Object LastWriteTime -Descending |
  185. Select-Object -Skip 5 |
  186. Remove-Item -Recurse -Force
  187. """
  188. }