furniture-3d-system-Wu/scripts/deploy-yydd-prod-from-git.ps1

658 lines
19 KiB
PowerShell

param(
[string]$RepoRoot = $(Split-Path -Parent $PSScriptRoot),
[string]$ProjectSubdir = "furniture-3d-system",
[string]$Ref = "origin/main",
[string]$RemoteBase = "/opt/furniture-3d-system-prod",
[string]$RemoteRepoDir = "",
[string]$RemoteGitUrl = "",
[string]$ServerHost = "ltai5tdb2ogyj59fquyedh.yydd.ai",
[int]$SshPort = 60022,
[string]$ServerUser = "root",
[string]$ServerPassword = "",
[string]$HostKeyFingerprint = "SHA256:JPHcomdjuUZOTM7sNdBYg0/xRkQSXzbXe7FEfNtIFAU",
[int]$AppPort = 3001,
[string]$Pm2Name = "furniture-3d-system-prod",
[int]$KeepReleases = 5,
[switch]$ForceArchiveFallback,
[switch]$KeepArtifacts
)
$ErrorActionPreference = "Stop"
$createScript = Join-Path $RepoRoot "scripts\create-yydd-prod-deploy.ps1"
$plinkPath = "C:\Program Files\PuTTY\plink.exe"
$pscpPath = "C:\Program Files\PuTTY\pscp.exe"
if (-not (Test-Path -LiteralPath $createScript)) {
throw "Missing package script: $createScript"
}
if (-not (Test-Path -LiteralPath $plinkPath)) {
throw "Missing plink.exe: $plinkPath"
}
if (-not (Test-Path -LiteralPath $pscpPath)) {
throw "Missing pscp.exe: $pscpPath"
}
if ([string]::IsNullOrWhiteSpace($RemoteRepoDir)) {
$RemoteRepoDir = ("{0}/repo" -f $RemoteBase)
}
function Invoke-NativeCommand {
param(
[string]$FilePath,
[string[]]$Arguments,
[string]$WorkingDirectory = $RepoRoot
)
$previousErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = "Continue"
Push-Location -LiteralPath $WorkingDirectory
try {
$output = & $FilePath @Arguments 2>&1
$exitCode = $LASTEXITCODE
}
finally {
Pop-Location
}
$lines = @(
$output |
ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) {
$_.ToString()
}
else {
[string]$_
}
} |
Where-Object { $_.Length -gt 0 }
)
if ($exitCode -ne 0) {
throw (($lines -join [Environment]::NewLine).Trim())
}
return $lines
}
finally {
$ErrorActionPreference = $previousErrorActionPreference
}
}
function Invoke-Git {
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Arguments
)
return Invoke-NativeCommand -FilePath "git" -Arguments (@("-C", $RepoRoot) + $Arguments) -WorkingDirectory $RepoRoot
}
function Invoke-Plink {
param(
[string]$Command
)
$arguments = @(
"-ssh"
"-batch"
"-hostkey"
$HostKeyFingerprint
"-P"
[string]$SshPort
)
if (-not [string]::IsNullOrWhiteSpace($ServerPassword)) {
$arguments += @("-pw", $ServerPassword)
}
$arguments += @(
("{0}@{1}" -f $ServerUser, $ServerHost)
$Command
)
return Invoke-NativeCommand -FilePath $plinkPath -Arguments $arguments -WorkingDirectory $RepoRoot
}
function Copy-WithPscp {
param(
[string]$LocalPath,
[string]$RemotePath
)
if (-not (Test-Path -LiteralPath $LocalPath)) {
throw "Upload source does not exist: $LocalPath"
}
$arguments = @(
"-batch"
"-hostkey"
$HostKeyFingerprint
"-P"
[string]$SshPort
)
if (-not [string]::IsNullOrWhiteSpace($ServerPassword)) {
$arguments += @("-pw", $ServerPassword)
}
$arguments += @(
$LocalPath
("{0}@{1}:{2}" -f $ServerUser, $ServerHost, $RemotePath)
)
return Invoke-NativeCommand -FilePath $pscpPath -Arguments $arguments -WorkingDirectory $RepoRoot
}
function New-RemoteGitDeployScript {
param(
[string]$OutputRoot,
[string]$RemoteBase,
[string]$RemoteRepoDir,
[string]$ProjectSubdir,
[string]$ReleaseId,
[string]$Commit,
[string]$SourceRef,
[string]$RemoteGitUrl,
[int]$AppPort,
[string]$Pm2Name,
[int]$KeepReleases
)
if (-not (Test-Path -LiteralPath $OutputRoot)) {
New-Item -ItemType Directory -Path $OutputRoot -Force | Out-Null
}
$scriptName = "deploy-furniture-prod-$ReleaseId-remote.sh"
$scriptPath = Join-Path $OutputRoot $scriptName
$shellScript = @'
#!/usr/bin/env bash
set -euo pipefail
BASE="__REMOTE_BASE__"
REPO_DIR="__REMOTE_REPO_DIR__"
RELEASES="$BASE/releases"
SHARED="$BASE/shared"
CURRENT="$BASE/current"
LEGACY_APP="$BASE/__PROJECT_SUBDIR__"
PROJECT_SUBDIR="__PROJECT_SUBDIR__"
RELEASE_ID="__RELEASE_ID__"
APP_PORT="__APP_PORT__"
PM2_NAME="__PM2_NAME__"
BOM_WORKER_NAME="${PM2_NAME}-bom-worker"
KEEP_RELEASES="__KEEP_RELEASES__"
COMMIT_SHA="__COMMIT_SHA__"
SOURCE_REF="__SOURCE_REF__"
REMOTE_GIT_URL="__REMOTE_GIT_URL__"
assert_path_in_base() {
local target="$1"
case "$target" in
"$BASE"/*) ;;
*)
echo "Refuse to touch path outside deployment base: $target" >&2
exit 1
;;
esac
}
copy_legacy_env_if_needed() {
if [ -f "$SHARED/.env" ]; then
return 0
fi
mkdir -p "$SHARED"
if [ -f "$BASE/.env" ]; then
cp "$BASE/.env" "$SHARED/.env"
return 0
fi
if [ -f "$LEGACY_APP/.env" ]; then
cp "$LEGACY_APP/.env" "$SHARED/.env"
return 0
fi
echo "Missing shared env file. Expected one of:" >&2
echo " $SHARED/.env" >&2
echo " $BASE/.env" >&2
echo " $LEGACY_APP/.env" >&2
exit 1
}
prepare_standalone_runtime() {
local app_root="$1"
local standalone_root="$app_root/.next/standalone"
if [ ! -f "$standalone_root/server.js" ]; then
echo "Missing Next standalone server: $standalone_root/server.js" >&2
exit 1
fi
if [ ! -d "$app_root/.next/static" ]; then
echo "Missing Next static assets: $app_root/.next/static" >&2
exit 1
fi
mkdir -p "$standalone_root/.next"
rm -rf -- "$standalone_root/.next/static" "$standalone_root/public"
cp -a "$app_root/.next/static" "$standalone_root/.next/static"
if [ -d "$app_root/public" ]; then
cp -a "$app_root/public" "$standalone_root/public"
fi
}
ensure_bom_costing_worker_secret() {
if grep -qE '^BOM_COSTING_WORKER_SECRET=.+$' "$SHARED/.env"; then
return 0
fi
local secret
secret="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))")"
if [ -z "$secret" ]; then
echo "Unable to generate BOM costing worker secret" >&2
exit 1
fi
umask 077
printf '\nBOM_COSTING_WORKER_SECRET=%s\n' "$secret" >> "$SHARED/.env"
chmod 600 "$SHARED/.env" || true
}
start_pm2_release() {
local release_root="$1"
local app_root="$release_root/$PROJECT_SUBDIR"
local start_command
local worker_command
local worker_timeout
prepare_standalone_runtime "$app_root"
worker_timeout="${BOM_COSTING_WORKER_REQUEST_TIMEOUT_SECONDS:-1800}"
if ! [[ "$worker_timeout" =~ ^[1-9][0-9]*$ ]]; then
worker_timeout=1800
fi
start_command="cd '$app_root' && set -a && source '$SHARED/.env' && set +a && exec env PORT=$APP_PORT HOSTNAME=0.0.0.0 node .next/standalone/server.js"
worker_command="cd '$app_root' && set -a && source '$SHARED/.env' && set +a && : \"\${BOM_COSTING_WORKER_SECRET:?BOM_COSTING_WORKER_SECRET is required}\"; while true; do curl --fail --silent --show-error --max-time $worker_timeout --request POST 'http://127.0.0.1:$APP_PORT/api/internal/bom-costing/worker/run' --header \"x-bom-costing-worker-secret: \$BOM_COSTING_WORKER_SECRET\" >/dev/null || echo '[BOM costing worker] loop request failed' >&2; sleep 10; done"
pm2 delete "$PM2_NAME" >/dev/null 2>&1 || true
pm2 delete "$BOM_WORKER_NAME" >/dev/null 2>&1 || true
pm2 start /bin/bash --name "$PM2_NAME" -- -lc "$start_command"
pm2 start /bin/bash --name "$BOM_WORKER_NAME" -- -lc "$worker_command"
pm2 save >/dev/null
}
install_virtual_inventory_push_cron() {
local cron_file
local cron_line
local lock_file="/tmp/furniture-3d-system-virtual-inventory-push.lock"
local endpoint="http://127.0.0.1:$APP_PORT/api/intake/non-integrated/jushuitan-virtual-inventory-push"
local log_file="$SHARED/logs/jushuitan-virtual-inventory-push.log"
mkdir -p "$SHARED/logs"
cron_file="$(mktemp)"
(crontab -l 2>/dev/null || true) |
sed '\|/api/intake/non-integrated/jushuitan-virtual-inventory-push|d' |
sed '\|furniture-3d-system-virtual-inventory-push|d' > "$cron_file"
cron_line="* * * * * /usr/bin/flock -n \"$lock_file\" /bin/bash -lc 'set -euo pipefail; set -a; source \"$SHARED/.env\"; set +a; : \"\${JST_VIRTUAL_INVENTORY_CRON_SECRET:?JST_VIRTUAL_INVENTORY_CRON_SECRET is required}\"; curl --fail --silent --show-error --max-time 55 --request POST \"$endpoint\" --header \"x-jst-virtual-inventory-secret: \${JST_VIRTUAL_INVENTORY_CRON_SECRET}\"' >> \"$log_file\" 2>&1 # furniture-3d-system-virtual-inventory-push"
printf '%s\n' "$cron_line" >> "$cron_file"
crontab "$cron_file"
rm -f -- "$cron_file"
}
cleanup_old_releases() {
if [ "$KEEP_RELEASES" -le 0 ]; then
return 0
fi
mapfile -t release_dirs < <(find "$RELEASES" -mindepth 1 -maxdepth 1 -type d | sort)
local total="${#release_dirs[@]}"
if [ "$total" -le "$KEEP_RELEASES" ]; then
return 0
fi
local current_real=""
current_real="$(readlink -f "$CURRENT" 2>/dev/null || true)"
local remove_count=$((total - KEEP_RELEASES))
for ((i=0; i<remove_count; i++)); do
local candidate="${release_dirs[$i]}"
if [ -n "$current_real" ] && [ "$candidate" = "$current_real" ]; then
continue
fi
assert_path_in_base "$candidate"
rm -rf -- "$candidate"
done
}
ensure_repo() {
assert_path_in_base "$REPO_DIR"
mkdir -p "$(dirname "$REPO_DIR")"
if [ -d "$REPO_DIR/.git" ]; then
git -C "$REPO_DIR" remote set-url origin "$REMOTE_GIT_URL"
return 0
fi
rm -rf -- "$REPO_DIR"
mkdir -p "$REPO_DIR"
git -C "$REPO_DIR" init >/dev/null
git -C "$REPO_DIR" remote add origin "$REMOTE_GIT_URL"
}
resolve_fetch_ref() {
case "$SOURCE_REF" in
origin/*)
printf '%s\n' "${SOURCE_REF#origin/}"
;;
refs/remotes/origin/*)
printf '%s\n' "${SOURCE_REF#refs/remotes/origin/}"
;;
refs/heads/*)
printf '%s\n' "${SOURCE_REF#refs/heads/}"
;;
*)
printf '%s\n' "$SOURCE_REF"
;;
esac
}
fetch_target_commit() {
local remote_ref="$1"
if [ -z "$remote_ref" ]; then
echo "Missing remote fetch ref derived from SOURCE_REF=$SOURCE_REF" >&2
exit 1
fi
git -C "$REPO_DIR" fetch --prune --depth 1 origin "$remote_ref"
if ! git -C "$REPO_DIR" cat-file -e "$COMMIT_SHA^{commit}" 2>/dev/null; then
git -C "$REPO_DIR" fetch --prune origin "$remote_ref"
fi
git -C "$REPO_DIR" cat-file -e "$COMMIT_SHA^{commit}"
}
ensure_shared_non_integrated_assets() {
local app_root="$1"
local shared_assets="$SHARED/non-integrated-assets"
local legacy_shared_assets="$SHARED/non-integrated-test-assets"
local release_data_assets="$app_root/.data/non-integrated-assets"
local release_public_assets="$app_root/public/non-integrated-test-assets"
assert_path_in_base "$shared_assets"
mkdir -p "$shared_assets"
if [ -n "$PREVIOUS_RELEASE" ]; then
if [ -d "$PREVIOUS_RELEASE/$PROJECT_SUBDIR/.data/non-integrated-assets" ]; then
cp -a "$PREVIOUS_RELEASE/$PROJECT_SUBDIR/.data/non-integrated-assets/." "$shared_assets/" 2>/dev/null || true
fi
if [ -d "$PREVIOUS_RELEASE/$PROJECT_SUBDIR/public/non-integrated-test-assets" ]; then
cp -a "$PREVIOUS_RELEASE/$PROJECT_SUBDIR/public/non-integrated-test-assets/." "$shared_assets/" 2>/dev/null || true
fi
fi
if [ -d "$legacy_shared_assets" ] && [ ! -L "$legacy_shared_assets" ]; then
cp -a "$legacy_shared_assets/." "$shared_assets/" 2>/dev/null || true
fi
if [ -d "$LEGACY_APP/public/non-integrated-test-assets" ] && [ ! -L "$LEGACY_APP/public/non-integrated-test-assets" ]; then
cp -a "$LEGACY_APP/public/non-integrated-test-assets/." "$shared_assets/" 2>/dev/null || true
fi
rm -rf -- "$release_data_assets" "$release_public_assets"
mkdir -p "$(dirname "$release_data_assets")" "$(dirname "$release_public_assets")"
ln -sfn "$shared_assets" "$release_data_assets"
}
mkdir -p "$BASE" "$RELEASES" "$SHARED"
copy_legacy_env_if_needed
ensure_bom_costing_worker_secret
ensure_repo
fetch_target_commit "$(resolve_fetch_ref)"
RELEASE_DIR="$RELEASES/$RELEASE_ID"
APP_DIR="$RELEASE_DIR/$PROJECT_SUBDIR"
PREVIOUS_RELEASE=""
PREVIOUS_RELEASE="$(readlink -f "$CURRENT" 2>/dev/null || true)"
HEALTHCHECK_FILE="/tmp/furniture-3d-system-prod-healthcheck-$RELEASE_ID.txt"
DEPLOY_SUCCEEDED=0
cleanup_failed_release() {
local status="$1"
rm -f -- "${HEALTHCHECK_FILE:-}" >/dev/null 2>&1 || true
if [ "$DEPLOY_SUCCEEDED" -eq 1 ]; then
exit "$status"
fi
if [ -n "${RELEASE_DIR:-}" ] && [ -d "$RELEASE_DIR" ]; then
local current_real=""
current_real="$(readlink -f "$CURRENT" 2>/dev/null || true)"
if [ -n "$current_real" ] && [ "$current_real" = "$RELEASE_DIR" ]; then
if [ -n "$PREVIOUS_RELEASE" ] && [ -d "$PREVIOUS_RELEASE" ]; then
ln -sfn "$PREVIOUS_RELEASE" "$CURRENT"
if ! start_pm2_release "$CURRENT"; then
echo "Rollback start failed for previous release: $PREVIOUS_RELEASE" >&2
status=1
fi
fi
fi
assert_path_in_base "$RELEASE_DIR"
rm -rf -- "$RELEASE_DIR"
fi
exit "$status"
}
trap 'cleanup_failed_release "$?"' EXIT
assert_path_in_base "$RELEASE_DIR"
rm -rf -- "$RELEASE_DIR"
mkdir -p "$RELEASE_DIR"
git -C "$REPO_DIR" archive --format=tar --prefix="$PROJECT_SUBDIR/" "$COMMIT_SHA:$PROJECT_SUBDIR" | tar -xf - -C "$RELEASE_DIR"
if [ ! -d "$APP_DIR" ]; then
echo "Archive extracted but app directory missing: $APP_DIR" >&2
exit 1
fi
printf '%s\n' "$COMMIT_SHA" > "$RELEASE_DIR/REVISION"
printf '%s\n' "$SOURCE_REF" > "$RELEASE_DIR/SOURCE_REF"
ln -sfn "$SHARED/.env" "$APP_DIR/.env"
cd "$APP_DIR"
export PUPPETEER_SKIP_DOWNLOAD=true
npm ci --no-audit --no-fund
npx prisma generate
npx prisma migrate deploy
npm run build
ensure_shared_non_integrated_assets "$APP_DIR"
ln -sfn "$RELEASE_DIR" "$CURRENT"
start_pm2_release "$CURRENT"
sleep 5
if ! curl -fsS -I "http://127.0.0.1:$APP_PORT" >"$HEALTHCHECK_FILE"; then
echo "New release health check failed. Rolling back..." >&2
if [ -n "$PREVIOUS_RELEASE" ] && [ -d "$PREVIOUS_RELEASE" ]; then
ln -sfn "$PREVIOUS_RELEASE" "$CURRENT"
start_pm2_release "$CURRENT"
fi
exit 1
fi
sed -n '1,12p' "$HEALTHCHECK_FILE"
install_virtual_inventory_push_cron
cleanup_old_releases
rm -f -- "$HEALTHCHECK_FILE"
DEPLOY_SUCCEEDED=1
trap - EXIT
echo "=== REMOTE GIT DEPLOY DONE ==="
pm2 describe "$PM2_NAME" | sed -n '1,35p'
pm2 describe "$BOM_WORKER_NAME" | sed -n '1,35p'
'@
$shellScript = $shellScript.Replace("__REMOTE_BASE__", $RemoteBase)
$shellScript = $shellScript.Replace("__REMOTE_REPO_DIR__", $RemoteRepoDir)
$shellScript = $shellScript.Replace("__PROJECT_SUBDIR__", $ProjectSubdir)
$shellScript = $shellScript.Replace("__RELEASE_ID__", $ReleaseId)
$shellScript = $shellScript.Replace("__APP_PORT__", [string]$AppPort)
$shellScript = $shellScript.Replace("__PM2_NAME__", $Pm2Name)
$shellScript = $shellScript.Replace("__KEEP_RELEASES__", [string]$KeepReleases)
$shellScript = $shellScript.Replace("__COMMIT_SHA__", $Commit)
$shellScript = $shellScript.Replace("__SOURCE_REF__", $SourceRef)
$shellScript = $shellScript.Replace("__REMOTE_GIT_URL__", $RemoteGitUrl)
$shellScript = $shellScript -replace "`r`n", "`n"
[System.IO.File]::WriteAllText($scriptPath, $shellScript, [System.Text.UTF8Encoding]::new($false))
return $scriptPath
}
function Invoke-ArchiveFallbackDeploy {
param(
[string]$DeployRef
)
$package = & $createScript `
-RepoRoot $RepoRoot `
-Ref $DeployRef `
-RemoteBase $RemoteBase `
-ServerHost $ServerHost `
-AppPort $AppPort `
-Pm2Name $Pm2Name `
-KeepReleases $KeepReleases
if (-not $package) {
throw "Package script returned no result"
}
$archivePath = [string]$package.Archive
$scriptPath = [string]$package.Script
$remoteArchivePath = [string]$package.RemoteArchivePath
$remoteScriptPath = [string]$package.RemoteScriptPath
$remoteIncoming = ("{0}/incoming" -f $RemoteBase)
$mkdirIncomingCommand = "mkdir -p '{0}'" -f $remoteIncoming
$runDeployCommand = "chmod +x '{0}' && bash '{0}'" -f $remoteScriptPath
try {
Invoke-Plink $mkdirIncomingCommand | Out-Null
Copy-WithPscp -LocalPath $archivePath -RemotePath $remoteArchivePath | Out-Null
Copy-WithPscp -LocalPath $scriptPath -RemotePath $remoteScriptPath | Out-Null
$deployOutput = Invoke-Plink $runDeployCommand
}
finally {
if (-not $KeepArtifacts) {
try {
Invoke-Plink ("rm -f '{0}' '{1}'" -f $remoteArchivePath, $remoteScriptPath) | Out-Null
}
catch {
Write-Warning ("Failed to clean remote temp files: {0}" -f $_.Exception.Message)
}
if (Test-Path -LiteralPath $archivePath) {
Remove-Item -LiteralPath $archivePath -Force
}
if (Test-Path -LiteralPath $scriptPath) {
Remove-Item -LiteralPath $scriptPath -Force
}
}
}
return [PSCustomObject]@{
Mode = "archive-fallback"
Ref = [string]$package.Ref
Commit = [string]$package.Commit
RemoteBase = $RemoteBase
RemoteArchivePath = $remoteArchivePath
RemoteScriptPath = $remoteScriptPath
ServerHost = $ServerHost
SshPort = $SshPort
Pm2Name = $Pm2Name
Output = ($deployOutput -join [Environment]::NewLine)
}
}
if ($Ref -match "^(origin/|refs/remotes/origin/)") {
Invoke-Git fetch origin | Out-Null
}
$commit = (Invoke-Git rev-parse $Ref | Select-Object -First 1).Trim()
if ([string]::IsNullOrWhiteSpace($commit)) {
throw "Unable to resolve deploy ref: $Ref"
}
if ([string]::IsNullOrWhiteSpace($RemoteGitUrl)) {
$RemoteGitUrl = (Invoke-Git remote get-url origin | Select-Object -First 1).Trim()
}
if ([string]::IsNullOrWhiteSpace($RemoteGitUrl)) {
throw "Unable to resolve origin remote URL"
}
$releaseId = "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss"), $commit.Substring(0, 7)
$tempOutputRoot = Join-Path $env:TEMP "3danzhuang-deploy-artifacts\prod"
$remoteScriptPath = ("{0}/incoming/deploy-furniture-prod-{1}-remote.sh" -f $RemoteBase, $releaseId)
$runDeployCommand = "chmod +x '{0}' && bash '{0}'" -f $remoteScriptPath
if (-not $ForceArchiveFallback) {
try {
Invoke-Plink ("git ls-remote '{0}' HEAD >/dev/null" -f $RemoteGitUrl) | Out-Null
$localScriptPath = New-RemoteGitDeployScript `
-OutputRoot $tempOutputRoot `
-RemoteBase $RemoteBase `
-RemoteRepoDir $RemoteRepoDir `
-ProjectSubdir $ProjectSubdir `
-ReleaseId $releaseId `
-Commit $commit `
-SourceRef $Ref `
-RemoteGitUrl $RemoteGitUrl `
-AppPort $AppPort `
-Pm2Name $Pm2Name `
-KeepReleases $KeepReleases
try {
Invoke-Plink ("mkdir -p '{0}/incoming'" -f $RemoteBase) | Out-Null
Copy-WithPscp -LocalPath $localScriptPath -RemotePath $remoteScriptPath | Out-Null
$deployOutput = Invoke-Plink $runDeployCommand
}
finally {
if (-not $KeepArtifacts) {
try {
Invoke-Plink ("rm -f '{0}'" -f $remoteScriptPath) | Out-Null
}
catch {
Write-Warning ("Failed to clean remote deploy script: {0}" -f $_.Exception.Message)
}
if (Test-Path -LiteralPath $localScriptPath) {
Remove-Item -LiteralPath $localScriptPath -Force
}
}
}
return [PSCustomObject]@{
Mode = "remote-git-incremental"
Ref = $Ref
Commit = $commit
RemoteBase = $RemoteBase
RemoteRepoDir = $RemoteRepoDir
ServerHost = $ServerHost
SshPort = $SshPort
Pm2Name = $Pm2Name
Output = ($deployOutput -join [Environment]::NewLine)
}
}
catch {
Write-Warning ("Server-side Git deploy failed, falling back to package deploy: {0}" -f $_.Exception.Message)
}
}
Invoke-ArchiveFallbackDeploy -DeployRef $Ref