<# .SYNOPSIS Harat's Booking Deploy — builds and sends release to production server. .DESCRIPTION 1. Bumps version.json with current build timestamp 2. Builds client (unless -SkipBuild) 3. Creates a tar.gz archive (no node_modules, no .env) 4. Uploads to server via SCP 5. Runs deploy-remote.sh on the server via SSH .PARAMETER Server Production server IP/hostname. .PARAMETER User SSH username on the server. .PARAMETER Port SSH port (default 2182). .PARAMETER SkipBuild Skip client build (use existing dist/). .EXAMPLE .\deploy\deploy.ps1 # full deploy .\deploy\deploy.ps1 -SkipBuild # skip client build #> param( [string]$Server = "178.169.87.152", [string]$User = "eugenius", [int]$Port = 2182, [switch]$SkipBuild ) $ErrorActionPreference = "Stop" $ProjectRoot = Split-Path -Parent $PSScriptRoot # harats/ $DeployDir = "/apps/harats" # -- Error reporting ------------------------------------------------------- function Write-DeployError { param( [string]$Step, [string]$Message, [string]$Detail = "", [string]$Hint = "" ) Write-Host "" Write-Host " +============================================+" -ForegroundColor Red Write-Host " | DEPLOY FAILED |" -ForegroundColor Red Write-Host " +============================================+" -ForegroundColor Red Write-Host "" Write-Host " Step: " -NoNewline -ForegroundColor DarkGray Write-Host "$Step" -ForegroundColor White Write-Host " Error: " -NoNewline -ForegroundColor DarkGray Write-Host "$Message" -ForegroundColor Yellow if ($Detail) { Write-Host " Detail: " -NoNewline -ForegroundColor DarkGray Write-Host "$Detail" -ForegroundColor Gray } if ($Hint) { Write-Host "" Write-Host " Hint: " -NoNewline -ForegroundColor DarkGray Write-Host "$Hint" -ForegroundColor Cyan } Write-Host "" } # -- Main deploy pipeline -------------------------------------------------- $currentStep = "Init" try { # -- 1. Read version -- $currentStep = "[1/5] Version read" $versionFile = Join-Path $ProjectRoot "version.json" if (-not (Test-Path $versionFile)) { throw "version.json not found at: $versionFile" } $versionData = Get-Content $versionFile -Raw | ConvertFrom-Json $version = $versionData.version if (-not $version) { throw "version.json is missing the 'version' field" } $build = Get-Date -Format "yyyyMMdd-HHmm" $tag = "${version}+${build}" Write-Host "" Write-Host "=======================================" -ForegroundColor Cyan Write-Host " Harat's Deploy - v${tag}" -ForegroundColor Cyan Write-Host " Target: ${User}@${Server}:${Port}" -ForegroundColor Cyan Write-Host "=======================================" -ForegroundColor Cyan Write-Host "" # -- 2. Update version.json -- $currentStep = "[1/5] Version update" $versionData.build = $build $versionData.buildDate = (Get-Date -Format "o") $jsonOut = $versionData | ConvertTo-Json -Depth 3 [System.IO.File]::WriteAllText($versionFile, $jsonOut, [System.Text.UTF8Encoding]::new($false)) Write-Host "[1/5] Version: $tag" -ForegroundColor Green # -- 3. Build client -- $currentStep = "[2/5] Client build" if (-not $SkipBuild) { Write-Host "[2/5] Building client..." -ForegroundColor Yellow # Build shared first $sharedDir = Join-Path $ProjectRoot "packages\shared" if (Test-Path (Join-Path $sharedDir "package.json")) { Push-Location $sharedDir try { npm run build 2>$null } finally { Pop-Location } } $clientDir = Join-Path $ProjectRoot "client" if (-not (Test-Path (Join-Path $clientDir "package.json"))) { throw "client/package.json not found -- is the workspace intact?" } Push-Location $clientDir try { npm run build if ($LASTEXITCODE -ne 0) { throw "Vite build exited with code $LASTEXITCODE -- check the compiler output above" } } finally { Pop-Location } # Build server Write-Host " Building server..." -ForegroundColor Yellow $serverDir = Join-Path $ProjectRoot "server" Push-Location $serverDir try { npm run build if ($LASTEXITCODE -ne 0) { throw "Server tsc build exited with code $LASTEXITCODE" } } finally { Pop-Location } Write-Host " Build OK" -ForegroundColor Green } else { Write-Host "[2/5] Skipping build" -ForegroundColor DarkGray } # -- 4. Create archive -- $currentStep = "[3/5] Archive creation" Write-Host "[3/5] Creating archive..." -ForegroundColor Yellow $archiveName = "harats-${tag}.tar.gz" $archivePath = Join-Path $ProjectRoot $archiveName # Verify git is available (needed for GNU tar) $gitCmd = Get-Command git -ErrorAction SilentlyContinue if (-not $gitCmd) { throw "git not found in PATH -- GNU tar from Git for Windows is required to create archives" } $gitRoot = $gitCmd.Source | Split-Path | Split-Path $gnuTar = Join-Path $gitRoot "usr\bin\tar.exe" if (-not (Test-Path $gnuTar)) { throw "GNU tar not found at: $gnuTar -- expected Git for Windows installation" } Push-Location $ProjectRoot try { $oldPath = $env:PATH $env:PATH = "$gitRoot\usr\bin;$env:PATH" & $gnuTar -czf $archiveName ` --exclude="node_modules" ` --exclude="*.tar.gz" ` --exclude=".git" ` --exclude="envs/prod/.env" ` server/dist/ ` server/prisma/ ` server/package.json ` server/package-lock.json ` client/dist/ ` packages/shared/ ` Dockerfile ` .dockerignore ` docker-compose.prod.yml ` ecosystem.config.cjs ` version.json ` envs/ ` deploy/ if ($LASTEXITCODE -ne 0) { throw "tar exited with code $LASTEXITCODE -- check file list for missing paths" } $env:PATH = $oldPath } finally { Pop-Location } if (-not (Test-Path $archivePath)) { throw "Archive was not created at: $archivePath" } $sizeMB = [math]::Round((Get-Item $archivePath).Length / 1MB, 1) if ($sizeMB -lt 0.1) { Write-Host " Warning: archive is suspiciously small (${sizeMB} MB)" -ForegroundColor DarkYellow } Write-Host " Archive: $archiveName - ${sizeMB} MB" -ForegroundColor Green # -- 5. Upload -- $currentStep = "[4/5] SCP upload" Write-Host "[4/5] Uploading to ${Server}..." -ForegroundColor Yellow scp -P $Port $archivePath "${User}@${Server}:${DeployDir}/releases/" if ($LASTEXITCODE -ne 0) { $hint = switch ($LASTEXITCODE) { 255 { "Connection refused or auth failed. Check SSH key, port ($Port), and server availability." } 1 { "SCP error -- target directory ${DeployDir}/releases/ may not exist on the server." } default { "SCP exited with code $LASTEXITCODE." } } throw $hint } Write-Host " Upload OK" -ForegroundColor Green # -- 6. Remote deploy -- $currentStep = "[5/5] Remote deploy (SSH)" Write-Host "[5/5] Deploying on server..." -ForegroundColor Yellow ssh -p $Port "${User}@${Server}" "cd ${DeployDir}; cp deploy/deploy-remote.sh /tmp/_harats-deploy.sh; bash /tmp/_harats-deploy.sh '${archiveName}' '${tag}'" if ($LASTEXITCODE -ne 0) { $hint = switch ($LASTEXITCODE) { 255 { "SSH connection failed. Is the server reachable at ${Server}:${Port}?" } 126 { "deploy-remote.sh exists but is not executable." } 127 { "deploy-remote.sh not found on server at ${DeployDir}/deploy/deploy-remote.sh" } default { "Remote script exited with code $LASTEXITCODE -- check server logs" } } throw $hint } # -- Cleanup local archive -- $currentStep = "Cleanup" Remove-Item $archivePath -Force -ErrorAction SilentlyContinue Write-Host "" Write-Host "=======================================" -ForegroundColor Green Write-Host " Deployed Harat's v${tag}" -ForegroundColor Green Write-Host "=======================================" -ForegroundColor Green Write-Host "" } catch { Write-DeployError ` -Step $currentStep ` -Message $_.Exception.Message ` -Detail ($_.ScriptStackTrace -split "`n" | Select-Object -First 3 | Out-String).Trim() ` -Hint "Fix the issue above and re-run: .\deploy\deploy.ps1$(if ($SkipBuild) { ' -SkipBuild' })" # Cleanup partial archive if it exists if ($archivePath -and (Test-Path $archivePath -ErrorAction SilentlyContinue)) { Remove-Item $archivePath -Force -ErrorAction SilentlyContinue } exit 1 }