1
0

11 Commits

Author SHA1 Message Date
407d008e19 chore: 版本升迁 v0.1.7 2026-05-05 22:00:04 +08:00
a2751eab31 feat: 原生 Git hooks 方案,增强版本升迁工作流 2026-05-05 21:58:30 +08:00
5655fc5560 chore: 移除 Windows arm64 构建与发布支持
Windows ARM64 使用场景极少,windows-11-arm runner 上 MSYS2
CLANGARM64 交叉编译不稳定,CGO 编译问题难以排查,维护成本
远超收益。移除 arm64 的 CI 矩阵条目、Makefile Windows 变量、
versionctl 资产白名单、README 文档和规范中的相关需求。
Linux 和 macOS arm64 不受影响。
2026-05-05 20:20:04 +08:00
49b47a1ae0 chore: 版本升迁 v0.1.6 2026-05-05 19:31:21 +08:00
bcf82d42bc fix: 修复 Windows arm64 可执行文件图标嵌入构建失败
MSYS2 CLANGARM64 环境下使用 llvm-windres,需使用 LLVM target triple
(aarch64-w64-mingw32) 而非 GNU BFD 格式 (pe-aarch64)。新增双格式变量
并增强 windres 检测逻辑,通过 --version 输出区分 GNU/LLVM 工具。
2026-05-05 19:30:22 +08:00
394025c8ea chore: 版本升迁 v0.1.5 2026-05-05 18:59:09 +08:00
34bd749741 fix: 修复发布流水线 Windows arm64 CGO 工具链和 macOS 磁盘空间问题
- Windows arm64: 在 workflow matrix 中设置 CC=clang/CXX=clang++ 并注入构建环境
- macOS: 在 Makefile 关键节点清理中间产物和临时目录释放磁盘空间
- 预检步骤改用 $CC/$CXX 替代硬编码编译器名称,与 matrix 声明保持一致
- 同步新增 release-pipeline spec 需求: Windows arm64 CGO 编译器指定
2026-05-05 18:58:50 +08:00
290f299e22 chore: 版本升迁 v0.1.4 2026-05-05 13:15:45 +08:00
859dec8ada fix: 修复 Windows arm64 发布构建 CI 失败
将 Windows arm64 构建从 x86_64 runner 上的交叉编译改为使用 windows-11-arm
原生 ARM64 runner,消除 CLANGARM64 环境在 x86_64 上的 Exec format error。
同时去掉 Linux/Windows 构建步骤中冗余的 TARGET_ARCH 显式传参,统一依赖
Makefile 中 go env GOARCH 自动检测。
2026-05-05 13:15:00 +08:00
993c0a72d6 chore: 版本升迁 v0.1.3 2026-05-05 12:38:12 +08:00
c9c3a84b33 feat: 扩展发布打包支持多组件多架构多格式产物
- 新增 web 组件独立发布为 nex-web_<version>.tar.gz
- server 新增 arm64 架构、macOS universal、Windows arm64 产物
- desktop 新增 arm64 架构支持(Linux/Windows)
- Linux desktop 新增 AppImage、deb、rpm 安装包格式
- macOS desktop 新增 unsigned DMG 安装包
- 统一发布资产命名为 {component}_{version}_{platform}_{arch}.{ext}
- 新增 SHA256SUMS 校验和清单覆盖全部发布资产
- versionctl 新增 asset-name CLI 支持按参数生成资产文件名
- Makefile release target 重构为组件/平台/架构参数化
- GitHub Actions release workflow 扩展多组件多架构构建矩阵
- 同步更新 openspec 主规范(desktop-app/release-pipeline/workspace-command-flows)
2026-05-05 12:36:33 +08:00
24 changed files with 1286 additions and 228 deletions

View File

