大修改

This commit is contained in:
Yeomai committed 2026-09-30 03:38:10 +08:00
1 parent a1cbc9404a
commit e81481623c
47 files changed
+1453 -285

No files matched your search

+209
View File
@@ -0,0 +1,209 @@
#Requires -Version 5.1
<#
One-command deploy for yelaymaths_api / yelaymaths_admin.
Packs source files locally, uploads over SSH, builds on the server and restarts the process.
No service is installed on the server; it reuses the SSH access you already have.
Usage:
.\deploy\deploy.ps1 api
.\deploy\deploy.ps1 admin
.\deploy\deploy.ps1 all
.\deploy\deploy.ps1 api -DryRun # pack only, show what would be uploaded
.\deploy\deploy.ps1 api -WithInstall # also install dependencies (only after package.json changes)
Files are packed from an explicit whitelist, so .env / node_modules / dist / .next
are never uploaded and your server-side .env stays untouched.
#>
[CmdletBinding()]
param(
[ValidateSet('api', 'admin', 'all')]
[string]$Target = 'all',
[switch]$DryRun,
[switch]$WithInstall
)
$ErrorActionPreference = 'Stop'
$OutputEncoding = New-Object System.Text.UTF8Encoding $false
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = Split-Path -Parent $scriptDir
$configPath = Join-Path $scriptDir 'deploy.config.ps1'
if (-not (Test-Path $configPath)) {
Write-Host 'ERROR: deploy.config.ps1 not found.' -ForegroundColor Red
Write-Host 'Copy deploy.config.example.ps1 to deploy.config.ps1 and fill in your server details.'
exit 1
}
. $configPath
foreach ($exe in @('ssh', 'scp', 'tar')) {
if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) {
throw "Missing required command: $exe (Windows 10+ ships OpenSSH and tar)"
}
}
$sshTarget = '{0}@{1}' -f $Config.User, $Config.Host
function Invoke-AppDeploy {
param([hashtable]$App)
$local = Join-Path $repoRoot $App.LocalPath
if (-not (Test-Path $local)) { throw "Local path not found: $local" }
# Whitelist: only include paths that exist. Everything else (secrets, build output)
# is structurally impossible to upload.
$items = @()
foreach ($p in $App.DeployPaths) {
if (Test-Path (Join-Path $local $p)) { $items += $p }
}
if ($items.Count -eq 0) { throw "Nothing to deploy for $($App.Key)" }
$excludes = @()
foreach ($p in ($App.ExcludePaths ?? @())) { $excludes += "--exclude=$p" }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$tarName = 'yelay-{0}-{1}.tar.gz' -f $App.Key, $stamp
$tarFile = Join-Path $env:TEMP $tarName
$remoteTar = '/tmp/' + $tarName
Write-Host ''
Write-Host ('=== {0} ===' -f $App.Label) -ForegroundColor Cyan
# ---- 本地构建:把耗时的编译放在本机,服务器只接收产物 ----
if ($App.LocalBuildCmd) {
Write-Host ('[1/5] Building locally: {0}' -f $App.LocalBuildCmd)
# 国内拉取 Prisma 引擎很慢,指向镜像
$env:PRISMA_ENGINES_MIRROR = 'https://registry.npmmirror.com/-/binary/prisma'
Push-Location $local
try {
Invoke-Expression $App.LocalBuildCmd
if ($LASTEXITCODE -ne 0) { throw "Local build failed: $($App.LocalBuildCmd)" }
} finally {
Pop-Location
}
} else {
Write-Host '[1/5] Skipping local build'
}
Write-Host ('[2/5] Packing {0}' -f $App.LocalPath)
Push-Location $local
try {
& tar -czf $tarFile @excludes @items
if ($LASTEXITCODE -ne 0) { throw 'tar failed' }
} finally {
Pop-Location
}
$sizeKb = [math]::Round((Get-Item $tarFile).Length / 1KB, 1)
Write-Host (' {0} KB ({1})' -f $sizeKb, ($items -join ', '))
if ($DryRun) {
Write-Host ' DryRun: nothing uploaded.'
Remove-Item $tarFile -Force
return
}
Write-Host ('[3/5] Uploading to {0}' -f $App.RemotePath)
& scp -P $Config.Port -q $tarFile ('{0}:{1}' -f $sshTarget, $remoteTar)
if ($LASTEXITCODE -ne 0) { throw 'scp failed' }
$install = ''
if ($WithInstall) { $install = $App.InstallCmd }
# 部署前先在服务器上删掉指定的旧目录。
#
# 为什么必须要这一步:Next.js 会把预渲染好的页面缓存在 .next/cache 里,
# 命中时直接返回缓存(响应头 x-nextjs-cache: HIT),不再读磁盘上的新文件。
# 而 .next/cache 体积很大(约 73MB),上传时会排除掉,于是它永远是旧的 ——
# 表现就是「代码传上去了、也重启了,页面却一直没变」。这里先删掉,让它重建。
$cleanCmd = ''
foreach ($p in ($App.CleanPaths ?? @())) {
$cleanCmd += "rm -rf '$($App.RemotePath)/$p'`n"
}
# 部署后清空 Nginx 反代缓存。
#
# 为什么必须清:宝塔在 /www/server/nginx/conf/proxy.conf 里全局开了
# proxy_cache cache_one,而 Next.js 给静态页面发的响应头是
# Cache-Control: s-maxage=31536000,等于告诉 Nginx「这一页可以存一年」。
# 于是新构建传上去、进程也重启了,访问到的还是缓存的旧 HTML;旧 HTML
# 引用的分包早被新构建删掉,一请求就是 404,页面渲染到一半就断,
# 表现就是「怎么改都不生效」,甚至某个区块整块消失。
# 清掉缓存目录再 reload,让它回源读新构建。
$nginxCacheCmd = ''
if ($Config.NginxCacheDir) {
$nginxCacheCmd += "find '$($Config.NginxCacheDir)' -mindepth 1 -delete 2>/dev/null || true`n"
if ($Config.NginxBin) {
$nginxCacheCmd += "'$($Config.NginxBin)' -s reload 2>/dev/null || true`n"
}
}
# 启动命令。
#
# RestartCmd 只负责杀掉旧进程,宝塔不会自动把新进程拉起来,
# 所以这里自己拉。先 sleep 2 是为了让刚被杀掉的进程彻底放开端口,
# 否则新进程会 EADDRINUSE 起不来。
$startCmd = ''
if ($App.StartCmd) {
$startCmd = "sleep 2`n$($App.StartCmd)`n"
}
# 启动后探活:确认端口真的在响应,而不是「命令敲下去就当成功」。
# 30 秒还没起来就让部署直接失败,免得又出现「显示部署成功、线上却是 502」。
$healthCmd = ''
if ($App.HealthUrl) {
$healthCmd = "for i in {1..30}; do curl -sf -o /dev/null '$($App.HealthUrl)' && break; sleep 1; done`n"
$healthCmd += "curl -sf -o /dev/null '$($App.HealthUrl)' || { echo 'ERROR: service did not come up: $($App.HealthUrl)'; exit 1; }`n"
}
# Remote script is intentionally ASCII-only to avoid encoding issues over SSH.
$remoteScript = @"
set -e
export PATH=$($Config.NodeBin):`$PATH
mkdir -p '$($App.RemotePath)'
cd '$($App.RemotePath)'
$cleanCmd
tar -xzf '$remoteTar'
rm -f '$remoteTar'
$install
$($App.BuildCmd)
$($App.RestartCmd)
$startCmd
$healthCmd
$nginxCacheCmd
echo '__DEPLOY_DONE__'
"@
Write-Host '[4/5] Running on server'
$remoteScript | & ssh -p $Config.Port $sshTarget 'bash -s'
if ($LASTEXITCODE -ne 0) { throw 'remote build or restart failed' }
Remove-Item $tarFile -Force -ErrorAction SilentlyContinue
if ($App.RestartCmd) {
Write-Host '[5/5] Restarted' -ForegroundColor Green
} else {
Write-Host '[5/5] Done' -ForegroundColor Green
Write-Host (' NOTE: restart "{0}" from the BT panel now.' -f $App.Label) -ForegroundColor Yellow
}
if ($App.StartCmd) {
Write-Host (' Started, health check passed: {0}' -f $App.HealthUrl) -ForegroundColor Green
}
if ($nginxCacheCmd) {
Write-Host ' Nginx proxy cache cleared and reloaded' -ForegroundColor Green
}
}
$apps = switch ($Target) {
'api' { @($Config.Api) }
'admin' { @($Config.Admin) }
'all' { @($Config.Api, $Config.Admin) }
}
$started = Get-Date
foreach ($app in $apps) { Invoke-AppDeploy -App $app }
if (-not $DryRun) {
Write-Host ''
Write-Host ('All done in {0:N1}s' -f ((Get-Date) - $started).TotalSeconds) -ForegroundColor Green
}