From 4338f63e3dbceeda6b9dfd26802e6f4cd7c753fd Mon Sep 17 00:00:00 2001 From: xuans Date: Tue, 15 Sep 2026 20:20:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8F=91=E5=B8=83=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E8=BF=BD=E6=BA=AF=E5=92=8C=E5=AE=9E=E6=9C=BA=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=94=81=E5=86=B2=E7=AA=81=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deployment/remote_acceptance_test.go | 19 +++++++++++ build/README.md | 34 ++++++++++++++----- build/build-release.ps1 | 31 +++++++++++++++++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/backend/internal/deployment/remote_acceptance_test.go b/backend/internal/deployment/remote_acceptance_test.go index 2ac58a0..2b2fc3f 100644 --- a/backend/internal/deployment/remote_acceptance_test.go +++ b/backend/internal/deployment/remote_acceptance_test.go @@ -122,6 +122,25 @@ func TestRemoteDeploymentIsolatedAcceptance(t *testing.T) { } } verify() + // 真实连接竞争同一文件锁时必须拒绝部署,也不得释放另一事务的锁。 + lockClient, err := factory.Connect(t.Context(), server) + if err != nil { + t.Fatal(err) + } + lockPath := server.PrivateKeyPath + ".scert-lock" + if err := lockClient.Lock(t.Context(), lockPath); err != nil { + _ = lockClient.Close() + t.Fatal(err) + } + lockedReport := service.Deploy(t.Context(), []Target{{Server: server}}, original) + lockExists, lockErr := lockClient.Exists(t.Context(), lockPath) + unlockErr := lockClient.Unlock(t.Context(), lockPath) + _ = lockClient.Close() + if lockedReport.Status != "failed" || !strings.Contains(lockedReport.Results[0].Message, "无法取得部署锁") || !lockExists || lockErr != nil || unlockErr != nil { + t.Fatalf("真实部署锁冲突未安全拒绝:%+v exists=%t check=%v unlock=%v", lockedReport, lockExists, lockErr, unlockErr) + } + t.Log("真实部署锁冲突已拒绝,其他事务的锁保持不变") + verify() for _, scenario := range []string{"validation_failure", "reload_failure", "validation_timeout"} { candidate := server ctx := t.Context() diff --git a/build/README.md b/build/README.md index eabe64a..8f10143 100644 --- a/build/README.md +++ b/build/README.md @@ -4,14 +4,20 @@ ```text build/release/ -├── manifest.json -├── windows-amd64/ -│ ├── scert.exe -│ ├── scert-admin.exe -│ └── ScertDesktop.exe -└── linux-amd64/ - ├── scert - └── scert-admin +├── Scert-20260915-V0.1.0/ +│ ├── manifest.json +│ ├── web-build/ +│ ├── windows-amd64/ +│ │ ├── scert.exe +│ │ ├── scert-admin.exe +│ │ └── ScertDesktop.exe +│ └── linux-amd64/ +│ ├── scert +│ ├── scert-admin +│ ├── web/ +│ └── SHA256SUMS +├── Scert-20260915-V0.1.0-linux-amd64.tar.gz +└── Scert-20260915-V0.1.0-windows-amd64.zip ``` ## 使用方法 @@ -42,6 +48,16 @@ powershell -ExecutionPolicy Bypass -File .\build\build-release.ps1 -OutputDirect 桌面版构建需要已安装 Wails v2、Go 和 Node.js。Wails 桌面版使用当前主机原生平台构建;Windows 主机不能直接完成 Linux GTK 桌面交叉构建,Linux 桌面产物需要在 Linux 主机上执行同一脚本。 -脚本不会删除输出目录中的其他文件,只会创建目录并覆盖本次目标平台对应的程序文件和校验清单。 +脚本仅创建新的发布目录。发布目录或对应归档已存在时,在构建前拒绝执行,不覆盖旧包。前端直接构建到本次发布目录中的 `web-build/`,不清空或覆盖已有 `frontend/dist/`。失败产生的目录保留供排查,再次构建请使用新的输出目录。 + +`manifest.json` 记录源码提交、已跟踪文件是否未提交、实际 Go 工具链、目标平台、是否请求桌面端以及文件和归档的 SHA256。`source_dirty=true` 的包仅用于开发,正式发布必须从已复审提交构建。未跟踪本地产物不计入源码脏状态,也不会自动打包。 + +仅构建本轮 Linux 服务候选包示例: + +```powershell +powershell -ExecutionPolicy Bypass -File .\build\build-release.ps1 -Target linux-amd64 -SkipDesktop -OutputDirectory .\build\release-candidate-20260915 +``` + +此命令不构建 Windows 或 Wails 桌面端,不代表跨平台发布验收。归档包含 `scert`、`scert-admin`、Web 静态资源、安装器、systemd 模板和部署说明。解压后先运行 `sha256sum -c SHA256SUMS`,再按部署说明操作。 构建通过不等于生产验收通过;真实 DNS、ACME、SSH、Windows Service、systemd、WebView2 和 HTTPS 仍需在授权环境复核。 diff --git a/build/build-release.ps1 b/build/build-release.ps1 index 1f29422..1dffc54 100644 --- a/build/build-release.ps1 +++ b/build/build-release.ps1 @@ -261,6 +261,28 @@ $releaseDirectory = Join-Path $output $releaseName if (Test-Path -LiteralPath $releaseDirectory) { throw "发布目录已存在,拒绝覆盖:$releaseDirectory" } +if (@($Target | Select-Object -Unique).Count -ne $Target.Count) { + throw "目标平台不能重复。" +} +foreach ($item in $Target) { + $suffix = if ($item.StartsWith("linux-")) { "tar.gz" } else { "zip" } + $archivePath = Join-Path $output "$releaseName-$item.$suffix" + if (Test-Path -LiteralPath $archivePath) { + throw "发布归档已存在,拒绝覆盖:$archivePath" + } +} +$sourceCommit = (& git -C $backend rev-parse HEAD) +if ($LASTEXITCODE -ne 0) { + throw "无法取得源码提交,不能生成可追溯发布包。" +} +$sourceChanges = @(& git -C $backend status --porcelain --untracked-files=no) +if ($LASTEXITCODE -ne 0) { + throw "无法检查源码工作区状态。" +} +$sourceDirty = $sourceChanges.Count -gt 0 +if ($sourceDirty) { + Write-Warning "存在未提交的已跟踪文件改动,清单将标记 source_dirty=true,不能作为正式发布包。" +} New-Item -ItemType Directory -Path $releaseDirectory | Out-Null $frontendDistribution = Join-Path $releaseDirectory "web-build" @@ -287,6 +309,10 @@ $hostDesktopTarget = Get-HostDesktopTarget Push-Location $backend try { + $goVersion = (& go version) + if ($LASTEXITCODE -ne 0) { + throw "无法取得实际构建使用的 Go 工具链版本。" + } foreach ($item in $Target) { $parts = $item.Split("-", 2) $goos = $parts[0] @@ -379,6 +405,11 @@ $manifest = [ordered]@{ product = "Scert" release_date = $ReleaseDate version = $Version + source_commit = $sourceCommit.Trim() + source_dirty = $sourceDirty + go_toolchain = $goVersion.Trim() + targets = @($Target) + desktop_requested = -not [bool]$SkipDesktop generated_at = [DateTimeOffset]::UtcNow.ToString("o") artifacts = $manifestArtifacts }