-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdeploy.ps1
More file actions
343 lines (275 loc) · 11.3 KB
/
deploy.ps1
File metadata and controls
343 lines (275 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
<#
.SYNOPSIS
Go 项目自动化部署脚本 (Tar + SSH)
.DESCRIPTION
1. 自动检测本地 SSH 密钥
2. 检查并自动创建远程目标目录
3. 使用 Git Bash 的 tar + ssh 管道流式上传文件
4. 备份旧版本、重启 Supervisor 服务
5. 健康检查,失败时自动回滚
.EXAMPLE
.\deploy.ps1
#>
# ============================================================
# 配置区域 (请在此修改)
# ============================================================
# 服务器地址 (IP 或域名)
$Server = "my.server"
# SSH 端口
$Port = 22
# 登录用户名
$User = "root"
# 远程部署路径
$RemotePath = "/root/easy-qfnu-api-go"
# SSH 私钥路径 (留空则自动检测 ~/.ssh/id_rsa 或 id_ed25519)
$IdentityFile = ""
# 项目名称 (用于生成二进制文件名)
$ProjectName = "easy-qfnu-api-go"
# 目标操作系统
$TargetOS = "linux"
# 目标架构
$TargetArch = "amd64"
# Supervisor 服务名称
$SupervisorService = "easy-qfnu-api-go:easy-qfnu-api-go_00"
# 健康检查 URL
$HealthCheckUrl = "http://127.0.0.1:8141"
# 健康检查超时时间 (秒)
$HealthCheckTimeout = 30
# 健康检查重试次数
$HealthCheckRetries = 3
# 健康检查重试间隔 (秒)
$HealthCheckInterval = 5
# ============================================================
# 配置区域结束
# ============================================================
$ErrorActionPreference = "Stop"
# 1. 自动检测 SSH 密钥
if ([string]::IsNullOrEmpty($IdentityFile)) {
$sshDir = "$env:USERPROFILE\.ssh"
$possibleKeys = @("id_rsa", "id_ed25519")
foreach ($keyName in $possibleKeys) {
$path = Join-Path $sshDir $keyName
if (Test-Path $path) {
$IdentityFile = $path
Write-Host "[-] 自动检测到 SSH 密钥: $IdentityFile" -ForegroundColor Cyan
break
}
}
}
if (-not (Test-Path $IdentityFile)) {
Write-Error "未找到 SSH 密钥,请在配置区域指定 IdentityFile。"
}
# 构建基础 SSH 命令前缀
$sshCmdPrefix = @("ssh", "-i", "$IdentityFile", "-p", "$Port", "-o", "StrictHostKeyChecking=no", "$User@$Server")
# 辅助函数:执行远程命令 (返回输出和退出码)
function Invoke-RemoteCommand {
param(
[string]$Command,
[switch]$Silent
)
$cmd = $sshCmdPrefix + $Command
$output = & $cmd[0] $cmd[1..($cmd.Length - 1)] 2>&1
$code = $LASTEXITCODE
if (-not $Silent -and $output) {
Write-Host $output -ForegroundColor DarkGray
}
return @{
Output = $output
ExitCode = $code
}
}
# 辅助函数:健康检查 (在远程服务器上执行)
function Test-ServiceHealth {
param(
[string]$Url,
[int]$Timeout,
[int]$Retries,
[int]$Interval
)
for ($i = 1; $i -le $Retries; $i++) {
Write-Host "[-] 健康检查 (第 $i/$Retries 次)..." -ForegroundColor Cyan
# 在远程服务器上使用 curl 进行健康检查
$healthCmd = "curl -s -o /dev/null -w '%{http_code}' --connect-timeout $Timeout '$Url'"
$httpCode = & $sshCmdPrefix[0] $sshCmdPrefix[1..($sshCmdPrefix.Length - 1)] $healthCmd 2>$null
if ($httpCode -eq "200") {
Write-Host "[+] 健康检查通过! (HTTP $httpCode)" -ForegroundColor Green
return $true
}
elseif ($httpCode) {
Write-Host "[!] 健康检查返回非 200 状态码: $httpCode" -ForegroundColor Yellow
}
else {
Write-Host "[!] 健康检查失败: 无法连接到服务" -ForegroundColor Yellow
}
if ($i -lt $Retries) {
Write-Host "[-] 等待 $Interval 秒后重试..." -ForegroundColor DarkGray
Start-Sleep -Seconds $Interval
}
}
return $false
}
# 2. 交叉编译 (Windows -> Linux)
Write-Host "[-] 正在编译 $TargetOS ($TargetArch) 二进制文件..." -ForegroundColor Cyan
$BinaryName = "${ProjectName}-${TargetOS}-${TargetArch}"
$BackupBinaryName = "${BinaryName}.backup"
# 保存旧的环境变量
$OriginalGOOS = $env:GOOS
$OriginalGOARCH = $env:GOARCH
try {
$env:CGO_ENABLED = "0"
$env:GOOS = $TargetOS
$env:GOARCH = $TargetArch
go build -ldflags "-s -w" -o $BinaryName .
if ($LASTEXITCODE -ne 0) {
Write-Error "编译失败,请检查 Go 环境或代码错误。"
}
Write-Host "[-] 编译成功: $BinaryName" -ForegroundColor Green
}
finally {
# 恢复环境变量
$env:GOOS = $OriginalGOOS
$env:GOARCH = $OriginalGOARCH
}
# 3. 检查并修复远程路径 (mkdir -p)
Write-Host "[-] 正在检查/创建远程目录: $RemotePath" -ForegroundColor Cyan
$result = Invoke-RemoteCommand "mkdir -p $RemotePath"
if ($result.ExitCode -ne 0) {
Write-Host "[!] 错误详情: $($result.Output)" -ForegroundColor Red
Write-Error "无法创建远程目录,请检查连接或权限。"
}
# 4. 备份旧版本
Write-Host "[-] 正在备份旧版本..." -ForegroundColor Cyan
$backupCmd = "if [ -f '$RemotePath/$BinaryName' ]; then cp -f '$RemotePath/$BinaryName' '$RemotePath/$BackupBinaryName'; echo 'BACKUP_OK'; else echo 'NO_BINARY'; fi"
$result = Invoke-RemoteCommand $backupCmd -Silent
$backupOutput = ($result.Output | Out-String).Trim()
if ($backupOutput -match "BACKUP_OK") {
Write-Host "[-] 旧版本已备份: $BackupBinaryName" -ForegroundColor Green
}
elseif ($backupOutput -match "NO_BINARY") {
Write-Host "[-] 没有旧版本需要备份 (首次部署)" -ForegroundColor DarkGray
}
else {
Write-Host "[!] 备份输出: $backupOutput" -ForegroundColor Yellow
Write-Warning "备份旧版本可能失败,继续部署..."
}
# 5. 使用 Tar + SSH 上传二进制文件 (通过 Git Bash)
Write-Host "[-] 正在上传二进制文件..." -ForegroundColor Cyan
# 查找 Git Bash (优先使用 Git for Windows,避免 WSL)
$gitBashPaths = @(
"$env:ProgramFiles\Git\bin\bash.exe",
"${env:ProgramFiles(x86)}\Git\bin\bash.exe",
"$env:LOCALAPPDATA\Programs\Git\bin\bash.exe",
"$env:ProgramFiles\Git\usr\bin\bash.exe"
)
$bashExe = $null
foreach ($path in $gitBashPaths) {
if (Test-Path $path) {
$bashExe = $path
break
}
}
# 如果预设路径找不到,尝试从 PATH 中查找 Git 目录下的 bash
if (-not $bashExe) {
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
if ($gitCmd) {
# git.exe 通常在 Git\cmd 目录,bash 在 Git\bin 目录
$gitDir = Split-Path (Split-Path $gitCmd.Source -Parent) -Parent
$gitBash = Join-Path $gitDir "bin\bash.exe"
if (Test-Path $gitBash) {
$bashExe = $gitBash
}
}
}
if (-not $bashExe) {
Write-Error "未找到 Git Bash,请确保已安装 Git for Windows。"
}
Write-Host "[-] 使用 Git Bash: $bashExe" -ForegroundColor DarkGray
# 将 Windows 路径转换为 Unix 风格路径 (用于 Git Bash)
$unixIdentityFile = $IdentityFile -replace '\\', '/' -replace '^([A-Za-z]):', '/$1'
# 构造 bash 命令:
# 1. 本地 tar 打包二进制文件
# 2. SSH 传输
# 3. 远程 tar 解压
# 4. 远程 chmod +x 赋予执行权限
$bashCmd = "tar -c $BinaryName | ssh -i '$unixIdentityFile' -p $Port -o StrictHostKeyChecking=no $User@$Server 'tar -x -C $RemotePath && chmod +x $RemotePath/$BinaryName'"
Write-Host "Executing: Upload..." -ForegroundColor DarkGray
& $bashExe -c $bashCmd
if ($LASTEXITCODE -eq 0) {
Write-Host "[+] 文件上传成功!" -ForegroundColor Green
# 删除本地构建产物
if (Test-Path $BinaryName) {
Remove-Item $BinaryName -Force
Write-Host "[-] 已清理本地构建产物: $BinaryName" -ForegroundColor DarkGray
}
}
else {
Write-Error "文件上传失败。"
}
# 6. 重启 Supervisor 服务
Write-Host "[-] 正在重启 Supervisor 服务: $SupervisorService" -ForegroundColor Cyan
$result = Invoke-RemoteCommand "supervisorctl restart $SupervisorService"
if ($result.ExitCode -ne 0) {
Write-Host "[!] Supervisor 重启错误: $($result.Output)" -ForegroundColor Yellow
Write-Warning "Supervisor 重启命令返回非零状态码"
}
# 等待服务启动
Write-Host "[-] 等待服务启动..." -ForegroundColor Cyan
Start-Sleep -Seconds 3
# 7. 健康检查
Write-Host "[-] 开始健康检查: $HealthCheckUrl" -ForegroundColor Cyan
$isHealthy = Test-ServiceHealth -Url $HealthCheckUrl -Timeout $HealthCheckTimeout -Retries $HealthCheckRetries -Interval $HealthCheckInterval
if ($isHealthy) {
Write-Host "[+] 部署成功! 服务运行正常。" -ForegroundColor Green
# 删除备份文件
Write-Host "[-] 清理备份文件..." -ForegroundColor DarkGray
Invoke-RemoteCommand "rm -f '$RemotePath/$BackupBinaryName'" -Silent | Out-Null
}
else {
Write-Host "[!] 健康检查失败! 正在回滚..." -ForegroundColor Red
# 检查备份是否存在
$checkBackup = "test -f '$RemotePath/$BackupBinaryName' && echo 'EXISTS' || echo 'NOT_EXISTS'"
$result = Invoke-RemoteCommand $checkBackup -Silent
$backupStatus = ($result.Output | Out-String).Trim()
Write-Host "[-] 备份文件状态: $backupStatus" -ForegroundColor DarkGray
if ($backupStatus -match "EXISTS") {
Write-Host "[-] 正在恢复旧版本..." -ForegroundColor Yellow
# 恢复备份
$restoreCmd = "cp -f '$RemotePath/$BackupBinaryName' '$RemotePath/$BinaryName' && chmod +x '$RemotePath/$BinaryName'"
$result = Invoke-RemoteCommand $restoreCmd
if ($result.ExitCode -eq 0) {
Write-Host "[-] 旧版本已恢复,正在重启服务..." -ForegroundColor Yellow
# 重启服务
$result = Invoke-RemoteCommand "supervisorctl restart $SupervisorService"
# 等待服务启动
Start-Sleep -Seconds 3
# 检查 Supervisor 进程状态 (回滚后只检查进程是否运行,不检查 HTTP)
Write-Host "[-] 检查服务进程状态..." -ForegroundColor Cyan
$statusResult = Invoke-RemoteCommand "supervisorctl status $SupervisorService" -Silent
$statusOutput = ($statusResult.Output | Out-String).Trim()
Write-Host " $statusOutput" -ForegroundColor DarkGray
if ($statusOutput -match "RUNNING") {
Write-Host "[+] 回滚成功! 服务进程已运行。" -ForegroundColor Yellow
Write-Host "[!] 注意: 请检查健康检查 URL 配置是否正确: $HealthCheckUrl" -ForegroundColor Yellow
}
else {
Write-Host "[!] 回滚后服务进程状态异常,请手动检查!" -ForegroundColor Red
}
}
else {
Write-Host "[!] 恢复旧版本失败: $($result.Output)" -ForegroundColor Red
}
# 获取 Supervisor 日志
Write-Host "`n[-] Supervisor 服务日志:" -ForegroundColor Cyan
$logResult = Invoke-RemoteCommand "supervisorctl tail -1000 $SupervisorService stderr"
Write-Host $logResult.Output -ForegroundColor DarkGray
}
else {
Write-Host "[!] 没有找到备份文件,无法回滚! 请手动检查服务状态。" -ForegroundColor Red
# 获取 Supervisor 日志
Write-Host "`n[-] Supervisor 服务日志:" -ForegroundColor Cyan
$logResult = Invoke-RemoteCommand "supervisorctl tail -1000 $SupervisorService stderr"
Write-Host $logResult.Output -ForegroundColor DarkGray
}
Write-Error "部署失败,已尝试回滚。"
}
Write-Host "`n部署流程结束。" -ForegroundColor Cyan