| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- // =====================================================================
- // BITFORUM Frontend Jenkinsfile (Next.js 15 standalone + WinSW + IIS reverse proxy)
- // - Branches: main / dev -> deploy
- // others -> build/test only
- // - Agent: CTL-Window-Server (Node v24)
- // - Deploy path: C:\workspace\IIS\bitforum-front (?쒕쾭 愿濡)
- // - Process: WinSW service 'bitforum-frontend' (node server.js on :3001)
- // - IIS: bitforum-front site (URL Rewrite) reverse-proxies :80 -> 127.0.0.1:3001
- // =====================================================================
- pipeline {
- agent { label 'CTL-Window-Server' }
- options {
- timestamps()
- disableConcurrentBuilds()
- buildDiscarder(logRotator(numToKeepStr: '20'))
- }
- environment {
- DEPLOY_ROOT = 'C:\\workspace\\IIS\\bitforum-front'
- RELEASES = 'C:\\workspace\\IIS\\_releases'
- SVC_NAME = 'bitforum-frontend'
- }
- stages {
- stage('Install') {
- steps {
- bat 'chcp 65001 >NUL && node -v && npm -v'
- script {
- // package-lock.json 蹂寃?+ node_modules 議댁옱 ?щ? 議고빀?쇰줈 npm ci ?ㅽ궢.
- // 泥?鍮뚮뱶 ?먮뒗 lock 蹂寃??쒖뿉留?full install (罹먯떆 ?쒖슜 ?꾪빐 --prefer-offline).
- 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)') {
- when { expression { !env.BRANCH_NAME || env.BRANCH_NAME in ['main', 'dev'] } }
- steps {
- script {
- // Backend ?⑦꽩怨??숈씪: 釉뚮옖移섎퀎濡?Jenkins Secret File 二쇱엯
- // dev ??DEV-BITFORUM-FRONTEND-ENV (api.bitforum.io ??dev ?쒕쾭 ?댁쁺 ?꾨찓??
- // main ??PROD-BITFORUM-FRONTEND-ENV (api.bitforum.io)
- // .env.production ? .gitignore ??(Backend ??appsettings.Production.json 泥섎읆 鍮뚮뱶 ?쒖젏?먮쭔 議댁옱).
- def credId = (env.BRANCH_NAME == 'main') ? 'PROD-BITFORUM-FRONTEND-ENV' : 'DEV-BITFORUM-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') {
- when { expression { !env.BRANCH_NAME || env.BRANCH_NAME in ['main', 'dev'] } }
- steps {
- script { deployFrontend() }
- }
- }
- }
- post {
- success { echo "Frontend build & deploy completed (branch=${env.BRANCH_NAME ?: 'unknown'})" }
- failure { echo '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 + 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:
- # .next/standalone/ -> server.js + minimal node_modules + .next folder (server bundles)
- # .next/static/ -> static assets (must be copied to <dest>/.next/static)
- # public/ -> public assets (must be copied to <dest>/public)
- 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
- }
- # Server-side env (.env.production) ??Next.js standalone runtime reads from cwd.
- # NEXT_PUBLIC_* ? 鍮뚮뱶 ??inline ?섏?留? server-side (API_URL, SIGNALR_CHAT_URL ?? ???고???濡쒕뱶.
- 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 - server-side env will fall back to OS/WinSW env'
- }
- # Server-side env (.env.production) ??Next.js standalone runtime reads from cwd
- # NEXT_PUBLIC_* ? 鍮뚮뱶 ??inline ?섏?留? server-side (API_URL, SIGNALR_CHAT_URL ?? ???고???濡쒕뱶
- 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 - server-side env will fall back to OS/WinSW env'
- }
- # Infra files (IIS reverse-proxy web.config + WinSW service xml) - carried in repo, placed into dest.
- Write-Host '[${env.SVC_NAME}] Copying infra files (web.config + WinSW xml)...'
- if (Test-Path 'web.config') { Copy-Item 'web.config' (Join-Path \$dest 'web.config') -Force }
- if (Test-Path '${env.SVC_NAME}.xml') { Copy-Item '${env.SVC_NAME}.xml' (Join-Path \$dest '${env.SVC_NAME}.xml') -Force }
- # Auto-provision WinSW binary from antooza-front (same WinSW exe, renamed) if missing.
- \$svcExe = Join-Path \$dest '${env.SVC_NAME}.exe'
- if (-not (Test-Path \$svcExe)) {
- \$srcExe = 'C:\\workspace\\IIS\\antooza-front\\antooza-frontend.exe'
- if (Test-Path \$srcExe) { Write-Host '[${env.SVC_NAME}] Provisioning WinSW binary from antooza-front'; Copy-Item \$srcExe \$svcExe -Force }
- }
- # 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 + ' ??place ${env.SVC_NAME}.exe (renamed WinSW) + ${env.SVC_NAME}.xml in ' + \$dest)
- }
- & \$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: HTTP 200/3xx on http://127.0.0.1:3001/
- \$ok = \$false
- 1..15 | ForEach-Object {
- Start-Sleep -Seconds 3
- try {
- \$r = Invoke-WebRequest -Uri 'http://127.0.0.1:3001/' -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.'
- }
- # Retain only 5 recent backups
- 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
- """
- }
|