From 8d92e137cb9daba09a99666cdb0043b28953cf2d Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Wed, 29 Jul 2026 11:43:47 +0800 Subject: [PATCH] =?UTF-8?q?ops(deploy):=20=E5=88=87=E6=8D=A2=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E7=AE=A1=E7=90=86=E8=B7=B3=E6=9D=BF=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/workflow/主线同步操作手册.md | 2 +- .../docs/workflow/团队协作模式.md | 2 +- .../docs/workflow/生产部署入口说明.md | 37 ++-- scripts/deploy-yydd-prod-via-jump.ps1 | 179 ++++++++++++++++++ 4 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 scripts/deploy-yydd-prod-via-jump.ps1 diff --git a/furniture-3d-system/docs/workflow/主线同步操作手册.md b/furniture-3d-system/docs/workflow/主线同步操作手册.md index 612428ec..13f9228b 100644 --- a/furniture-3d-system/docs/workflow/主线同步操作手册.md +++ b/furniture-3d-system/docs/workflow/主线同步操作手册.md @@ -185,7 +185,7 @@ git push --force-with-lease origin <你的个人任务分支> 生产发布必须由用户明确授权。 -发布代码默认来自 `origin/main`,部署入口以仓库根目录下的 `scripts/deploy-yydd-prod-from-git.ps1` 为准,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。 +发布代码默认来自 `origin/main`,部署首选入口以仓库根目录下的 `scripts/deploy-yydd-prod-via-jump.ps1` 为准;它通过新管理跳板链路调用 `scripts/deploy-yydd-prod-from-git.ps1`,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。 部署前至少核对: diff --git a/furniture-3d-system/docs/workflow/团队协作模式.md b/furniture-3d-system/docs/workflow/团队协作模式.md index 900d0440..df0634ef 100644 --- a/furniture-3d-system/docs/workflow/团队协作模式.md +++ b/furniture-3d-system/docs/workflow/团队协作模式.md @@ -169,7 +169,7 @@ SPU、SKU、barcode、批次必须分清: ## 8. 生产和部署协作 生产发布必须由用户明确授权。 -发布代码默认以 `origin/main` 为来源,常用入口是仓库根目录下的 `scripts/deploy-yydd-prod-from-git.ps1`,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。 +发布代码默认以 `origin/main` 为来源,首选入口是仓库根目录下的 `scripts/deploy-yydd-prod-via-jump.ps1`;它通过新管理跳板链路调用 `scripts/deploy-yydd-prod-from-git.ps1`,底层打包脚本是 `scripts/create-yydd-prod-deploy.ps1`。 部署前至少核对: diff --git a/furniture-3d-system/docs/workflow/生产部署入口说明.md b/furniture-3d-system/docs/workflow/生产部署入口说明.md index fac4c1d5..22841557 100644 --- a/furniture-3d-system/docs/workflow/生产部署入口说明.md +++ b/furniture-3d-system/docs/workflow/生产部署入口说明.md @@ -6,19 +6,31 @@ ## 权威生产目标 -- SSH 入口:`root@ltai5tdb2ogyj59fquyedh.yydd.ai` -- SSH 端口:`60022` +- 管理跳板机:`root@218.93.9.38:22` +- 生产 SSH:通过跳板机本地回环 `127.0.0.1:26022` 转发,不直接暴露公网 - 生产部署目录:`/opt/furniture-3d-system-prod` - PM2 进程名:`furniture-3d-system-prod` - 应用本机端口:`3001` - 默认部署来源:`origin/main` +旧 `ltai5tdb2ogyj59fquyedh.yydd.ai:60022` 入口已于 2026-07-29 停用。它曾因公网扫描占满 SSH `MaxStartups`,导致部署连接在认证前被拒绝。不要重新启用旧代理或用提高连接上限掩盖该问题。 + ## 本仓库部署脚本 -- 首选入口:仓库根目录 `scripts/deploy-yydd-prod-from-git.ps1` +- 首选入口:仓库根目录 `scripts/deploy-yydd-prod-via-jump.ps1` +- 跳板下层入口:仓库根目录 `scripts/deploy-yydd-prod-from-git.ps1` - 底层打包脚本:仓库根目录 `scripts/create-yydd-prod-deploy.ps1` -执行部署或生产核查前,优先读取上面两个脚本的当前默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。部署入口默认从 `origin/main` 取代码,发布前必须核对远端 `refs/heads/main` 和本地 `origin/main` 是否一致。 +首选脚本负责建立临时本地转发,调用原 Git 增量部署脚本,并在结束后关闭本地转发。两个服务器密码都必须从本地安全材料传入参数,不得写进脚本、Git 或文档。 + +```powershell +.\scripts\deploy-yydd-prod-via-jump.ps1 ` + -Ref origin/main ` + -JumpPassword $jumpPassword ` + -ServerPassword $productionPassword +``` + +执行部署或生产核查前,优先读取上述脚本的当前默认参数,并在远端实时确认当前 PM2 进程、`current` 软链接和健康检查结果。部署入口默认从 `origin/main` 取代码,发布前必须核对远端 `refs/heads/main` 和本地 `origin/main` 是否一致。 底层发布流程会执行依赖安装、`prisma generate`、迁移、构建、PM2 重启和健康检查。因此生产发布前必须先确认迁移目录、迁移历史和主数据 strict 巡检口径没有分叉。 @@ -45,14 +57,15 @@ 生产相关动作开始前,至少确认: -1. 目标主机是 `ltai5tdb2ogyj59fquyedh.yydd.ai:60022`。 -2. 远端存在 `/opt/furniture-3d-system-prod`。 -3. PM2 中存在 `furniture-3d-system-prod`。 -4. 应用监听端口是 `3001`。 -5. 部署来源是 `origin/main`,且远端 `main` 和本地 `origin/main` 一致。 -6. 生产只读地基核查优先使用 `npm run db:foundation:audit:production-readonly`。 -7. 生产只读 manifest 回传后,使用 `npm run db:foundation:verify-production-readonly -- --manifest ` 验收,不能人工挑几个字段就宣称生产通过。 -8. 不把密码、密钥或连接串写入 Git、文档或长期记忆。 +1. 跳板机是 `218.93.9.38:22`,生产管理代理只允许跳板机本机访问 `127.0.0.1:26022`。 +2. 跳板机的 `frps-furniture-prod.service` 和 `furniture-mgmt-port-guard.service` 均为 `active`。 +3. 生产机的 `frpc-furniture-mgmt.service` 为 `active`。 +4. 远端存在 `/opt/furniture-3d-system-prod`。 +5. PM2 中存在 `furniture-3d-system-prod`,应用监听端口是 `3001`。 +6. 部署来源是 `origin/main`,且远端 `main` 和本地 `origin/main` 一致。 +7. 生产只读地基核查优先使用 `npm run db:foundation:audit:production-readonly`。 +8. 生产只读 manifest 回传后,使用 `npm run db:foundation:verify-production-readonly -- --manifest ` 验收,不能人工挑几个字段就宣称生产通过。 +9. 不把密码、密钥或连接串写入 Git、文档或长期记忆。 ## 部署后核查 diff --git a/scripts/deploy-yydd-prod-via-jump.ps1 b/scripts/deploy-yydd-prod-via-jump.ps1 new file mode 100644 index 00000000..edf921c5 --- /dev/null +++ b/scripts/deploy-yydd-prod-via-jump.ps1 @@ -0,0 +1,179 @@ +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]$JumpHost = "218.93.9.38", + [int]$JumpPort = 22, + [string]$JumpUser = "root", + [string]$JumpPassword = "", + [string]$JumpHostKeyFingerprint = "SHA256:UwFngDHnyUFNAszlwbtVzBc4NdZMwCCfOz+8yoJvgKs", + [int]$RemoteManagementPort = 26022, + [int]$LocalForwardPort = 0, + [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" + +$deployScript = Join-Path $RepoRoot "scripts\deploy-yydd-prod-from-git.ps1" +$plinkPath = "C:\Program Files\PuTTY\plink.exe" + +if (-not (Test-Path -LiteralPath $deployScript)) { + throw "Missing deployment script: $deployScript" +} +if (-not (Test-Path -LiteralPath $plinkPath)) { + throw "Missing plink.exe: $plinkPath" +} +if ([string]::IsNullOrWhiteSpace($JumpPassword)) { + throw "JumpPassword is required. Read it from local secure material; do not store it in Git." +} +if ([string]::IsNullOrWhiteSpace($ServerPassword)) { + throw "ServerPassword is required. Read it from local secure material; do not store it in Git." +} + +function Get-FreeLoopbackPort { + $listener = [System.Net.Sockets.TcpListener]::new( + [System.Net.IPAddress]::Loopback, + 0 + ) + + try { + $listener.Start() + return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port + } + finally { + $listener.Stop() + } +} + +function Wait-LoopbackPort { + param( + [int]$Port, + [System.Diagnostics.Process]$Process, + [int]$TimeoutSeconds = 12 + ) + + $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds) + while ([DateTime]::UtcNow -lt $deadline) { + if ($Process.HasExited) { + $errorOutput = $Process.StandardError.ReadToEnd().Trim() + if ([string]::IsNullOrWhiteSpace($errorOutput)) { + $errorOutput = "exit code $($Process.ExitCode)" + } + throw "Management tunnel exited before it became ready: $errorOutput" + } + + $client = [System.Net.Sockets.TcpClient]::new() + try { + $pending = $client.BeginConnect("127.0.0.1", $Port, $null, $null) + if ($pending.AsyncWaitHandle.WaitOne(500, $false)) { + $client.EndConnect($pending) + return + } + } + catch { + # The listener is not ready yet. + } + finally { + $client.Dispose() + } + + Start-Sleep -Milliseconds 250 + } + + throw "Timed out waiting for the local management tunnel on port $Port." +} + +if ($LocalForwardPort -le 0) { + $LocalForwardPort = Get-FreeLoopbackPort +} + +$existingListener = Get-NetTCPConnection ` + -State Listen ` + -LocalPort $LocalForwardPort ` + -ErrorAction SilentlyContinue +if ($existingListener) { + throw "Local forwarding port $LocalForwardPort is already in use." +} + +$processInfo = [System.Diagnostics.ProcessStartInfo]::new() +$processInfo.FileName = $plinkPath +$processInfo.UseShellExecute = $false +$processInfo.CreateNoWindow = $true +$processInfo.RedirectStandardError = $true +$processInfo.RedirectStandardOutput = $true + +$forwardArguments = @( + "-batch", + "-N", + "-ssh", + "-P", + [string]$JumpPort, + "-l", + $JumpUser, + "-pw", + $JumpPassword, + "-hostkey", + $JumpHostKeyFingerprint, + "-L", + ("127.0.0.1:{0}:127.0.0.1:{1}" -f $LocalForwardPort, $RemoteManagementPort), + $JumpHost +) + +foreach ($argument in $forwardArguments) { + [void]$processInfo.ArgumentList.Add($argument) +} + +$tunnelProcess = $null +try { + $tunnelProcess = [System.Diagnostics.Process]::Start($processInfo) + Wait-LoopbackPort -Port $LocalForwardPort -Process $tunnelProcess + + $deployArguments = @{ + RepoRoot = $RepoRoot + ProjectSubdir = $ProjectSubdir + Ref = $Ref + RemoteBase = $RemoteBase + ServerHost = "127.0.0.1" + SshPort = $LocalForwardPort + ServerUser = $ServerUser + ServerPassword = $ServerPassword + HostKeyFingerprint = $HostKeyFingerprint + AppPort = $AppPort + Pm2Name = $Pm2Name + KeepReleases = $KeepReleases + } + + if (-not [string]::IsNullOrWhiteSpace($RemoteRepoDir)) { + $deployArguments.RemoteRepoDir = $RemoteRepoDir + } + if (-not [string]::IsNullOrWhiteSpace($RemoteGitUrl)) { + $deployArguments.RemoteGitUrl = $RemoteGitUrl + } + if ($ForceArchiveFallback) { + $deployArguments.ForceArchiveFallback = $true + } + if ($KeepArtifacts) { + $deployArguments.KeepArtifacts = $true + } + + & $deployScript @deployArguments +} +finally { + if ($null -ne $tunnelProcess) { + if (-not $tunnelProcess.HasExited) { + $tunnelProcess.Kill() + $tunnelProcess.WaitForExit() + } + $tunnelProcess.Dispose() + } +}