html vers, deploy
This commit is contained in:
+418
@@ -0,0 +1,418 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Lean 101 Website deployment script — ships the Flask site to the Digital Ocean droplet over SSH.
|
||||
|
||||
.DESCRIPTION
|
||||
Tars the local source tree, uploads it to the droplet, and runs
|
||||
docker compose up --build. No git required on the server.
|
||||
|
||||
Coexists with the clients app on the same droplet — different container name,
|
||||
different host port, different remote path. Host nginx routes by hostname.
|
||||
|
||||
.PARAMETER RemoteHost
|
||||
Hostname or IP of the Digital Ocean droplet. Required.
|
||||
|
||||
.PARAMETER RemoteUser
|
||||
SSH user. Defaults to 'root'.
|
||||
|
||||
.PARAMETER AppName
|
||||
Human-readable app name shown in the banner. Defaults to 'Website'.
|
||||
|
||||
.PARAMETER AppSlug
|
||||
Lowercase slug used in container/log labels. Defaults to 'website'.
|
||||
|
||||
.PARAMETER RemotePath
|
||||
Absolute path on the droplet. Defaults to '/srv/lean-101-website-flask'
|
||||
(deliberately distinct from the old /srv/lean101-website SvelteKit dir).
|
||||
|
||||
.PARAMETER ContainerName
|
||||
Backend container name to inspect. Defaults to 'lean101-website-flask'
|
||||
(must match container_name in docker-compose.yml).
|
||||
|
||||
.PARAMETER PortEnvKey
|
||||
Env var name in the env file that holds the published port.
|
||||
Defaults to 'WEBSITE_APP_PORT'.
|
||||
|
||||
.PARAMETER EnvFile
|
||||
Local path to the production env file. Defaults to '.env.production'.
|
||||
|
||||
.PARAMETER SshKey
|
||||
Optional path to an SSH private key.
|
||||
|
||||
.PARAMETER ComposeFile
|
||||
Compose file name on the remote host. Defaults to 'docker-compose.yml'.
|
||||
|
||||
.PARAMETER Logs
|
||||
Tail logs for ~60 lines after deploy.
|
||||
|
||||
.PARAMETER SkipBuild
|
||||
Pass --no-build to docker compose (use when only env changed).
|
||||
|
||||
.PARAMETER NoBanner
|
||||
Suppress the ASCII banner.
|
||||
|
||||
.EXAMPLE
|
||||
./Deploy.ps1 -RemoteHost 209.38.24.231
|
||||
|
||||
.EXAMPLE
|
||||
./Deploy.ps1 -RemoteHost 209.38.24.231 -Logs
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] [string] $RemoteHost,
|
||||
[string] $RemoteUser = "root",
|
||||
[string] $AppName = "Website",
|
||||
[string] $AppSlug = "website",
|
||||
[string] $RemotePath = "/srv/lean-101-website-flask",
|
||||
[string] $ContainerName = "lean101-website-flask",
|
||||
[string] $PortEnvKey = "WEBSITE_APP_PORT",
|
||||
[string] $EnvFile = ".env.production",
|
||||
[string] $SshKey,
|
||||
[string] $ComposeFile = "docker-compose.yml",
|
||||
[switch] $Logs,
|
||||
[switch] $SkipBuild,
|
||||
[switch] $NoBanner
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
# Palette
|
||||
$Esc = [char]27
|
||||
$C = @{
|
||||
Reset = "$Esc[0m"; Dim = "$Esc[2m"; Bold = "$Esc[1m"
|
||||
Magenta = "$Esc[38;5;177m"; Pink = "$Esc[38;5;213m"
|
||||
Cyan = "$Esc[38;5;87m"; Teal = "$Esc[38;5;79m"
|
||||
Green = "$Esc[38;5;120m"; Yellow = "$Esc[38;5;221m"
|
||||
Red = "$Esc[38;5;203m"; Grey = "$Esc[38;5;244m"
|
||||
Blue = "$Esc[38;5;117m"
|
||||
}
|
||||
$Glyph = @{ Step='▸'; OK='✓'; Warn='!'; Info='·'; Fail='✗'; Arrow='→'; Spark='✦' }
|
||||
|
||||
function Write-Banner {
|
||||
if ($NoBanner) { return }
|
||||
$line = '─' * 62
|
||||
$title = "Lean 101 Website Deployment"
|
||||
$sub = "App: $AppName Slug: $AppSlug Target: $RemoteUser@$RemoteHost"
|
||||
Write-Host ""
|
||||
Write-Host ("$($C.Magenta)╭$line╮$($C.Reset)")
|
||||
Write-Host ("$($C.Magenta)│$($C.Reset) $($C.Bold)$($C.Cyan)$($Glyph.Spark) $title$($C.Reset)" + (' ' * (60 - $title.Length - 3)) + "$($C.Magenta)│$($C.Reset)")
|
||||
Write-Host ("$($C.Magenta)│$($C.Reset) $($C.Dim)$($C.Grey)$sub$($C.Reset)" + (' ' * [Math]::Max(0, 60 - $sub.Length - 2)) + "$($C.Magenta)│$($C.Reset)")
|
||||
Write-Host ("$($C.Magenta)╰$line╯$($C.Reset)")
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
$script:StepIndex = 0
|
||||
function Write-Step([string]$msg) {
|
||||
$script:StepIndex++
|
||||
$num = "{0:D2}" -f $script:StepIndex
|
||||
Write-Host ("$($C.Dim)$($C.Grey)[$num]$($C.Reset) $($C.Bold)$($C.Cyan)$($Glyph.Step)$($C.Reset) $($C.Bold)$msg$($C.Reset)")
|
||||
}
|
||||
function Write-Ok([string]$msg) { Write-Host (" $($C.Green)$($Glyph.OK)$($C.Reset) $($C.Green)$msg$($C.Reset)") }
|
||||
function Write-Warn([string]$msg) { Write-Host (" $($C.Yellow)$($Glyph.Warn)$($C.Reset) $($C.Yellow)$msg$($C.Reset)") }
|
||||
function Write-Info([string]$msg) { Write-Host (" $($C.Dim)$($C.Grey)$($Glyph.Info) $msg$($C.Reset)") }
|
||||
function Write-Fail([string]$msg) { Write-Host (" $($C.Red)$($Glyph.Fail)$($C.Reset) $($C.Red)$msg$($C.Reset)") }
|
||||
|
||||
$Spinner = @('⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏')
|
||||
|
||||
function Invoke-Spinner {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] [string] $Label,
|
||||
[Parameter(Mandatory = $true)] [string] $FilePath,
|
||||
[string[]] $ArgList = @(),
|
||||
[string] $StdinFile,
|
||||
[switch] $ShowOutput
|
||||
)
|
||||
$outFile = [System.IO.Path]::GetTempFileName()
|
||||
$errFile = [System.IO.Path]::GetTempFileName()
|
||||
$startParams = @{
|
||||
FilePath = $FilePath; NoNewWindow = $true; PassThru = $true
|
||||
RedirectStandardOutput = $outFile; RedirectStandardError = $errFile
|
||||
}
|
||||
if ($ArgList.Count -gt 0) { $startParams.ArgumentList = $ArgList }
|
||||
if ($StdinFile) { $startParams.RedirectStandardInput = $StdinFile }
|
||||
|
||||
$proc = Start-Process @startParams
|
||||
$start = Get-Date
|
||||
$i = 0
|
||||
try {
|
||||
while (-not $proc.HasExited) {
|
||||
$elapsed = ((Get-Date) - $start).TotalSeconds
|
||||
$frame = $Spinner[$i % $Spinner.Count]
|
||||
Write-Host ("`r $($C.Cyan)$frame$($C.Reset) $($C.Dim)$($C.Grey)$Label $($C.Reset)$($C.Teal)$('{0,5:0.0}s' -f $elapsed)$($C.Reset) ") -NoNewline
|
||||
Start-Sleep -Milliseconds 90
|
||||
$i++
|
||||
}
|
||||
$proc.WaitForExit()
|
||||
$elapsed = ((Get-Date) - $start).TotalSeconds
|
||||
$stdout = if (Test-Path $outFile) { Get-Content $outFile -Raw } else { '' }
|
||||
$stderr = if (Test-Path $errFile) { Get-Content $errFile -Raw } else { '' }
|
||||
|
||||
if ($proc.ExitCode -eq 0) {
|
||||
Write-Host ("`r $($C.Green)$($Glyph.OK)$($C.Reset) $Label $($C.Dim)$($C.Grey)$('{0,5:0.0}s' -f $elapsed)$($C.Reset)" + (' ' * 12))
|
||||
if ($ShowOutput -and $stdout) {
|
||||
foreach ($ln in ($stdout -split "`r?`n")) { if ($ln) { Write-Info $ln } }
|
||||
}
|
||||
return $stdout
|
||||
} else {
|
||||
Write-Host ("`r $($C.Red)$($Glyph.Fail)$($C.Reset) $Label $($C.Dim)$($C.Grey)$('{0,5:0.0}s' -f $elapsed) (exit $($proc.ExitCode))$($C.Reset)" + (' ' * 8))
|
||||
if ($stdout) { foreach ($ln in ($stdout -split "`r?`n")) { if ($ln) { Write-Host " $($C.Dim)$($C.Grey)$ln$($C.Reset)" } } }
|
||||
if ($stderr) { foreach ($ln in ($stderr -split "`r?`n")) { if ($ln) { Write-Host " $($C.Red)$ln$($C.Reset)" } } }
|
||||
throw "$Label failed (exit $($proc.ExitCode))"
|
||||
}
|
||||
} finally {
|
||||
Remove-Item $outFile -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $errFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepoRoot {
|
||||
if ($PSScriptRoot) { return $PSScriptRoot }
|
||||
return (Get-Location).Path
|
||||
}
|
||||
|
||||
function Get-EnvValue([string] $path, [string] $key) {
|
||||
foreach ($line in Get-Content $path) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith("#")) { continue }
|
||||
if ($trimmed -notmatch "=") { continue }
|
||||
$parts = $trimmed -split "=", 2
|
||||
if ($parts[0].Trim() -eq $key) { return $parts[1].Trim() }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$RepoRoot = Get-RepoRoot
|
||||
$SshTarget = "$RemoteUser@$RemoteHost"
|
||||
$SshOpts = @("-o", "StrictHostKeyChecking=accept-new", "-o", "BatchMode=no", "-i", $SshKey)
|
||||
|
||||
# Resolve env file path early so we can read SSH_PASSWORD before the main flow.
|
||||
$EnvPathEarly = if ([System.IO.Path]::IsPathRooted($EnvFile)) { $EnvFile } else { Join-Path $RepoRoot $EnvFile }
|
||||
|
||||
function Resolve-PuttyTool([string] $name) {
|
||||
$cmd = Get-Command $name -ErrorAction SilentlyContinue
|
||||
if ($cmd) { return $cmd.Source }
|
||||
foreach ($candidate in @(
|
||||
"$env:ProgramFiles\PuTTY\$name.exe",
|
||||
"${env:ProgramFiles(x86)}\PuTTY\$name.exe",
|
||||
"$env:LOCALAPPDATA\Programs\PuTTY\$name.exe"
|
||||
)) {
|
||||
if ($candidate -and (Test-Path $candidate)) { return $candidate }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$SshPassword = $null
|
||||
$PlinkPath = $null
|
||||
$PscpPath = $null
|
||||
if (-not $SshKey) {
|
||||
if (-not (Test-Path $EnvPathEarly)) {
|
||||
throw "Env file not found at '$EnvPathEarly'. Create it and add SSH_PASSWORD=<droplet root password> (or pass -SshKey to use key auth)."
|
||||
}
|
||||
$SshPassword = Get-EnvValue $EnvPathEarly 'SSH_PASSWORD'
|
||||
if (-not $SshPassword) {
|
||||
throw "SSH_PASSWORD is not set in '$EnvPathEarly'. Add a line `SSH_PASSWORD=<your droplet password>` (or pass -SshKey to use key auth instead)."
|
||||
}
|
||||
$PlinkPath = Resolve-PuttyTool 'plink'
|
||||
$PscpPath = Resolve-PuttyTool 'pscp'
|
||||
if (-not $PlinkPath -or -not $PscpPath) {
|
||||
throw "plink/pscp (PuTTY) are required for password auth on Windows but were not found. Install PuTTY: ``winget install --id PuTTY.PuTTY -e`` (or ``choco install putty -y``), then reopen PowerShell."
|
||||
}
|
||||
|
||||
# Pre-cache the SSH host key so subsequent -batch calls don't fail on first connect.
|
||||
# Use cmd's pipe — most reliable way to feed `y` to plink's host-key prompt on Windows.
|
||||
$cacheOut = & cmd /c "echo y | `"$PlinkPath`" -ssh -pw `"$SshPassword`" `"$SshTarget`" exit 2>&1"
|
||||
# Ignore exit code: the goal is just to write the host key into the PuTTY registry cache.
|
||||
$null = $cacheOut
|
||||
}
|
||||
|
||||
function Invoke-Ssh([string] $cmd, [string] $Label, [switch] $ShowOutput) {
|
||||
if (-not $Label) { $Label = "ssh $($cmd.Substring(0, [Math]::Min(48, $cmd.Length)))..." }
|
||||
if ($SshPassword) {
|
||||
$argList = @('-ssh', '-pw', $SshPassword, '-batch', $SshTarget, $cmd)
|
||||
Invoke-Spinner -Label $Label -FilePath $PlinkPath -ArgList $argList -ShowOutput:$ShowOutput
|
||||
} else {
|
||||
Invoke-Spinner -Label $Label -FilePath 'ssh' -ArgList (@($SshOpts) + @($SshTarget, $cmd)) -ShowOutput:$ShowOutput
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Scp([string] $local, [string] $remote, [string] $Label) {
|
||||
if (-not $Label) { $Label = "scp $(Split-Path -Leaf $local) $($Glyph.Arrow) $remote" }
|
||||
if ($SshPassword) {
|
||||
$argList = @('-scp', '-pw', $SshPassword, '-batch', $local, "${SshTarget}:${remote}")
|
||||
Invoke-Spinner -Label $Label -FilePath $PscpPath -ArgList $argList
|
||||
} else {
|
||||
Invoke-Spinner -Label $Label -FilePath 'scp' -ArgList (@($SshOpts) + @($local, "${SshTarget}:${remote}"))
|
||||
}
|
||||
}
|
||||
|
||||
function Try-Ssh([string] $cmd) {
|
||||
if ($SshPassword) {
|
||||
& $PlinkPath -ssh -pw $SshPassword -batch $SshTarget $cmd
|
||||
} else {
|
||||
& ssh @SshOpts $SshTarget $cmd
|
||||
}
|
||||
return $LASTEXITCODE
|
||||
}
|
||||
|
||||
function Invoke-SshScript([string] $script, [string] $Label) {
|
||||
if (-not $Label) { $Label = "ssh (remote script)" }
|
||||
$tmp = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
# Use LF line endings so bash on the remote doesn't choke on CR.
|
||||
[System.IO.File]::WriteAllText($tmp, ($script -replace "`r`n", "`n"), [System.Text.UTF8Encoding]::new($false))
|
||||
if ($SshPassword) {
|
||||
$argList = @('-ssh', '-pw', $SshPassword, '-batch', $SshTarget, 'bash -s')
|
||||
Invoke-Spinner -Label $Label -FilePath $PlinkPath -ArgList $argList -StdinFile $tmp -ShowOutput
|
||||
} else {
|
||||
Invoke-Spinner -Label $Label -FilePath 'ssh' -ArgList (@($SshOpts) + @($SshTarget, "bash -s")) -StdinFile $tmp -ShowOutput
|
||||
}
|
||||
} finally {
|
||||
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-Banner
|
||||
|
||||
Push-Location $RepoRoot
|
||||
try {
|
||||
$EnvPath = if ([System.IO.Path]::IsPathRooted($EnvFile)) { $EnvFile } else { Join-Path $RepoRoot $EnvFile }
|
||||
if (-not (Test-Path $EnvPath)) {
|
||||
throw "Env file not found at '$EnvPath'. Copy .env.production.example to .env.production and fill in RESEND_API_KEY."
|
||||
}
|
||||
|
||||
$AppPort = Get-EnvValue $EnvPath $PortEnvKey
|
||||
if (-not $AppPort) { $AppPort = "8083" }
|
||||
|
||||
Write-Step "Preflight"
|
||||
Write-Info "App : $AppName ($AppSlug)"
|
||||
Write-Info "Remote host : $SshTarget"
|
||||
Write-Info "Remote path : $RemotePath"
|
||||
Write-Info "Container : $ContainerName"
|
||||
Write-Info "Env file : $EnvPath"
|
||||
Write-Info "Compose file : $ComposeFile"
|
||||
Write-Info "Port ($PortEnvKey) : $AppPort"
|
||||
|
||||
Write-Step "Verifying SSH connectivity"
|
||||
Invoke-Ssh "echo connected as `$(whoami) on `$(hostname)" -Label "ssh handshake" -ShowOutput
|
||||
|
||||
Write-Step "Checking that remote port $AppPort is free for $ContainerName"
|
||||
$portCheckCmd = @'
|
||||
set -e
|
||||
PORT='__APP_PORT__'
|
||||
SELF='__CONTAINER__'
|
||||
OWNER=$(docker ps --format '{{.Names}} {{.Ports}}' | grep -m1 ":${PORT}->" | cut -d' ' -f1 || true)
|
||||
if [ -n "$OWNER" ] && [ "$OWNER" != "$SELF" ]; then
|
||||
echo "Port $PORT is already owned by container: $OWNER" >&2
|
||||
exit 2
|
||||
fi
|
||||
'@.Replace('__APP_PORT__', $AppPort).Replace('__CONTAINER__', $ContainerName)
|
||||
try {
|
||||
Invoke-SshScript $portCheckCmd -Label "port $AppPort availability"
|
||||
} catch {
|
||||
throw "Remote port $AppPort is already in use by another container. Change $PortEnvKey in '$EnvPath' or retire the conflicting service first."
|
||||
}
|
||||
|
||||
Write-Step "Packaging source tree (excluding caches, secrets, logs)"
|
||||
$TarFile = Join-Path $env:TEMP "lean101-website-deploy-$(Get-Date -Format 'yyyyMMdd-HHmmss').tar.gz"
|
||||
$excludes = @(
|
||||
"--exclude=./.git",
|
||||
"--exclude=./.pytest_cache",
|
||||
"--exclude=./__pycache__",
|
||||
"--exclude=./**/__pycache__",
|
||||
"--exclude=./*.pyc",
|
||||
"--exclude=./.venv",
|
||||
"--exclude=./venv",
|
||||
"--exclude=./.env",
|
||||
"--exclude=./.env.production",
|
||||
"--exclude=./.env.*.local",
|
||||
"--exclude=./*.log",
|
||||
"--exclude=./*.err"
|
||||
)
|
||||
Invoke-Spinner -Label "tar -czf $(Split-Path -Leaf $TarFile)" -FilePath 'tar' -ArgList (@('-czf', $TarFile) + $excludes + @('-C', $RepoRoot, '.'))
|
||||
$TarSize = [math]::Round((Get-Item $TarFile).Length / 1MB, 2)
|
||||
Write-Info "Archive: $TarFile ($($C.Bold)$TarSize MB$($C.Reset))"
|
||||
|
||||
Write-Step "Ensuring remote path exists"
|
||||
Invoke-Ssh "mkdir -p '$RemotePath'" -Label "mkdir -p $RemotePath"
|
||||
|
||||
Write-Step "Uploading env file"
|
||||
Invoke-Scp $EnvPath "$RemotePath/.env.production" -Label "scp .env.production $($Glyph.Arrow) $RemotePath/"
|
||||
Invoke-Ssh "chmod 600 '$RemotePath/.env.production'" -Label "chmod 600 .env.production"
|
||||
|
||||
Write-Step "Uploading source archive ($TarSize MB)"
|
||||
Invoke-Scp $TarFile "/tmp/lean101-website-deploy.tar.gz" -Label "scp archive $($Glyph.Arrow) /tmp/"
|
||||
Remove-Item $TarFile -Force
|
||||
|
||||
Write-Step "Extracting archive on server"
|
||||
Invoke-Ssh "tar -xzf /tmp/lean101-website-deploy.tar.gz -C '$RemotePath' && rm /tmp/lean101-website-deploy.tar.gz" -Label "untar into $RemotePath"
|
||||
|
||||
$ComposeArgs = "--env-file .env.production -f $ComposeFile"
|
||||
$BuildFlag = if ($SkipBuild) { "--no-build" } else { "--build" }
|
||||
|
||||
$buildMsg = if ($SkipBuild) { "without rebuild" } else { "with --build" }
|
||||
Write-Step "Bringing the $AppName stack up $buildMsg"
|
||||
$composeUpCmd = "cd '$RemotePath' && docker compose $ComposeArgs up -d $BuildFlag --remove-orphans"
|
||||
try {
|
||||
Invoke-Ssh $composeUpCmd -Label "docker compose up $BuildFlag"
|
||||
} catch {
|
||||
Write-Warn "docker compose up failed; collecting status and logs"
|
||||
Try-Ssh "cd '$RemotePath' && docker compose $ComposeArgs ps" | Out-Null
|
||||
Try-Ssh "cd '$RemotePath' && docker compose $ComposeArgs logs --tail=120" | Out-Null
|
||||
throw
|
||||
}
|
||||
|
||||
Write-Step "Waiting for container to be running ($ContainerName)"
|
||||
$runCheck = @"
|
||||
set -e
|
||||
for i in `$(seq 1 30); do
|
||||
status=`$(docker inspect --format='{{.State.Status}}' $ContainerName 2>/dev/null || echo missing)
|
||||
case "`$status" in
|
||||
running) echo "container is running"; exit 0 ;;
|
||||
*) printf '.'; sleep 2 ;;
|
||||
esac
|
||||
done
|
||||
echo; echo "container did not reach running state" >&2; exit 1
|
||||
"@
|
||||
Invoke-SshScript $runCheck -Label "container status (up to ~60s)"
|
||||
|
||||
Write-Step "HTTP readiness check on 127.0.0.1:$AppPort"
|
||||
$curlCmd = "for i in `$(seq 1 20); do code=`$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 5 http://127.0.0.1:$AppPort/ 2>/dev/null || echo 000); if [ `"`$code`" = `"200`" ]; then echo `"HTTP `$code`"; exit 0; fi; sleep 2; done; echo `"never got 200`" >&2; exit 1"
|
||||
Invoke-Ssh $curlCmd -Label "curl http://127.0.0.1:$AppPort/" -ShowOutput
|
||||
|
||||
Write-Step "Stack status"
|
||||
Invoke-Ssh "cd '$RemotePath' && docker compose $ComposeArgs ps" -Label "docker compose ps" -ShowOutput
|
||||
|
||||
if ($Logs) {
|
||||
Write-Step "Recent logs (last 60 lines)"
|
||||
Invoke-Ssh "cd '$RemotePath' && docker compose $ComposeArgs logs --tail=60" -Label "docker compose logs --tail=60" -ShowOutput
|
||||
}
|
||||
|
||||
Write-Step "Next steps for cutover"
|
||||
Write-Info "New container is healthy on $($C.Bold)127.0.0.1:$AppPort$($C.Reset)"
|
||||
Write-Info "Public domain still hits the OLD container until you swap the nginx vhost:"
|
||||
Write-Info " ssh $SshTarget"
|
||||
Write-Info " sed -i.bak 's|proxy_pass http://127.0.0.1:8091;|proxy_pass http://127.0.0.1:$AppPort;|' /etc/nginx/sites-enabled/lean-101.com.au"
|
||||
Write-Info " nginx -t && systemctl reload nginx"
|
||||
Write-Info "Then verify: curl -I https://www.lean-101.com.au/"
|
||||
Write-Info "Once happy: cd /srv/lean101-website && docker compose down"
|
||||
|
||||
$line = '─' * 62
|
||||
Write-Host ""
|
||||
Write-Host ("$($C.Green)╭$line╮$($C.Reset)")
|
||||
$done = "$($Glyph.Spark) $AppName deployed to ${SshTarget}:$AppPort"
|
||||
$pad = [Math]::Max(0, 60 - ($done.Length - 2))
|
||||
Write-Host ("$($C.Green)│$($C.Reset) $($C.Bold)$($C.Green)$done$($C.Reset)" + (' ' * $pad) + "$($C.Green)│$($C.Reset)")
|
||||
Write-Host ("$($C.Green)╰$line╯$($C.Reset)")
|
||||
Write-Host ""
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-Fail "Deployment aborted: $($_.Exception.Message)"
|
||||
Write-Host ""
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user