802 lines
26 KiB
PowerShell
802 lines
26 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",
|
|
[string]$ExpectedHostname = "mr-dao-Default-string",
|
|
[int]$SshPort = 60022,
|
|
[string]$ServerUser = "root",
|
|
[string]$ServerPassword = "",
|
|
[string]$IdentityFile = "",
|
|
[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"
|
|
$useOpenSshIdentity = -not [string]::IsNullOrWhiteSpace($IdentityFile)
|
|
$sshPath = ""
|
|
$scpPath = ""
|
|
$knownHostsPath = Join-Path $env:USERPROFILE ".ssh\known_hosts"
|
|
|
|
if (-not (Test-Path -LiteralPath $createScript)) {
|
|
throw "Missing package script: $createScript"
|
|
}
|
|
if ($useOpenSshIdentity) {
|
|
if (-not (Test-Path -LiteralPath $IdentityFile)) {
|
|
throw "SSH identity file does not exist: $IdentityFile"
|
|
}
|
|
if (-not (Test-Path -LiteralPath $knownHostsPath)) {
|
|
throw "OpenSSH known_hosts file is missing: $knownHostsPath"
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($ServerPassword)) {
|
|
throw "Specify either IdentityFile or ServerPassword, not both."
|
|
}
|
|
|
|
$sshPath = (Get-Command "ssh.exe" -ErrorAction Stop).Source
|
|
$scpPath = (Get-Command "scp.exe" -ErrorAction Stop).Source
|
|
}
|
|
else {
|
|
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
|
|
)
|
|
|
|
if ($ExpectedHostname -notmatch '^[a-zA-Z0-9._-]+$' -or $RemoteBase -notmatch '^/[a-zA-Z0-9/_-]+$') {
|
|
throw "Invalid production identity parameters"
|
|
}
|
|
$guard = 'test "$(hostname)" = ''{0}'' && test -d ''{1}'' || exit 91' -f $ExpectedHostname, $RemoteBase
|
|
$Command = $guard + "`n" + $Command
|
|
|
|
if ($useOpenSshIdentity) {
|
|
$arguments = @(
|
|
"-o"
|
|
"BatchMode=yes"
|
|
"-o"
|
|
"StrictHostKeyChecking=yes"
|
|
"-o"
|
|
("UserKnownHostsFile={0}" -f $knownHostsPath)
|
|
"-o"
|
|
"ConnectTimeout=20"
|
|
"-o"
|
|
"ConnectionAttempts=1"
|
|
"-o"
|
|
"ServerAliveInterval=15"
|
|
"-o"
|
|
"ServerAliveCountMax=2"
|
|
"-i"
|
|
$IdentityFile
|
|
"-p"
|
|
[string]$SshPort
|
|
("{0}@{1}" -f $ServerUser, $ServerHost)
|
|
$Command
|
|
)
|
|
|
|
return Invoke-NativeCommand -FilePath $sshPath -Arguments $arguments -WorkingDirectory $RepoRoot
|
|
}
|
|
|
|
$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"
|
|
}
|
|
|
|
if ($useOpenSshIdentity) {
|
|
$arguments = @(
|
|
"-q"
|
|
"-o"
|
|
"BatchMode=yes"
|
|
"-o"
|
|
"StrictHostKeyChecking=yes"
|
|
"-o"
|
|
("UserKnownHostsFile={0}" -f $knownHostsPath)
|
|
"-o"
|
|
"ConnectTimeout=20"
|
|
"-o"
|
|
"ConnectionAttempts=1"
|
|
"-i"
|
|
$IdentityFile
|
|
"-P"
|
|
[string]$SshPort
|
|
$LocalPath
|
|
("{0}@{1}:{2}" -f $ServerUser, $ServerHost, $RemotePath)
|
|
)
|
|
|
|
return Invoke-NativeCommand -FilePath $scpPath -Arguments $arguments -WorkingDirectory $RepoRoot
|
|
}
|
|
|
|
$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
|
|
}
|
|
|
|
__RELEASE_GUARDS__
|
|
|
|
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
|
|
}
|
|
|
|
ensure_product_development_todo_secret() {
|
|
if grep -qE '^PRODUCT_DEVELOPMENT_TODO_CRON_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 product development todo cron secret" >&2
|
|
exit 1
|
|
fi
|
|
|
|
umask 077
|
|
printf '\nPRODUCT_DEVELOPMENT_TODO_CRON_SECRET=%s\n' "$secret" >> "$SHARED/.env"
|
|
chmod 600 "$SHARED/.env" || true
|
|
}
|
|
|
|
ensure_image_search_model() {
|
|
local model_dir="$SHARED/models/dinov2-small"
|
|
local source_base="${IMAGE_SEARCH_MODEL_SOURCE_BASE_URL:-https://www.modelscope.cn/models/Xenova/dinov2-small/resolve/master}"
|
|
|
|
if [ -s "$model_dir/config.json" ] && \
|
|
[ -s "$model_dir/preprocessor_config.json" ] && \
|
|
[ -s "$model_dir/onnx/model_quantized.onnx" ]; then
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$model_dir/onnx"
|
|
curl --fail --silent --show-error --location "$source_base/config.json" -o "$model_dir/config.json.tmp"
|
|
curl --fail --silent --show-error --location "$source_base/preprocessor_config.json" -o "$model_dir/preprocessor_config.json.tmp"
|
|
curl --fail --silent --show-error --location "$source_base/onnx/model_quantized.onnx" -o "$model_dir/onnx/model_quantized.onnx.tmp"
|
|
mv "$model_dir/config.json.tmp" "$model_dir/config.json"
|
|
mv "$model_dir/preprocessor_config.json.tmp" "$model_dir/preprocessor_config.json"
|
|
mv "$model_dir/onnx/model_quantized.onnx.tmp" "$model_dir/onnx/model_quantized.onnx"
|
|
}
|
|
|
|
start_pm2_release() {
|
|
local release_root="$1"
|
|
local app_root="$release_root/$PROJECT_SUBDIR"
|
|
local start_command
|
|
local worker_command
|
|
local worker_timeout
|
|
assert_release_ready "$app_root" || return 1
|
|
# Read the shared environment before resolving the curl limit. The worker
|
|
# command itself sources this file later, but doing it only there leaves the
|
|
# already-interpolated --max-time stuck at an unsafe long default.
|
|
set -a
|
|
source "$SHARED/.env"
|
|
set +a
|
|
ensure_image_search_model
|
|
worker_timeout="${BOM_COSTING_WORKER_REQUEST_TIMEOUT_SECONDS:-1200}"
|
|
# One worker turn contains its own per-request limits. Never let a stale
|
|
# environment value turn a failed model call into hours of silent waiting.
|
|
if ! [[ "$worker_timeout" =~ ^[1-9][0-9]*$ ]] || [ "$worker_timeout" -lt 60 ] || [ "$worker_timeout" -gt 1200 ]; then
|
|
worker_timeout=1200
|
|
fi
|
|
# The BOM worker calls the same Next process. Bound V8 old-space so a long
|
|
# sequence of streamed model responses is collected incrementally instead
|
|
# of letting one case grow the application near the host memory limit.
|
|
start_command="cd '$app_root' && set -a && source '$SHARED/.env' && set +a && exec env NODE_OPTIONS='--max-old-space-size=768' IMAGE_SEARCH_MODEL_PATH='$SHARED/models/dinov2-small' 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 fixed_cron_line
|
|
local todo_cron_line
|
|
local lock_file="/tmp/furniture-3d-system-virtual-inventory-push.lock"
|
|
local fixed_lock_file="/tmp/furniture-3d-system-fixed-virtual-inventory-push.lock"
|
|
local todo_lock_file="/tmp/furniture-3d-system-product-development-todos.lock"
|
|
local endpoint="http://127.0.0.1:$APP_PORT/api/intake/non-integrated/jushuitan-virtual-inventory-push"
|
|
local fixed_endpoint="http://127.0.0.1:$APP_PORT/api/intake/non-integrated/jushuitan-fixed-virtual-inventory-push"
|
|
local todo_endpoint="http://127.0.0.1:$APP_PORT/api/internal/product-development-todos/run?mode=send"
|
|
local log_file="$SHARED/logs/jushuitan-virtual-inventory-push.log"
|
|
local fixed_log_file="$SHARED/logs/jushuitan-fixed-virtual-inventory-push.log"
|
|
local todo_log_file="$SHARED/logs/product-development-todos.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' |
|
|
sed '\|/api/intake/non-integrated/jushuitan-fixed-virtual-inventory-push|d' |
|
|
sed '\|furniture-3d-system-fixed-virtual-inventory-push|d' |
|
|
sed '\|/api/internal/product-development-todos/run|d' |
|
|
sed '\|furniture-3d-system-product-development-todos|d' |
|
|
sed '\|^CRON_TZ=Asia/Shanghai$|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"
|
|
fixed_cron_line="0 20 * * * /usr/bin/flock -n \"$fixed_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 1800 --request POST \"$fixed_endpoint\" --header \"x-jst-virtual-inventory-secret: \${JST_VIRTUAL_INVENTORY_CRON_SECRET}\"' >> \"$fixed_log_file\" 2>&1 # furniture-3d-system-fixed-virtual-inventory-push"
|
|
todo_cron_line="0 10 * * * /usr/bin/flock -n \"$todo_lock_file\" /bin/bash -lc 'set -euo pipefail; set -a; source \"$SHARED/.env\"; set +a; : \"\${PRODUCT_DEVELOPMENT_TODO_CRON_SECRET:?PRODUCT_DEVELOPMENT_TODO_CRON_SECRET is required}\"; curl --fail --silent --show-error --max-time 300 --request POST \"$todo_endpoint\" --header \"x-product-development-todo-secret: \${PRODUCT_DEVELOPMENT_TODO_CRON_SECRET}\"' >> \"$todo_log_file\" 2>&1 # furniture-3d-system-product-development-todos"
|
|
printf '%s\n' "$cron_line" >> "$cron_file"
|
|
printf '%s\n' "$fixed_cron_line" >> "$cron_file"
|
|
printf '%s\n' 'CRON_TZ=Asia/Shanghai' >> "$cron_file"
|
|
printf '%s\n' "$todo_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"
|
|
}
|
|
|
|
test "$(hostname)" = "__EXPECTED_HOSTNAME__" && test -d "$BASE" || exit 91
|
|
acquire_deploy_lock
|
|
mkdir -p "$RELEASES" "$SHARED"
|
|
copy_legacy_env_if_needed
|
|
ensure_bom_costing_worker_secret
|
|
ensure_product_development_todo_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)"
|
|
mkdir -p "$SHARED/deploy-checks"
|
|
HEALTHCHECK_FILE="$SHARED/deploy-checks/$RELEASE_ID.log"
|
|
DEPLOY_SUCCEEDED=0
|
|
if [ -n "$PREVIOUS_RELEASE" ]; then
|
|
assert_release_ready "$PREVIOUS_RELEASE/$PROJECT_SUBDIR"
|
|
fi
|
|
|
|
cleanup_failed_release() {
|
|
local status="$1"
|
|
|
|
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
|
|
|
|
echo "Failed release retained for inspection: $RELEASE_DIR" >&2
|
|
fi
|
|
|
|
exit "$status"
|
|
}
|
|
|
|
trap 'cleanup_failed_release "$?"' EXIT
|
|
|
|
assert_path_in_base "$RELEASE_DIR"
|
|
if [ -e "$RELEASE_DIR" ]; then
|
|
echo "Release already exists: $RELEASE_DIR" >&2
|
|
exit 1
|
|
fi
|
|
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
|
|
export ONNXRUNTIME_NODE_INSTALL_CUDA=skip
|
|
npm ci --no-audit --no-fund
|
|
npx prisma generate
|
|
npx prisma migrate deploy
|
|
set -a
|
|
source "$SHARED/.env"
|
|
set +a
|
|
node scripts/verify-prisma-runtime-schema.js
|
|
npm run build
|
|
ensure_shared_non_integrated_assets "$APP_DIR"
|
|
prepare_standalone_runtime "$APP_DIR"
|
|
assert_release_ready "$APP_DIR"
|
|
|
|
ln -sfn "$RELEASE_DIR" "$CURRENT"
|
|
start_pm2_release "$CURRENT"
|
|
|
|
if ! verify_release_http "http://127.0.0.1:$APP_PORT" >"$HEALTHCHECK_FILE" 2>&1 ||
|
|
! verify_release_http "https://cp.jiansun.vip" >>"$HEALTHCHECK_FILE" 2>&1; then
|
|
cat "$HEALTHCHECK_FILE" >&2
|
|
echo "Release verification failed; restoring previous release." >&2
|
|
exit 1
|
|
fi
|
|
|
|
sed -n '1,12p' "$HEALTHCHECK_FILE"
|
|
install_virtual_inventory_push_cron
|
|
cleanup_old_releases
|
|
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("__RELEASE_GUARDS__", (Get-Content -Raw (Join-Path $PSScriptRoot "yydd-prod-release-guards.sh")))
|
|
$shellScript = $shellScript.Replace("__EXPECTED_HOSTNAME__", $ExpectedHostname)
|
|
$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 `
|
|
-ExpectedHostname $ExpectedHostname `
|
|
-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) {
|
|
$remoteDeployStarted = $false
|
|
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
|
|
$remoteDeployStarted = $true
|
|
$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 {
|
|
if ($remoteDeployStarted) {
|
|
throw
|
|
}
|
|
Write-Warning ("Server-side Git deploy failed, falling back to package deploy: {0}" -f $_.Exception.Message)
|
|
}
|
|
}
|
|
|
|
Invoke-ArchiveFallbackDeploy -DeployRef $Ref
|