@@ -37,8 +37,8 @@ jobs:
go run ./versionctl verify-tag "${GITHUB_REF_NAME}" go run ./versionctl verify-tag "${GITHUB_REF_NAME}"
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT" printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
build-linux: build-web:
name: Build Linux Assets name: Build Web Asset
needs: prepare needs: prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -60,14 +60,65 @@ jobs:
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install Linux desktop build dependencies - name: Preflight web release toolchain
run: |
set -euo pipefail
command -v go
go version
command -v bun
bun --version
make release-assets-check
- name: Build web release asset
run: make release-assets-web
- name: Upload web release asset
uses: actions/upload-artifact@v4
with:
name: release-web
path: build/release/*
if-no-files-found: error
build-linux:
name: Build Linux ${{ matrix.arch }} Assets
needs: prepare
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
with:
lfs: true
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.work
cache-dependency-path: |
backend/go.sum
versionctl/go.sum
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install Linux desktop and package dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libayatana-appindicator3-dev libgtk-3-dev sudo apt-get install -y curl file libayatana-appindicator3-dev libgtk-3-dev rpm
- name: Preflight Linux release toolchain - name: Preflight Linux release toolchain
run: | run: |
set -euo pipefail set -euo pipefail
printf 'runner arch: %s\n' "$(uname -m)"
command -v go command -v go
go version go version
command -v bun command -v bun
@@ -77,6 +128,11 @@ jobs:
command -v pkg-config command -v pkg-config
pkg-config --modversion ayatana-appindicator3-0.1 pkg-config --modversion ayatana-appindicator3-0.1
pkg-config --modversion gtk+-3.0 pkg-config --modversion gtk+-3.0
command -v curl
command -v dpkg-deb
dpkg-deb --version
command -v rpmbuild
rpmbuild --version
make release-assets-check make release-assets-check
- name: Build Linux release assets - name: Build Linux release assets
@@ -85,13 +141,26 @@ jobs:
- name: Upload Linux release assets - name: Upload Linux release assets
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: release-linux name: release-linux-${{ matrix.arch }}
path: build/release/* path: build/release/*
if-no-files-found: error
build-windows: build-windows:
name: Build Windows Assets name: Build Windows ${{ matrix.arch }} Assets
needs: prepare needs: prepare
runs-on: windows-latest runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: windows-latest
msystem: MINGW64
cc: gcc
cxx: g++
packages: >-
make
mingw-w64-x86_64-gcc
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -114,15 +183,16 @@ jobs:
- name: Setup MSYS2 toolchain - name: Setup MSYS2 toolchain
uses: msys2/setup-msys2@v2 uses: msys2/setup-msys2@v2
with: with:
msystem: MINGW64 msystem: ${{ matrix.msystem }}
path-type: inherit path-type: inherit
update: true update: true
install: >- install: ${{ matrix.packages }}
make
mingw-w64-x86_64-gcc
- name: Preflight Windows release toolchain - name: Preflight Windows release toolchain
shell: msys2 {0} shell: msys2 {0}
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
run: | run: |
set -euo pipefail set -euo pipefail
command -v go command -v go
@@ -131,8 +201,10 @@ jobs:
bun --version bun --version
command -v make command -v make
make --version make --version
command -v gcc command -v "$CC"
gcc --version "$CC" --version
command -v "$CXX"
"$CXX" --version
command -v windres command -v windres
windres --version windres --version
if command -v powershell.exe >/dev/null 2>&1; then if command -v powershell.exe >/dev/null 2>&1; then
@@ -145,18 +217,22 @@ jobs:
- name: Build Windows release assets - name: Build Windows release assets
shell: msys2 {0} shell: msys2 {0}
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
run: make release-assets-windows run: make release-assets-windows
- name: Upload Windows release assets - name: Upload Windows release assets
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: release-windows name: release-windows-${{ matrix.arch }}
path: build/release/* path: build/release/*
if-no-files-found: error
build-macos: build-macos:
name: Build macOS Assets name: Build macOS Assets
needs: prepare needs: prepare
runs-on: macos-latest runs-on: macos-15
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -179,11 +255,13 @@ jobs:
- name: Preflight macOS release toolchain - name: Preflight macOS release toolchain
run: | run: |
set -euo pipefail set -euo pipefail
printf 'runner arch: %s\n' "$(uname -m)"
command -v go command -v go
go version go version
command -v bun command -v bun
bun --version bun --version
command -v ditto command -v ditto
command -v hdiutil
xcrun --find lipo xcrun --find lipo
xcrun --find vtool xcrun --find vtool
make release-assets-check make release-assets-check
@@ -196,14 +274,18 @@ jobs:
with: with:
name: release-macos name: release-macos
path: build/release/* path: build/release/*
if-no-files-found: error
draft-release: draft-release:
name: Create Draft Release name: Create Draft Release
needs: [prepare, build-linux, build-windows, build-macos] needs: [prepare, build-web, build-linux, build-windows, build-macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
steps: steps:
- name: Checkout
uses: actions/checkout@v5
- name: Download release assets - name: Download release assets
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -211,6 +293,9 @@ jobs:
merge-multiple: true merge-multiple: true
path: dist path: dist
- name: Generate checksums
run: make release-assets-checksums RELEASE_DIR=dist
- name: Publish draft release - name: Publish draft release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:

394
Makefile
View File

@@ -1,15 +1,21 @@
.PHONY: \ .PHONY: \
lint test clean \ lint test clean hooks-install hooks-check hooks-test \
version-sync version-check version-bump \ version-sync version-check version-bump \
server-run server-build server-lint server-test server-clean \ server-run server-build server-lint server-test server-clean \
desktop-build-mac desktop-build-win desktop-build-linux \ desktop-build-mac desktop-build-win desktop-build-linux \
desktop-lint desktop-test desktop-clean \ desktop-lint desktop-test desktop-clean \
release-assets-check release-assets-linux release-assets-windows release-assets-macos \ release-assets-check release-assets-web release-assets-linux release-assets-windows release-assets-macos release-assets-checksums \
release-assets-server-linux release-assets-server-windows release-assets-server-macos \
release-assets-desktop-linux release-assets-desktop-windows release-assets-desktop-macos \
_backend-lint _backend-test _backend-clean _backend-build \ _backend-lint _backend-test _backend-clean _backend-build \
_versionctl-lint _versionctl-test \ _versionctl-lint _versionctl-test \
_frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \ _frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \
_hooks-pre-commit _check-clean-worktree \
_desktop-test _desktop-clean _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource \ _desktop-test _desktop-clean _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource \
_server-run-backend _server-run-frontend _server-run-backend _server-run-frontend \
_check-linux-target-arch _check-windows-target-arch _ensure-appimagetool \
_package-linux-tar _package-linux-appimage _package-linux-deb _package-linux-rpm \
_package-macos-zip _package-macos-dmg
# Delay shell lookups until a target needs them, then cache the result for this make run. # Delay shell lookups until a target needs them, then cache the result for this make run.
lazy_shell = $(or $($(1)),$(eval $(1) := $(shell $(2)))$($(1))) lazy_shell = $(or $($(1)),$(eval $(1) := $(shell $(2)))$($(1)))
@@ -17,16 +23,32 @@ lazy_shell = $(or $($(1)),$(eval $(1) := $(shell $(2)))$($(1)))
VERSION = $(call lazy_shell,_VERSION,go run ./versionctl print) VERSION = $(call lazy_shell,_VERSION,go run ./versionctl print)
GIT_COMMIT ?= $(call lazy_shell,_GIT_COMMIT,git rev-parse --short HEAD 2>/dev/null || printf 'unknown') GIT_COMMIT ?= $(call lazy_shell,_GIT_COMMIT,git rev-parse --short HEAD 2>/dev/null || printf 'unknown')
BUILD_TIME ?= $(call lazy_shell,_BUILD_TIME,date -u +"%Y-%m-%dT%H:%M:%SZ") BUILD_TIME ?= $(call lazy_shell,_BUILD_TIME,date -u +"%Y-%m-%dT%H:%M:%SZ")
TARGET_ARCH ?= $(call lazy_shell,_TARGET_ARCH,go env GOARCH)
GO_LDFLAGS = -X nex/backend/pkg/buildinfo.version=$(VERSION) -X nex/backend/pkg/buildinfo.commit=$(GIT_COMMIT) -X nex/backend/pkg/buildinfo.buildTime=$(BUILD_TIME) GO_LDFLAGS = -X nex/backend/pkg/buildinfo.version=$(VERSION) -X nex/backend/pkg/buildinfo.commit=$(GIT_COMMIT) -X nex/backend/pkg/buildinfo.buildTime=$(BUILD_TIME)
GO_LDFLAGS_WIN = $(GO_LDFLAGS) -H=windowsgui GO_LDFLAGS_WIN = $(GO_LDFLAGS) -H=windowsgui
RELEASE_DIR := build/release RELEASE_DIR ?= build/release
SERVER_LINUX_ASSET = $(call lazy_shell,_SERVER_LINUX_ASSET,go run ./versionctl asset-name server linux amd64) LINUX_DESKTOP_BINARY = build/nex-linux-$(TARGET_ARCH)
SERVER_WINDOWS_ASSET = $(call lazy_shell,_SERVER_WINDOWS_ASSET,go run ./versionctl asset-name server windows amd64) WINDOWS_DESKTOP_BINARY = build/nex-win-$(TARGET_ARCH).exe
SERVER_DARWIN_AMD64_ASSET = $(call lazy_shell,_SERVER_DARWIN_AMD64_ASSET,go run ./versionctl asset-name server darwin amd64) WINDOWS_SERVER_BINARY = build/nex-server-windows-$(TARGET_ARCH).exe
SERVER_DARWIN_ARM64_ASSET = $(call lazy_shell,_SERVER_DARWIN_ARM64_ASSET,go run ./versionctl asset-name server darwin arm64) WINDRES ?= windres
DESKTOP_LINUX_ASSET = $(call lazy_shell,_DESKTOP_LINUX_ASSET,go run ./versionctl asset-name desktop linux)
DESKTOP_WINDOWS_ASSET = $(call lazy_shell,_DESKTOP_WINDOWS_ASSET,go run ./versionctl asset-name desktop windows) ifeq ($(TARGET_ARCH),arm64)
DESKTOP_MACOS_ASSET = $(call lazy_shell,_DESKTOP_MACOS_ASSET,go run ./versionctl asset-name desktop macos) APPIMAGE_ARCH := aarch64
DEB_ARCH := arm64
RPM_ARCH := aarch64
else
APPIMAGE_ARCH := x86_64
DEB_ARCH := amd64
RPM_ARCH := x86_64
endif
WINDOWS_WINDRES_FORMAT_BFD := pe-x86-64
WINDOWS_WINDRES_FORMAT_LLVM := x86_64-w64-mingw32
WINDOWS_RESOURCE := rsrc_windows_amd64.syso
APPIMAGETOOL_PATH := build/tools/appimagetool-$(APPIMAGE_ARCH).AppImage
APPIMAGETOOL_URL ?= https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(APPIMAGE_ARCH).AppImage
APPIMAGETOOL ?= $(APPIMAGETOOL_PATH)
# ============================================ # ============================================
# 全局命令 # 全局命令
@@ -41,6 +63,82 @@ test: _backend-test _frontend-test _desktop-test _versionctl-test
clean: _backend-clean _frontend-clean _desktop-clean clean: _backend-clean _frontend-clean _desktop-clean
@printf 'Clean complete\n' @printf 'Clean complete\n'
# ============================================
# Git hooks
# ============================================
hooks-install:
@hooks_dir=$$(git rev-parse --git-path hooks); \
mkdir -p "$$hooks_dir"; \
cp scripts/git-hooks/pre-commit "$$hooks_dir/pre-commit"; \
cp scripts/git-hooks/commit-msg "$$hooks_dir/commit-msg"; \
chmod +x "$$hooks_dir/pre-commit" "$$hooks_dir/commit-msg"; \
printf 'Installed Git hooks to %s\n' "$$hooks_dir"
hooks-check:
@hooks_dir=$$(git rev-parse --git-path hooks); \
status=0; \
for hook in pre-commit commit-msg; do \
if [ -x "$$hooks_dir/$$hook" ]; then \
printf 'OK: %s\n' "$$hook"; \
else \
printf 'MISSING: %s (%s/%s)\n' "$$hook" "$$hooks_dir" "$$hook"; \
status=1; \
fi; \
done; \
exit $$status
hooks-test:
@scripts/git-hooks/test-hooks.sh
_hooks-pre-commit:
@set -e; \
staged_files=$$(git diff --cached --name-only --diff-filter=ACM); \
if [ -z "$$staged_files" ]; then \
printf 'No staged files to check\n'; \
exit 0; \
fi; \
printf '%s\n' "$$staged_files" | while IFS= read -r file; do \
[ -n "$$file" ] || continue; \
case "$$file" in scripts/git-hooks/*) continue ;; esac; \
if git show ":$$file" 2>/dev/null | grep -Eq '^(<<<<<<<|=======|>>>>>>>)'; then \
printf 'Found conflict markers in staged file: %s\n' "$$file" >&2; \
printf 'Resolve conflict markers before committing.\n' >&2; \
exit 1; \
fi; \
size=$$(git cat-file -s ":$$file" 2>/dev/null || printf '0'); \
if [ "$$size" -gt 512000 ] 2>/dev/null; then \
if git show ":$$file" 2>/dev/null | LC_ALL=C grep -Iq .; then \
printf 'Warning: large staged text file (%s bytes): %s\n' "$$size" "$$file" >&2; \
fi; \
fi; \
case "$$file" in \
backend/*.go) \
rel=$${file#backend/}; \
printf 'Go lint: backend/%s\n' "$$rel"; \
(cd backend && go tool golangci-lint run "$$rel"); \
;; \
versionctl/*.go) \
rel=$${file#versionctl/}; \
printf 'Go lint: versionctl/%s\n' "$$rel"; \
(cd versionctl && go tool golangci-lint run "$$rel"); \
;; \
frontend/*.ts|frontend/*.tsx) \
rel=$${file#frontend/}; \
printf 'Frontend lint: frontend/%s\n' "$$rel"; \
(cd frontend && bunx eslint "$$rel"); \
printf 'Frontend format: frontend/%s\n' "$$rel"; \
(cd frontend && bunx prettier --check "$$rel"); \
;; \
frontend/*.scss) \
rel=$${file#frontend/}; \
printf 'Frontend format: frontend/%s\n' "$$rel"; \
(cd frontend && bunx prettier --check "$$rel"); \
;; \
esac; \
done; \
printf 'Pre-commit checks passed\n'
# ============================================ # ============================================
# 版本管理 # 版本管理
# ============================================ # ============================================
@@ -52,13 +150,21 @@ version-check:
go run ./versionctl check go run ./versionctl check
version-bump: BUMP ?= patch version-bump: BUMP ?= patch
version-bump: version-bump: lint test _check-clean-worktree
$(eval _BUMP_ARG := $(if $(SET_VERSION),$(SET_VERSION),$(BUMP))) @set -e; \
$(eval _NEW_VERSION := $(shell go run ./versionctl bump $(_BUMP_ARG))) bump_arg="$(if $(SET_VERSION),$(SET_VERSION),$(BUMP))"; \
git add VERSION frontend/ new_version=$$(go run ./versionctl bump "$$bump_arg"); \
git commit -m "chore: 版本升迁 v$(_NEW_VERSION)" git add VERSION frontend/; \
git tag "v$(_NEW_VERSION)" git commit -m "chore: 版本升迁 v$$new_version"; \
@printf '版本升迁完成: v%s\n' "$(_NEW_VERSION)" git tag "v$$new_version"; \
printf '版本升迁完成: v%s\n' "$$new_version"
_check-clean-worktree:
@if [ -n "$$(git status --porcelain)" ]; then \
printf '工作区不干净,请先提交或清理改动后再执行版本升迁。\n' >&2; \
git status --short; \
exit 1; \
fi
# ============================================ # ============================================
# Server 模式 # Server 模式
@@ -94,13 +200,17 @@ desktop-build-mac: version-check _desktop-prepare-frontend _desktop-prepare-embe
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-arm64 ./cmd/desktop cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-arm64 ./cmd/desktop
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-amd64 ./cmd/desktop cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-mac-amd64 ./cmd/desktop
lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal
lipo -info build/nex-mac-universal | grep -q 'x86_64 arm64'
rm -f build/nex-mac-arm64 build/nex-mac-amd64
@printf 'Packaging macOS app bundle...\n' @printf 'Packaging macOS app bundle...\n'
rm -rf build/Nex.app
mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources
cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex
@if [ -f assets/icon.icns ]; then \ @if [ -f assets/icon.icns ]; then \
cp assets/icon.icns build/Nex.app/Contents/Resources/; \ cp assets/icon.icns build/Nex.app/Contents/Resources/; \
else \ else \
printf 'Missing assets/icon.icns\n'; \ printf 'Missing assets/icon.icns\n'; \
exit 1; \
fi fi
@MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \ @MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \
if [ -z "$$MIN_MACOS_VERSION" ]; then \ if [ -z "$$MIN_MACOS_VERSION" ]; then \
@@ -111,20 +221,16 @@ desktop-build-mac: version-check _desktop-prepare-frontend _desktop-prepare-embe
chmod +x build/Nex.app/Contents/MacOS/nex chmod +x build/Nex.app/Contents/MacOS/nex
@printf 'macOS desktop build complete\n' @printf 'macOS desktop build complete\n'
desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource _check-windows-target-arch
@printf 'Building Windows desktop...\n' @printf 'Building Windows desktop $(TARGET_ARCH)...\n'
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "New-Item -ItemType Directory -Path 'build' -Force | Out-Null"
cd backend && set "CGO_ENABLED=1"&& set "GOOS=windows"&& set "GOARCH=amd64"&& go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-win-amd64.exe ./cmd/desktop
else
mkdir -p build mkdir -p build
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-win-amd64.exe ./cmd/desktop cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../$(WINDOWS_DESKTOP_BINARY) ./cmd/desktop
endif
@printf 'Windows desktop build complete\n' @printf 'Windows desktop build complete\n'
desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _check-linux-target-arch
@printf 'Building Linux desktop...\n' @printf 'Building Linux desktop $(TARGET_ARCH)...\n'
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-linux-amd64 ./cmd/desktop mkdir -p build
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS)" -o ../$(LINUX_DESKTOP_BINARY) ./cmd/desktop
@printf 'Linux desktop build complete\n' @printf 'Linux desktop build complete\n'
desktop-lint: _backend-lint _frontend-check desktop-lint: _backend-lint _frontend-check
@@ -144,40 +250,28 @@ _desktop-clean:
_desktop-prepare-frontend: _frontend-install _desktop-prepare-frontend: _frontend-install
@printf 'Preparing frontend for desktop...\n' @printf 'Preparing frontend for desktop...\n'
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "Copy-Item -LiteralPath 'frontend/.env.desktop' -Destination 'frontend/.env.production.local' -Force"
cd frontend && bun run build
powershell -NoProfile -Command "Remove-Item -LiteralPath 'frontend/.env.production.local' -Force -ErrorAction SilentlyContinue"
else
cd frontend && cp .env.desktop .env.production.local cd frontend && cp .env.desktop .env.production.local
cd frontend && bun run build cd frontend && bun run build
rm -f frontend/.env.production.local rm -f frontend/.env.production.local
endif
_desktop-prepare-embedfs: _desktop-prepare-embedfs:
@printf 'Preparing embedded filesystem...\n' @printf 'Preparing embedded filesystem...\n'
ifeq ($(OS),Windows_NT)
powershell -NoProfile -Command "Remove-Item -LiteralPath 'embedfs/assets' -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item -LiteralPath 'embedfs/frontend-dist' -Recurse -Force -ErrorAction SilentlyContinue; Copy-Item -LiteralPath 'assets' -Destination 'embedfs/assets' -Recurse; Copy-Item -LiteralPath 'frontend/dist' -Destination 'embedfs/frontend-dist' -Recurse"
else
rm -rf embedfs/assets embedfs/frontend-dist rm -rf embedfs/assets embedfs/frontend-dist
cp -r assets embedfs/assets cp -r assets embedfs/assets
cp -r frontend/dist embedfs/frontend-dist cp -r frontend/dist embedfs/frontend-dist
endif
_desktop-prepare-windows-resource: _desktop-prepare-windows-resource: _check-windows-target-arch
@printf 'Preparing Windows executable icon...\n' @printf 'Preparing Windows $(TARGET_ARCH) executable icon...\n'
ifeq ($(OS),Windows_NT) @WINDRES_CMD="$(WINDRES)"; \
cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso WINDRES_FMT="$(WINDOWS_WINDRES_FORMAT_BFD)"; \
else if command -v llvm-windres >/dev/null 2>&1; then \
@if command -v x86_64-w64-mingw32-windres >/dev/null 2>&1; then \ WINDRES_CMD=llvm-windres; \
cd backend/cmd/desktop && x86_64-w64-mingw32-windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso; \ WINDRES_FMT="$(WINDOWS_WINDRES_FORMAT_LLVM)"; \
elif command -v windres >/dev/null 2>&1; then \ elif "$$WINDRES_CMD" --version 2>&1 | grep -qi LLVM; then \
cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso; \ WINDRES_FMT="$(WINDOWS_WINDRES_FORMAT_LLVM)"; \
else \ fi; \
printf 'Missing windres for Windows icon resource generation\n'; \ command -v "$$WINDRES_CMD" >/dev/null 2>&1 || { printf 'Missing windres tool: %s\n' "$$WINDRES_CMD"; exit 1; }; \
exit 1; \ cd backend/cmd/desktop && "$$WINDRES_CMD" -O coff -F "$$WINDRES_FMT" -i icon_windows.rc -o $(WINDOWS_RESOURCE)
fi
endif
# ============================================ # ============================================
# 发布资产 # 发布资产
@@ -187,32 +281,184 @@ release-assets-check:
go run ./versionctl release-assets-check go run ./versionctl release-assets-check
@printf 'Release assets check passed\n' @printf 'Release assets check passed\n'
release-assets-linux: version-check release-assets-check desktop-build-linux release-assets-web: version-check release-assets-check _frontend-build
rm -rf "$(RELEASE_DIR)" rm -rf "$(RELEASE_DIR)"
mkdir -p "$(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 asset=$$(go run ./versionctl asset-name web tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$(SERVER_LINUX_ASSET)" nex-server-linux-amd64 tar -C frontend -czf "$(RELEASE_DIR)/$$asset" dist
tar -C build -czf "$(RELEASE_DIR)/$(DESKTOP_LINUX_ASSET)" nex-linux-amd64
release-assets-windows: version-check release-assets-check desktop-build-win release-assets-linux: version-check release-assets-check _check-linux-target-arch
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
powershell -NoProfile -Command "Compress-Archive -LiteralPath 'build/nex-server-win-amd64.exe' -DestinationPath '$(RELEASE_DIR)/$(SERVER_WINDOWS_ASSET)' -Force"
powershell -NoProfile -Command "Compress-Archive -LiteralPath 'build/nex-win-amd64.exe' -DestinationPath '$(RELEASE_DIR)/$(DESKTOP_WINDOWS_ASSET)' -Force"
else
@printf 'release-assets-windows requires Windows\n'
@exit 1
endif
release-assets-macos: version-check release-assets-check desktop-build-mac
rm -rf "$(RELEASE_DIR)" rm -rf "$(RELEASE_DIR)"
mkdir -p "$(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 @$(MAKE) release-assets-server-linux TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-darwin-arm64 ./cmd/server @$(MAKE) release-assets-desktop-linux TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
tar -C build -czf "$(RELEASE_DIR)/$(SERVER_DARWIN_AMD64_ASSET)" nex-server-darwin-amd64
tar -C build -czf "$(RELEASE_DIR)/$(SERVER_DARWIN_ARM64_ASSET)" nex-server-darwin-arm64 release-assets-windows: version-check release-assets-check _check-windows-target-arch
ditto -c -k --keepParent build/Nex.app "$(RELEASE_DIR)/$(DESKTOP_MACOS_ASSET)" rm -rf "$(RELEASE_DIR)"
mkdir -p "$(RELEASE_DIR)"
@$(MAKE) release-assets-server-windows TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
@$(MAKE) release-assets-desktop-windows TARGET_ARCH=$(TARGET_ARCH) RELEASE_DIR="$(RELEASE_DIR)"
release-assets-macos: version-check release-assets-check
rm -rf "$(RELEASE_DIR)"
mkdir -p "$(RELEASE_DIR)"
@$(MAKE) release-assets-server-macos RELEASE_DIR="$(RELEASE_DIR)"
@$(MAKE) release-assets-desktop-macos RELEASE_DIR="$(RELEASE_DIR)"
release-assets-server-linux: version-check _check-linux-target-arch
mkdir -p build "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-linux-$(TARGET_ARCH) ./cmd/server
asset=$$(go run ./versionctl asset-name server linux $(TARGET_ARCH) tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-linux-$(TARGET_ARCH)
release-assets-server-windows: version-check _check-windows-target-arch
mkdir -p build "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=$(TARGET_ARCH) go build -ldflags "$(GO_LDFLAGS)" -o ../$(WINDOWS_SERVER_BINARY) ./cmd/server
asset=$$(go run ./versionctl asset-name server windows $(TARGET_ARCH) zip); \
if command -v powershell.exe >/dev/null 2>&1; then POWERSHELL=powershell.exe; else POWERSHELL=powershell; fi; \
"$$POWERSHELL" -NoProfile -Command "Compress-Archive -LiteralPath '$(WINDOWS_SERVER_BINARY)' -DestinationPath '$(RELEASE_DIR)/$$asset' -Force"
release-assets-server-macos: version-check
mkdir -p build "$(RELEASE_DIR)"
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-macos-amd64 ./cmd/server
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-macos-arm64 ./cmd/server
lipo -create build/nex-server-macos-amd64 build/nex-server-macos-arm64 -output build/nex-server-macos-universal
lipo -info build/nex-server-macos-universal | grep -q 'x86_64 arm64'
asset=$$(go run ./versionctl asset-name server macos amd64 tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-macos-amd64
asset=$$(go run ./versionctl asset-name server macos arm64 tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-macos-arm64
asset=$$(go run ./versionctl asset-name server macos universal tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-server-macos-universal
rm -f build/nex-server-macos-amd64 build/nex-server-macos-arm64 build/nex-server-macos-universal
release-assets-desktop-linux: version-check release-assets-check desktop-build-linux _package-linux-tar _package-linux-appimage _package-linux-deb _package-linux-rpm
release-assets-desktop-windows: version-check release-assets-check desktop-build-win
mkdir -p "$(RELEASE_DIR)"
asset=$$(go run ./versionctl asset-name desktop windows $(TARGET_ARCH) zip); \
if command -v powershell.exe >/dev/null 2>&1; then POWERSHELL=powershell.exe; else POWERSHELL=powershell; fi; \
"$$POWERSHELL" -NoProfile -Command "Compress-Archive -LiteralPath '$(WINDOWS_DESKTOP_BINARY)' -DestinationPath '$(RELEASE_DIR)/$$asset' -Force"
release-assets-desktop-macos: version-check release-assets-check desktop-build-mac _package-macos-zip _package-macos-dmg
rm -rf build/Nex.app build/dmg
release-assets-checksums:
@cd "$(RELEASE_DIR)" && \
rm -f SHA256SUMS && \
for asset in *; do \
[ -f "$$asset" ] || continue; \
if command -v sha256sum >/dev/null 2>&1; then \
sha256sum "$$asset"; \
elif command -v shasum >/dev/null 2>&1; then \
shasum -a 256 "$$asset"; \
else \
printf 'Missing sha256sum or shasum\n' >&2; \
exit 1; \
fi; \
done > SHA256SUMS && \
test -s SHA256SUMS
_check-linux-target-arch:
@if [ "$(TARGET_ARCH)" != "amd64" ] && [ "$(TARGET_ARCH)" != "arm64" ]; then \
printf 'Unsupported Linux TARGET_ARCH: %s\n' "$(TARGET_ARCH)"; \
exit 1; \
fi
_check-windows-target-arch:
@if [ "$(TARGET_ARCH)" != "amd64" ]; then \
printf 'Unsupported Windows TARGET_ARCH: %s\n' "$(TARGET_ARCH)"; \
exit 1; \
fi
_ensure-appimagetool:
@mkdir -p build/tools
@if [ ! -x "$(APPIMAGETOOL)" ]; then \
printf 'Downloading appimagetool for %s...\n' "$(APPIMAGE_ARCH)"; \
command -v curl >/dev/null 2>&1 || { printf 'Missing curl for appimagetool download\n'; exit 1; }; \
curl -L "$(APPIMAGETOOL_URL)" -o "$(APPIMAGETOOL)"; \
chmod +x "$(APPIMAGETOOL)"; \
fi; \
printf 'Using appimagetool: %s\n' "$(APPIMAGETOOL)"
_package-linux-tar:
mkdir -p "$(RELEASE_DIR)"
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) tar.gz); \
tar -C build -czf "$(RELEASE_DIR)/$$asset" nex-linux-$(TARGET_ARCH)
_package-linux-appimage: _ensure-appimagetool
mkdir -p "$(RELEASE_DIR)"
appdir="build/appimage/nex-$(TARGET_ARCH).AppDir"; \
rm -rf "$$appdir"; \
mkdir -p "$$appdir/usr/bin" "$$appdir/usr/share/applications" "$$appdir/usr/share/icons"; \
install -m 0755 "$(LINUX_DESKTOP_BINARY)" "$$appdir/usr/bin/nex"; \
install -m 0644 packaging/linux/nex.desktop "$$appdir/nex.desktop"; \
install -m 0644 packaging/linux/nex.desktop "$$appdir/usr/share/applications/nex.desktop"; \
install -m 0755 packaging/linux/AppRun "$$appdir/AppRun"; \
cp -R assets/icons/hicolor "$$appdir/usr/share/icons/"; \
cp assets/icon.png "$$appdir/nex.png"; \
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) AppImage); \
ARCH=$(APPIMAGE_ARCH) APPIMAGE_EXTRACT_AND_RUN=1 "$(APPIMAGETOOL)" "$$appdir" "$(RELEASE_DIR)/$$asset"; \
chmod +x "$(RELEASE_DIR)/$$asset"; \
test -s "$(RELEASE_DIR)/$$asset"
_package-linux-deb:
mkdir -p "$(RELEASE_DIR)"
pkgdir="build/pkg/deb/nex-$(TARGET_ARCH)"; \
rm -rf "$$pkgdir"; \
mkdir -p "$$pkgdir/DEBIAN" "$$pkgdir/usr/bin" "$$pkgdir/usr/share/applications" "$$pkgdir/usr/share/icons"; \
install -m 0755 "$(LINUX_DESKTOP_BINARY)" "$$pkgdir/usr/bin/nex"; \
install -m 0644 packaging/linux/nex.desktop "$$pkgdir/usr/share/applications/nex.desktop"; \
cp -R assets/icons/hicolor "$$pkgdir/usr/share/icons/"; \
printf '%s\n' \
'Package: nex' \
'Version: $(VERSION)' \
'Section: utils' \
'Priority: optional' \
'Architecture: $(DEB_ARCH)' \
'Maintainer: Nex Maintainers <noreply@example.com>' \
'Depends: libgtk-3-0, libayatana-appindicator3-1, xdg-utils' \
'Description: AI Gateway desktop application' \
' Nex is an AI Gateway desktop application.' \
> "$$pkgdir/DEBIAN/control"; \
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) deb); \
dpkg-deb --build --root-owner-group "$$pkgdir" "$(RELEASE_DIR)/$$asset"; \
dpkg-deb -I "$(RELEASE_DIR)/$$asset" >/dev/null
_package-linux-rpm:
mkdir -p "$(RELEASE_DIR)"
topdir="$(abspath build/rpmbuild-$(TARGET_ARCH))"; \
rm -rf "$$topdir"; \
mkdir -p "$$topdir/BUILD" "$$topdir/BUILDROOT" "$$topdir/RPMS" "$$topdir/SOURCES" "$$topdir/SPECS" "$$topdir/SRPMS"; \
rpmbuild -bb --target "$(RPM_ARCH)" \
--define "_topdir $$topdir" \
--define "nex_version $(VERSION)" \
--define "nex_binary $(abspath $(LINUX_DESKTOP_BINARY))" \
--define "nex_desktop_file $(abspath packaging/linux/nex.desktop)" \
--define "nex_icons_dir $(abspath assets/icons/hicolor)" \
packaging/linux/nex.spec; \
rpm_file=$$(find "$$topdir/RPMS" -type f -name '*.rpm' | sort | tail -n 1); \
test -n "$$rpm_file"; \
asset=$$(go run ./versionctl asset-name desktop linux $(TARGET_ARCH) rpm); \
cp "$$rpm_file" "$(RELEASE_DIR)/$$asset"; \
rpm -qip "$(RELEASE_DIR)/$$asset" >/dev/null
_package-macos-zip:
mkdir -p "$(RELEASE_DIR)"
asset=$$(go run ./versionctl asset-name desktop macos universal zip); \
ditto -c -k --keepParent build/Nex.app "$(RELEASE_DIR)/$$asset"
_package-macos-dmg:
mkdir -p "$(RELEASE_DIR)"
dmgdir="build/dmg/Nex"; \
rm -rf "$$dmgdir"; \
mkdir -p "$$dmgdir"; \
cp -R build/Nex.app "$$dmgdir/Nex.app"; \
ln -s /Applications "$$dmgdir/Applications"; \
asset=$$(go run ./versionctl asset-name desktop macos universal dmg); \
hdiutil create -volname Nex -srcfolder "$$dmgdir" -ov -format UDZO "$(RELEASE_DIR)/$$asset"; \
hdiutil verify "$(RELEASE_DIR)/$$asset" >/dev/null && \
rm -rf "$$dmgdir"
# ============================================ # ============================================
# 共享 helper targets # 共享 helper targets

View File

@@ -39,6 +39,8 @@ nex/
│ ├── icon.icns # macOS 应用图标 │ ├── icon.icns # macOS 应用图标
│ └── icon.ico # Windows 应用图标 │ └── icon.ico # Windows 应用图标
├── packaging/ # 桌面发布包元数据Linux desktop entry、RPM spec 等)
└── README.md # 本文件 └── README.md # 本文件
``` ```
@@ -109,10 +111,13 @@ make desktop-build-win
# Linux # Linux
make desktop-build-linux make desktop-build-linux
# Linux arm64
make desktop-build-linux TARGET_ARCH=arm64
``` ```
**使用桌面应用** **使用桌面应用**
- 双击启动应用macOS: Nex.appWindows: nex-win-amd64.exeLinux: nex-linux-amd64 - 双击启动应用macOS: Nex.appWindows: nex-win-amd64.exeLinux: nex-linux-amd64 / nex-linux-arm64
- 系统托盘图标出现,浏览器自动打开管理界面 - 系统托盘图标出现,浏览器自动打开管理界面
- 点击托盘图标显示菜单,可打开管理界面或退出 - 点击托盘图标显示菜单,可打开管理界面或退出
- 关闭浏览器后服务继续运行,可通过托盘重新打开 - 关闭浏览器后服务继续运行,可通过托盘重新打开
@@ -120,8 +125,10 @@ make desktop-build-linux
**注意事项** **注意事项**
- 桌面应用需要 CGO 支持 - 桌面应用需要 CGO 支持
- macOS: 自带 Xcode Command Line Tools - macOS: 自带 Xcode Command Line Tools
- Linux: 自带 gcc部分桌面环境需要 `libappindicator3-dev` - Linux 构建: 需要 gcc、pkg-config、GTK3 开发包和 Ayatana AppIndicator 开发包Ubuntu/Debian: `libgtk-3-dev``libayatana-appindicator3-dev`
- Windows: 需要 MinGW-w64 或在 Windows 环境构建 - Linux 运行: 需要 GTK3、Ayatana AppIndicator 和 xdg-utilsAppImage 也依赖系统提供 AppImage runtime/FUSE 能力,不承诺完全自包含
- Windows: 需要对应架构的 MinGW-w64/MSYS2 工具链desktop 使用 GUI linker flags 隐藏控制台窗口
- macOS DMG: 发布包暂不签名、不 notarize首次打开可能出现 Gatekeeper 提示
**Linux 桌面环境兼容性** **Linux 桌面环境兼容性**
- GNOME: 需要 AppIndicator 扩展 - GNOME: 需要 AppIndicator 扩展
@@ -151,6 +158,38 @@ make server-run
make server-build make server-build
``` ```
### Release 产物
发布流程由 Git tag `vX.Y.Z` 触发GitHub Actions 会创建 Draft Release 并上传 server、web 和 desktop 三类产物,同时生成 `SHA256SUMS`
**server 产物**(不内置 Web 管理界面):
| 平台 | 产物 |
|------|------|
| Linux amd64 | `nex-server_<version>_linux_amd64.tar.gz` |
| Linux arm64 | `nex-server_<version>_linux_arm64.tar.gz` |
| macOS amd64 | `nex-server_<version>_macos_amd64.tar.gz` |
| macOS arm64 | `nex-server_<version>_macos_arm64.tar.gz` |
| macOS universal | `nex-server_<version>_macos_universal.tar.gz` |
| Windows amd64 | `nex-server_<version>_windows_amd64.zip` |
**web 产物**
| 内容 | 产物 |
|------|------|
| `frontend/dist` | `nex-web_<version>.tar.gz` |
**desktop 产物**
| 平台 | 产物 |
|------|------|
| Linux amd64 | `nex-desktop_<version>_linux_amd64.tar.gz``.AppImage``.deb``.rpm` |
| Linux arm64 | `nex-desktop_<version>_linux_arm64.tar.gz``.AppImage``.deb``.rpm` |
| macOS universal | `nex-desktop_<version>_macos_universal.zip``nex-desktop_<version>_macos_universal.dmg` |
| Windows amd64 | `nex-desktop_<version>_windows_amd64.zip` |
Linux deb 包声明 `libgtk-3-0``libayatana-appindicator3-1``xdg-utils` 运行依赖rpm 包声明 `gtk3``libayatana-appindicator-gtk3``xdg-utils` 运行依赖。Rocky Linux 9 等发行版可能需要启用 EPEL 才能解析 Ayatana AppIndicator 依赖。
## API 接口 ## API 接口
### 代理接口(对外部应用) ### 代理接口(对外部应用)
@@ -289,7 +328,13 @@ backend 分类测试、MySQL 专项测试和前端 E2E 测试请分别查看 `ba
```bash ```bash
# 首次克隆后安装 Git hooks # 首次克隆后安装 Git hooks
lefthook install make hooks-install
# 检查 Git hooks 安装状态
make hooks-check
# 运行 Git hooks 回归测试
make hooks-test
# 全局命令 # 全局命令
make lint # 前后端共享检查 make lint # 前后端共享检查
@@ -312,6 +357,11 @@ make desktop-test # desktop 专属测试
make desktop-clean # 清理 desktop 产物 make desktop-clean # 清理 desktop 产物
``` ```
Git hooks 使用仓库内 `scripts/git-hooks/` 的原生脚本,不依赖额外 hook 框架。当前 hooks 包含:
- pre-commit检查 staged files 的冲突标记、Go lint、前端 lint/格式和大文件告警
- commit-msg校验提交信息格式为 `类型: 简短描述`,描述需使用中文
## 版本与发布 ## 版本与发布
### 统一版本源 ### 统一版本源
@@ -322,7 +372,7 @@ make desktop-clean # 清理 desktop 产物
### 本地版本演进 ### 本地版本演进
```bash ```bash
# 递增版本(自动 sync + check + commit + tag # 递增版本(自动 lint + test + 工作区检查 + sync/check + commit + tag
make version-bump BUMP=minor make version-bump BUMP=minor
# 或指定具体版本号 # 或指定具体版本号

View File

@@ -1 +1 @@
0.1.2 0.1.7

View File

@@ -1,2 +1,2 @@
VITE_API_BASE= VITE_API_BASE=
VITE_APP_VERSION=0.1.2 VITE_APP_VERSION=0.1.7

View File

@@ -1,2 +1,2 @@
VITE_API_BASE= VITE_API_BASE=
VITE_APP_VERSION=0.1.2 VITE_APP_VERSION=0.1.7

View File

@@ -1,2 +1,2 @@
VITE_API_BASE=/api VITE_API_BASE=/api
VITE_APP_VERSION=0.1.2 VITE_APP_VERSION=0.1.7

View File

@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "0.1.2", "version": "0.1.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -1,5 +0,0 @@
pre-commit:
commands:
backend-lint:
glob: "backend/**/*.go"
run: cd backend && go tool golangci-lint run --new-from-rev HEAD ./...

