Big changes
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deploys the complete Memby Docker Compose stack to MATT-NAS.
|
||||
|
||||
.DESCRIPTION
|
||||
Packages the local server build context, Docker Compose file and .env.example,
|
||||
then streams them to the NAS over one SSH connection.
|
||||
|
||||
The remote deployment:
|
||||
- checks Docker and Docker Compose;
|
||||
- installs .env from the local .env.example, overwriting the deployed
|
||||
copy, and keeps the old one alongside as .env.previous;
|
||||
- preserves the named database volume;
|
||||
- pulls Redis and PostgreSQL, then builds the Memby server;
|
||||
- starts each service and waits for its health check;
|
||||
- restores the previous application files if activation fails.
|
||||
|
||||
Configuration lives in the local .env.example and is included directly in every
|
||||
deployment. It does not need to be committed or pushed before deployment.
|
||||
|
||||
SSH performs the password prompt directly. The password is never read or stored
|
||||
by this script.
|
||||
|
||||
This deploys the current local working tree, including uncommitted server changes.
|
||||
|
||||
.EXAMPLE
|
||||
.\deploy-server.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\deploy-server.ps1 -Destination /share/Docker/Memby-test -HealthTimeoutSeconds 180
|
||||
#>
|
||||
|
||||
#Requires -Version 7.2
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $SourceDirectory = $PSScriptRoot,
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $RemoteHost = '10.0.0.213',
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $RemoteUser = 'ssh',
|
||||
|
||||
[Parameter()]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Destination = '/share/Docker/Memby',
|
||||
|
||||
[Parameter()]
|
||||
[ValidateRange(30, 600)]
|
||||
[int] $HealthTimeoutSeconds = 120
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$script:CurrentStep = 0
|
||||
$script:TotalSteps = 5
|
||||
|
||||
function Write-Banner {
|
||||
Write-Host ''
|
||||
Write-Host '╭─ Memby deployment' -ForegroundColor Magenta
|
||||
Write-Host "│ Source $SourceDirectory (local working tree)" -ForegroundColor DarkGray
|
||||
Write-Host "│ Target ${RemoteUser}@${RemoteHost}:$Destination" -ForegroundColor DarkGray
|
||||
Write-Host '╰─' -ForegroundColor Magenta
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Write-Step {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
$script:CurrentStep++
|
||||
Write-Host ("● [{0}/{1}] {2}" -f $script:CurrentStep, $script:TotalSteps, $Message) -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Detail {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
Write-Host " ↳ $Message" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $Message
|
||||
)
|
||||
|
||||
Write-Host "✓ $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Get-RequiredCommand {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $Name
|
||||
)
|
||||
|
||||
$command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $command) {
|
||||
throw "Required command '$Name' was not found in PATH."
|
||||
}
|
||||
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
function Assert-SafeRemoteSettings {
|
||||
if ($RemoteHost -notmatch '^[A-Za-z0-9._:-]+$') {
|
||||
throw "RemoteHost contains unsupported characters: '$RemoteHost'."
|
||||
}
|
||||
|
||||
if ($RemoteUser -notmatch '^[A-Za-z0-9._-]+$') {
|
||||
throw "RemoteUser contains unsupported characters: '$RemoteUser'."
|
||||
}
|
||||
|
||||
if ($Destination -notmatch '^/[A-Za-z0-9._/-]+$') {
|
||||
throw "Destination must be an absolute Linux path using only letters, numbers, '.', '_', '-', and '/'."
|
||||
}
|
||||
|
||||
$segments = $Destination.Split('/', [System.StringSplitOptions]::RemoveEmptyEntries)
|
||||
if ($segments -contains '..') {
|
||||
throw "Destination cannot contain '..' path segments."
|
||||
}
|
||||
|
||||
$broadPaths = @(
|
||||
'/', '/bin', '/boot', '/dev', '/etc', '/home', '/lib', '/lib64',
|
||||
'/opt', '/proc', '/root', '/run', '/sbin', '/srv', '/sys', '/tmp',
|
||||
'/usr', '/var'
|
||||
)
|
||||
if ($Destination -in $broadPaths) {
|
||||
throw "Destination '$Destination' is too broad to replace safely."
|
||||
}
|
||||
}
|
||||
|
||||
function New-DeploymentArchive {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $RepositoryDirectory,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $ArchivePath
|
||||
)
|
||||
|
||||
& $script:TarCommand -cf $ArchivePath -C $RepositoryDirectory server docker-compose.yml .env.example
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Unable to create the deployment archive (tar exit code $LASTEXITCODE)."
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ArchivePath -PathType Leaf)) {
|
||||
throw 'The deployment archive was not created.'
|
||||
}
|
||||
}
|
||||
|
||||
function Send-ArchiveOverSsh {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $ArchivePath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $RemoteCommand
|
||||
)
|
||||
|
||||
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = $script:SshCommand
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.RedirectStandardInput = $true
|
||||
|
||||
# Force interactive password authentication as requested. OpenSSH reads the
|
||||
# password from the console while stdin carries the tar archive.
|
||||
foreach ($argument in @(
|
||||
'-o', 'PubkeyAuthentication=no',
|
||||
'-o', 'PreferredAuthentications=keyboard-interactive,password',
|
||||
'-o', 'NumberOfPasswordPrompts=3',
|
||||
"$RemoteUser@$RemoteHost",
|
||||
$RemoteCommand
|
||||
)) {
|
||||
[void] $startInfo.ArgumentList.Add($argument)
|
||||
}
|
||||
|
||||
$process = [System.Diagnostics.Process]::new()
|
||||
$process.StartInfo = $startInfo
|
||||
$archiveStream = $null
|
||||
$processStarted = $false
|
||||
$exitCode = $null
|
||||
|
||||
try {
|
||||
if (-not $process.Start()) {
|
||||
throw 'SSH could not be started.'
|
||||
}
|
||||
$processStarted = $true
|
||||
|
||||
$archiveStream = [System.IO.File]::OpenRead($ArchivePath)
|
||||
try {
|
||||
$archiveStream.CopyTo($process.StandardInput.BaseStream)
|
||||
}
|
||||
catch [System.IO.IOException] {
|
||||
# The remote side exited before reading the whole archive — a failed early
|
||||
# step, or a shell that rejected the command. Its own message is already on
|
||||
# screen, so let the exit code below do the explaining rather than burying it
|
||||
# under a broken-pipe stack trace.
|
||||
}
|
||||
try { $process.StandardInput.Close() } catch [System.IO.IOException] { }
|
||||
$process.WaitForExit()
|
||||
$exitCode = $process.ExitCode
|
||||
}
|
||||
catch {
|
||||
if ($processStarted -and -not $process.HasExited) {
|
||||
$process.StandardInput.Close()
|
||||
$process.WaitForExit()
|
||||
}
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
if ($archiveStream) {
|
||||
$archiveStream.Dispose()
|
||||
}
|
||||
$process.Dispose()
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
throw "Remote deployment failed with SSH exit code $exitCode. Review the last remote step above."
|
||||
}
|
||||
}
|
||||
|
||||
Write-Banner
|
||||
$deploymentTimer = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
|
||||
Write-Step 'Checking local prerequisites and settings'
|
||||
Assert-SafeRemoteSettings
|
||||
$script:TarCommand = Get-RequiredCommand -Name 'tar'
|
||||
$script:SshCommand = Get-RequiredCommand -Name 'ssh'
|
||||
Write-Detail "SSH $script:SshCommand"
|
||||
Write-Success 'Local prerequisites are ready'
|
||||
Write-Host ''
|
||||
|
||||
$workDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ("memby-deploy-" + [guid]::NewGuid().ToString('N'))
|
||||
$archivePath = Join-Path $workDirectory 'memby-deployment.tar'
|
||||
|
||||
try {
|
||||
[void] (New-Item -ItemType Directory -Path $workDirectory)
|
||||
|
||||
Write-Step 'Selecting the local deployment source'
|
||||
$checkoutDirectory = (Resolve-Path -LiteralPath $SourceDirectory -ErrorAction Stop).Path
|
||||
Write-Success "Using $checkoutDirectory"
|
||||
Write-Host ''
|
||||
|
||||
Write-Step 'Validating the Compose deployment payload'
|
||||
$requiredPaths = @(
|
||||
(Join-Path $checkoutDirectory 'server'),
|
||||
(Join-Path $checkoutDirectory 'server/Dockerfile'),
|
||||
(Join-Path $checkoutDirectory 'server/go.mod'),
|
||||
(Join-Path $checkoutDirectory 'docker-compose.yml'),
|
||||
(Join-Path $checkoutDirectory '.env.example')
|
||||
)
|
||||
foreach ($requiredPath in $requiredPaths) {
|
||||
if (-not (Test-Path -LiteralPath $requiredPath)) {
|
||||
throw "Required deployment file is missing: $requiredPath"
|
||||
}
|
||||
}
|
||||
Write-Detail 'server/ build context'
|
||||
Write-Detail 'docker-compose.yml'
|
||||
Write-Detail '.env.example'
|
||||
Write-Success 'Deployment payload is complete'
|
||||
Write-Host ''
|
||||
|
||||
Write-Step 'Packaging the release'
|
||||
New-DeploymentArchive -RepositoryDirectory $checkoutDirectory -ArchivePath $archivePath
|
||||
$archiveSize = (Get-Item -LiteralPath $archivePath).Length
|
||||
Write-Detail ("Archive size {0:N1} MiB" -f ($archiveSize / 1MB))
|
||||
Write-Success 'Release archive is ready'
|
||||
Write-Host ''
|
||||
|
||||
# This template is single-quoted so PowerShell does not expand the remote
|
||||
# shell's variables. Replacement values are validated before insertion.
|
||||
$remoteCommand = @'
|
||||
set -eu
|
||||
|
||||
destination='__DESTINATION__'
|
||||
health_timeout=__HEALTH_TIMEOUT__
|
||||
parent=$(dirname "$destination")
|
||||
staging="${destination}.new.$$"
|
||||
backup="${destination}.previous.$$"
|
||||
activated=0
|
||||
previous_stopped=0
|
||||
|
||||
step() {
|
||||
printf '\033[36m ● %s\033[0m\n' "$1"
|
||||
}
|
||||
|
||||
detail() {
|
||||
printf '\033[90m ↳ %s\033[0m\n' "$1"
|
||||
}
|
||||
|
||||
success() {
|
||||
printf '\033[32m ✓ %s\033[0m\n' "$1"
|
||||
}
|
||||
|
||||
failure() {
|
||||
printf '\033[31m ✗ %s\033[0m\n' "$1" >&2
|
||||
}
|
||||
|
||||
rollback() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
|
||||
if [ "$status" -eq 0 ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
failure "Deployment failed; cleaning up"
|
||||
rm -rf -- "$staging"
|
||||
|
||||
if [ "$activated" -eq 1 ]; then
|
||||
if [ -f "$destination/docker-compose.yml" ]; then
|
||||
detail "Stopping the incomplete application release"
|
||||
(
|
||||
cd "$destination"
|
||||
docker compose down --remove-orphans >/dev/null 2>&1
|
||||
) || true
|
||||
fi
|
||||
|
||||
rm -rf -- "$destination"
|
||||
if [ -e "$backup" ] || [ -L "$backup" ]; then
|
||||
detail "Restoring the previous application release"
|
||||
mv -- "$backup" "$destination"
|
||||
(
|
||||
cd "$destination"
|
||||
docker compose up -d --build --remove-orphans >/dev/null 2>&1
|
||||
) || true
|
||||
failure "Previous application files were restored"
|
||||
fi
|
||||
elif [ "$previous_stopped" -eq 1 ] && [ -f "$destination/docker-compose.yml" ]; then
|
||||
detail "Restarting the previous application release"
|
||||
(
|
||||
cd "$destination"
|
||||
docker compose up -d --build --remove-orphans >/dev/null 2>&1
|
||||
) || true
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
wait_for_service() {
|
||||
service="$1"
|
||||
elapsed=0
|
||||
detail "Waiting for $service"
|
||||
|
||||
while [ "$elapsed" -lt "$health_timeout" ]; do
|
||||
container_id=$(docker compose ps --all -q "$service" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$container_id" ]; then
|
||||
state=$(docker inspect --format '{{.State.Status}}' "$container_id" 2>/dev/null || true)
|
||||
health=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" 2>/dev/null || true)
|
||||
|
||||
if [ "$state" = 'running' ] && { [ "$health" = 'healthy' ] || [ "$health" = 'none' ]; }; then
|
||||
success "$service is $state ($health)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$state" = 'exited' ] || [ "$state" = 'dead' ]; then
|
||||
failure "$service entered state: $state"
|
||||
docker compose logs --no-color --tail 60 "$service" || true
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
failure "$service did not become healthy within ${health_timeout}s"
|
||||
docker compose logs --no-color --tail 60 "$service" || true
|
||||
return 1
|
||||
}
|
||||
|
||||
trap rollback EXIT
|
||||
trap 'exit 130' INT TERM
|
||||
|
||||
step '[remote 1/8] Checking Docker'
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
failure 'Docker is not installed on the NAS'
|
||||
exit 1
|
||||
fi
|
||||
docker info >/dev/null 2>&1 || {
|
||||
failure 'Docker is installed but the daemon is unavailable'
|
||||
exit 1
|
||||
}
|
||||
docker compose version >/dev/null 2>&1 || {
|
||||
failure 'The Docker Compose v2 plugin is not installed'
|
||||
exit 1
|
||||
}
|
||||
success "$(docker --version)"
|
||||
success "$(docker compose version)"
|
||||
|
||||
step '[remote 2/8] Extracting the release'
|
||||
# Checked before anything is created: the staging directory, the swap and the backup all
|
||||
# need write access to the parent, and "can't create directory" from BusyBox halfway
|
||||
# through a deployment is a poor way to learn the account cannot write there.
|
||||
mkdir -p -- "$parent" 2>/dev/null || true
|
||||
if [ ! -d "$parent" ]; then
|
||||
failure "$parent does not exist and could not be created by $(id -un)"
|
||||
detail 'Pick a path this account can write to with -Destination'
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -w "$parent" ]; then
|
||||
failure "$(id -un) cannot write to $parent"
|
||||
detail 'Deploy somewhere this account owns, for example:'
|
||||
detail ' .\deploy-server.ps1 -Destination /share/Docker/Memby'
|
||||
detail "or grant this account write access to $parent"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf -- "$staging"
|
||||
mkdir -- "$staging"
|
||||
tar -xf - -C "$staging"
|
||||
test -f "$staging/docker-compose.yml"
|
||||
test -f "$staging/server/Dockerfile"
|
||||
success 'Release extracted'
|
||||
|
||||
step '[remote 3/8] Installing configuration from .env.example'
|
||||
# The local .env.example is the single source of truth for configuration and carries
|
||||
# real values rather than placeholders. Every deployment overwrites the deployed .env
|
||||
# with it, so neither a Git push nor an SSH edit is needed.
|
||||
previous_password=''
|
||||
if [ -f "$destination/.env" ]; then
|
||||
previous_password=$(sed -n 's/^POSTGRES_PASSWORD=//p' "$destination/.env" | head -n 1 | tr -d '\r')
|
||||
# Kept beside the new one purely so a bad edit is recoverable by hand.
|
||||
cp -- "$destination/.env" "$staging/.env.previous"
|
||||
detail 'Previous .env saved as .env.previous'
|
||||
fi
|
||||
|
||||
cp -- "$staging/.env.example" "$staging/.env"
|
||||
success 'Installed .env from .env.example'
|
||||
|
||||
new_password=$(sed -n 's/^POSTGRES_PASSWORD=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
admin_token=$(sed -n 's/^MEMBY_ADMIN_TOKEN=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
emby_url=$(sed -n 's/^MEMBY_EMBY_URL=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
configured_port=$(sed -n 's/^MEMBY_PORT=//p' "$staging/.env" | head -n 1 | tr -d '\r')
|
||||
if [ -z "$new_password" ]; then
|
||||
failure 'POSTGRES_PASSWORD is empty in .env.example; Compose will refuse to start'
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$emby_url" ]; then
|
||||
failure 'MEMBY_EMBY_URL is empty in .env.example'
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$admin_token" ]; then
|
||||
failure 'MEMBY_ADMIN_TOKEN is empty in .env.example; refusing to deploy with /admin disabled'
|
||||
exit 1
|
||||
fi
|
||||
if [ "$configured_port" != '32768' ]; then
|
||||
failure "MEMBY_PORT must be 32768 for the mserver.sublogue.com reverse proxy (found: ${configured_port:-unset})"
|
||||
exit 1
|
||||
fi
|
||||
success 'Required gateway configuration is present'
|
||||
if [ -n "$previous_password" ] && [ "$previous_password" != "$new_password" ]; then
|
||||
failure 'POSTGRES_PASSWORD differs from the deployed value'
|
||||
detail 'The PostgreSQL volume is always preserved; restore the deployed password in .env.example'
|
||||
detail 'Change the database credential separately with an explicit migration if required'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$staging"
|
||||
docker compose config --quiet
|
||||
)
|
||||
success 'Compose configuration is valid'
|
||||
|
||||
step '[remote 4/8] Pulling PostgreSQL and Redis'
|
||||
(
|
||||
cd "$staging"
|
||||
docker compose pull postgres redis
|
||||
)
|
||||
success 'Dependency images are ready'
|
||||
|
||||
step '[remote 5/8] Building memby-server'
|
||||
(
|
||||
cd "$staging"
|
||||
docker compose build --pull server
|
||||
)
|
||||
success 'Server image built'
|
||||
|
||||
step '[remote 6/8] Activating the release'
|
||||
rm -rf -- "$backup"
|
||||
if [ -e "$destination" ] || [ -L "$destination" ]; then
|
||||
if [ -f "$destination/docker-compose.yml" ]; then
|
||||
# Compose projects created by older releases may use a different project
|
||||
# name. Stop them from their original directory before moving it so their
|
||||
# published ports (especially 32768) are released for the new stack.
|
||||
detail 'Stopping the previous Compose application'
|
||||
(
|
||||
cd "$destination"
|
||||
docker compose down --remove-orphans
|
||||
)
|
||||
previous_stopped=1
|
||||
success 'Previous Compose application stopped'
|
||||
fi
|
||||
|
||||
mv -- "$destination" "$backup"
|
||||
fi
|
||||
mv -- "$staging" "$destination"
|
||||
activated=1
|
||||
success 'Release activated'
|
||||
|
||||
step '[remote 7/8] Starting the Compose stack'
|
||||
cd "$destination"
|
||||
docker compose up -d --remove-orphans
|
||||
success 'Compose start command completed'
|
||||
|
||||
step '[remote 8/8] Waiting for healthy services'
|
||||
wait_for_service postgres
|
||||
wait_for_service redis
|
||||
wait_for_service server
|
||||
|
||||
published_address=$(docker compose port server 32768 | head -n 1)
|
||||
actual_port=${published_address##*:}
|
||||
if [ "$actual_port" != '32768' ]; then
|
||||
failure "Memby published the wrong NAS port: ${published_address:-none}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
server_container_id=$(docker compose ps -q server)
|
||||
if ! docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$server_container_id" |
|
||||
grep -q '^MEMBY_ADMIN_TOKEN=.'; then
|
||||
failure 'MEMBY_ADMIN_TOKEN was not passed into the running server container'
|
||||
exit 1
|
||||
fi
|
||||
success 'Runtime configuration includes the admin token'
|
||||
|
||||
printf '\n'
|
||||
docker compose ps
|
||||
printf '\n'
|
||||
|
||||
rm -rf -- "$backup"
|
||||
activated=0
|
||||
trap - EXIT INT TERM
|
||||
success 'Remote deployment is healthy'
|
||||
success 'Memby gateway: https://mserver.sublogue.com'
|
||||
'@
|
||||
|
||||
$remoteCommand = $remoteCommand.Replace('__DESTINATION__', $Destination)
|
||||
$remoteCommand = $remoteCommand.Replace('__HEALTH_TIMEOUT__', $HealthTimeoutSeconds.ToString())
|
||||
# This file is edited on Windows, so the here-string above arrives with whatever line
|
||||
# endings it was saved with. A remote shell reads a trailing carriage return as part
|
||||
# of the token — 'set -eu\r' fails with "illegal option" before anything runs — so the
|
||||
# script is normalised to LF here rather than depending on how it was saved.
|
||||
$remoteCommand = $remoteCommand.Replace("`r`n", "`n").Replace("`r", "`n")
|
||||
|
||||
Write-Step "Deploying to $RemoteHost"
|
||||
Write-Detail 'One SSH password prompt will appear'
|
||||
Write-Detail 'Remote build output follows'
|
||||
Write-Host ''
|
||||
Send-ArchiveOverSsh -ArchivePath $archivePath -RemoteCommand $remoteCommand
|
||||
|
||||
$deploymentTimer.Stop()
|
||||
Write-Host ''
|
||||
Write-Success ("Deployment complete in {0:mm\:ss}" -f $deploymentTimer.Elapsed)
|
||||
Write-Host ' Memby gateway: https://mserver.sublogue.com' -ForegroundColor White
|
||||
Write-Host " NAS endpoint: http://${RemoteHost}:32768" -ForegroundColor DarkGray
|
||||
Write-Host " Install path: ${RemoteHost}:$Destination" -ForegroundColor DarkGray
|
||||
}
|
||||
catch {
|
||||
$deploymentTimer.Stop()
|
||||
Write-Host ''
|
||||
Write-Host "✗ Deployment stopped after $($deploymentTimer.Elapsed.ToString('mm\:ss'))" -ForegroundColor Red
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $workDirectory) {
|
||||
Remove-Item -LiteralPath $workDirectory -Recurse -Force
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user