# GERP frontend deploy script: zero-downtime build and sync to production dir (TempLate/GERP) # # IMPORTANT: keep this file pure ASCII. PS 5.1 parses BOM-less UTF-8 files # using the ANSI codepage, so non-ASCII text would be garbled. # # Order matters (zero downtime): # 1. Build first (dist/) -- prod keeps serving the old build meanwhile # 2. Copy new assets/dirs (additive) -- old index.html still points to old # assets, which are still present, so no request can 404 # 3. Swap index.html last -- every asset it references already exists # 4. Prune stale files in the mirror dirs (assets / tinymce) # 5. Verify prod matches dist # # Never delete index.html / assets before the new build is ready: SubdomainView # (gvsds/apps/core/views.py) serves gerp.gvsds.com straight out of this directory. # # Usage: # Run: .\deploy.ps1 # (if execution policy blocks: Set-ExecutionPolicy -Scope Process Bypass) $ErrorActionPreference = 'Stop' $ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $DistDir = Join-Path $ProjectRoot 'dist' $ProdRoot = Join-Path (Split-Path -Parent $ProjectRoot) 'TempLate\GERP' $ProdAssetsDir = Join-Path $ProdRoot 'assets' $IndexHtml = Join-Path $ProdRoot 'index.html' Write-Host "`n===== GERP DEPLOY START =====" -ForegroundColor Cyan Write-Host "Project dir : $ProjectRoot" Write-Host "Prod dir : $ProdRoot" Write-Host '' # 1. Full build (vue-tsc type check + vite build). Non-destructive for prod. Write-Host '[1/5] Running npm run build (type check + full build)...' -ForegroundColor Yellow Push-Location $ProjectRoot try { npm run build if ($LASTEXITCODE -ne 0) { throw "Build failed, exit code = $LASTEXITCODE" } } finally { Pop-Location } Write-Host ' Build OK' Write-Host '' if (-not (Test-Path $ProdRoot)) { New-Item -ItemType Directory -Path $ProdRoot -Force | Out-Null } # 2. Copy new assets/dirs first (additive; stale files are pruned in step 4 so the # still-active old index.html never dangles) Write-Host '[2/5] Syncing new assets to prod dir...' -ForegroundColor Yellow Get-ChildItem -Path $DistDir -Force | Where-Object { $_.Name -ne 'index.html' } | ForEach-Object { Copy-Item -Recurse -Force $_.FullName $ProdRoot Write-Host " synced: $($_.Name)" } Write-Host '' # 3. Swap index.html last: at this point every asset it references already exists Write-Host '[3/5] Swapping index.html...' -ForegroundColor Yellow Copy-Item -Force (Join-Path $DistDir 'index.html') $IndexHtml Write-Host ' index.html updated' Write-Host '' # 4. Prune stale files in the mirror dirs (assets / tinymce) that the new build no # longer ships. Only mirror dirs are touched -- prod-only dirs like pages/ are kept. Write-Host '[4/5] Pruning stale files...' -ForegroundColor Yellow function Remove-StaleFiles([string]$distDir, [string]$prodDir) { if (-not (Test-Path $prodDir)) { return } if (-not (Test-Path $distDir)) { # Whole dir is gone from the build: remove it from prod Remove-Item -Recurse -Force $prodDir Write-Host " removed dir: $($prodDir.Substring($ProdRoot.Length))" return } $distNames = @(Get-ChildItem -Path $distDir -Recurse -File | ForEach-Object { $_.FullName.Substring($distDir.Length) }) $stale = @(Get-ChildItem -Path $prodDir -Recurse -File | Where-Object { $_.FullName.Substring($prodDir.Length) -notin $distNames }) foreach ($f in $stale) { Remove-Item -Force $f.FullName Write-Host " removed: $($f.FullName.Substring($ProdRoot.Length))" } # Drop now-empty sub dirs (deepest first) Get-ChildItem -Path $prodDir -Recurse -Directory | Sort-Object { $_.FullName.Length } -Descending | Where-Object { -not (Get-ChildItem -Path $_.FullName -Force) } | ForEach-Object { Remove-Item -Force $_.FullName Write-Host " removed empty dir: $($_.FullName.Substring($ProdRoot.Length))" } } Remove-StaleFiles (Join-Path $DistDir 'assets') $ProdAssetsDir Remove-StaleFiles (Join-Path $DistDir 'tinymce') (Join-Path $ProdRoot 'tinymce') Write-Host '' # 5. Verify prod dir matches dist (file name sets) Write-Host '[5/5] Verifying artifact consistency...' -ForegroundColor Yellow function Get-AssetNames([string]$dir) { if (-not (Test-Path $dir)) { return @() } return @(Get-ChildItem -Path $dir -Recurse -File | ForEach-Object { $_.FullName.Substring($dir.Length) }) } $distNames = Get-AssetNames (Join-Path $DistDir 'assets') $prodNames = Get-AssetNames $ProdAssetsDir $distSet = @($distNames | Sort-Object -Unique) $prodSet = @($prodNames | Sort-Object -Unique) $onlyDist = @($distSet | Where-Object { $_ -notin $prodSet }) $onlyProd = @($prodSet | Where-Object { $_ -notin $distSet }) if ($onlyDist.Count -eq 0 -and $onlyProd.Count -eq 0) { Write-Host " OK: $($distSet.Count) assets, prod matches dist exactly" -ForegroundColor Green } else { Write-Host ' MISMATCH:' -ForegroundColor Red if ($onlyDist.Count) { Write-Host " only in dist: $onlyDist" -ForegroundColor Red } if ($onlyProd.Count) { Write-Host " only in prod: $onlyProd" -ForegroundColor Red } throw 'Artifact verification failed!' } # Verify the entry JS referenced by index.html exists $indexContent = Get-Content $IndexHtml -Raw $entryMatch = [regex]::Match($indexContent, 'src="([^"]+\.js)"') if ($entryMatch.Success) { $entryPath = Join-Path $ProdRoot ($entryMatch.Groups[1].Value.TrimStart('/')) if (Test-Path $entryPath) { Write-Host " entry JS OK: $($entryMatch.Groups[1].Value)" -ForegroundColor Green } else { throw "index.html entry JS not found: $entryPath" } } else { throw 'No entry JS reference found in index.html' } Write-Host '' Write-Host "===== GERP DEPLOY DONE =====" -ForegroundColor Cyan Write-Host 'Please hard-refresh the browser (Ctrl+F5) to load the new build.' -ForegroundColor Cyan