View File

@@ -139,29 +139,92 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
### Requirement: 跨平台构建 ### Requirement: 跨平台构建
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,中间构建产物文件名 SHALL 保持 `nex-{os}-{arch}[.exe]` 格式,最终桌面发布资产文件名 SHALL 包含统一版本号和平台标识 系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台和架构分离或参数化,中间构建产物文件名 SHALL 保持可区分目标平台和架构,最终桌面发布资产文件名 SHALL 包含统一版本号、组件、平台、架构和格式信息
#### Scenario: macOS 构建 #### Scenario: macOS 构建
- **WHEN** 执行 `desktop-build-mac` 构建命令且当前版本为 `1.2.3` - **WHEN** 执行 macOS desktop 构建命令且当前版本为 `1.2.3`
- **THEN** 生成 `nex-mac-arm64``nex-mac-amd64` 可执行文件 - **THEN** 系统 SHALL 生成 macOS arm64 和 amd64 桌面可执行文件
- **AND** 系统生成可打包为 `.app` bundle 的 macOS 桌面产物 - **AND** 系统 SHALL 使用 `lipo` 生成 macOS universal 桌面可执行文件
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3``macOS` 平台标识 - **AND** 系统 SHALL 生成可打包为 `.app` bundle 的 macOS desktop 产物
- **AND** 最终 macOS desktop 发布资产文件名 SHALL 包含 `1.2.3``macos``universal`
#### Scenario: Windows 构建 #### Scenario: Windows 构建
- **WHEN** 执行 `desktop-build-win` 构建命令且当前版本为 `1.2.3` - **WHEN** 执行 Windows desktop 构建命令且当前版本为 `1.2.3`
- **THEN** 系统生成 Windows 桌面可执行文件 - **THEN** 系统 SHALL 生成 Windows amd64 desktop 可执行文件
- **AND** 生成 `nex-win-amd64.exe` 可执行文件 - **AND** Windows desktop 构建 SHALL 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口 - **AND** 最终 Windows desktop 发布资产文件名 SHALL 包含 `1.2.3``windows``amd64`
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3``windows` 平台标识
#### Scenario: Linux 构建 #### Scenario: Linux 构建
- **WHEN** 执行 `desktop-build-linux` 构建命令且当前版本为 `1.2.3` - **WHEN** 执行 Linux desktop 构建命令且当前版本为 `1.2.3`
- **THEN** 系统生成 Linux 桌面可执行文件 - **THEN** 系统 SHALL 生成 Linux amd64 和 arm64 desktop 可执行文件
- **AND** 生成 `nex-linux-amd64` 可执行文件 - **AND** Linux desktop 构建 SHALL 使用 CGO 和 GTK/AppIndicator 构建依赖
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3``linux` 平台标识 - **AND** 最终 Linux desktop 发布资产文件名 SHALL 包含 `1.2.3``linux` 和对应架构标识
### Requirement: Linux 桌面发布安装包
系统 SHALL 为 Linux desktop amd64 和 arm64 生成 tar.gz、AppImage、deb 和 rpm 发布安装包,并 SHALL 在安装包中包含标准桌面集成元数据。
#### Scenario: Linux desktop tar.gz 裸包
- **WHEN** 构建 Linux desktop 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.tar.gz`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.tar.gz`
#### Scenario: Linux desktop AppImage 包
- **WHEN** 构建 Linux desktop AppImage 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.AppImage`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.AppImage`
- **AND** AppImage SHALL 包含 desktop entry、应用图标和 desktop 可执行文件
- **AND** AppImage SHALL 依赖目标系统提供 GTK3、Ayatana AppIndicator 和运行 AppImage 所需的 runtime/FUSE 能力
#### Scenario: Linux desktop deb 包
- **WHEN** 构建 Linux desktop deb 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.deb`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.deb`
- **AND** deb 包 SHALL 将可执行文件安装到 `/usr/bin/nex`
- **AND** deb 包 SHALL 将 desktop entry 安装到 `/usr/share/applications/nex.desktop`
- **AND** deb 包 SHALL 将 hicolor 图标安装到 `/usr/share/icons/hicolor/.../apps/nex.png`
- **AND** deb 包 SHALL 声明 `libgtk-3-0``libayatana-appindicator3-1``xdg-utils` 运行时依赖
- **AND** deb 包 metadata 的架构字段 SHALL 使用 `amd64``arm64`
#### Scenario: Linux desktop rpm 包
- **WHEN** 构建 Linux desktop rpm 发布资产
- **THEN** 系统 SHALL 为 amd64 生成 `nex-desktop_<version>_linux_amd64.rpm`
- **AND** 系统 SHALL 为 arm64 生成 `nex-desktop_<version>_linux_arm64.rpm`
- **AND** rpm 包 SHALL 将可执行文件安装到 `/usr/bin/nex`
- **AND** rpm 包 SHALL 将 desktop entry 安装到 `/usr/share/applications/nex.desktop`
- **AND** rpm 包 SHALL 将 hicolor 图标安装到 `/usr/share/icons/hicolor/.../apps/nex.png`
- **AND** rpm 包 SHALL 声明 `gtk3``libayatana-appindicator-gtk3``xdg-utils` 运行时依赖
- **AND** rpm 包 metadata 的架构字段 SHALL 使用 `x86_64``aarch64`
### Requirement: macOS DMG 打包
系统 SHALL 为 macOS desktop universal `.app` 生成 unsigned DMG 安装包,并 SHALL 保留 universal zip 发布资产。
#### Scenario: macOS universal zip 包
- **WHEN** 构建 macOS desktop 发布资产且当前版本为 `1.2.3`
- **THEN** 系统 SHALL 生成 `nex-desktop_1.2.3_macos_universal.zip`
- **AND** zip 包 SHALL 包含 `Nex.app`
#### Scenario: macOS universal DMG 包
- **WHEN** 构建 macOS desktop DMG 发布资产且当前版本为 `1.2.3`
- **THEN** 系统 SHALL 生成 `nex-desktop_1.2.3_macos_universal.dmg`
- **AND** DMG SHALL 包含 `Nex.app`
- **AND** DMG SHALL 包含指向 `/Applications` 的快捷方式
- **AND** DMG SHALL NOT 要求 macOS 签名或 notarization 才能完成构建
#### Scenario: macOS universal 架构校验
- **WHEN** macOS desktop universal 可执行文件生成完成
- **THEN** 系统 SHALL 验证该可执行文件包含 amd64 和 arm64 架构
### Requirement: macOS .app 打包 ### Requirement: macOS .app 打包

View File

@@ -0,0 +1,167 @@
# git-hooks
## Purpose
定义仓库原生 Git hooks 的安装、校验、测试与跨平台执行规则,确保提交前快速检查和提交信息格式校验符合项目规范。
## Requirements
### Requirement: pre-commit hook 快速检查
pre-commit hook SHALL 在 `git commit` 执行前对 staged files 进行快速检查,仅检查本次提交涉及的文件。
#### Scenario: 无 Go 和前端文件变更时跳过
- **WHEN** staged files 中既无 `.go` 文件也无 `.ts`/`.tsx`/`.scss` 文件
- **THEN** pre-commit hook SHALL 直接通过,不运行任何 linter
#### Scenario: 冲突标记检测
- **WHEN** staged files 中包含 `<<<<<<<``=======``>>>>>>>` 冲突标记
- **THEN** pre-commit hook SHALL 报告错误并列出包含冲突的文件名
- **THEN** commit SHALL 被阻止
#### Scenario: Go 文件 lint 检查
- **WHEN** staged files 中包含 `.go` 文件
- **THEN** pre-commit hook SHALL 对 staged `.go` 文件运行 `golangci-lint run`(复用 `backend/.golangci.yml` 配置)
- **THEN** 若 lint 报告任何错误commit SHALL 被阻止
#### Scenario: 前端文件 lint 检查
- **WHEN** staged files 中包含 `.ts``.tsx` 文件
- **THEN** pre-commit hook SHALL 对 staged 前端文件运行 ESLint复用 `frontend/eslint.config.js` 配置)
- **THEN** 若 ESLint 报告任何错误commit SHALL 被阻止
#### Scenario: 前端文件格式检查
- **WHEN** staged files 中包含 `.ts``.tsx``.scss` 文件
- **THEN** pre-commit hook SHALL 对 staged 前端文件运行 Prettier 格式检查(复用 `frontend/.prettierrc` 配置)
- **THEN** 若存在格式不符合规范的文件commit SHALL 被阻止
#### Scenario: 大文件告警
- **WHEN** staged files 中存在超过 500KB 的文本文件
- **THEN** pre-commit hook SHALL 输出警告信息(不阻止提交),提示检查是否误提交
#### Scenario: commit 被阻止时显示修复提示
- **WHEN** pre-commit hook 检查失败
- **THEN** hook SHALL 输出明确的修复提示(如 `bun run fix`、手动解决冲突标记等)
### Requirement: commit-msg hook 校验提交信息格式
commit-msg hook SHALL 在 `git commit` 输入提交信息后校验格式,确保符合项目规范。提交描述 SHALL 使用中文;版本号、英文专有名词可与中文描述混用。
#### Scenario: 合法格式通过
- **WHEN** 提交信息首行格式为 `<类型>: <描述>`,类型为 `feat``fix``refactor``docs``style``test``chore` 之一
- **THEN** commit-msg hook SHALL 通过commit 正常执行
#### Scenario: 非法类型被拒绝
- **WHEN** 提交信息首行使用的类型不在允许列表中(如 `update: xxx`
- **THEN** commit-msg hook SHALL 报告错误显示允许的类型列表commit SHALL 被阻止
#### Scenario: 英文描述被拒绝
- **WHEN** 提交信息首行为 `feat: add auth`
- **THEN** commit-msg hook SHALL 报告错误,提示提交描述需使用中文
- **THEN** commit SHALL 被阻止
#### Scenario: 缺少冒号空格被拒绝
- **WHEN** 提交信息首行为 `feat:xxx`(冒号后无空格)或 `feat xxx`
- **THEN** commit-msg hook SHALL 报告格式错误commit SHALL 被阻止
#### Scenario: 首行过长告警
- **WHEN** 提交信息首行超过 72 个字符
- **THEN** commit-msg hook SHALL 输出警告(不阻止提交),提示首行应简短
#### Scenario: Merge commit 自动放行
- **WHEN** 提交信息首行以 `Merge` 开头
- **THEN** commit-msg hook SHALL 直接通过,不进行格式校验
#### Scenario: 格式错误时显示示例
- **WHEN** commit-msg hook 检查失败
- **THEN** hook SHALL 输出包含正确格式示例的错误信息(如 `feat: 添加供应商批量管理功能`
### Requirement: hooks-install 安装命令
`make hooks-install` SHALL 将 `scripts/git-hooks/` 下的 hook 脚本安装到 `.git/hooks/`,不覆盖 Git LFS 管理的 hook。
#### Scenario: 安装 pre-commit 和 commit-msg
- **WHEN** 执行 `make hooks-install`
- **THEN** `scripts/git-hooks/pre-commit` SHALL 被复制到 `.git/hooks/pre-commit`
- **THEN** `scripts/git-hooks/commit-msg` SHALL 被复制到 `.git/hooks/commit-msg`
- **THEN** 两个文件 SHALL 被设置为可执行(`chmod +x`
#### Scenario: 不覆盖 LFS 管理的 hook
- **WHEN** `.git/hooks/post-checkout``.git/hooks/post-commit``.git/hooks/post-merge``.git/hooks/pre-push` 已由 Git LFS 管理
- **THEN** `make hooks-install` SHALL NOT 覆盖或修改这些文件
#### Scenario: 重复安装幂等
- **WHEN** `make hooks-install` 被执行多次
- **THEN** hook 文件 SHALL 被正确覆盖更新,不会产生重复或损坏
#### Scenario: hooks-check 验证安装状态
- **WHEN** 执行 `make hooks-check`
- **THEN** 命令 SHALL 检查 `.git/hooks/pre-commit``.git/hooks/commit-msg` 是否存在且可执行
- **THEN** SHALL 输出每个 hook 的安装状态
### Requirement: hooks-test 回归测试命令
`make hooks-test` SHALL 运行仓库内 hook 回归测试,覆盖 commit-msg 格式校验和 pre-commit staged-file 检查,不污染真实 git index。
#### Scenario: 运行 hook 回归测试
- **WHEN** 执行 `make hooks-test`
- **THEN** SHALL 运行 `scripts/git-hooks/test-hooks.sh`
- **THEN** 测试 SHALL 使用临时 `GIT_INDEX_FILE` 构造 staged fixture
- **THEN** 若任一 hook 行为不符合预期,命令 SHALL 返回非零退出码
### Requirement: 跨平台可用
pre-commit 和 commit-msg hook 脚本 SHALL 可在 macOS 和 WindowsGit Bash上正常执行。
#### Scenario: macOS 上正常执行
- **WHEN** hook 脚本在 macOS 上被 git 调用
- **THEN** `#!/bin/sh` shebang SHALL 被系统正确解析
- **THEN** `exec make` SHALL 正确调用 Makefile target
#### Scenario: Windows Git Bash 上正常执行
- **WHEN** hook 脚本在 Windows 的 Git Bash 环境中被 git 调用
- **THEN** Git for Windows 自带的 sh.exe SHALL 正确解析 `#!/bin/sh`
- **THEN** `exec make` SHALL 正确调用 Makefile target依赖 Git Bash/MINGW64 环境中 `make` 可用)
- **THEN** Go 和 Bun 工具链 SHALL 通过 PATH 可被 Makefile 调用
### Requirement: pre-commit 核心逻辑在 Makefile 中复用
pre-commit hook 的检查逻辑 SHALL 通过 Makefile target 调用项目已有工具链,不重复实现 hook 框架逻辑。commit-msg hook SHALL 在脚本内直接完成格式校验。
#### Scenario: Go lint 复用后端配置
- **WHEN** pre-commit 需要检查 Go 文件
- **THEN** SHALL 调用 Makefile 逻辑,在 `backend/` 目录对 staged `.go` 文件运行 `go tool golangci-lint run`
- **THEN** SHALL 复用 `backend/.golangci.yml` 中的 lint 配置
#### Scenario: 前端 lint 使用 staged 文件参数
- **WHEN** pre-commit 需要检查前端文件
- **THEN** SHALL 调用 Makefile 逻辑,在 `frontend/` 目录对 staged 前端文件运行 ESLint 和 Prettier 的文件参数模式
- **THEN** SHALL NOT 在 pre-commit 阶段运行全量 `bun run check`
#### Scenario: 终端直接调试
- **WHEN** 开发者执行 `make _hooks-pre-commit`
- **THEN** SHALL 执行与 pre-commit hook 完全相同的检查逻辑
- **THEN** 输出 SHALL 与 hook 触发时一致

