- 打包脚本默认 tag 改为 latest,支持 --service 单服务构建/导出 - 新增 api/player/admin 三个独立 bat,更新文档与 manifest - 联赛赛事面板操作列加宽、横向滚动与按钮换行,避免操作按钮被裁切 Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
4.3 KiB
PowerShell
141 lines
4.3 KiB
PowerShell
# 构建 api / player / admin 生产镜像并导出为 tar
|
||
# 用法(在项目根目录或本目录执行均可):
|
||
# .\docs\docker\build-and-export-images.ps1
|
||
# .\docs\docker\build-and-export-images.ps1 -Service admin
|
||
# .\docs\docker\build-and-export-images.ps1 -Tag v1.2.3
|
||
# .\docs\docker\build-and-export-images.ps1 -UseCache
|
||
# .\docs\docker\build-and-export-images.ps1 -ExportOnly
|
||
|
||
param(
|
||
[ValidateSet("all", "api", "player", "admin")]
|
||
[string]$Service = "all",
|
||
[string]$Tag = $env:IMAGE_TAG,
|
||
[switch]$UseCache,
|
||
[switch]$ExportOnly,
|
||
[string]$Output = ""
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
$Root = (Resolve-Path (Join-Path $PSScriptRoot "../..")).Path
|
||
Set-Location $Root
|
||
|
||
$ComposeFile = "docker-compose.prod.yml"
|
||
$EnvFile = ".env.docker"
|
||
$AllServices = @("api", "player", "admin")
|
||
|
||
function Get-DefaultTag { "latest" }
|
||
|
||
function Get-DefaultOutput {
|
||
param([string]$Svc, [string]$ImageTag)
|
||
if ($Svc -eq "all") { return "thebet365-images-$ImageTag.tar" }
|
||
return "thebet365-$Svc-$ImageTag.tar"
|
||
}
|
||
|
||
function Assert-ImageTag {
|
||
param([string]$Value)
|
||
if ($Value -notmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') {
|
||
throw "镜像 tag 不合法: $Value"
|
||
}
|
||
}
|
||
|
||
if (-not (Test-Path $ComposeFile)) {
|
||
throw "未找到 $ComposeFile(当前目录: $Root)"
|
||
}
|
||
|
||
if (-not (Test-Path $EnvFile)) {
|
||
if (Test-Path ".env.docker.example") {
|
||
Write-Warning "未找到 .env.docker,使用 .env.docker.example(生产请复制并修改密钥)"
|
||
$EnvFile = ".env.docker.example"
|
||
} else {
|
||
throw "未找到 .env.docker 或 .env.docker.example"
|
||
}
|
||
}
|
||
|
||
if ([string]::IsNullOrWhiteSpace($Tag)) {
|
||
$Tag = Get-DefaultTag
|
||
}
|
||
Assert-ImageTag $Tag
|
||
|
||
$BuildServices = if ($Service -eq "all") { $AllServices } else { @($Service) }
|
||
$SaveImages = $BuildServices | ForEach-Object { "thebet365-${_}:${Tag}" }
|
||
|
||
if ([string]::IsNullOrWhiteSpace($Output)) {
|
||
$Output = Get-DefaultOutput -Svc $Service -ImageTag $Tag
|
||
}
|
||
|
||
if (-not $ExportOnly) {
|
||
Write-Host "==> 构建镜像: $($BuildServices -join ', ') (tag: $Tag)"
|
||
$buildArgs = @(
|
||
"compose", "-f", $ComposeFile, "--env-file", $EnvFile, "build"
|
||
)
|
||
if (-not $UseCache) {
|
||
$buildArgs += "--no-cache"
|
||
}
|
||
$buildArgs += $BuildServices
|
||
$oldImageTag = $env:IMAGE_TAG
|
||
try {
|
||
$env:IMAGE_TAG = $Tag
|
||
& docker @buildArgs
|
||
if ($LASTEXITCODE -ne 0) { throw "docker build 失败,退出码 $LASTEXITCODE" }
|
||
} finally {
|
||
$env:IMAGE_TAG = $oldImageTag
|
||
}
|
||
}
|
||
|
||
$OutputPath = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $Root $Output }
|
||
|
||
Write-Host "==> 导出镜像 ($Service) -> $OutputPath"
|
||
& docker save @SaveImages -o $OutputPath
|
||
if ($LASTEXITCODE -ne 0) { throw "docker save 失败,退出码 $LASTEXITCODE" }
|
||
|
||
$manifestPath = if ($OutputPath.EndsWith(".tar")) {
|
||
$OutputPath.Substring(0, $OutputPath.Length - 4) + ".manifest.txt"
|
||
} else {
|
||
"$OutputPath.manifest.txt"
|
||
}
|
||
|
||
$gitCommit = "unknown"
|
||
$gitDirty = "unknown"
|
||
try {
|
||
$gitCommit = (& git rev-parse HEAD 2>$null).Trim()
|
||
& git diff --quiet
|
||
$diffExit = $LASTEXITCODE
|
||
& git diff --cached --quiet
|
||
$cachedExit = $LASTEXITCODE
|
||
$gitDirty = if ($diffExit -eq 0 -and $cachedExit -eq 0) { "false" } else { "true" }
|
||
} catch {}
|
||
|
||
$tarHash = (Get-FileHash -Algorithm SHA256 $OutputPath).Hash.ToLowerInvariant()
|
||
$manifestLines = @(
|
||
"tag=$Tag"
|
||
"service=$Service"
|
||
"built_at=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"))"
|
||
"git_commit=$gitCommit"
|
||
"git_dirty=$gitDirty"
|
||
"tar=$([System.IO.Path]::GetFileName($OutputPath))"
|
||
"tar_sha256=$tarHash"
|
||
)
|
||
foreach ($svc in $BuildServices) {
|
||
$imageId = (& docker image inspect "thebet365-${svc}:${Tag}" --format "{{.Id}}").Trim()
|
||
if ($Service -eq "all") {
|
||
$manifestLines += "${svc}_image=thebet365-${svc}:${Tag}"
|
||
$manifestLines += "${svc}_image_id=$imageId"
|
||
} else {
|
||
$manifestLines += "image=thebet365-${svc}:${Tag}"
|
||
$manifestLines += "image_id=$imageId"
|
||
}
|
||
}
|
||
$manifestLines | Set-Content -Encoding UTF8 $manifestPath
|
||
|
||
$sizeMb = [math]::Round((Get-Item $OutputPath).Length / 1MB, 2)
|
||
Write-Host "完成: $OutputPath (${sizeMb} MB)"
|
||
Write-Host "Manifest: $manifestPath"
|
||
|
||
$recreate = if ($Service -eq "all") { "api player admin" } else { $Service }
|
||
Write-Host @"
|
||
|
||
服务器导入并重建:
|
||
docker load -i $([System.IO.Path]::GetFileName($OutputPath))
|
||
docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate $recreate
|
||
"@
|