Files
gw-svelte/deploy.ps1
T

281 lines
8.1 KiB
PowerShell
Raw Normal View History

2026-05-02 12:39:55 +12:00
[CmdletBinding()]
param(
[switch]$Force,
2026-05-02 19:44:45 +12:00
[switch]$SkipSiteCheck,
[string]$Service
2026-05-02 12:39:55 +12:00
)
# ---------------------------------------------------------------------------
# Goodwalk production deployment settings
# Update these values before the first real deployment.
# This script targets the main Svelte Goodwalk stack that uses the top-level
# docker-compose.yml. It does not touch the legacy WordPress compose files.
# ---------------------------------------------------------------------------
$SshUser = 'root'
$ServerHost = 'gw-prod'
# Leave blank to use interactive password prompts instead of an SSH key.
$SshKeyPath = ''
$LocalProjectPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$SshConfigPath = Join-Path $LocalProjectPath 'ssh-config'
$RemoteDeploymentPath = '/docker/goodwalk-svelte'
$ComposeFileName = 'docker-compose.prod.yml'
$DockerProjectName = 'goodwalk-svelte'
# Optional deployment settings.
$VerifyUrl = 'https://www.goodwalk.co.nz/api/health'
$RemoteArchivePath = '/tmp/goodwalk-deploy.tgz'
$RemoteHelperPath = '/tmp/goodwalk-deploy-remote.sh'
$LocalRemoteHelperPath = Join-Path $LocalProjectPath 'scripts\deploy-remote.sh'
2026-05-02 19:44:45 +12:00
$GeneratedHomepageContentPath = Join-Path $LocalProjectPath 'deploy-data\homepage-content.json'
2026-05-02 12:39:55 +12:00
function Assert-NotBlank {
param(
[string]$Name,
[string]$Value
)
if ([string]::IsNullOrWhiteSpace($Value)) {
throw "Required setting '$Name' is blank. Update deploy.ps1 before running it."
}
}
function Assert-Command {
param([string]$Name)
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "Required command '$Name' is not available in PATH."
}
}
function Invoke-External {
param(
[string]$FilePath,
[string[]]$Arguments
)
& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Command failed: $FilePath $($Arguments -join ' ')"
}
}
function Get-SshArgumentList {
$args = @()
if (-not [string]::IsNullOrWhiteSpace($SshConfigPath)) {
$args += @('-F', $SshConfigPath)
}
if (-not [string]::IsNullOrWhiteSpace($SshKeyPath)) {
$args += @('-i', $SshKeyPath)
}
return $args
}
function New-DeployArchive {
param(
[string]$ProjectPath
)
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$archivePath = Join-Path ([System.IO.Path]::GetTempPath()) "goodwalk-deploy-$timestamp.tgz"
$excludeArgs = @(
'--exclude=.git',
'--exclude=node_modules',
'--exclude=.svelte-kit',
'--exclude=build',
'--exclude=.env',
'--exclude=.env.*',
'--exclude=logs',
'--exclude=mail-api/__pycache__',
'--exclude=mail-api/*.pyc',
'--exclude=migration-backups'
)
if (Test-Path $archivePath) {
Remove-Item -LiteralPath $archivePath -Force
}
Push-Location $ProjectPath
try {
$tarArguments = @('-czf', $archivePath) + $excludeArgs + @('.')
Invoke-External -FilePath 'tar' -Arguments $tarArguments
}
finally {
Pop-Location
}
return $archivePath
}
2026-05-02 19:44:45 +12:00
function Export-HomepageContent {
param(
[string]$ProjectPath,
[string]$OutputPath
)
$scriptPath = Join-Path $ProjectPath 'scripts\export-homepage-content.mjs'
if (-not (Test-Path -LiteralPath $scriptPath)) {
throw "Homepage content export script not found: $scriptPath"
}
Push-Location $ProjectPath
try {
Invoke-External -FilePath 'node' -Arguments @(
'--experimental-strip-types',
$scriptPath,
$OutputPath
)
}
finally {
Pop-Location
}
}
2026-05-02 12:39:55 +12:00
function Invoke-SiteCheck {
param([string]$Url)
Write-Host ''
Write-Host "[deploy] Checking production site: $Url"
try {
$response = Invoke-WebRequest -Uri $Url -MaximumRedirection 5 -TimeoutSec 30
Write-Host "[deploy] Site responded with HTTP $($response.StatusCode)"
}
catch {
Write-Warning "Production site check failed: $($_.Exception.Message)"
}
}
Assert-Command ssh
Assert-Command scp
Assert-Command tar
2026-05-02 19:44:45 +12:00
Assert-Command node
2026-05-02 12:39:55 +12:00
Assert-NotBlank -Name 'SshUser' -Value $SshUser
Assert-NotBlank -Name 'ServerHost' -Value $ServerHost
Assert-NotBlank -Name 'LocalProjectPath' -Value $LocalProjectPath
Assert-NotBlank -Name 'RemoteDeploymentPath' -Value $RemoteDeploymentPath
Assert-NotBlank -Name 'ComposeFileName' -Value $ComposeFileName
Assert-NotBlank -Name 'DockerProjectName' -Value $DockerProjectName
2026-05-02 19:44:45 +12:00
if (-not [string]::IsNullOrWhiteSpace($Service)) {
$Service = $Service.Trim()
}
2026-05-02 12:39:55 +12:00
if (-not [string]::IsNullOrWhiteSpace($SshConfigPath) -and -not (Test-Path -LiteralPath $SshConfigPath)) {
throw "SSH config file not found: $SshConfigPath"
}
if (-not [string]::IsNullOrWhiteSpace($SshKeyPath) -and -not (Test-Path -LiteralPath $SshKeyPath)) {
throw "SSH key not found: $SshKeyPath"
}
if (-not (Test-Path -LiteralPath $LocalProjectPath)) {
throw "Local project path not found: $LocalProjectPath"
}
if (-not (Test-Path -LiteralPath (Join-Path $LocalProjectPath $ComposeFileName))) {
throw "Compose file not found in local project path: $(Join-Path $LocalProjectPath $ComposeFileName)"
}
if (-not (Test-Path -LiteralPath $LocalRemoteHelperPath)) {
throw "Remote deployment helper not found: $LocalRemoteHelperPath"
}
$sshTarget = '{0}@{1}' -f $SshUser, $ServerHost
$scpArchiveTarget = '{0}:{1}' -f $sshTarget, $RemoteArchivePath
$scpHelperTarget = '{0}:{1}' -f $sshTarget, $RemoteHelperPath
$sshArgs = Get-SshArgumentList
Write-Host '[deploy] Main Goodwalk website deployment'
Write-Host "[deploy] Local project path: $LocalProjectPath"
Write-Host "[deploy] Remote deployment path: $RemoteDeploymentPath"
Write-Host "[deploy] Remote compose file: $ComposeFileName"
Write-Host "[deploy] Docker project name: $DockerProjectName"
Write-Host "[deploy] SSH target: $sshTarget"
Write-Host "[deploy] SSH config: $SshConfigPath"
2026-05-02 19:44:45 +12:00
if (-not [string]::IsNullOrWhiteSpace($Service)) {
Write-Host "[deploy] Target service: $Service"
}
2026-05-02 12:39:55 +12:00
if ([string]::IsNullOrWhiteSpace($SshKeyPath)) {
Write-Host '[deploy] SSH auth: interactive password prompt'
} else {
Write-Host "[deploy] SSH auth: key file $SshKeyPath"
}
Write-Host ''
Write-Host '[deploy] Safety notes:'
Write-Host ' - Only the top-level Goodwalk compose project will be updated.'
Write-Host ' - Legacy WordPress/onboarding compose files are not used.'
Write-Host ' - Remote .env files are preserved because they are not uploaded.'
Write-Host ' - No global Docker prune/stop/delete commands are used.'
if (-not $Force) {
$confirmation = Read-Host "Type DEPLOY to continue with the remote path '$RemoteDeploymentPath'"
if ($confirmation -ne 'DEPLOY') {
throw 'Deployment cancelled.'
}
}
$archivePath = $null
try {
2026-05-02 19:44:45 +12:00
Write-Host ''
Write-Host '[deploy] Exporting current homepage content for PostgreSQL sync'
Export-HomepageContent -ProjectPath $LocalProjectPath -OutputPath $GeneratedHomepageContentPath
2026-05-02 12:39:55 +12:00
Write-Host ''
Write-Host '[deploy] Creating deployment archive'
$archivePath = New-DeployArchive -ProjectPath $LocalProjectPath
Write-Host "[deploy] Archive ready: $archivePath"
Write-Host ''
Write-Host '[deploy] Uploading remote helper'
Invoke-External -FilePath 'scp' -Arguments ($sshArgs + @($LocalRemoteHelperPath, $scpHelperTarget))
Write-Host ''
Write-Host '[deploy] Uploading application archive'
Invoke-External -FilePath 'scp' -Arguments ($sshArgs + @($archivePath, $scpArchiveTarget))
Write-Host ''
Write-Host '[deploy] Running remote deployment'
Invoke-External -FilePath 'ssh' -Arguments ($sshArgs + @(
$sshTarget,
'bash',
$RemoteHelperPath,
'--archive',
$RemoteArchivePath,
'--deploy-path',
$RemoteDeploymentPath,
'--compose-file',
$ComposeFileName,
'--project-name',
$DockerProjectName
2026-05-02 19:44:45 +12:00
) + $(if (-not [string]::IsNullOrWhiteSpace($Service)) { @('--service', $Service) } else { @() }))
2026-05-02 12:39:55 +12:00
Write-Host ''
Write-Host '[deploy] Cleaning remote temporary files'
Invoke-External -FilePath 'ssh' -Arguments ($sshArgs + @(
$sshTarget,
'rm',
'-f',
$RemoteArchivePath,
$RemoteHelperPath
))
if (-not $SkipSiteCheck) {
Invoke-SiteCheck -Url $VerifyUrl
}
Write-Host ''
Write-Host '[deploy] Deployment completed successfully'
}
finally {
if ($archivePath -and (Test-Path -LiteralPath $archivePath)) {
Remove-Item -LiteralPath $archivePath -Force
}
}