From 6181923d8d65efbdfd55f4ed6676bb71e07c1bcb Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 5 May 2026 09:57:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E6=B5=81=E6=B0=B4=E7=BA=BF=20LFS=20=E8=B5=84=E4=BA=A7=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 11 +++ Makefile | 12 ++-- openspec/specs/release-pipeline/spec.md | 31 +++++++++ versionctl/main.go | 7 +- versionctl/projectversion/release_assets.go | 69 +++++++++++++++++++ .../projectversion/release_assets_test.go | 58 ++++++++++++++++ 6 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 versionctl/projectversion/release_assets.go create mode 100644 versionctl/projectversion/release_assets_test.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9a809d..aa779d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + lfs: true - name: Setup Go uses: actions/setup-go@v6 @@ -44,6 +46,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + lfs: true - name: Setup Go uses: actions/setup-go@v6 @@ -73,6 +77,7 @@ jobs: command -v pkg-config pkg-config --modversion ayatana-appindicator3-0.1 pkg-config --modversion gtk+-3.0 + make release-assets-check - name: Build Linux release assets run: make release-assets-linux @@ -92,6 +97,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + lfs: true - name: Setup Go uses: actions/setup-go@v6 @@ -134,6 +141,7 @@ jobs: command -v powershell powershell -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' fi + make release-assets-check - name: Build Windows release assets shell: msys2 {0} @@ -154,6 +162,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + lfs: true - name: Setup Go uses: actions/setup-go@v6 @@ -176,6 +186,7 @@ jobs: command -v ditto xcrun --find lipo xcrun --find vtool + make release-assets-check - name: Build macOS release assets run: make release-assets-macos diff --git a/Makefile b/Makefile index e9ed8eb..7c63aa0 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ server-run server-build server-lint server-test server-clean \ desktop-build-mac desktop-build-win desktop-build-linux \ desktop-lint desktop-test desktop-clean \ - release-assets-linux release-assets-windows release-assets-macos \ + release-assets-check release-assets-linux release-assets-windows release-assets-macos \ _backend-lint _backend-test _backend-clean _backend-build \ _versionctl-lint _versionctl-test \ _frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \ @@ -183,14 +183,18 @@ endif # 发布资产 # ============================================ -release-assets-linux: version-check desktop-build-linux +release-assets-check: + go run ./versionctl release-assets-check + @printf 'Release assets check passed\n' + +release-assets-linux: version-check release-assets-check desktop-build-linux rm -rf "$(RELEASE_DIR)" mkdir -p "$(RELEASE_DIR)" cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-linux-amd64 ./cmd/server tar -C build -czf "$(RELEASE_DIR)/$(SERVER_LINUX_ASSET)" nex-server-linux-amd64 tar -C build -czf "$(RELEASE_DIR)/$(DESKTOP_LINUX_ASSET)" nex-linux-amd64 -release-assets-windows: version-check desktop-build-win +release-assets-windows: version-check release-assets-check desktop-build-win ifeq ($(OS),Windows_NT) powershell -NoProfile -Command "Remove-Item -LiteralPath '$(RELEASE_DIR)' -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Path '$(RELEASE_DIR)' -Force | Out-Null" cd backend && set "CGO_ENABLED=1"&& set "GOOS=windows"&& set "GOARCH=amd64"&& go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-server-win-amd64.exe ./cmd/server @@ -201,7 +205,7 @@ else @exit 1 endif -release-assets-macos: version-check desktop-build-mac +release-assets-macos: version-check release-assets-check desktop-build-mac rm -rf "$(RELEASE_DIR)" mkdir -p "$(RELEASE_DIR)" cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-darwin-amd64 ./cmd/server diff --git a/openspec/specs/release-pipeline/spec.md b/openspec/specs/release-pipeline/spec.md index eeb384e..212cf4c 100644 --- a/openspec/specs/release-pipeline/spec.md +++ b/openspec/specs/release-pipeline/spec.md @@ -84,6 +84,37 @@ - **THEN** 发布流水线 SHALL 在正式构建前失败 - **AND** 系统 SHALL 在日志中标识缺失的工具链名称 +### Requirement: 发布流水线 LFS 资产拉取 + +发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验或平台构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。 + +#### Scenario: 发布 job 获取真实 LFS 图标资产 + +- **WHEN** 发布流水线执行 `prepare`、`build-linux`、`build-windows` 或 `build-macos` job 的 checkout 步骤 +- **THEN** checkout 步骤 SHALL 拉取 Git LFS 文件 +- **AND** `assets/icon.ico`、`assets/icon.icns`、`assets/icon.png` 和 `frontend/public/icon.png` SHALL 在后续步骤中表现为真实图标文件而非 LFS pointer 文本 + +### Requirement: 发布资产图标预检 + +发布流水线 SHALL 在正式执行各平台发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。 + +#### Scenario: 图标资产为 LFS pointer + +- **WHEN** 发布资产预检发现关键图标文件内容为 Git LFS pointer 文本 +- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 +- **AND** 系统 SHALL 在日志中标识对应图标文件需要拉取 Git LFS 真实内容 + +#### Scenario: 图标资产格式无效 + +- **WHEN** 发布资产预检发现关键图标文件不是对应格式的有效资源 +- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 +- **AND** 系统 SHALL 在日志中标识格式无效的图标文件路径 + +#### Scenario: 图标资产预检通过 + +- **WHEN** `assets/icon.ico`、`assets/icon.icns`、`assets/icon.png` 和 `frontend/public/icon.png` 均为真实且格式可用的图标资产 +- **THEN** 发布流水线 SHALL 继续执行对应平台的 `make release-assets-*` 构建 + ### Requirement: 发布流水线运行时兼容性 系统 SHALL 保持与 GitHub-hosted runner 当前受支持的 workflow runtime 约束兼容,避免发布流程依赖已声明弃用的 runtime 或执行约束。 diff --git a/versionctl/main.go b/versionctl/main.go index d362270..d40fb26 100644 --- a/versionctl/main.go +++ b/versionctl/main.go @@ -53,6 +53,11 @@ func run(args []string) error { return printMacOSPlist(root, args[1]) case "asset-name": return printAssetName(root, args[1:]) + case "release-assets-check": + if len(args) != 1 { + return fmt.Errorf("release-assets-check 不需要额外参数") + } + return projectversion.CheckReleaseAssets(root) default: return usageError() } @@ -147,5 +152,5 @@ func mustGetwd() string { } func usageError() error { - return fmt.Errorf("用法: version ") + return fmt.Errorf("用法: version ") } diff --git a/versionctl/projectversion/release_assets.go b/versionctl/projectversion/release_assets.go new file mode 100644 index 0000000..4719d5b --- /dev/null +++ b/versionctl/projectversion/release_assets.go @@ -0,0 +1,69 @@ +package projectversion + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" +) + +var releaseAssetChecks = []releaseAssetCheck{ + { + path: "assets/icon.ico", + description: "Windows ICO 图标", + magic: []byte{0x00, 0x00, 0x01, 0x00}, + }, + { + path: "assets/icon.icns", + description: "macOS ICNS 图标", + magic: []byte("icns"), + }, + { + path: "assets/icon.png", + description: "PNG 图标", + magic: []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, + }, + { + path: "frontend/public/icon.png", + description: "前端 PNG 图标", + magic: []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, + }, +} + +var gitLFSPointerPrefix = []byte("version https://git-lfs.github.com/spec/v1") + +type releaseAssetCheck struct { + path string + description string + magic []byte +} + +func CheckReleaseAssets(root string) error { + var errs []error + + for _, check := range releaseAssetChecks { + if err := checkReleaseAsset(root, check); err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +func checkReleaseAsset(root string, check releaseAssetCheck) error { + content, err := os.ReadFile(filepath.Join(root, check.path)) + if err != nil { + return fmt.Errorf("%s 不可读取: %w", check.path, err) + } + + if bytes.HasPrefix(content, gitLFSPointerPrefix) { + return fmt.Errorf("%s 是 Git LFS pointer,请先拉取 Git LFS 真实内容", check.path) + } + + if !bytes.HasPrefix(content, check.magic) { + return fmt.Errorf("%s 不是有效的%s", check.path, check.description) + } + + return nil +} diff --git a/versionctl/projectversion/release_assets_test.go b/versionctl/projectversion/release_assets_test.go new file mode 100644 index 0000000..86b9bb4 --- /dev/null +++ b/versionctl/projectversion/release_assets_test.go @@ -0,0 +1,58 @@ +package projectversion + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCheckReleaseAssets(t *testing.T) { + t.Run("valid assets", func(t *testing.T) { + root := setupReleaseAssetRoot(t) + + require.NoError(t, CheckReleaseAssets(root)) + }) + + t.Run("lfs pointer", func(t *testing.T) { + root := setupReleaseAssetRoot(t) + writeReleaseAsset(t, root, "assets/icon.ico", []byte("version https://git-lfs.github.com/spec/v1\noid sha256:abc\nsize 123\n")) + + err := CheckReleaseAssets(root) + + require.Error(t, err) + assert.Contains(t, err.Error(), "assets/icon.ico 是 Git LFS pointer") + }) + + t.Run("invalid format", func(t *testing.T) { + root := setupReleaseAssetRoot(t) + writeReleaseAsset(t, root, "frontend/public/icon.png", []byte("not a png")) + + err := CheckReleaseAssets(root) + + require.Error(t, err) + assert.Contains(t, err.Error(), "frontend/public/icon.png 不是有效的前端 PNG 图标") + }) +} + +func setupReleaseAssetRoot(t *testing.T) string { + t.Helper() + + root := t.TempDir() + writeReleaseAsset(t, root, "assets/icon.ico", []byte{0x00, 0x00, 0x01, 0x00, 0x01}) + writeReleaseAsset(t, root, "assets/icon.icns", []byte("icnsdata")) + writeReleaseAsset(t, root, "assets/icon.png", []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00}) + writeReleaseAsset(t, root, "frontend/public/icon.png", []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00}) + + return root +} + +func writeReleaseAsset(t *testing.T, root, relPath string, content []byte) { + t.Helper() + + fullPath := filepath.Join(root, relPath) + require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755)) + require.NoError(t, os.WriteFile(fullPath, content, 0o600)) +}