CaiWuXiTong/scripts/deploy-from-workstation.ps1

229 lines
8.4 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Server,
[Parameter(Mandatory = $true)]
[string]$KeyPath,
[string]$SshUser = 'root',
[string]$RemotePath = '/opt/caiwuxitong',
[string]$AppUrl,
[string]$BuilderName = 'caiwu-builder',
[string]$BuildkitImage = 'm.daocloud.io/docker.io/moby/buildkit:buildx-stable-1',
[int]$KeepImages = 2,
[switch]$BootstrapServer,
[switch]$AllowNonMain,
[switch]$AllowMigrations
)
$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$keyFullPath = (Resolve-Path $KeyPath).Path
$sshTarget = "${SshUser}@${Server}"
$imageName = 'caiwu_akaunting'
$tempRoot = Join-Path ([IO.Path]::GetTempPath()) ("caiwu-deploy-" + [Guid]::NewGuid().ToString('N'))
$localTar = Join-Path $tempRoot 'image.tar'
$localArchive = Join-Path $tempRoot 'image.tar.gz'
$remoteArchive = $null
$bootstrapRemote = $null
$buildkitConfig = Join-Path $repoRoot 'deploy/buildkitd.toml'
function Invoke-Native {
param(
[Parameter(Mandatory = $true)]
[scriptblock]$Command,
[Parameter(Mandatory = $true)]
[string]$FailureMessage
)
& $Command
if ($LASTEXITCODE -ne 0) {
throw $FailureMessage
}
}
function Remove-OldProjectImages {
param([string]$CurrentImage)
$projectImages = @(
docker image ls $imageName --format '{{.Repository}}:{{.Tag}}' |
Where-Object { $_ -and $_ -ne '<none>:<none>' } |
Select-Object -Unique
)
$keep = @($CurrentImage)
foreach ($candidate in $projectImages) {
if ($keep.Count -ge $KeepImages) { break }
if ($candidate -notin $keep) { $keep += $candidate }
}
foreach ($candidate in $projectImages) {
if ($candidate -notin $keep) {
docker image rm $candidate 2>$null | Out-Null
}
}
}
function Compress-Gzip {
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Destination
)
$inputStream = $null
$outputStream = $null
$gzipStream = $null
try {
$inputStream = [IO.File]::OpenRead($Source)
$outputStream = [IO.File]::Create($Destination)
$gzipStream = [IO.Compression.GZipStream]::new(
$outputStream,
[IO.Compression.CompressionLevel]::Fastest
)
$inputStream.CopyTo($gzipStream)
}
finally {
if ($gzipStream) { $gzipStream.Dispose() }
if ($outputStream) { $outputStream.Dispose() }
if ($inputStream) { $inputStream.Dispose() }
}
}
Push-Location $repoRoot
try {
$branch = (git branch --show-current).Trim()
if (-not $AllowNonMain -and $branch -ne 'main') {
throw "当前分支是 $branch。正式发布只允许 main;测试分支需显式添加 -AllowNonMain。"
}
$trackedChanges = git status --porcelain --untracked-files=no
if ($trackedChanges) {
throw '工作区存在未提交的受控文件改动,拒绝构建。请先提交或处理清楚。'
}
$deploySha = (git rev-parse HEAD).Trim()
$imageTag = $deploySha.Substring(0, 12)
$fullImage = "${imageName}:$imageTag"
$remoteArchive = "$RemotePath/image-$imageTag.tar.gz"
$buildDate = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
if (-not $AppUrl) {
$AppUrl = "http://$Server"
}
Invoke-Native { docker version | Out-Null } 'Docker Desktop 未启动或不可用。'
& (Join-Path $PSScriptRoot 'build-frontend.ps1')
docker buildx inspect $BuilderName 2>$null | Out-Null
if ($LASTEXITCODE -ne 0) {
Invoke-Native {
docker buildx create `
--name $BuilderName `
--driver docker-container `
--driver-opt "image=$BuildkitImage" `
--buildkitd-config $buildkitConfig `
--use | Out-Null
} '创建项目专属 BuildKit 构建器失败。'
}
Invoke-Native { docker buildx inspect $BuilderName --bootstrap | Out-Null } '启动 BuildKit 构建器失败。'
Invoke-Native {
docker buildx build `
--builder $BuilderName `
--load `
--tag $fullImage `
--build-context 'frontend=.caiwu-build/public' `
--build-arg "VCS_REF=$deploySha" `
--build-arg "BUILD_DATE=$buildDate" `
--label 'com.caiwu.project=CaiWuXiTong' `
--file deploy/Dockerfile `
.
} '本机镜像构建失败。服务器未发生任何变更。'
$previousShaOutput = @(& ssh -i $keyFullPath -o BatchMode=yes -o StrictHostKeyChecking=accept-new $sshTarget "cat '$RemotePath/.deployed-sha' 2>/dev/null || true")
$previousSha = ($previousShaOutput -join "`n").Trim()
if ($LASTEXITCODE -ne 0) {
throw '无法连接服务器。'
}
$previousShaExists = $false
if ($previousSha) {
git cat-file -e "${previousSha}^{commit}" 2>$null
$previousShaExists = $LASTEXITCODE -eq 0
}
if ($previousSha -and -not $previousShaExists -and -not $AllowMigrations) {
throw "本地缺少服务器当前提交 $previousSha,无法安全比较数据库迁移。请先 git fetch,或在负责人授权后使用 -AllowMigrations。"
}
if ($previousSha -and $previousShaExists) {
$migrationChanges = git diff --name-only "$previousSha..$deploySha" -- database/migrations
if ($migrationChanges -and -not $AllowMigrations) {
throw "检测到数据库迁移:$($migrationChanges -join ', ')。需负责人确认后添加 -AllowMigrations。"
}
}
New-Item -ItemType Directory -Path $tempRoot | Out-Null
if ($BootstrapServer) {
$bootstrapRemote = "/tmp/caiwu-bootstrap-$imageTag.sh"
Invoke-Native {
scp -i $keyFullPath -o BatchMode=yes deploy/bootstrap-server.sh "${sshTarget}:$bootstrapRemote"
} '上传服务器初始化脚本失败。'
Invoke-Native {
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "chmod 700 '$bootstrapRemote' && '$bootstrapRemote' '$AppUrl' '$RemotePath' && rm -f '$bootstrapRemote'"
} '服务器初始化失败。'
}
Invoke-Native {
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "docker version >/dev/null && docker compose version >/dev/null && install -d -m 700 '$RemotePath'"
} '服务器 Docker 未就绪;首次发布请添加 -BootstrapServer。'
Invoke-Native { docker save --output $localTar $fullImage } '导出镜像失败。'
Compress-Gzip -Source $localTar -Destination $localArchive
Remove-Item -LiteralPath $localTar -Force
$archiveSizeMB = [math]::Round((Get-Item -LiteralPath $localArchive).Length / 1MB, 1)
Write-Host "压缩传输包:$archiveSizeMB MiB"
Invoke-Native { scp -i $keyFullPath -o BatchMode=yes $localArchive "${sshTarget}:$remoteArchive" } '上传镜像失败。'
Invoke-Native { scp -i $keyFullPath -o BatchMode=yes compose.jiangling.yml deploy/activate-release.sh "${sshTarget}:$RemotePath/" } '上传运行配置失败。'
$migrationFlag = if ($AllowMigrations) { '1' } else { '0' }
Invoke-Native {
ssh -i $keyFullPath -o BatchMode=yes $sshTarget "chmod 700 '$RemotePath/activate-release.sh' && '$RemotePath/activate-release.sh' '$remoteArchive' '$imageTag' '$RemotePath' '$deploySha' '$migrationFlag' '$KeepImages'"
} '服务器激活或健康检查失败。'
Remove-OldProjectImages -CurrentImage $fullImage
Write-Host "发布完成:$fullImage -> $AppUrl"
}
finally {
Pop-Location
if (Test-Path -LiteralPath $tempRoot) {
Remove-Item -LiteralPath $tempRoot -Recurse -Force
}
if ($remoteArchive) {
ssh -i $keyFullPath -o BatchMode=yes -o ConnectTimeout=5 $sshTarget "rm -f '$remoteArchive'" 2>$null | Out-Null
}
if ($bootstrapRemote) {
ssh -i $keyFullPath -o BatchMode=yes -o ConnectTimeout=5 $sshTarget "rm -f '$bootstrapRemote'" 2>$null | Out-Null
}
$pruneHelp = docker buildx prune --help 2>$null | Out-String
docker buildx prune --builder $BuilderName --filter 'until=168h' --force 2>$null | Out-Null
if ($pruneHelp -match '--max-used-space') {
docker buildx prune --builder $BuilderName --max-used-space 8GB --force 2>$null | Out-Null
}
elseif ($pruneHelp -match '--keep-storage') {
docker buildx prune --builder $BuilderName --keep-storage 8GB --force 2>$null | Out-Null
}
docker system df
}