diff --git a/docs/开发与部署SOP.md b/docs/开发与部署SOP.md index 94088872..4ed1039c 100644 --- a/docs/开发与部署SOP.md +++ b/docs/开发与部署SOP.md @@ -119,7 +119,15 @@ git pull --ff-only 13. 开发机、服务器 Docker 和服务器 Registry 均仅保留当前和上一个本项目版本; 14. 清理 7 天前的本项目构建缓存,并将总量压到最多 3GB。 -### 5.1 程序资源与业务文件的固定边界 +### 5.1 安全软件拦截 regctl 时的降级 + +第 6 步默认用项目专用 `regctl`(未签名)把 BuildKit 原生 OCI 分层包推入服务器本机 Registry。少数开发机的 360 安全卫士、金山毒霸等安全软件会拦截未签名程序,表现为 `regctl-v0.11.5.exe` 启动即报“拒绝访问”。 + +- 脚本会自动降级:regctl 无法运行或连续上传失败时,改用 `docker save` 全量包经 `scp` 上传,在服务器上 `docker load`、推入本机 Registry,再执行同一个 `activate-incremental-release.sh`(回滚、健康检查、双版本保留和旧层回收逻辑不变)。降级只增加传输量,功能与发布结果一致。 +- 也可以在受拦截的开发机上显式跳过 regctl:`.\scripts\deploy-from-workstation.ps1 -SkipRegctl`。 +- 建议在这类开发机上把 `%LOCALAPPDATA%\CaiWuXiTong\tools` 加入安全软件信任区,以恢复只上传变化层的增量方式。 + +### 5.2 程序资源与业务文件的固定边界 - 程序资源:JavaScript、CSS、字体、PWA 图标和仓库图片。它们只随 Git 提交和应用镜像发布,浏览器从当前应用域名加载。 - 业务文件:用户上传的图片、票据、PDF、表格和附件。生产环境统一写入项目唯一的私有七牛桶,本地开发默认仍写入本机 `storage/app/uploads`。 diff --git a/scripts/deploy-from-workstation.ps1 b/scripts/deploy-from-workstation.ps1 index dbdb6343..f23d4565 100644 --- a/scripts/deploy-from-workstation.ps1 +++ b/scripts/deploy-from-workstation.ps1 @@ -21,7 +21,8 @@ param( [switch]$BootstrapServer, [switch]$ConfigureDingTalkCallback, [switch]$AllowNonMain, - [switch]$AllowMigrations + [switch]$AllowMigrations, + [switch]$SkipRegctl ) $ErrorActionPreference = 'Stop' @@ -504,6 +505,8 @@ try { # docker save 导出未压缩层,每次导入重新压缩会改变摘要,导致 Registry 无法复用。 # 直接导出 BuildKit 原生 OCI 层,让未变化层的压缩摘要跨版本保持稳定。 + # 注意:360/金山等安全软件会拦截未签名的 regctl(启动即“拒绝访问”), + # 这类开发机可以用 -SkipRegctl,或者交给下面的自动降级改走 docker 直传。 Invoke-Native { docker buildx build ` --builder $BuilderName ` @@ -518,49 +521,89 @@ try { --file deploy/Dockerfile ` . } '导出稳定分层的 OCI 应用镜像失败。' - $regctlPath = Get-RegctlPath - $localRegistryPort = Get-AvailableLocalPort - $sshTunnel = Start-RegistryTunnel -LocalPort $localRegistryPort - $registryImage = "127.0.0.1:${localRegistryPort}/caiwuxitong/app:$imageTag" $serverRegistryImage = "127.0.0.1:5000/caiwuxitong/app:$imageTag" $beforeBytesOutput = @(& ssh @sshOptions $sshTarget "du -sb /var/lib/docker-registry 2>/dev/null | cut -f1") $beforeBytes = [long](($beforeBytesOutput -join '').Trim()) $uploadTimer = [Diagnostics.Stopwatch]::StartNew() $uploadSucceeded = $false - for ($uploadAttempt = 1; $uploadAttempt -le 2; $uploadAttempt++) { - if ($uploadAttempt -gt 1) { - if ($sshTunnel) { - if (-not $sshTunnel.HasExited) { - $sshTunnel.Kill() - $sshTunnel.WaitForExit() - } - $sshTunnel.Dispose() - } - + if (-not $SkipRegctl) { + try { + $regctlPath = Get-RegctlPath $localRegistryPort = Get-AvailableLocalPort $sshTunnel = Start-RegistryTunnel -LocalPort $localRegistryPort $registryImage = "127.0.0.1:${localRegistryPort}/caiwuxitong/app:$imageTag" + + for ($uploadAttempt = 1; $uploadAttempt -le 2; $uploadAttempt++) { + if ($uploadAttempt -gt 1) { + if ($sshTunnel) { + if (-not $sshTunnel.HasExited) { + $sshTunnel.Kill() + $sshTunnel.WaitForExit() + } + $sshTunnel.Dispose() + } + + $localRegistryPort = Get-AvailableLocalPort + $sshTunnel = Start-RegistryTunnel -LocalPort $localRegistryPort + $registryImage = "127.0.0.1:${localRegistryPort}/caiwuxitong/app:$imageTag" + } + + if ($sshTunnel.HasExited) { + $uploadExitCode = 255 + } + else { + & $regctlPath ` + --host "reg=127.0.0.1:${localRegistryPort},tls=disabled" ` + image import $registryImage $localTar + $uploadExitCode = $LASTEXITCODE + } + + if ($uploadExitCode -eq 0) { + $uploadSucceeded = $true + break + } + + if ($uploadAttempt -eq 1) { + Write-Warning '增量镜像隧道在上传期间中断,正在重新建立隧道并重试一次。' + } + } + } + catch { + Write-Warning "regctl 无法运行(本机安全软件可能拦截了未签名程序):$($_.Exception.Message)" + } + } + else { + Write-Host '已指定 -SkipRegctl:跳过 regctl,直接用 docker 直传镜像。' + } + + if (-not $uploadSucceeded) { + if (-not $SkipRegctl) { + Write-Warning '改用 docker 直传方式上传镜像(与 regctl 增量导入等效,只是改为传输完整镜像包)。' } - if ($sshTunnel.HasExited) { - $uploadExitCode = 255 - } - else { - & $regctlPath ` - --host "reg=127.0.0.1:${localRegistryPort},tls=disabled" ` - image import $registryImage $localTar - $uploadExitCode = $LASTEXITCODE + if ($sshTunnel) { + if (-not $sshTunnel.HasExited) { + $sshTunnel.Kill() + $sshTunnel.WaitForExit() + } + $sshTunnel.Dispose() + $sshTunnel = $null } - if ($uploadExitCode -eq 0) { - $uploadSucceeded = $true - break + if (Test-Path -LiteralPath $localTar) { + Remove-Item -LiteralPath $localTar -Force } - if ($uploadAttempt -eq 1) { - Write-Warning '增量镜像隧道在上传期间中断,正在重新建立隧道并重试一次。' - } + Invoke-Native { docker save --output $localTar $fullImage } '导出应用镜像失败。' + Compress-Gzip -Source $localTar -Destination $localArchive + $archiveSizeMB = [math]::Round((Get-Item -LiteralPath $localArchive).Length / 1MB, 1) + Write-Host "docker 直传镜像包:$archiveSizeMB MiB" + Invoke-Native { scp @scpOptions @sshOptions $localArchive "${sshTarget}:$remoteArchive" } '上传镜像失败。' + Invoke-Native { + ssh @sshOptions $sshTarget "gunzip -cf '$remoteArchive' | docker load && docker tag '$fullImage' '$serverRegistryImage' && docker push '$serverRegistryImage'" + } '服务器加载镜像或推入本机 Registry 失败。' + $uploadSucceeded = $true } if (-not $uploadSucceeded) {