merge: 生产管理跳板通道
This commit is contained in:
commit
bdc8cdab8c
@ -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`。
|
||||
|
||||
部署前至少核对:
|
||||
|
||||
|
||||
@ -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`。
|
||||
|
||||
部署前至少核对:
|
||||
|
||||
|
||||
@ -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 <manifest.json>` 验收,不能人工挑几个字段就宣称生产通过。
|
||||
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 <manifest.json>` 验收,不能人工挑几个字段就宣称生产通过。
|
||||
9. 不把密码、密钥或连接串写入 Git、文档或长期记忆。
|
||||
|
||||
## 部署后核查
|
||||
|
||||
|
||||
179
scripts/deploy-yydd-prod-via-jump.ps1
Normal file
179
scripts/deploy-yydd-prod-via-jump.ps1
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user