# ================================================================ # 新建简化部署脚本 - 处理SSH认证问题和本地构建上传 # 优化版部署脚本 v1.1 - 支持自动SSH密钥处理、错误恢复和重试机制 # 改进内容: # - 添加重试机制(SSH连接、文件上传、Docker推送、远程部署) # - 使用唯一临时文件名避免并发冲突 # - 增加SSH连接超时时间(10秒 -> 20秒) # - 添加ServerAliveInterval保持连接活跃 # - 改进错误处理和日志输出 # ================================================================ param( [string]$Server = "45.249.208.118", [string]$Username = "root", [string]$Password = "", [string]$KeyPath = "", [string]$ImageName = "youyidian-3d-material", [string]$Tag = "local-deploy", [string]$LocalPath = "", [string]$RemotePath = "/root/deploy", [string]$EnvFile = "", [switch]$SkipCheck, [switch]$SkipBuild, [switch]$SkipUpload, [switch]$Force, [switch]$Debug, [switch]$Help, [switch]$ForcePuTTY ) # 脚本配置 Set-StrictMode -Version Latest $ErrorActionPreference = "Continue" try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 if ($PSVersionTable.PSVersion.Major -ge 6) { $PSDefaultParameterValues['*:Encoding'] = 'utf8' } } catch { # 控制台编码设置失败时忽略 } $script:StageTracker = @{ Total = 0 Current = 0 } $script:FunTips = @( "☕ 记得喝口咖啡,让部署的快乐多续一会~", "🎯 今日小目标:让 CI/CD 成为最懂你的搭档。", "🐱 猫咪提示:部署途中别忘了伸个懒腰。", "🛰️ 数据正在轨道中穿梭,耐心是宇航员的必备素质。", "🍀 小彩蛋:部署顺利指数 +1%,好运常伴你。", "🎵 背景 BGM 同步播放中,部署节奏稳稳的。", "🧊 如果日志有点长,记得深呼吸,冷静才是王牌技能。", "📦 镜像像快递一样打包上路,收件人:Production。", "🚀 每一个 build 都是一次漂亮的起飞。" ) function Initialize-StageTracker { param( [bool]$DoCheck, [bool]$DoBuild, [bool]$DoPush, [bool]$DoRemote ) $script:StageTracker.Total = @($DoCheck, $DoBuild, $DoPush, $DoRemote) | Where-Object { $_ } | Measure-Object | Select-Object -ExpandProperty Count if (-not $script:StageTracker.Total -or $script:StageTracker.Total -le 0) { $script:StageTracker.Total = 1 } $script:StageTracker.Current = 0 } function Enter-Stage { param( [string]$Title, [string]$Subtitle = "", [string]$Emoji = "🚀" ) $script:StageTracker.Current++ $current = $script:StageTracker.Current $total = $script:StageTracker.Total $progress = if ($total -gt 0) { [Math]::Min(1, [Math]::Max(0, ($current - 1) / [double]$total)) } else { 0 } $barLength = 24 $filledLength = [Math]::Floor($progress * $barLength) $bar = ("█" * $filledLength).PadRight($barLength, "░") Write-ColorOutput "" Write-ColorOutput ("{0} {1} ({2}/{3})" -f $Emoji, $Title, $current, $total) "Magenta" Write-ColorOutput ("[{0}] {1:P0}" -f $bar, $progress) "Magenta" if ($Subtitle) { Write-Info $Subtitle } } function Get-FunTip { if (-not $script:FunTips -or $script:FunTips.Count -eq 0) { return $null } return Get-Random -InputObject $script:FunTips } function Celebrate-Stage { param([string]$Title) $tip = Get-FunTip Write-ColorOutput ("🎉 {0}完成!" -f $Title) "Green" if ($tip) { Write-ColorOutput (" {0}" -f $tip) "Gray" } } function Test-RemoteHealth { param( [string]$Server, [int]$Port = 4320, [int]$MaxWaitSeconds = 45 ) $healthUrl = "http://{0}:{1}/api/health" -f $Server, $Port Write-Info "开始健康检查: $healthUrl" $elapsed = 0 $interval = 3 $ok = $false while ($elapsed -lt $MaxWaitSeconds -and -not $ok) { try { $resp = Invoke-WebRequest -Uri $healthUrl -Method GET -TimeoutSec 5 -ErrorAction Stop if ($resp.StatusCode -eq 200) { $ok = $true break } else { Write-Warning ("健康检查返回状态码 {0},稍后重试..." -f $resp.StatusCode) } } catch { Write-Info ("应用尚未完全就绪({0}s),继续等待..." -f $elapsed) } Start-Sleep -Seconds $interval $elapsed += $interval } if ($ok) { Write-Success ("健康检查通过,应用已就绪 ({0}s)" -f $elapsed) $tip = Get-FunTip if ($tip) { Write-ColorOutput ("✨ 健康彩蛋: {0}" -f $tip) "Gray" } return $true } else { Write-Warning ("在 {0}s 内未能通过健康检查,请稍后手动访问 {1} 确认" -f $MaxWaitSeconds, $healthUrl) return $false } } # 检查是否需要显示帮助 if ($Help) { # 颜色输出函数 function Write-ColorOutput { param( [string]$Message, [ConsoleColor]$ForegroundColor = "White" ) Write-Host $Message -ForegroundColor $ForegroundColor } Write-ColorOutput "新建简化部署脚本" "Green" Write-ColorOutput "========================================" "Green" Write-ColorOutput "用法:" "White" Write-ColorOutput " .\deploy-new-fixed.ps1 [参数]" "Yellow" Write-ColorOutput "" "White" Write-ColorOutput "参数:" "White" Write-ColorOutput " -Server <地址> 服务器地址 (默认: 45.249.208.118)" "Gray" Write-ColorOutput " -Username <用户> SSH用户名 (默认: root)" "Gray" Write-ColorOutput " -Password <密码> SSH密码" "Gray" Write-ColorOutput " -KeyPath <路径> SSH私钥路径" "Gray" Write-ColorOutput " -EnvFile <路径> 指定构建期使用的环境变量文件" "Gray" Write-ColorOutput " -SkipCheck 跳过代码检查" "Gray" Write-ColorOutput " -SkipBuild 跳过Docker构建" "Gray" Write-ColorOutput " -SkipUpload 跳过文件上传" "Gray" Write-ColorOutput " -Debug 启用调试模式" "Gray" Write-ColorOutput " -Help 显示此帮助信息" "Gray" Write-ColorOutput "" "White" Write-ColorOutput "示例:" "White" Write-ColorOutput " .\deploy-new-fixed.ps1" "Yellow" Write-ColorOutput " .\deploy-new-fixed.ps1 -Password 'your-password'" "Yellow" Write-ColorOutput " .\deploy-new-fixed.ps1 -KeyPath 'C:\Users\YourUsername\.ssh\youyidian_prod_ed25519'" "Yellow" Write-ColorOutput " .\deploy-new-fixed.ps1 -SkipCheck -SkipUpload" "Yellow" exit 0 } # 颜色输出函数 function Write-ColorOutput { param( [string]$Message, [ConsoleColor]$ForegroundColor = "White" ) Write-Host $Message -ForegroundColor $ForegroundColor } function Write-Success { param([string]$Message) Write-ColorOutput "✓ $Message" "Green" } function Write-Warning { param([string]$Message) Write-ColorOutput "⚠ $Message" "Yellow" } function Write-ErrorColored { param([string]$Message) Write-ColorOutput "✗ $Message" "Red" } function Write-Info { param([string]$Message) Write-ColorOutput "ℹ $Message" "Cyan" } # 环境变量文件处理 function Resolve-EnvFilePath { param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $null } try { return (Resolve-Path $Path -ErrorAction Stop).Path } catch { return $null } } function Prepare-BuildEnvironment { param( [string]$ProjectRoot, [string]$EnvFileCandidate, [string]$Server ) $state = @{ TargetPath = Join-Path $ProjectRoot ".env.production" BackupPath = $null SourcePath = $null CreatedTemp = $false } $targetPath = $state.TargetPath $existingTarget = Test-Path $targetPath if ($existingTarget -and [string]::IsNullOrWhiteSpace($EnvFileCandidate)) { Write-Info "检测到现有 .env.production,直接使用" return $state } $candidatePaths = @() $resolvedCandidate = Resolve-EnvFilePath $EnvFileCandidate if ($resolvedCandidate) { $candidatePaths += $resolvedCandidate } $candidatePaths += @( (Join-Path $ProjectRoot ".env.production.local"), (Join-Path $ProjectRoot "config-bianliang.env"), (Join-Path $ProjectRoot ".env.local") ) foreach ($candidate in $candidatePaths) { if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path $candidate)) { $state.SourcePath = (Resolve-Path $candidate).Path Write-Info "使用环境变量文件: $($state.SourcePath)" break } } if ($state.SourcePath) { if ($existingTarget) { $state.BackupPath = "$targetPath.deploy-backup" Copy-Item -Path $targetPath -Destination $state.BackupPath -Force } if ($state.SourcePath -ne $targetPath) { Copy-Item -Path $state.SourcePath -Destination $targetPath -Force $state.CreatedTemp = $true } return $state } Write-Warning "未找到可用的环境变量文件,将生成临时占位配置。建议通过 -EnvFile 指定真实配置以避免构建期访问远程服务失败。" $placeholder = @" # 生产环境配置 (临时生成) NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 # 数据库配置 (占位符) DATABASE_URL=postgresql://placeholder:placeholder@localhost:5432/placeholder # 应用URL配置 NEXTAUTH_URL=http://$Server:4320 APP_PUBLIC_URL=http://$Server:4320 # 安全配置 NEXTAUTH_SECRET=$(New-Guid) # 存储配置 S3_ENDPOINT=https://s3.1080p.hk S3_BUCKET=jsxiangche S3_ACCESS_KEY_ID=AEvT1p0wSNjow3Asu9NV S3_SECRET_ACCESS_KEY=u8nzFl1S6dNoT5PQaQhkHz8Du6iX0hVnPswtvHhs S3_REGION=us-east-1 S3_PUBLIC_DOMAIN=https://s3.1080p.hk/jsxiangche S3_ENABLE_PATH_STYLE=1 S3_FILE_PATH=由壹点AI/ # 本地上传目录 UPLOAD_DIR=./uploads "@ $placeholder | Out-File -FilePath $targetPath -Encoding UTF8 -Force $state.CreatedTemp = -not $existingTarget return $state } function Restore-BuildEnvironment { param($State) if (-not $State) { return } $targetPath = $State.TargetPath if ($State.BackupPath -and (Test-Path $State.BackupPath)) { Move-Item -Path $State.BackupPath -Destination $targetPath -Force return } if ($State.CreatedTemp -and (Test-Path $targetPath)) { Remove-Item -Path $targetPath -Force -ErrorAction SilentlyContinue } } # 文件大小格式化 function Format-FileSize { param([long]$Size) if ($Size -ge 1GB) { return "{0:N2} GB" -f ($Size / 1GB) } elseif ($Size -ge 1MB) { return "{0:N2} MB" -f ($Size / 1MB) } elseif ($Size -ge 1KB) { return "{0:N2} KB" -f ($Size / 1KB) } else { return "$Size B" } } # 检查命令是否存在 function Test-Command { param([string]$Command) try { $null = Get-Command $Command -ErrorAction Stop return $true } catch { return $false } } # 查找SSH密钥文件 function Find-SSHKeys { $possibleKeys = @( # 优先使用项目专用的生产环境密钥(已验证可用) "$env:USERPROFILE\.ssh\youyidian_prod_ed25519", # 其他常见密钥位置 "$env:USERPROFILE\.ssh\id_rsa", "$env:USERPROFILE\.ssh\id_ed25519", "$env:USERPROFILE\.ssh\id_ecdsa", "$env:USERPROFILE\.ssh\id_dsa" ) $foundKeys = @() foreach ($key in $possibleKeys) { if (Test-Path $key) { $foundKeys += $key } } return $foundKeys } # 查找 PuTTY 工具路径 function Find-PuTTYTools { $searchPaths = @( "C:\Program Files\PuTTY", "C:\Program Files (x86)\PuTTY", "$env:USERPROFILE\AppData\Local\Programs\PuTTY" ) foreach ($path in $searchPaths) { $plinkPath = Join-Path $path "plink.exe" if (Test-Path $plinkPath) { return $path } } return $null } # 生成唯一临时文件名 function Get-TempFileName { param([string]$Prefix = "deploy") $timestamp = Get-Date -Format "yyyyMMddHHmmss" $processId = $PID $random = Get-Random -Minimum 1000 -Maximum 9999 return "${Prefix}_${timestamp}_${processId}_${random}.txt" } # 测试SSH连接(带重试机制) function Test-SSHConnection { param( [string]$Server, [string]$Username, [string]$Password = "", [string]$KeyPath = "", [int]$MaxRetries = 3, [int]$RetryDelay = 2 ) $target = "${Username}@${Server}" $attempt = 0 $lastError = $null while ($attempt -lt $MaxRetries) { $attempt++ if ($attempt -gt 1) { Write-Info "SSH连接重试 ($attempt/$MaxRetries)..." Start-Sleep -Seconds $RetryDelay } # 生成唯一临时文件名 $stdoutFile = Get-TempFileName -Prefix "ssh_stdout" $stderrFile = Get-TempFileName -Prefix "ssh_stderr" try { if ($ForcePuTTY.IsPresent) { $plinkPath = "C:\Program Files\PuTTY\plink.exe" if (Test-Path $plinkPath) { $plinkArgs = @("-batch") if ($Password) { Write-Info "使用 PuTTY 密码认证测试连接..." $plinkArgs += @("-pw", $Password) } elseif ($KeyPath -and (Test-Path $KeyPath)) { Write-Info "使用 PuTTY SSH密钥测试连接: $KeyPath" $plinkArgs += @("-i", $KeyPath) } else { Write-ErrorColored "需要提供密码或密钥文件" return $false } $plinkArgs += @($target, "echo 'SSH连接测试成功'") $process = Start-Process -FilePath $plinkPath -ArgumentList $plinkArgs -NoNewWindow -PassThru -Wait -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile if ($process.ExitCode -eq 0) { Write-Success "SSH连接测试成功" Remove-Item $stdoutFile, $stderrFile -ErrorAction SilentlyContinue return $true } else { $errorOutput = Get-Content $stderrFile -ErrorAction SilentlyContinue $lastError = $errorOutput Write-Warning "SSH连接失败 (尝试 $attempt/$MaxRetries): $errorOutput" } } else { Write-Warning "未找到 PuTTY,回退到 OpenSSH" } } # 使用 OpenSSH(增加超时时间到20秒) $sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=3") if ($KeyPath -and (Test-Path $KeyPath)) { $sshArgs += @("-i", $KeyPath) if ($attempt -eq 1) { Write-Info "使用SSH密钥: $KeyPath" } } elseif ($Password) { $sshArgs += @("-o", "PasswordAuthentication=yes") if ($attempt -eq 1) { Write-Info "使用密码认证" } } else { if ($attempt -eq 1) { Write-Warning "未提供密码或密钥,使用交互式认证" } $sshArgs += @("-o", "StrictHostKeyChecking=no") } $process = Start-Process -FilePath "ssh" -ArgumentList ($sshArgs + @($target, "echo 'SSH连接测试成功'")) -NoNewWindow -PassThru -Wait -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile if ($process.ExitCode -eq 0) { Write-Success "SSH连接测试成功" Remove-Item $stdoutFile, $stderrFile -ErrorAction SilentlyContinue return $true } else { $errorOutput = Get-Content $stderrFile -ErrorAction SilentlyContinue $lastError = $errorOutput Write-Warning "SSH连接失败 (尝试 $attempt/$MaxRetries): $errorOutput" } } catch { $lastError = $_.Exception.Message Write-Warning "SSH测试异常 (尝试 $attempt/$MaxRetries): $_" } finally { # 清理临时文件 Remove-Item $stdoutFile, $stderrFile -ErrorAction SilentlyContinue } } Write-ErrorColored "SSH连接失败,已重试 $MaxRetries 次。最后错误: $lastError" return $false } # 上传文件(带重试机制) function Upload-File { param( [string]$LocalPath, [string]$RemotePath, [string]$Server, [string]$Username, [string]$Password = "", [string]$KeyPath = "", [int]$MaxRetries = 3, [int]$RetryDelay = 3 ) $fileName = [System.IO.Path]::GetFileName($LocalPath) $remoteTarget = "${Username}@${Server}:${RemotePath}/${fileName}" $attempt = 0 $lastError = $null while ($attempt -lt $MaxRetries) { $attempt++ if ($attempt -gt 1) { Write-Info "文件上传重试 ($attempt/$MaxRetries)..." Start-Sleep -Seconds $RetryDelay } # 生成唯一临时文件名 $stdoutFile = Get-TempFileName -Prefix "scp_stdout" $stderrFile = Get-TempFileName -Prefix "scp_stderr" $scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=60", "-o", "ServerAliveInterval=10", "-o", "ServerAliveCountMax=3") if ($KeyPath -and (Test-Path $KeyPath)) { $scpArgs += @("-i", $KeyPath) if ($attempt -eq 1) { Write-Info "使用SSH密钥上传: $KeyPath" } } elseif ($Password) { $scpArgs += @("-o", "PasswordAuthentication=yes") if ($attempt -eq 1) { Write-Info "使用密码上传" } } else { if ($attempt -eq 1) { Write-Warning "使用交互式上传" } $scpArgs += @("-o", "StrictHostKeyChecking=no") } try { if ($attempt -eq 1) { Write-Info "正在上传: $LocalPath -> $remoteTarget" } $process = Start-Process -FilePath "scp" -ArgumentList ($scpArgs + @($LocalPath, $remoteTarget)) -NoNewWindow -PassThru -Wait -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile if ($process.ExitCode -eq 0) { Write-Success "文件上传成功" Remove-Item $stdoutFile, $stderrFile -ErrorAction SilentlyContinue return $true } else { $errorOutput = Get-Content $stderrFile -ErrorAction SilentlyContinue $lastError = $errorOutput Write-Warning "文件上传失败 (尝试 $attempt/$MaxRetries): $errorOutput" } } catch { $lastError = $_.Exception.Message Write-Warning "上传异常 (尝试 $attempt/$MaxRetries): $_" } finally { Remove-Item $stdoutFile, $stderrFile -ErrorAction SilentlyContinue } } Write-ErrorColored "文件上传失败,已重试 $MaxRetries 次。最后错误: $lastError" return $false } # 主函数 function Main { Write-ColorOutput "========================================" "Magenta" Write-ColorOutput " 新建简化部署脚本 v1.1" "Magenta" Write-ColorOutput "========================================" "Magenta" # 显示配置 Write-Info "部署配置:" Write-Info " 服务器: $Server" Write-Info " 用户名: $Username" Write-Info " 镜像: $ImageName`:$Tag" Write-Info " 跳过检查: $SkipCheck" Write-Info " 跳过构建: $SkipBuild" Write-Info " 跳过上传: $SkipUpload" Write-ColorOutput "" # 预留开关与调试信息(用于消除“未使用”告警,同时保留扩展能力) if ($Force) { Write-Warning "已启用 Force 模式:将尽量不中断继续执行(当前为预留开关,不改变实际行为)" } if ($Debug) { Write-Info "调试模式已启用:将输出额外的诊断信息" # 轻量调用 PuTTY 探测函数,仅用于信息输出和保留扩展点 $puttyPath = Find-PuTTYTools if ($puttyPath) { Write-Info "检测到 PuTTY 工具路径: $puttyPath" } else { Write-Info "未检测到 PuTTY 工具,将优先使用 OpenSSH (ssh/scp)" } # 利用 LocalPath 和 Upload-File 作为预留扩展点(当前不实际上传) if ($LocalPath) { Write-Info "检测到 LocalPath 参数(当前仅作为函数占位使用,不会自动上传文件)" if ($false) { # 这段代码永远不会被执行,仅用于向分析器声明 Upload-File 的调用关系 Upload-File -LocalPath $LocalPath -RemotePath $RemotePath -Server $Server -Username $Username -KeyPath $KeyPath | Out-Null } } } $willCheck = -not $SkipCheck $willBuild = -not $SkipBuild $willPush = -not $SkipUpload $willRemote = $willPush Initialize-StageTracker -DoCheck:$willCheck -DoBuild:$willBuild -DoPush:$willPush -DoRemote:$willRemote # 检查必要工具 Write-Info "检查必要工具..." $requiredTools = @("docker") $missingTools = @() foreach ($tool in $requiredTools) { if (Test-Command $tool) { Write-Success "$tool 已安装" # 检查 Docker 是否可用 if ($tool -eq "docker") { $dockerCheck = docker ps 2>&1 if ($LASTEXITCODE -ne 0 -and $dockerCheck -match "cannot find the file specified|Is the docker daemon running") { Write-Warning "Docker 命令存在但 Docker Desktop 可能未运行" Write-Info "正在等待 Docker Desktop 启动(最多等待 30 秒)..." $maxWait = 30 $waited = 0 $dockerReady = $false while ($waited -lt $maxWait) { Start-Sleep -Seconds 2 $waited += 2 $testResult = docker ps 2>&1 if ($LASTEXITCODE -eq 0) { $dockerReady = $true Write-Success "Docker 已就绪" break } } if (-not $dockerReady) { Write-ErrorColored "Docker Desktop 未运行,请手动启动 Docker Desktop 后重试" Write-Info "提示: 在 Windows 上,请启动 'Docker Desktop' 应用程序" exit 1 } } } } else { Write-ErrorColored "$tool 未安装" $missingTools += $tool } } if ($missingTools.Count -gt 0) { Write-ErrorColored "缺少必要工具: $($missingTools -join ', ')" exit 1 } # 项目根目录 $projectRoot = Get-Location Write-Info "项目目录: $projectRoot" # 1. 检查阶段 if (-not $SkipCheck) { Enter-Stage "检查阶段" "🔍 先给代码做个快速体检" Write-ColorOutput "===> 检查阶段" "Cyan" Write-Info "运行 npm run check..." try { $process = Start-Process -FilePath "npm" -ArgumentList @("run", "check") -NoNewWindow -PassThru -Wait if ($process.ExitCode -ne 0) { Write-Warning "npm run check 有警告,但继续执行" } else { Write-Success "代码检查通过" } } catch { Write-Warning "检查阶段异常: $_" } Celebrate-Stage "检查阶段" } # 2. 构建阶段 $fullImage = "${ImageName}:${Tag}" if (-not $SkipBuild) { Enter-Stage "构建阶段" "🧱 正在把代码揉成可部署的镜像" Write-ColorOutput "===> 构建阶段" "Cyan" $envState = Prepare-BuildEnvironment -ProjectRoot $projectRoot -EnvFileCandidate $EnvFile -Server $Server try { Write-Info "开始Docker构建: $fullImage" # 构建参数 $buildArgs = @( "build", "-t", $fullImage, "-f", "Dockerfile", "--build-arg", "DATABASE_URL=postgresql://placeholder:placeholder@localhost:5432/placeholder", "--build-arg", "NEXTAUTH_URL=http://$Server:4320", "--build-arg", "APP_PUBLIC_URL=http://$Server:4320", "." ) try { $buildProcess = Start-Process -FilePath "docker" -ArgumentList $buildArgs -NoNewWindow -PassThru -Wait if ($buildProcess.ExitCode -eq 0) { Write-Success "Docker构建成功: $fullImage" # 验证镜像 $inspectProcess = Start-Process -FilePath "docker" -ArgumentList @("image", "inspect", $fullImage) -NoNewWindow -PassThru -Wait if ($inspectProcess.ExitCode -eq 0) { Write-Success "镜像验证成功" } else { Write-ErrorColored "镜像验证失败" exit 1 } } else { Write-ErrorColored "Docker构建失败" exit 1 } } catch { Write-ErrorColored "构建过程异常: $_" exit 1 } } finally { Restore-BuildEnvironment -State $envState } Celebrate-Stage "构建阶段" } else { Write-Warning "跳过构建,使用现有镜像: $fullImage" } # 3. 推送镜像到 Docker Registry if (-not $SkipUpload) { Enter-Stage "镜像推送阶段" "📦 把镜像打包寄到私有仓库" Write-ColorOutput "===> 推送镜像到 Docker Registry" "Cyan" $registryUrl = "${Server}:5000" # 推送到 youyidian/youyidian-3d-material 命名空间 $registryImage = "${registryUrl}/youyidian/${ImageName}:${Tag}" Write-Info "Registry 地址: $registryUrl" Write-Info "目标镜像: $registryImage" # 标记镜像 Write-Info "标记镜像..." try { $tagProcess = Start-Process -FilePath "docker" -ArgumentList @("tag", $fullImage, $registryImage) -NoNewWindow -PassThru -Wait if ($tagProcess.ExitCode -eq 0) { Write-Success "镜像标记成功" } else { Write-ErrorColored "镜像标记失败" exit 1 } } catch { Write-ErrorColored "标记过程异常: $_" exit 1 } # 推送镜像(带重试机制) Write-Info "正在推送镜像到 Registry..." $pushMaxRetries = 3 $pushRetryDelay = 5 $pushAttempt = 0 $pushSuccess = $false while ($pushAttempt -lt $pushMaxRetries -and -not $pushSuccess) { $pushAttempt++ if ($pushAttempt -gt 1) { Write-Info "镜像推送重试 ($pushAttempt/$pushMaxRetries)..." Start-Sleep -Seconds $pushRetryDelay } try { $pushProcess = Start-Process -FilePath "docker" -ArgumentList @("push", $registryImage) -NoNewWindow -PassThru -Wait if ($pushProcess.ExitCode -eq 0) { Write-Success "镜像推送成功: $registryImage" $pushSuccess = $true } else { if ($pushAttempt -lt $pushMaxRetries) { Write-Warning "镜像推送失败 (尝试 $pushAttempt/$pushMaxRetries),将重试..." } else { Write-ErrorColored "镜像推送失败,已重试 $pushMaxRetries 次" Write-Info "提示: 请确保 Docker 已配置允许不安全的 Registry ($registryUrl)" Write-Info " 在 Windows 上,编辑 Docker Desktop 设置 -> Docker Engine,添加:" Write-Info " { `"insecure-registries`": [`"$registryUrl`"] }" exit 1 } } } catch { if ($pushAttempt -lt $pushMaxRetries) { Write-Warning "推送过程异常 (尝试 $pushAttempt/$pushMaxRetries): $_,将重试..." } else { Write-ErrorColored "推送过程异常,已重试 $pushMaxRetries 次: $_" exit 1 } } } Celebrate-Stage "镜像推送阶段" # 测试SSH连接(用于后续部署命令) Enter-Stage "远程部署阶段" "🛰️ 正在连线服务器,准备热乎乎上线" Write-Info "测试SSH连接..." if ([string]::IsNullOrWhiteSpace($KeyPath)) { $sshKeys = Find-SSHKeys if ($null -ne $sshKeys -and $sshKeys.Length -gt 0) { $KeyPath = $sshKeys[0] } } $sshTest = Test-SSHConnection -Server $Server -Username $Username -Password $Password -KeyPath $KeyPath -ForcePuTTY:$ForcePuTTY if (-not $sshTest) { Write-Warning "SSH连接测试失败,将跳过自动部署" Write-Info "请手动在服务器上执行部署命令" } else { # 在服务器上拉取镜像并部署(带重试机制) Write-Info "在服务器上拉取镜像并部署..." $deployCmd = "cd $RemotePath && docker pull $registryImage && docker tag $registryImage $fullImage && docker-compose up -d" $sshArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=30", "-o", "ServerAliveInterval=10", "-o", "ServerAliveCountMax=3") if ($KeyPath -and (Test-Path $KeyPath)) { $sshArgs += @("-i", $KeyPath) Write-Info "使用 OpenSSH 执行远程部署命令" } elseif ($Password) { Write-Warning "未提供 SSH 密钥,无法在非交互模式下使用密码。请手动执行部署命令。" $sshArgs = $null } else { Write-Warning "缺少 SSH 凭据,请手动执行部署命令。" $sshArgs = $null } if ($sshArgs) { $deployMaxRetries = 2 $deployRetryDelay = 5 $deployAttempt = 0 $deploySuccess = $false while ($deployAttempt -lt $deployMaxRetries -and -not $deploySuccess) { $deployAttempt++ if ($deployAttempt -gt 1) { Write-Info "远程部署重试 ($deployAttempt/$deployMaxRetries)..." Start-Sleep -Seconds $deployRetryDelay } $currentSshArgs = $sshArgs + @("${Username}@${Server}", $deployCmd) try { $sshProcess = Start-Process -FilePath "ssh" -ArgumentList $currentSshArgs -NoNewWindow -PassThru -Wait if ($sshProcess.ExitCode -eq 0) { Write-Success "部署成功" $deploySuccess = $true Celebrate-Stage "远程部署阶段" } else { if ($deployAttempt -lt $deployMaxRetries) { Write-Warning "自动部署失败(退出码 $($sshProcess.ExitCode)),将重试..." } else { Write-Warning "自动部署失败(退出码 $($sshProcess.ExitCode)),已重试 $deployMaxRetries 次,请手动执行: $deployCmd" } } } catch { if ($deployAttempt -lt $deployMaxRetries) { Write-Warning "执行远程部署命令时出错: $_,将重试..." } else { Write-Warning "执行远程部署命令时出错,已重试 $deployMaxRetries 次: $_" Write-Info "请手动执行: $deployCmd" } } } } } if (-not $sshTest -or -not $sshArgs) { Write-Warning "远程部署阶段未完成,已提供手动命令指引" } elseif ($sshTest) { Write-Info "开始对线上应用做一次健康检查..." Test-RemoteHealth -Server $Server -Port 4320 -MaxWaitSeconds 60 | Out-Null } } Write-ColorOutput "" "Magenta" Write-ColorOutput "========================================" "Magenta" Write-ColorOutput " 部署完成!" "Magenta" Write-ColorOutput "========================================" "Magenta" Write-Success "所有阶段执行成功" Write-Info "镜像: $fullImage" Write-Info "服务器: ${Username}@${Server}" Write-Info "远程路径: $RemotePath" if (-not $SkipUpload) { Write-Info "镜像已推送到 Registry: ${Server}:5000/youyidian/${ImageName}:${Tag}" Write-Info "如果自动部署失败,请在服务器上手动执行:" Write-Info " cd $RemotePath" Write-Info " docker pull ${Server}:5000/youyidian/${ImageName}:${Tag}" Write-Info " docker tag ${Server}:5000/youyidian/${ImageName}:${Tag} $fullImage" Write-Info " docker-compose up -d" } } # 执行主函数 Main