flnxi-Wu/deploy-new-fixed.ps1
WuYongJie e4dcb6260a
Some checks failed
CI Check / check (push) Has been cancelled
feat: Admin页面重构 & UI三层架构系统
- 创建统一UI三层架构系统(基础层、颜色层、组合层)
- 拆分Admin页面为独立Tab组件(MonitorTab、UsersTab、DepartmentsTab等)
- 提取公共逻辑到Hooks(useToast、useDataFetch、useUserOperations等)
- 提取工具函数(validators、formatters、api等)
- 创建统一弹窗组件(Modal)和useModal Hook
- 添加错误边界和统一状态组件
- 完善类型定义和可访问性支持
- 更新文档(优化记录、优化计划、更新日志)
2025-11-22 15:17:10 +08:00

916 lines
34 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ================================================================
# 新建简化部署脚本 - 处理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