View File

@@ -32,88 +32,126 @@
### Requirement: 三平台发布构建 ### Requirement: 三平台发布构建
系统 SHALL 在发布流水线中构建 server 与 desktop 的 Linux、Windows、macOS 三个平台产物 系统 SHALL 在发布流水线中构建 server、web 与 desktop 的发布产物,并覆盖 Linux、Windows、macOS 的目标架构和格式矩阵
#### Scenario: Linux 发布构建 #### Scenario: server 发布构建
- **WHEN** 发布流水线执行 Linux 构建 job - **WHEN** 发布流水线执行 server 发布构建
- **THEN** 系统 SHALL 在可访问 Go、Bun 和 Linux 桌面构建依赖的 shell 环境中执行 Linux 发布构建 - **THEN** 系统 SHALL 生成 `nex-server_<version>_linux_amd64.tar.gz`
- **AND** 系统 SHALL 生成 Linux server 发布资产 - **AND** 系统 SHALL 生成 `nex-server_<version>_linux_arm64.tar.gz`
- **AND** 系统 SHALL 生成 Linux desktop 发布资产 - **AND** 系统 SHALL 生成 `nex-server_<version>_macos_amd64.tar.gz`
- **AND** 系统 SHALL 生成 `nex-server_<version>_macos_arm64.tar.gz`
- **AND** 系统 SHALL 生成 `nex-server_<version>_macos_universal.tar.gz`
- **AND** 系统 SHALL 生成 `nex-server_<version>_windows_amd64.zip`
#### Scenario: Windows 发布构建 #### Scenario: web 发布构建
- **WHEN** 发布流水线执行 Windows 构建 job - **WHEN** 发布流水线执行 web 发布构建
- **THEN** 系统 SHALL 在包含 MSYS2 / MINGW64 构建工具且可访问 Go 与 Bun 工具链的 shell 环境中执行 Windows 发布构建 - **THEN** 系统 SHALL 使用 Bun 构建 `frontend/dist`
- **AND** 系统 SHALL 生成 Windows server 发布资产 - **AND** 系统 SHALL 将前端静态资源打包为 `nex-web_<version>.tar.gz`
- **AND** 系统 SHALL 生成 Windows desktop 发布资产 - **AND** server 发布资产 SHALL NOT 内置 Web 管理界面静态资源
#### Scenario: macOS 发布构建 #### Scenario: Linux desktop 发布构建
- **WHEN** 发布流水线执行 macOS 构建 job - **WHEN** 发布流水线执行 Linux desktop 发布构建
- **THEN** 系统 SHALL 在可访问 Go、Bun 和 macOS 打包工具链的 shell 环境中执行 macOS 发布构建 - **THEN** 系统 SHALL 在可访问 Go、Bun、CGO、GTK3、Ayatana AppIndicator 和 Linux 打包工具链的环境中构建
- **AND** 系统 SHALL 生成 darwin-amd64 server 发布资产 - **AND** 系统 SHALL `amd64``arm64` 分别生成 tar.gz、AppImage、deb 和 rpm desktop 发布资产
- **AND** 系统 SHALL 生成 darwin-arm64 server 发布资产 - **AND** Linux amd64 desktop 发布构建 SHALL 在 `ubuntu-latest` runner 上执行
- **AND** 系统 SHALL 生成 macOS desktop universal 发布资产 - **AND** Linux arm64 desktop 发布构建 SHALL 在 `ubuntu-24.04-arm` runner 上执行
- **AND** 系统 SHALL NOT 在构建步骤中显式传递 TARGET_ARCH 参数
#### Scenario: Windows desktop 发布构建
- **WHEN** 发布流水线执行 Windows desktop 发布构建
- **THEN** 系统 SHALL 在包含对应架构 MSYS2/MinGW 或等价 CGO 工具链的环境中构建
- **AND** Windows amd64 desktop 发布构建 SHALL 在 `windows-latest` runner 上的 MSYS2 MINGW64 环境中执行
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_windows_amd64.zip`
- **AND** 系统 SHALL NOT 在构建步骤中显式传递 TARGET_ARCH 参数
#### Scenario: macOS desktop 发布构建
- **WHEN** 发布流水线执行 macOS desktop 发布构建
- **THEN** 系统 SHALL 在可访问 Go、Bun、Xcode 命令行工具、`lipo``hdiutil` 和 zip 打包工具的 macOS 环境中构建
- **AND** 系统 SHALL 在 ARM64 macOS runner 上编译 amd64 和 arm64 双架构二进制并使用 `lipo` 合并为 universal binary
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_macos_universal.zip`
- **AND** 系统 SHALL 生成 `nex-desktop_<version>_macos_universal.dmg`
#### Scenario: 原生架构构建
- **WHEN** 发布流水线执行 Linux 或 Windows 的 server/desktop 构建步骤
- **THEN** 系统 SHALL NOT 显式传递 TARGET_ARCH 参数
- **AND** Makefile SHALL 通过 `go env GOARCH` 自动检测目标架构
- **AND** 原生 runner 的实际架构 SHALL 与 `go env GOARCH` 返回值一致
### Requirement: 三平台发布构建预检 ### Requirement: 三平台发布构建预检
系统 SHALL 在正式执行各平台 `make release-assets-*` 前验证对应发布 job 的关键工具链可用性,并在环境不完整时快速失败且输出明确诊断。 系统 SHALL 在正式执行各平台 release 构建前验证对应 job 的关键工具链可用性,并在环境不完整时快速失败且输出明确诊断。
#### Scenario: Linux 预检通过后开始构建 #### Scenario: Linux 预检通过后开始构建
- **WHEN** Linux 发布 job 中的 `go``bun` Linux 桌面构建依赖均可用 - **WHEN** Linux 发布 job 中的 `go``bun``gcc``pkg-config`、GTK3、Ayatana AppIndicator 和 Linux 打包工具均可用
- **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径
- **AND** 系统 SHALL 继续执行 `make release-assets-linux` - **AND** 系统 SHALL 继续执行对应 Linux release 构建
#### Scenario: Windows 预检通过后开始构建 #### Scenario: Windows 预检通过后开始构建
- **WHEN** Windows 发布 job 中的 `go``bun` 与 MSYS2 构建工具均可用 - **WHEN** Windows 发布 job 中的 `go``bun``make`、对应架构 CGO 编译器和 resource 生成工具均可用
- **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径
- **AND** 系统 SHALL 继续执行 `make release-assets-windows` - **AND** 系统 SHALL 继续执行对应 Windows release 构建
#### Scenario: macOS 预检通过后开始构建 #### Scenario: macOS 预检通过后开始构建
- **WHEN** macOS 发布 job 中的 `go``bun` 与 macOS 打包工具均可用 - **WHEN** macOS 发布 job 中的 `go``bun``ditto``lipo``vtool``hdiutil` 均可用
- **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径 - **THEN** 系统 SHALL 输出关键工具的版本信息或解析路径
- **AND** 系统 SHALL 继续执行 `make release-assets-macos` - **AND** 系统 SHALL 继续执行对应 macOS release 构建
#### Scenario: 任一平台预检发现工具缺失 #### Scenario: web 预检通过后开始构建
- **WHEN** 任一平台发布 job 中存在关键工具不可用 - **WHEN** web 发布 job 中`bun` 和前端构建依赖均可用
- **THEN** 系统 SHALL 输出 Bun 版本信息
- **AND** 系统 SHALL 继续执行 web release 构建
#### Scenario: 任一预检发现工具缺失
- **WHEN** 任一发布 job 中存在关键工具不可用
- **THEN** 发布流水线 SHALL 在正式构建前失败 - **THEN** 发布流水线 SHALL 在正式构建前失败
- **AND** 系统 SHALL 在日志中标识缺失的工具链名称 - **AND** 系统 SHALL 在日志中标识缺失的工具链名称
### Requirement: 发布流水线 LFS 资产拉取 ### Requirement: 发布流水线 LFS 资产拉取
发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验或平台构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。 发布流水线 SHALL 在所有会 checkout 仓库并参与版本校验、web 构建、server 构建或 desktop 构建的 job 中拉取 Git LFS 真实文件,确保发布构建读取到真实二进制资产而非 LFS pointer 文本。
#### Scenario: 发布 job 获取真实 LFS 图标资产 #### Scenario: 发布 job 获取真实 LFS 图标资产
- **WHEN** 发布流水线执行 `prepare``build-linux``build-windows``build-macos` job 的 checkout 步骤 - **WHEN** 发布流水线执行任一参与版本校验、web 构建、server 构建或 desktop 构建的 job 的 checkout 步骤
- **THEN** checkout 步骤 SHALL 拉取 Git LFS 文件 - **THEN** checkout 步骤 SHALL 拉取 Git LFS 文件
- **AND** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` SHALL 在后续步骤中表现为真实图标文件而非 LFS pointer 文本 - **AND** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` SHALL 在后续步骤中表现为真实图标文件而非 LFS pointer 文本
#### Scenario: 新增矩阵 job 获取真实 LFS 资产
- **WHEN** 发布流水线新增 server、web、desktop、platform 或 arch 矩阵 job
- **THEN** 该 job 的 checkout 步骤 SHALL 使用与现有发布 job 一致的 Git LFS 拉取配置
### Requirement: 发布资产图标预检 ### Requirement: 发布资产图标预检
发布流水线 SHALL 在正式执行各平台发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。 发布流水线 SHALL 在正式执行任何需要图标资产、前端 public 图标或 desktop 打包资源的发布构建前校验关键图标资产可用,并在检测到 LFS pointer 或错误格式时快速失败且输出明确诊断。
#### Scenario: 图标资产为 LFS pointer #### Scenario: 图标资产为 LFS pointer
- **WHEN** 发布资产预检发现关键图标文件内容为 Git LFS pointer 文本 - **WHEN** 发布资产预检发现关键图标文件内容为 Git LFS pointer 文本
- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 - **THEN** 发布流水线 SHALL 在执行对应 release 构建前失败
- **AND** 系统 SHALL 在日志中标识对应图标文件需要拉取 Git LFS 真实内容 - **AND** 系统 SHALL 在日志中标识对应图标文件需要拉取 Git LFS 真实内容
#### Scenario: 图标资产格式无效 #### Scenario: 图标资产格式无效
- **WHEN** 发布资产预检发现关键图标文件不是对应格式的有效资源 - **WHEN** 发布资产预检发现关键图标文件不是对应格式的有效资源
- **THEN** 发布流水线 SHALL 在执行平台发布构建前失败 - **THEN** 发布流水线 SHALL 在执行对应 release 构建前失败
- **AND** 系统 SHALL 在日志中标识格式无效的图标文件路径 - **AND** 系统 SHALL 在日志中标识格式无效的图标文件路径
#### Scenario: 图标资产预检通过 #### Scenario: 图标资产预检通过
- **WHEN** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` 均为真实且格式可用的图标资产 - **WHEN** `assets/icon.ico``assets/icon.icns``assets/icon.png``frontend/public/icon.png` 均为真实且格式可用的图标资产
- **THEN** 发布流水线 SHALL 继续执行对应平台的 `make release-assets-*` 构建 - **THEN** 发布流水线 SHALL 继续执行依赖这些资产的 release 构建
### Requirement: 发布流水线运行时兼容性 ### Requirement: 发布流水线运行时兼容性
@@ -131,34 +169,54 @@
### Requirement: 版本化发布资产命名 ### Requirement: 版本化发布资产命名
系统 SHALL 为 server 与 desktop 发布资产使用包含统一版本号和目标平台信息的文件名,确保 Release 页面可直接区分产物用途平台。 系统 SHALL 为 server、web 与 desktop 发布资产使用包含统一版本号、组件、目标平台和目标架构信息的文件名,确保 Release 页面可直接区分产物用途平台、架构和格式
#### Scenario: server 资产命名 #### Scenario: server 资产命名
- **WHEN** 当前发布版本为 `1.2.3` - **WHEN** 当前发布版本为 `1.2.3`
- **THEN** Linux server 发布资产文件名 SHALL 包含 `1.2.3``linux``amd64` - **THEN** Linux server 发布资产文件名 SHALL `nex-server_1.2.3_linux_amd64.tar.gz``nex-server_1.2.3_linux_arm64.tar.gz`
- **AND** Windows server 发布资产文件名 SHALL 包含 `1.2.3``windows``amd64` - **AND** macOS server 发布资产文件名 SHALL `nex-server_1.2.3_macos_amd64.tar.gz``nex-server_1.2.3_macos_arm64.tar.gz``nex-server_1.2.3_macos_universal.tar.gz`
- **AND** macOS server 发布资产文件名 SHALL 分别包含 `1.2.3``darwin``amd64``1.2.3``darwin``arm64` - **AND** Windows server 发布资产文件名 SHALL `nex-server_1.2.3_windows_amd64.zip`
#### Scenario: web 资产命名
- **WHEN** 当前发布版本为 `1.2.3`
- **THEN** web 发布资产文件名 SHALL 为 `nex-web_1.2.3.tar.gz`
- **AND** web 发布资产文件名 SHALL NOT 包含平台或架构字段
#### Scenario: desktop 资产命名 #### Scenario: desktop 资产命名
- **WHEN** 当前发布版本为 `1.2.3` - **WHEN** 当前发布版本为 `1.2.3`
- **THEN** Linux desktop 发布资产文件名 SHALL 包含 `1.2.3``linux` - **THEN** Linux desktop 发布资产文件名 SHALL 使用 `nex-desktop_1.2.3_linux_<arch>.<format>` 格式
- **AND** Windows desktop 发布资产文件名 SHALL 包含 `1.2.3``windows` - **AND** Windows desktop 发布资产文件名 SHALL `nex-desktop_1.2.3_windows_amd64.zip`
- **AND** macOS desktop universal 发布资产文件名 SHALL 包含 `1.2.3``macOS` - **AND** macOS desktop 发布资产文件名 SHALL `nex-desktop_1.2.3_macos_universal.zip``nex-desktop_1.2.3_macos_universal.dmg`
- **AND** 发布资产文件名中的 macOS 平台字段 SHALL 使用 `macos` 而非 `darwin`
### Requirement: Draft Release 组装 ### Requirement: Draft Release 组装
系统 SHALL 将发布流水线产物上传到 GitHub Draft Release由人工确认后再公开发布。 系统 SHALL 将发布流水线产物上传到 GitHub Draft Release由人工确认后再公开发布,并 SHALL 生成覆盖全部发布资产的校验和清单
#### Scenario: 发布成功时创建 Draft Release #### Scenario: 发布成功时创建 Draft Release
- **WHEN** 版本校验通过且三平台发布资产构建完成 - **WHEN** 版本校验通过且 server、web、desktop 的全部目标发布资产构建完成
- **THEN** 系统 SHALL 创建或更新与该 tag 对应的 GitHub Draft Release - **THEN** 系统 SHALL 创建或更新与该 tag 对应的 GitHub Draft Release
- **AND** 系统 SHALL 上传 server 与 desktop 的全部发布资产 - **AND** 系统 SHALL 上传 server、web 与 desktop 的全部发布资产
- **AND** 系统 SHALL 上传 `SHA256SUMS`
#### Scenario: 校验和覆盖全部资产
- **WHEN** Draft Release 组装步骤生成 `SHA256SUMS`
- **THEN** `SHA256SUMS` SHALL 包含除自身以外的全部发布资产文件
- **AND** `SHA256SUMS` 中的文件名 SHALL 与实际上传的 release asset 文件名一致
#### Scenario: 构建失败时阻止完成发布 #### Scenario: 构建失败时阻止完成发布
- **WHEN** 任一平台发布资产构建失败或版本校验失败 - **WHEN** 任一目标发布资产构建失败、打包失败、校验失败、artifact 上传为空或版本校验失败
- **THEN** 发布流水线 SHALL 失败 - **THEN** 发布流水线 SHALL 失败
- **AND** 系统 SHALL NOT 产生可直接公开的成功发布结果 - **AND** 系统 SHALL NOT 产生可直接公开的成功发布结果
#### Scenario: artifact 缺失时快速失败
- **WHEN** 任一构建 job 尝试上传 release artifact 但匹配不到目标文件
- **THEN** 该 job SHALL 失败
- **AND** Draft Release 组装 SHALL NOT 继续发布不完整资产集合

View File

@@ -90,22 +90,34 @@
### Requirement: 版本升迁 Makefile 编排 ### Requirement: 版本升迁 Makefile 编排
`make version-bump` SHALL 编排完整的版本升迁流程:工作区干净检查 → `version bump`(含 sync/check/倒退检查)→ git add → git commit → git tag。不传 `BUMP` 参数时 SHALL 默认执行 `BUMP=patch` `make version-bump` SHALL 编排完整的版本升迁流程:全量 lint 检查 → 全量单元测试 → 工作区干净检查 → `version bump`(含 sync/check/倒退检查)→ git add → git commit → git tag。不传 `BUMP` 参数时 SHALL 默认执行 `BUMP=patch`lint/test 前置检查 SHALL NOT 替代工作区干净检查。
#### Scenario: 完整升迁流程 #### Scenario: 完整升迁流程
- **WHEN** 执行 `make version-bump BUMP=minor`,工作区干净,当前版本 `0.1.0` - **WHEN** 执行 `make version-bump BUMP=minor`,工作区干净,当前版本 `0.1.0`
- **THEN** Makefile SHALL 依次执行:工作区检查 → `version bump minor``git add VERSION frontend/``git commit -m "chore: 版本升迁 v0.2.0"``git tag v0.2.0` - **THEN** Makefile SHALL 依次执行:`make lint``make test`工作区检查 → `version bump minor``git add VERSION frontend/``git commit -m "chore: 版本升迁 v0.2.0"``git tag v0.2.0`
#### Scenario: 不传 BUMP 默认 patch #### Scenario: 不传 BUMP 默认 patch
- **WHEN** 执行 `make version-bump`,工作区干净,当前版本 `0.1.0` - **WHEN** 执行 `make version-bump`,工作区干净,当前版本 `0.1.0`
- **THEN** Makefile SHALL 等效于执行 `make version-bump BUMP=patch`,将版本更新为 `0.1.1` - **THEN** Makefile SHALL 等效于执行 `make version-bump BUMP=patch`,将版本更新为 `0.1.1`
#### Scenario: lint 失败时终止
- **WHEN** 执行 `make version-bump`,但 `make lint` 报告错误
- **THEN** Makefile SHALL 以非零退出码失败SHALL NOT 执行 `version bump`、git commit、git tag
- **THEN** SHALL 输出错误信息提示修复 lint 问题后重试
#### Scenario: test 失败时终止
- **WHEN** 执行 `make version-bump`,但 `make test` 报告测试失败
- **THEN** Makefile SHALL 以非零退出码失败SHALL NOT 执行 `version bump`、git commit、git tag
- **THEN** SHALL 输出错误信息提示修复测试失败后重试
#### Scenario: 工作区不干净 #### Scenario: 工作区不干净
- **WHEN** 执行 `make version-bump BUMP=minor`,但工作区有未提交的改动 - **WHEN** 执行 `make version-bump BUMP=minor`,但工作区有未提交的改动
- **THEN** Makefile SHALL 以非零退出码失败并提示先提交或暂存改动 - **THEN** Makefile SHALL 以非零退出码失败并提示先提交或清理改动
#### Scenario: 支持指定版本号 #### Scenario: 支持指定版本号

View File

@@ -8,17 +8,26 @@
### Requirement: 根目录公开命令分层 ### Requirement: 根目录公开命令分层
根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。 根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。release 命令 SHALL 使用 `release-assets` 前缀,并 SHALL 通过清晰的目标名或变量参数表达 component、platform、arch 和 format。
#### Scenario: 查看根目录公开命令 #### Scenario: 查看根目录公开命令
- **WHEN** 开发者查看根目录 `Makefile` 的公开 target - **WHEN** 开发者查看根目录 `Makefile` 的公开 target
- **THEN** SHALL 仅看到 `lint``test``clean``version-sync``version-check``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` 这类公共入口 - **THEN** SHALL 仅看到 `lint``test``clean``version-sync``version-check``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` 前缀的 release 公共入口
- **AND** release 公共入口 SHALL 能覆盖 server、web、desktop 的目标发布产物
#### Scenario: 根目录不暴露局部和内部命令 #### Scenario: 根目录不暴露局部和内部命令
- **WHEN** 开发者查看根目录 `Makefile` 的公开 target - **WHEN** 开发者查看根目录 `Makefile` 的公开 target
- **THEN** SHALL NOT 暴露 `backend-*``frontend-*`、数据库迁移命令、MySQL 专项测试命令或 `desktop-prepare-*` 之类内部步骤 - **THEN** SHALL NOT 暴露 `backend-*``frontend-*`、数据库迁移命令、MySQL 专项测试命令或 `desktop-prepare-*` 之类内部步骤
- **THEN** SHALL NOT 暴露 `dev``build``all``desktop-dev``desktop-build` 这类模糊或聚合式公共命令 - **THEN** SHALL NOT 暴露 `dev``build``all``desktop-dev``desktop-build` 这类模糊或聚合式公共命令
#### Scenario: release 内部步骤保持内部化
- **WHEN** 根目录 `Makefile` 需要复用 release 构建、打包、校验辅助步骤
- **THEN** 内部辅助 target SHALL 使用 `_` 前缀或 Make 变量参数化方式表达
- **AND** 内部辅助 target SHALL NOT 成为文档化的公共入口
### Requirement: 全局质量与清理命令 ### Requirement: 全局质量与清理命令
根目录 `Makefile` SHALL 提供 `lint``test``clean` 作为全仓默认入口。 根目录 `Makefile` SHALL 提供 `lint``test``clean` 作为全仓默认入口。
@@ -97,12 +106,33 @@
### Requirement: Release 命令沿用根目录入口 ### Requirement: Release 命令沿用根目录入口
根目录 `Makefile` SHALL 继续提供 `release-assets-*` 作为发布资产入口,并与新的版本校验规则保持一致。 根目录 `Makefile` SHALL 继续提供 `release-assets` 前缀 target 作为发布资产入口,并与版本校验、发布资产预检和多组件打包规则保持一致。
#### Scenario: 执行 release 资产命令 #### Scenario: 执行 release 资产命令
- **WHEN** 执行 `make release-assets-linux``make release-assets-windows``make release-assets-macos`
- **WHEN** 执行任一 `release-assets` 前缀的公共 release target
- **THEN** SHALL 在构建发布资产前执行版本一致性校验 - **THEN** SHALL 在构建发布资产前执行版本一致性校验
- **THEN** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件 - **AND** SHALL 在需要图标或桌面资源的构建前执行发布资产预检
- **AND** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件
#### Scenario: release target 职责清晰
- **WHEN** GitHub Actions 调用根目录 `Makefile` 生成 release 产物
- **THEN** 对应 release target SHALL 明确生成的 component、platform、arch 或 format 范围
- **AND** GitHub Actions SHALL NOT 以内联脚本替代 Makefile 中已有的核心构建和打包逻辑
#### Scenario: web release 产物生成
- **WHEN** 执行 web release 资产命令
- **THEN** SHALL 使用 Bun 构建 `frontend/dist`
- **AND** SHALL 打包生成 `nex-web_<version>.tar.gz`
- **AND** SHALL NOT 修改前端版本镜像文件
#### Scenario: checksum release 产物生成
- **WHEN** 执行 release 汇总或 Draft Release 组装相关命令
- **THEN** SHALL 能基于当前 release 产物目录生成 `SHA256SUMS`
- **AND** `SHA256SUMS` SHALL 覆盖除自身以外的全部 release 资产
### Requirement: Backend 局部命令下沉 ### Requirement: Backend 局部命令下沉

5
packaging/linux/AppRun Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
APPDIR=$(dirname "$(readlink -f "$0")")
exec "$APPDIR/usr/bin/nex" "$@"

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=Nex
Comment=AI Gateway
Exec=nex
Icon=nex
Terminal=false
Categories=Development;Network;
StartupNotify=false

29
packaging/linux/nex.spec Normal file
View File

@@ -0,0 +1,29 @@
Name: nex
Version: %{nex_version}
Release: 1%{?dist}
Summary: AI Gateway desktop application
License: Apache-2.0
URL: https://github.com/nex/gateway
Requires: gtk3
Requires: libayatana-appindicator-gtk3
Requires: xdg-utils
%description
Nex is an AI Gateway desktop application.
%prep
%build
%install
mkdir -p %{buildroot}/usr/bin
install -m 0755 %{nex_binary} %{buildroot}/usr/bin/nex
mkdir -p %{buildroot}/usr/share/applications
install -m 0644 %{nex_desktop_file} %{buildroot}/usr/share/applications/nex.desktop
mkdir -p %{buildroot}/usr/share/icons/hicolor
cp -a %{nex_icons_dir}/. %{buildroot}/usr/share/icons/hicolor/
%files
/usr/bin/nex
/usr/share/applications/nex.desktop
/usr/share/icons/hicolor/*/apps/nex.png

42
scripts/git-hooks/commit-msg Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/sh
set -e
MSG_FILE=$1
if [ ! -f "$MSG_FILE" ]; then
printf '%s\n' '提交信息文件不存在。' >&2
exit 1
fi
IFS= read -r FIRST_LINE < "$MSG_FILE" || FIRST_LINE=
case "$FIRST_LINE" in
Merge*)
exit 0
;;
esac
if ! printf '%s\n' "$FIRST_LINE" | grep -Eq '^(feat|fix|refactor|docs|style|test|chore): .+$'; then
cat >&2 <<'EOF'
提交信息格式错误。
格式: <类型>: <简短描述>
类型: feat / fix / refactor / docs / style / test / chore
示例:
feat: 添加供应商批量管理功能
fix: 修复流式响应断连问题
chore: 版本升迁 v0.2.0
EOF
exit 1
fi
DESCRIPTION=${FIRST_LINE#*: }
if printf '%s\n' "$DESCRIPTION" | LC_ALL=C grep -Eq '^[ -~]+$'; then
printf '%s\n' '提交描述需使用中文。' >&2
exit 1
fi
if [ ${#FIRST_LINE} -gt 72 ]; then
printf '%s\n' '警告: 提交信息首行超过 72 个字符,建议保持简短。' >&2
fi

12
scripts/git-hooks/pre-commit Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
set -e
ROOT_DIR=$(git rev-parse --show-toplevel)
cd "$ROOT_DIR"
command -v make >/dev/null 2>&1 || {
printf '%s\n' '缺少 make 命令,请先安装 Make 或使用 Git Bash/MINGW64 环境。' >&2
exit 1
}
exec make _hooks-pre-commit

134
scripts/git-hooks/test-hooks.sh Executable file
View File

@@ -0,0 +1,134 @@
#!/bin/sh
set -eu
ROOT_DIR=$(git rev-parse --show-toplevel)
cd "$ROOT_DIR"
TMP_DIR=${TMPDIR:-/tmp}/nex-hooks-test.$$
mkdir -p "$TMP_DIR"
cleanup() {
rm -f \
backend/pkg/buildinfo/hook_bad_test_fixture.go \
frontend/src/hook_bad_fixture.ts \
frontend/src/hook_format_fixture.ts \
docs/hook-doc-fixture.md \
docs/hook-conflict-fixture.md \
docs/hook-large-fixture.txt
rm -rf "$TMP_DIR"
}
trap cleanup EXIT HUP INT TERM
pass() {
printf 'OK: %s\n' "$1"
}
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
write_msg() {
file=$1
shift
printf '%s\n' "$*" > "$file"
}
expect_success() {
name=$1
shift
if "$@" > "$TMP_DIR/out" 2>&1; then
pass "$name"
else
cat "$TMP_DIR/out" >&2
fail "$name"
fi
}
expect_failure() {
name=$1
shift
if "$@" > "$TMP_DIR/out" 2>&1; then
cat "$TMP_DIR/out" >&2
fail "$name"
fi
pass "$name"
}
run_precommit_for() {
index=$TMP_DIR/index
rm -f "$index"
GIT_INDEX_FILE=$index git read-tree HEAD
for file in "$@"; do
GIT_INDEX_FILE=$index git add -f "$file"
done
GIT_INDEX_FILE=$index make _hooks-pre-commit
}
MSG_FILE=$TMP_DIR/commit-msg.txt
write_msg "$MSG_FILE" 'feat: 添加 hook 测试'
expect_success 'commit-msg accepts Chinese description' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'feat: add hook tests'
expect_failure 'commit-msg rejects English-only description' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'update: 添加 hook 测试'
expect_failure 'commit-msg rejects invalid type' scripts/git-hooks/commit-msg "$MSG_FILE"
write_msg "$MSG_FILE" 'Merge branch feature'
expect_success 'commit-msg accepts merge commits' scripts/git-hooks/commit-msg "$MSG_FILE"
cat > backend/pkg/buildinfo/hook_bad_test_fixture.go <<'EOF'
package buildinfo
import "fmt"
func hookBadTestFixture() {
fmt.Println("bad")
}
EOF
expect_failure 'pre-commit rejects Go lint errors' run_precommit_for backend/pkg/buildinfo/hook_bad_test_fixture.go
rm -f backend/pkg/buildinfo/hook_bad_test_fixture.go
cat > frontend/src/hook_bad_fixture.ts <<'EOF'
console.log('bad')
EOF
expect_failure 'pre-commit rejects frontend lint errors' run_precommit_for frontend/src/hook_bad_fixture.ts
rm -f frontend/src/hook_bad_fixture.ts
cat > frontend/src/hook_format_fixture.ts <<'EOF'
const hookFormatFixture={foo:"bar"}
export { hookFormatFixture }
EOF
expect_failure 'pre-commit rejects frontend format errors' run_precommit_for frontend/src/hook_format_fixture.ts
rm -f frontend/src/hook_format_fixture.ts
cat > docs/hook-doc-fixture.md <<'EOF'
hook doc fixture
EOF
expect_success 'pre-commit skips non-code staged files' run_precommit_for docs/hook-doc-fixture.md
rm -f docs/hook-doc-fixture.md
cat > docs/hook-conflict-fixture.md <<'EOF'
<<<<<<< HEAD
conflict
=======
other
>>>>>>> branch
EOF
expect_failure 'pre-commit rejects conflict markers' run_precommit_for docs/hook-conflict-fixture.md
rm -f docs/hook-conflict-fixture.md
i=0
while [ "$i" -lt 40000 ]; do
printf 'large hook fixture line\n'
i=$((i + 1))
done > docs/hook-large-fixture.txt
if run_precommit_for docs/hook-large-fixture.txt > "$TMP_DIR/out" 2>&1 && grep -q 'Warning: large staged text file' "$TMP_DIR/out"; then
pass 'pre-commit warns for large text files'
else
cat "$TMP_DIR/out" >&2
fail 'pre-commit warns for large text files'
fi
rm -f docs/hook-large-fixture.txt

30
scripts/push-all-remotes.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR=$(git rev-parse --show-toplevel)
BRANCH=$(git branch --show-current)
if [[ -z "$BRANCH" ]]; then
echo "当前仓库处于 detached HEAD 状态,无法推送当前分支" >&2
exit 1
fi
cd "$ROOT_DIR"
REMOTES=()
while IFS= read -r REMOTE; do
REMOTES+=("$REMOTE")
done < <(git remote)
if [[ ${#REMOTES[@]} -eq 0 ]]; then
echo "当前仓库未配置任何远端" >&2
exit 1
fi
for REMOTE in "${REMOTES[@]}"; do
echo "推送分支 $BRANCH 和 tags 到远端 $REMOTE"
git push "$REMOTE" "$BRANCH" --tags
done
echo "已推送分支 $BRANCH 和 tags 到所有远端"

View File

@@ -106,8 +106,8 @@ func printMacOSPlist(root, minMacOSVersion string) error {
} }
func printAssetName(root string, args []string) error { func printAssetName(root string, args []string) error {
if len(args) < 2 { if len(args) == 0 {
return fmt.Errorf("asset-name 至少需要 kind 和 platform 参数") return fmt.Errorf("asset-name 需要组件参数: server|web|desktop")
} }
version, err := projectversion.ReadString(root) version, err := projectversion.ReadString(root)
@@ -115,30 +115,31 @@ func printAssetName(root string, args []string) error {
return err return err
} }
var platform, arch, format string
switch args[0] { switch args[0] {
case "server": case "server", "desktop":
if len(args) != 3 { if len(args) != 4 {
return fmt.Errorf("server 资产命名需要 platformarch 参数") return fmt.Errorf("%s 资产命名需要 platformarch 和 format 参数", args[0])
} }
name, nameErr := projectversion.ServerAssetName(version, args[1], args[2]) platform = args[1]
if nameErr != nil { arch = args[2]
return nameErr format = args[3]
} case "web":
fmt.Println(name)
return nil
case "desktop":
if len(args) != 2 { if len(args) != 2 {
return fmt.Errorf("desktop 资产命名只需要 platform 参数") return fmt.Errorf("web 资产命名只需要 format 参数")
} }
name, nameErr := projectversion.DesktopAssetName(version, args[1]) format = args[1]
if nameErr != nil {
return nameErr
}
fmt.Println(name)
return nil
default: default:
return fmt.Errorf("不支持的资产类型 %q", args[0]) return fmt.Errorf("不支持的资产组件 %q", args[0])
} }
name, nameErr := projectversion.ReleaseAssetName(version, args[0], platform, arch, format)
if nameErr != nil {
return nameErr
}
fmt.Println(name)
return nil
} }
func mustGetwd() string { func mustGetwd() string {

View File

@@ -263,44 +263,84 @@ func ReadEnvVar(content, key string) (string, bool) {
return "", false return "", false
} }
func ServerAssetName(version, goos, arch string) (string, error) { func ReleaseAssetName(version, component, platform, arch, format string) (string, error) {
if _, err := Parse(version); err != nil { if _, err := Parse(version); err != nil {
return "", err return "", err
} }
switch goos { switch component {
case "linux", "windows", "darwin": case "server":
return serverAssetName(version, platform, arch, format)
case "web":
return webAssetName(version, platform, arch, format)
case "desktop":
return desktopAssetName(version, platform, arch, format)
default: default:
return "", fmt.Errorf("不支持的 server 平台 %q", goos) return "", fmt.Errorf("不支持的资产组件 %q", component)
} }
if arch == "" {
return "", errors.New("server 资产命名缺少架构")
}
ext := ".tar.gz"
if goos == "windows" {
ext = ".zip"
}
return fmt.Sprintf("nex-server_%s_%s_%s%s", version, goos, arch, ext), nil
} }
func DesktopAssetName(version, platform string) (string, error) { func serverAssetName(version, platform, arch, format string) (string, error) {
if _, err := Parse(version); err != nil { if !validCombination(platform, arch, format, []releaseAssetTarget{
return "", err {platform: "linux", arch: "amd64", format: "tar.gz"},
{platform: "linux", arch: "arm64", format: "tar.gz"},
{platform: "macos", arch: "amd64", format: "tar.gz"},
{platform: "macos", arch: "arm64", format: "tar.gz"},
{platform: "macos", arch: "universal", format: "tar.gz"},
{platform: "windows", arch: "amd64", format: "zip"},
}) {
return "", fmt.Errorf("不支持的 server 资产目标 %s/%s/%s", platform, arch, format)
} }
switch platform { return fmt.Sprintf("nex-server_%s_%s_%s.%s", version, platform, arch, format), nil
case "linux": }
return fmt.Sprintf("Nex_%s_linux_amd64.tar.gz", version), nil
case "windows": func webAssetName(version, platform, arch, format string) (string, error) {
return fmt.Sprintf("Nex_%s_windows_amd64.zip", version), nil if platform != "" || arch != "" {
case "macos": return "", errors.New("web 资产命名不支持平台或架构参数")
return fmt.Sprintf("Nex_%s_macOS_universal.zip", version), nil
default:
return "", fmt.Errorf("不支持的 desktop 平台 %q", platform)
} }
if format != "tar.gz" {
return "", fmt.Errorf("不支持的 web 资产格式 %q", format)
}
return fmt.Sprintf("nex-web_%s.tar.gz", version), nil
}
func desktopAssetName(version, platform, arch, format string) (string, error) {
if !validCombination(platform, arch, format, []releaseAssetTarget{
{platform: "linux", arch: "amd64", format: "tar.gz"},
{platform: "linux", arch: "amd64", format: "AppImage"},
{platform: "linux", arch: "amd64", format: "deb"},
{platform: "linux", arch: "amd64", format: "rpm"},
{platform: "linux", arch: "arm64", format: "tar.gz"},
{platform: "linux", arch: "arm64", format: "AppImage"},
{platform: "linux", arch: "arm64", format: "deb"},
{platform: "linux", arch: "arm64", format: "rpm"},
{platform: "macos", arch: "universal", format: "zip"},
{platform: "macos", arch: "universal", format: "dmg"},
{platform: "windows", arch: "amd64", format: "zip"},
}) {
return "", fmt.Errorf("不支持的 desktop 资产目标 %s/%s/%s", platform, arch, format)
}
return fmt.Sprintf("nex-desktop_%s_%s_%s.%s", version, platform, arch, format), nil
}
type releaseAssetTarget struct {
platform string
arch string
format string
}
func validCombination(platform, arch, format string, targets []releaseAssetTarget) bool {
for _, target := range targets {
if target.platform == platform && target.arch == arch && target.format == format {
return true
}
}
return false
} }
func DesktopInfoPlist(version, minMacOSVersion string) (string, error) { func DesktopInfoPlist(version, minMacOSVersion string) (string, error) {

View File

@@ -83,20 +83,70 @@ func TestVerifyTag(t *testing.T) {
} }
func TestAssetNames(t *testing.T) { func TestAssetNames(t *testing.T) {
linuxServer, err := ServerAssetName("1.2.3", "linux", "amd64") testCases := []struct {
require.NoError(t, err) name string
assert.Equal(t, "nex-server_1.2.3_linux_amd64.tar.gz", linuxServer) component string
platform string
arch string
format string
want string
}{
{"server linux amd64", "server", "linux", "amd64", "tar.gz", "nex-server_1.2.3_linux_amd64.tar.gz"},
{"server linux arm64", "server", "linux", "arm64", "tar.gz", "nex-server_1.2.3_linux_arm64.tar.gz"},
{"server macos amd64", "server", "macos", "amd64", "tar.gz", "nex-server_1.2.3_macos_amd64.tar.gz"},
{"server macos arm64", "server", "macos", "arm64", "tar.gz", "nex-server_1.2.3_macos_arm64.tar.gz"},
{"server macos universal", "server", "macos", "universal", "tar.gz", "nex-server_1.2.3_macos_universal.tar.gz"},
{"server windows amd64", "server", "windows", "amd64", "zip", "nex-server_1.2.3_windows_amd64.zip"},
{"web", "web", "", "", "tar.gz", "nex-web_1.2.3.tar.gz"},
{"desktop linux amd64 tar", "desktop", "linux", "amd64", "tar.gz", "nex-desktop_1.2.3_linux_amd64.tar.gz"},
{"desktop linux amd64 appimage", "desktop", "linux", "amd64", "AppImage", "nex-desktop_1.2.3_linux_amd64.AppImage"},
{"desktop linux amd64 deb", "desktop", "linux", "amd64", "deb", "nex-desktop_1.2.3_linux_amd64.deb"},
{"desktop linux amd64 rpm", "desktop", "linux", "amd64", "rpm", "nex-desktop_1.2.3_linux_amd64.rpm"},
{"desktop linux arm64 tar", "desktop", "linux", "arm64", "tar.gz", "nex-desktop_1.2.3_linux_arm64.tar.gz"},
{"desktop linux arm64 appimage", "desktop", "linux", "arm64", "AppImage", "nex-desktop_1.2.3_linux_arm64.AppImage"},
{"desktop linux arm64 deb", "desktop", "linux", "arm64", "deb", "nex-desktop_1.2.3_linux_arm64.deb"},
{"desktop linux arm64 rpm", "desktop", "linux", "arm64", "rpm", "nex-desktop_1.2.3_linux_arm64.rpm"},
{"desktop macos zip", "desktop", "macos", "universal", "zip", "nex-desktop_1.2.3_macos_universal.zip"},
{"desktop macos dmg", "desktop", "macos", "universal", "dmg", "nex-desktop_1.2.3_macos_universal.dmg"},
{"desktop windows amd64", "desktop", "windows", "amd64", "zip", "nex-desktop_1.2.3_windows_amd64.zip"},
}
macServer, err := ServerAssetName("1.2.3", "darwin", "arm64") for _, tc := range testCases {
require.NoError(t, err) t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, "nex-server_1.2.3_darwin_arm64.tar.gz", macServer) got, err := ReleaseAssetName("1.2.3", tc.component, tc.platform, tc.arch, tc.format)
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})
}
macDesktop, err := DesktopAssetName("1.2.3", "macos") invalidCases := []struct {
require.NoError(t, err) name string
assert.Equal(t, "Nex_1.2.3_macOS_universal.zip", macDesktop) component string
platform string
arch string
format string
}{
{"invalid version", "server", "linux", "amd64", "tar.gz"},
{"invalid component", "mobile", "linux", "amd64", "tar.gz"},
{"darwin platform", "server", "darwin", "arm64", "tar.gz"},
{"server unsupported format", "server", "linux", "amd64", "zip"},
{"server unsupported arch", "server", "windows", "universal", "zip"},
{"web with platform", "web", "linux", "amd64", "tar.gz"},
{"web unsupported format", "web", "", "", "zip"},
{"desktop unsupported platform", "desktop", "ios", "arm64", "zip"},
{"desktop unsupported format", "desktop", "macos", "universal", "tar.gz"},
}
_, err = DesktopAssetName("1.2.3", "ios") for _, tc := range invalidCases {
assert.Error(t, err) t.Run(tc.name, func(t *testing.T) {
version := "1.2.3"
if tc.name == "invalid version" {
version = "1.2"
}
_, err := ReleaseAssetName(version, tc.component, tc.platform, tc.arch, tc.format)
assert.Error(t, err)
})
}
} }
func TestDesktopInfoPlist(t *testing.T) { func TestDesktopInfoPlist(t *testing.T) {