|
|
@@ -1,60 +1,213 @@
|
|
|
+// =====================================================================
|
|
|
+// 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 'built-in' }
|
|
|
- environment {
|
|
|
- DEPLOY_FRONTEND = '/mnt/h/bitforum/sources'
|
|
|
- }
|
|
|
- stages {
|
|
|
- stage('Install') {
|
|
|
- steps {
|
|
|
- script {
|
|
|
- // package-lock.json 해시 비교해서 변경됐을 때만 재설치
|
|
|
- def lockHash = sh(
|
|
|
- script: 'sha256sum package-lock.json | cut -d" " -f1',
|
|
|
- returnStdout: true
|
|
|
- ).trim()
|
|
|
- def cacheFile = '.npm-lock-hash'
|
|
|
- def prevHash = fileExists(cacheFile)
|
|
|
- ? readFile(cacheFile).trim()
|
|
|
- : ''
|
|
|
-
|
|
|
- if (lockHash != prevHash) {
|
|
|
- echo "package-lock.json 변경 감지 → npm ci 실행"
|
|
|
- sh 'npm ci'
|
|
|
- writeFile file: cacheFile, text: lockHash
|
|
|
- } else {
|
|
|
- echo "package-lock.json 변경 없음 → 설치 스킵"
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ 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('Build') {
|
|
|
- steps {
|
|
|
- sh 'npm run build'
|
|
|
- }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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('Deploy') {
|
|
|
- steps {
|
|
|
- sh '''
|
|
|
- rsync -a --stats --delete \
|
|
|
- --exclude="web.config" \
|
|
|
- --exclude="ecosystem.config.js" \
|
|
|
- --exclude="restart.ps1" \
|
|
|
- --exclude=".env*" \
|
|
|
- .next/standalone/ ${DEPLOY_FRONTEND}/
|
|
|
- '''
|
|
|
- sh 'rsync -a --stats --delete .next/static/ ${DEPLOY_FRONTEND}/.next/static/'
|
|
|
- sh 'rsync -a --stats --delete public/ ${DEPLOY_FRONTEND}/public/'
|
|
|
- }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
}
|
|
|
- stage('Restart Frontend') {
|
|
|
- agent { label 'windows-host' }
|
|
|
- steps {
|
|
|
- bat 'chcp 65001 && powershell.exe -ExecutionPolicy Bypass -File H:\\IIS\\bitforum\\restart.ps1'
|
|
|
- }
|
|
|
+ } 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)
|
|
|
+ }
|
|
|
}
|
|
|
- post {
|
|
|
- success { echo 'Frontend deploy success!' }
|
|
|
- failure { echo 'Frontend deploy failed!' }
|
|
|
+
|
|
|
+ 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
|
|
|
+ """
|
|
|
+}
|