param( [string]$RepoRoot = $(Split-Path -Parent $PSScriptRoot), [string]$Ref = "origin/main", [string]$RemoteBase = "/opt/furniture-3d-system-prod", [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]$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 "缺少打包脚本: $createScript" } if (-not (Test-Path -LiteralPath $plinkPath)) { throw "缺少 plink.exe: $plinkPath" } if (-not (Test-Path -LiteralPath $pscpPath)) { throw "缺少 pscp.exe: $pscpPath" } 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-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 "上传文件不存在: $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 } $package = & $createScript ` -RepoRoot $RepoRoot ` -Ref $Ref ` -RemoteBase $RemoteBase ` -ServerHost $ServerHost ` -AppPort $AppPort ` -Pm2Name $Pm2Name ` -KeepReleases $KeepReleases if (-not $package) { throw '打包脚本未返回结果' } $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 ("远端部署临时文件清理失败: {0}" -f $_.Exception.Message) } if (Test-Path -LiteralPath $archivePath) { Remove-Item -LiteralPath $archivePath -Force } if (Test-Path -LiteralPath $scriptPath) { Remove-Item -LiteralPath $scriptPath -Force } } } [PSCustomObject]@{ Mode = 'safe-git-release-deploy' 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) }