fix: 修复发布流水线 LFS 资产校验
This commit is contained in:
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
12
Makefile
12
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
|
||||
|
||||
@@ -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 或执行约束。
|
||||
|
||||
@@ -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 <print|sync|check|verify-tag|bump|macos-plist|asset-name>")
|
||||
return fmt.Errorf("用法: version <print|sync|check|verify-tag|bump|macos-plist|asset-name|release-assets-check>")
|
||||
}
|
||||
|
||||
69
versionctl/projectversion/release_assets.go
Normal file
69
versionctl/projectversion/release_assets.go
Normal file
@@ -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
|
||||
}
|
||||
58
versionctl/projectversion/release_assets_test.go
Normal file
58
versionctl/projectversion/release_assets_test.go
Normal file
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user