Compare commits
2 Commits
b00fa4dcee
...
2c401f7ae6
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c401f7ae6 | |||
| a9972360c2 |
151
.github/workflows/release.yml
vendored
Normal file
151
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.work
|
||||
|
||||
- name: Verify tag and VERSION
|
||||
id: version
|
||||
run: |
|
||||
version=$(go run ./backend/cmd/versionctl print)
|
||||
go run ./backend/cmd/versionctl verify-tag "${GITHUB_REF_NAME}"
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-linux:
|
||||
name: Build Linux Assets
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.work
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install Linux desktop build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libayatana-appindicator3-dev libgtk-3-dev
|
||||
|
||||
- name: Build Linux release assets
|
||||
run: make release-assets-linux
|
||||
|
||||
- name: Upload Linux release assets
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-linux
|
||||
path: build/release/*
|
||||
|
||||
build-windows:
|
||||
name: Build Windows Assets
|
||||
needs: prepare
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.work
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Setup MSYS2 toolchain
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
update: true
|
||||
install: >-
|
||||
make
|
||||
mingw-w64-x86_64-gcc
|
||||
|
||||
- name: Build Windows release assets
|
||||
shell: msys2 {0}
|
||||
run: make release-assets-windows
|
||||
|
||||
- name: Upload Windows release assets
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-windows
|
||||
path: build/release/*
|
||||
|
||||
build-macos:
|
||||
name: Build macOS Assets
|
||||
needs: prepare
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.work
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Build macOS release assets
|
||||
run: make release-assets-macos
|
||||
|
||||
- name: Upload macOS release assets
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-macos
|
||||
path: build/release/*
|
||||
|
||||
draft-release:
|
||||
name: Create Draft Release
|
||||
needs: [prepare, build-linux, build-windows, build-macos]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download release assets
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: release-*
|
||||
merge-multiple: true
|
||||
path: dist
|
||||
|
||||
- name: Publish draft release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: v${{ needs.prepare.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
draft: true
|
||||
files: |
|
||||
dist/*
|
||||
382
Makefile
382
Makefile
@@ -1,175 +1,148 @@
|
||||
.PHONY: all dev build test lint clean \
|
||||
backend-build backend-run backend-dev backend-test backend-test-all backend-test-unit backend-test-integration backend-test-coverage \
|
||||
backend-lint backend-clean backend-deps backend-generate \
|
||||
backend-db-up backend-db-down backend-db-status backend-db-create \
|
||||
test-mysql-up test-mysql-down test-mysql test-mysql-quick \
|
||||
frontend-build frontend-dev frontend-test frontend-test-watch frontend-test-coverage frontend-test-e2e frontend-lint frontend-clean \
|
||||
desktop-build desktop-build-mac desktop-build-win desktop-build-linux \
|
||||
desktop-dev desktop-test desktop-clean \
|
||||
desktop-prepare-frontend desktop-prepare-embedfs desktop-prepare-windows-resource
|
||||
.PHONY: \
|
||||
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 \
|
||||
_backend-lint _backend-test _backend-clean _backend-build \
|
||||
_frontend-install _frontend-build _frontend-check _frontend-test _frontend-dev _frontend-clean \
|
||||
_desktop-test _desktop-clean _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource \
|
||||
_server-run-backend _server-run-frontend
|
||||
|
||||
VERSION := $(shell go run ./backend/cmd/versionctl print)
|
||||
GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || printf 'unknown')
|
||||
BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
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
|
||||
RELEASE_DIR := build/release
|
||||
SERVER_LINUX_ASSET := $(shell go run ./backend/cmd/versionctl asset-name server linux amd64)
|
||||
SERVER_WINDOWS_ASSET := $(shell go run ./backend/cmd/versionctl asset-name server windows amd64)
|
||||
SERVER_DARWIN_AMD64_ASSET := $(shell go run ./backend/cmd/versionctl asset-name server darwin amd64)
|
||||
SERVER_DARWIN_ARM64_ASSET := $(shell go run ./backend/cmd/versionctl asset-name server darwin arm64)
|
||||
DESKTOP_LINUX_ASSET := $(shell go run ./backend/cmd/versionctl asset-name desktop linux)
|
||||
DESKTOP_WINDOWS_ASSET := $(shell go run ./backend/cmd/versionctl asset-name desktop windows)
|
||||
DESKTOP_MACOS_ASSET := $(shell go run ./backend/cmd/versionctl asset-name desktop macos)
|
||||
|
||||
# ============================================
|
||||
# 顶层便捷命令
|
||||
# 全局命令
|
||||
# ============================================
|
||||
|
||||
dev:
|
||||
@echo "🚀 Starting development environment..."
|
||||
@$(MAKE) -j2 backend-dev frontend-dev
|
||||
lint: _backend-lint _frontend-check
|
||||
@printf 'Lint complete\n'
|
||||
|
||||
build: backend-build frontend-build
|
||||
@echo "✅ Build complete"
|
||||
test: _backend-test _frontend-test _desktop-test
|
||||
@printf 'All tests passed\n'
|
||||
|
||||
test: backend-test desktop-test frontend-test
|
||||
@echo "✅ All tests passed"
|
||||
|
||||
lint: backend-lint frontend-lint
|
||||
@echo "✅ Lint complete"
|
||||
|
||||
all: build test lint
|
||||
clean: _backend-clean _frontend-clean _desktop-clean
|
||||
@printf 'Clean complete\n'
|
||||
|
||||
# ============================================
|
||||
# 后端
|
||||
# 版本管理
|
||||
# ============================================
|
||||
|
||||
backend-build:
|
||||
cd backend && go build -o bin/server ./cmd/server
|
||||
version-sync:
|
||||
go run ./backend/cmd/versionctl sync
|
||||
|
||||
backend-run:
|
||||
cd backend && go run ./cmd/server
|
||||
|
||||
backend-dev:
|
||||
cd backend && go run ./cmd/server
|
||||
|
||||
backend-test:
|
||||
cd backend && go test ./internal/... ./pkg/... ./tests/... ./cmd/server/... -v
|
||||
|
||||
backend-test-all:
|
||||
cd backend && go test ./... -v
|
||||
|
||||
backend-test-unit:
|
||||
cd backend && go test ./internal/... ./pkg/... -v
|
||||
|
||||
backend-test-integration:
|
||||
cd backend && go test ./tests/... -v
|
||||
|
||||
backend-test-coverage:
|
||||
cd backend && go test ./... -coverprofile=coverage.out
|
||||
cd backend && go tool cover -html=coverage.out -o coverage.html
|
||||
@echo "Coverage report generated: backend/coverage.html"
|
||||
|
||||
backend-lint:
|
||||
cd backend && go tool golangci-lint run ./...
|
||||
|
||||
backend-clean:
|
||||
rm -rf backend/bin/ backend/coverage.out backend/coverage.html
|
||||
|
||||
backend-deps:
|
||||
cd backend && go mod tidy
|
||||
|
||||
backend-generate:
|
||||
cd backend && go generate ./...
|
||||
|
||||
backend-db-up:
|
||||
@echo "Running database migration up..."
|
||||
cd backend && goose -dir migrations/sqlite sqlite3 "$(DB_PATH)" up
|
||||
|
||||
backend-db-down:
|
||||
@echo "Running database migration down..."
|
||||
cd backend && goose -dir migrations/sqlite sqlite3 "$(DB_PATH)" down
|
||||
|
||||
backend-db-status:
|
||||
@echo "Checking database migration status..."
|
||||
cd backend && goose -dir migrations/sqlite sqlite3 "$(DB_PATH)" status
|
||||
|
||||
backend-db-create:
|
||||
@read -p "Migration name: " name; \
|
||||
cd backend && goose -dir migrations/sqlite create $$name sql; \
|
||||
cd backend && goose -dir migrations/mysql create $$name sql
|
||||
version-check:
|
||||
go run ./backend/cmd/versionctl check
|
||||
|
||||
# ============================================
|
||||
# MySQL 专项测试
|
||||
# Server 模式
|
||||
# ============================================
|
||||
|
||||
test-mysql-up:
|
||||
@echo "Starting MySQL test container..."
|
||||
cd backend/tests/mysql && docker-compose up -d
|
||||
@echo "Waiting for MySQL to be ready..."
|
||||
@for i in $$(seq 1 30); do \
|
||||
if docker exec nex-mysql-test mysqladmin ping -h localhost -u root -ptestpass --silent 2>/dev/null; then \
|
||||
echo "MySQL is ready!"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
echo "Waiting... ($$i/30)"; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "MySQL failed to start"; \
|
||||
exit 1
|
||||
server-run:
|
||||
@$(MAKE) -j2 _server-run-backend _server-run-frontend
|
||||
|
||||
test-mysql-down:
|
||||
@echo "Stopping MySQL test container..."
|
||||
cd backend/tests/mysql && docker-compose down -v
|
||||
server-build: version-check _backend-build _frontend-build
|
||||
@printf 'Server build complete\n'
|
||||
|
||||
test-mysql: test-mysql-up
|
||||
@echo "Running MySQL tests..."
|
||||
cd backend && go test -tags=mysql ./tests/mysql/... -v -count=1
|
||||
$(MAKE) test-mysql-down
|
||||
server-lint: _backend-lint _frontend-check
|
||||
@printf 'Server lint complete\n'
|
||||
|
||||
test-mysql-quick:
|
||||
@echo "Running MySQL tests (without container management)..."
|
||||
cd backend && go test -tags=mysql ./tests/mysql/... -v -count=1
|
||||
server-test: _backend-test _frontend-test
|
||||
@printf 'Server tests passed\n'
|
||||
|
||||
server-clean: _backend-clean _frontend-clean
|
||||
@printf 'Server artifacts cleaned\n'
|
||||
|
||||
_server-run-backend:
|
||||
@$(MAKE) -C backend run
|
||||
|
||||
_server-run-frontend: _frontend-install
|
||||
cd frontend && bun run dev
|
||||
|
||||
# ============================================
|
||||
# 前端
|
||||
# Desktop 模式
|
||||
# ============================================
|
||||
|
||||
frontend-install:
|
||||
cd frontend && bun install
|
||||
desktop-build-mac: version-check _desktop-prepare-frontend _desktop-prepare-embedfs
|
||||
@printf 'Building macOS desktop...\n'
|
||||
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
|
||||
lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal
|
||||
@printf 'Packaging macOS app bundle...\n'
|
||||
mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources
|
||||
cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex
|
||||
@if [ -f assets/icon.icns ]; then \
|
||||
cp assets/icon.icns build/Nex.app/Contents/Resources/; \
|
||||
else \
|
||||
printf 'Missing assets/icon.icns\n'; \
|
||||
fi
|
||||
@MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \
|
||||
if [ -z "$$MIN_MACOS_VERSION" ]; then \
|
||||
printf 'Unable to read macOS minimum version\n'; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
go run ./backend/cmd/versionctl macos-plist "$$MIN_MACOS_VERSION" > build/Nex.app/Contents/Info.plist
|
||||
chmod +x build/Nex.app/Contents/MacOS/nex
|
||||
@printf 'macOS desktop build complete\n'
|
||||
|
||||
frontend-build: frontend-install
|
||||
cd frontend && bun run build
|
||||
desktop-build-win: version-check _desktop-prepare-frontend _desktop-prepare-embedfs _desktop-prepare-windows-resource
|
||||
@printf 'Building Windows desktop...\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
|
||||
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS_WIN)" -o ../build/nex-win-amd64.exe ./cmd/desktop
|
||||
endif
|
||||
@printf 'Windows desktop build complete\n'
|
||||
|
||||
frontend-dev: frontend-install
|
||||
cd frontend && bun dev
|
||||
desktop-build-linux: version-check _desktop-prepare-frontend _desktop-prepare-embedfs
|
||||
@printf 'Building Linux desktop...\n'
|
||||
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-linux-amd64 ./cmd/desktop
|
||||
@printf 'Linux desktop build complete\n'
|
||||
|
||||
frontend-test: frontend-install
|
||||
cd frontend && bun run test
|
||||
desktop-lint: _backend-lint _frontend-check
|
||||
@printf 'Desktop lint complete\n'
|
||||
|
||||
frontend-test-watch: frontend-install
|
||||
cd frontend && bun run test:watch
|
||||
desktop-test: _desktop-test
|
||||
@printf 'Desktop tests passed\n'
|
||||
|
||||
frontend-test-coverage: frontend-install
|
||||
cd frontend && bun run test:coverage
|
||||
desktop-clean: _desktop-clean
|
||||
@printf 'Desktop artifacts cleaned\n'
|
||||
|
||||
frontend-test-e2e: frontend-install
|
||||
cd frontend && bun run test:e2e
|
||||
_desktop-test:
|
||||
cd backend && go test ./cmd/desktop/... -v
|
||||
|
||||
frontend-lint: frontend-install
|
||||
cd frontend && bun run lint
|
||||
_desktop-clean:
|
||||
rm -rf build/ embedfs/assets embedfs/frontend-dist backend/cmd/desktop/rsrc_windows_amd64.syso
|
||||
|
||||
frontend-clean:
|
||||
rm -rf frontend/dist frontend/.next frontend/node_modules frontend/coverage frontend/playwright-report frontend/test-results frontend/tsconfig.tsbuildinfo
|
||||
|
||||
# ============================================
|
||||
# 桌面应用
|
||||
# ============================================
|
||||
|
||||
desktop-build: desktop-build-mac desktop-build-win desktop-build-linux
|
||||
@echo "✅ Desktop builds complete for all platforms"
|
||||
|
||||
desktop-prepare-frontend:
|
||||
@echo "📦 Preparing frontend for desktop..."
|
||||
_desktop-prepare-frontend: _frontend-install
|
||||
@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 install && bun run build
|
||||
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 && bun install && bun run build
|
||||
cd frontend && bun run build
|
||||
rm -f frontend/.env.production.local
|
||||
endif
|
||||
|
||||
desktop-prepare-embedfs:
|
||||
@echo "📦 Preparing embedded filesystem..."
|
||||
_desktop-prepare-embedfs:
|
||||
@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
|
||||
@@ -178,8 +151,8 @@ else
|
||||
cp -r frontend/dist embedfs/frontend-dist
|
||||
endif
|
||||
|
||||
desktop-prepare-windows-resource:
|
||||
@echo "📦 Preparing Windows executable icon..."
|
||||
_desktop-prepare-windows-resource:
|
||||
@printf 'Preparing Windows executable icon...\n'
|
||||
ifeq ($(OS),Windows_NT)
|
||||
cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso
|
||||
else
|
||||
@@ -188,97 +161,72 @@ else
|
||||
elif command -v windres >/dev/null 2>&1; then \
|
||||
cd backend/cmd/desktop && windres -O coff -F pe-x86-64 -i icon_windows.rc -o rsrc_windows_amd64.syso; \
|
||||
else \
|
||||
echo "❌ 未找到 windres,无法生成 Windows exe 图标资源"; \
|
||||
printf 'Missing windres for Windows icon resource generation\n'; \
|
||||
exit 1; \
|
||||
fi
|
||||
endif
|
||||
|
||||
desktop-build-mac: desktop-prepare-frontend desktop-prepare-embedfs
|
||||
@echo "🍎 Building macOS..."
|
||||
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../build/nex-mac-arm64 ./cmd/desktop
|
||||
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-mac-amd64 ./cmd/desktop
|
||||
lipo -create build/nex-mac-arm64 build/nex-mac-amd64 -output build/nex-mac-universal
|
||||
@echo "📦 Packaging macOS .app..."
|
||||
mkdir -p build/Nex.app/Contents/MacOS build/Nex.app/Contents/Resources
|
||||
cp build/nex-mac-universal build/Nex.app/Contents/MacOS/nex
|
||||
@if [ -f assets/icon.icns ]; then \
|
||||
cp assets/icon.icns build/Nex.app/Contents/Resources/; \
|
||||
else \
|
||||
echo "⚠️ 未找到 assets/icon.icns"; \
|
||||
fi
|
||||
@MIN_MACOS_VERSION=$$(vtool -show-build build/nex-mac-universal | awk '/minos / {print $$2; exit}'); \
|
||||
if [ -z "$$MIN_MACOS_VERSION" ]; then \
|
||||
echo "❌ 无法读取 macOS 最低系统版本"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
{ \
|
||||
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \
|
||||
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
|
||||
'<plist version="1.0">' \
|
||||
'<dict>' \
|
||||
' <key>CFBundleDevelopmentRegion</key>' \
|
||||
' <string>zh-Hans</string>' \
|
||||
' <key>CFBundleExecutable</key>' \
|
||||
' <string>nex</string>' \
|
||||
' <key>CFBundleIconFile</key>' \
|
||||
' <string>icon</string>' \
|
||||
' <key>CFBundleIdentifier</key>' \
|
||||
' <string>com.lanyuanxiaoyao.nex</string>' \
|
||||
' <key>CFBundleInfoDictionaryVersion</key>' \
|
||||
' <string>6.0</string>' \
|
||||
' <key>LSApplicationCategoryType</key>' \
|
||||
' <string>public.app-category.developer-tools</string>' \
|
||||
' <key>CFBundleName</key>' \
|
||||
' <string>Nex</string>' \
|
||||
' <key>CFBundleDisplayName</key>' \
|
||||
' <string>Nex</string>' \
|
||||
' <key>CFBundlePackageType</key>' \
|
||||
' <string>APPL</string>' \
|
||||
' <key>CFBundleShortVersionString</key>' \
|
||||
' <string>1.0.0</string>' \
|
||||
' <key>CFBundleVersion</key>' \
|
||||
' <string>1.0.0</string>' \
|
||||
' <key>NSHumanReadableCopyright</key>' \
|
||||
' <string>Copyright © 2026 Nex</string>' \
|
||||
' <key>LSMinimumSystemVersion</key>' \
|
||||
" <string>$$MIN_MACOS_VERSION</string>" \
|
||||
' <key>LSUIElement</key>' \
|
||||
' <true/>' \
|
||||
' <key>NSHighResolutionCapable</key>' \
|
||||
' <true/>' \
|
||||
'</dict>' \
|
||||
'</plist>'; \
|
||||
} > build/Nex.app/Contents/Info.plist
|
||||
chmod +x build/Nex.app/Contents/MacOS/nex
|
||||
@echo "✅ macOS app packaged: build/Nex.app"
|
||||
# ============================================
|
||||
# 发布资产
|
||||
# ============================================
|
||||
|
||||
desktop-build-win: desktop-prepare-frontend desktop-prepare-embedfs desktop-prepare-windows-resource
|
||||
@echo "🪟 Building Windows..."
|
||||
release-assets-linux: version-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
|
||||
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 "-H=windowsgui" -o ../build/nex-win-amd64.exe ./cmd/desktop
|
||||
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
|
||||
mkdir -p build
|
||||
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o ../build/nex-win-amd64.exe ./cmd/desktop
|
||||
@printf 'release-assets-windows requires Windows\n'
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
desktop-build-linux: desktop-prepare-frontend desktop-prepare-embedfs
|
||||
@echo "🐧 Building Linux..."
|
||||
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o ../build/nex-linux-amd64 ./cmd/desktop
|
||||
|
||||
desktop-dev: desktop-prepare-frontend desktop-prepare-embedfs
|
||||
@echo "🖥️ Starting desktop app in dev mode..."
|
||||
cd backend && go run ./cmd/desktop
|
||||
|
||||
desktop-test:
|
||||
cd backend && go test ./cmd/desktop/... -v
|
||||
|
||||
desktop-clean:
|
||||
rm -rf build/ embedfs/assets embedfs/frontend-dist
|
||||
release-assets-macos: version-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
|
||||
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -ldflags "$(GO_LDFLAGS)" -o ../build/nex-server-darwin-arm64 ./cmd/server
|
||||
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
|
||||
ditto -c -k --keepParent build/Nex.app "$(RELEASE_DIR)/$(DESKTOP_MACOS_ASSET)"
|
||||
|
||||
# ============================================
|
||||
# 清理
|
||||
# 共享 helper targets
|
||||
# ============================================
|
||||
|
||||
clean: backend-clean frontend-clean desktop-clean
|
||||
@echo "✅ Clean complete"
|
||||
_backend-build:
|
||||
@$(MAKE) -C backend build
|
||||
|
||||
_backend-lint:
|
||||
@$(MAKE) -C backend lint
|
||||
|
||||
_backend-test:
|
||||
@$(MAKE) -C backend test
|
||||
|
||||
_backend-clean:
|
||||
@$(MAKE) -C backend clean
|
||||
|
||||
_frontend-install:
|
||||
cd frontend && bun install
|
||||
|
||||
_frontend-build: _frontend-install
|
||||
cd frontend && bun run build
|
||||
|
||||
_frontend-check: _frontend-install
|
||||
cd frontend && bun run check
|
||||
|
||||
_frontend-test: _frontend-install
|
||||
cd frontend && bun run test
|
||||
|
||||
_frontend-dev: _frontend-install
|
||||
cd frontend && bun run dev
|
||||
|
||||
_frontend-clean:
|
||||
rm -rf frontend/dist frontend/.next frontend/coverage frontend/playwright-report frontend/test-results frontend/tsconfig.tsbuildinfo
|
||||
|
||||
141
README.md
141
README.md
@@ -109,9 +109,6 @@ make desktop-build-win
|
||||
|
||||
# Linux
|
||||
make desktop-build-linux
|
||||
|
||||
# 构建所有平台
|
||||
make desktop-build
|
||||
```
|
||||
|
||||
**使用桌面应用**:
|
||||
@@ -132,32 +129,28 @@ make desktop-build
|
||||
- Xfce: 需要 libappindicator
|
||||
- 其他支持 StatusNotifierItem 规范的环境
|
||||
|
||||
### CLI 模式
|
||||
|
||||
#### 后端
|
||||
### Server 模式(前后端分离)
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go mod download
|
||||
go run cmd/server/main.go
|
||||
make server-run
|
||||
```
|
||||
|
||||
后端服务将在 `http://localhost:9826` 启动。首次启动会自动:
|
||||
`make server-run` 会并行启动:
|
||||
- 后端服务:`http://localhost:9826`
|
||||
- 前端开发服务器:`http://localhost:5173`
|
||||
|
||||
前端请求会继续通过 Vite proxy 转发到后端。后端首次启动会自动:
|
||||
- 创建配置文件 `~/.nex/config.yaml`
|
||||
- 初始化数据库 `~/.nex/config.db`
|
||||
- 运行数据库迁移
|
||||
- 创建日志目录 `~/.nex/log/`
|
||||
|
||||
### 前端
|
||||
**构建 server 模式产物**:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
bun install
|
||||
bun dev
|
||||
make server-build
|
||||
```
|
||||
|
||||
前端开发服务器将在 `http://localhost:5173` 启动,API 请求通过 Vite proxy 转发到后端。
|
||||
|
||||
## API 接口
|
||||
|
||||
### 代理接口(对外部应用)
|
||||
@@ -279,53 +272,101 @@ export NEX_DATABASE_DBNAME=nex
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 顶层便捷命令
|
||||
make test # 运行所有测试
|
||||
# 全局默认测试(不含 MySQL 和前端 E2E)
|
||||
make test
|
||||
|
||||
# 后端测试
|
||||
make backend-test # 后端测试
|
||||
make backend-test-coverage # 后端覆盖率
|
||||
make backend-test-unit # 后端单元测试
|
||||
make backend-test-integration # 后端集成测试
|
||||
|
||||
# 前端测试
|
||||
make frontend-test # 前端测试
|
||||
make frontend-test-e2e # 前端 E2E 测试
|
||||
make frontend-test-coverage # 前端覆盖率
|
||||
# 产品级测试
|
||||
make server-test
|
||||
make desktop-test
|
||||
```
|
||||
|
||||
backend 分类测试、MySQL 专项测试和前端 E2E 测试请分别查看 `backend/README.md` 与 `frontend/README.md`。
|
||||
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
# 首次克隆后安装 Git hooks
|
||||
lefthook install
|
||||
|
||||
# 顶层便捷命令
|
||||
make dev # 启动开发环境(并行启动后端和前端)
|
||||
make build # 构建所有产物
|
||||
make lint # 检查所有代码
|
||||
make clean # 清理所有构建产物
|
||||
# 全局命令
|
||||
make lint # 前后端共享检查
|
||||
make test # 默认全量测试(不含 MySQL/E2E)
|
||||
make clean # 清理所有构建产物和测试报告
|
||||
|
||||
# 后端开发
|
||||
make backend-build # 构建后端
|
||||
make backend-run # 运行后端
|
||||
make backend-dev # 后端开发模式
|
||||
make backend-lint # 后端代码检查
|
||||
make backend-clean # 清理后端构建产物
|
||||
# server 模式
|
||||
make server-run # 并行启动后端和前端开发服务
|
||||
make server-build # 构建 backend/bin/server 和 frontend/dist
|
||||
make server-lint # server 模式检查
|
||||
make server-test # server 模式测试
|
||||
make server-clean # 清理 server 模式产物
|
||||
|
||||
# 数据库操作
|
||||
make backend-db-up # 数据库迁移
|
||||
make backend-db-down # 数据库回滚
|
||||
make backend-db-status # 数据库迁移状态
|
||||
make backend-db-create # 创建新迁移
|
||||
|
||||
# 前端开发
|
||||
make frontend-build # 构建前端
|
||||
make frontend-dev # 前端开发模式
|
||||
make frontend-lint # 前端代码检查
|
||||
make frontend-clean # 清理前端构建产物
|
||||
# desktop 模式
|
||||
make desktop-build-mac # 构建 macOS 桌面应用
|
||||
make desktop-build-win # 构建 Windows 桌面应用
|
||||
make desktop-build-linux # 构建 Linux 桌面应用
|
||||
make desktop-lint # desktop 模式检查
|
||||
make desktop-test # desktop 专属测试
|
||||
make desktop-clean # 清理 desktop 产物
|
||||
```
|
||||
|
||||
## 版本与发布
|
||||
|
||||
### 统一版本源
|
||||
|
||||
- 仓库根目录 `VERSION` 是全仓唯一版本源,格式固定为 `x.y.z`
|
||||
- `frontend/package.json` 和前端 `.env.*` 中的 `VITE_APP_VERSION` 由仓库工具同步,不能手工漂移
|
||||
|
||||
### 本地版本演进
|
||||
|
||||
1. 手工修改根目录 `VERSION` 为新的 `x.y.z`
|
||||
2. 同步镜像文件:
|
||||
|
||||
```bash
|
||||
make version-sync
|
||||
```
|
||||
|
||||
3. 校验版本一致性:
|
||||
|
||||
```bash
|
||||
make version-check
|
||||
```
|
||||
|
||||
4. 提交版本变更后,创建发布 tag:
|
||||
|
||||
```bash
|
||||
git tag -a vX.Y.Z -m "Release vX.Y.Z"
|
||||
git push origin main
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
### 本地生成发布资产
|
||||
|
||||
```bash
|
||||
# Linux: server + desktop
|
||||
make release-assets-linux
|
||||
|
||||
# Windows: server + desktop(需在 Windows 环境执行)
|
||||
make release-assets-windows
|
||||
|
||||
# macOS: darwin-amd64 server、darwin-arm64 server、desktop universal
|
||||
make release-assets-macos
|
||||
```
|
||||
|
||||
生成的版本化发布资产位于 `build/release/`。
|
||||
|
||||
### GitHub Draft Release
|
||||
|
||||
- 推送 `vX.Y.Z` tag 后,`.github/workflows/release.yml` 会自动执行发布流水线
|
||||
- 流水线会先校验 tag 与 `VERSION` 一致,再构建以下资产并上传到 GitHub Draft Release:
|
||||
- Linux server
|
||||
- Windows server
|
||||
- darwin-amd64 server
|
||||
- darwin-arm64 server
|
||||
- Linux desktop
|
||||
- Windows desktop
|
||||
- macOS desktop universal
|
||||
- Release 默认以 Draft 形式创建,需人工检查后再公开发布
|
||||
|
||||
## 开发规范
|
||||
|
||||
详见各子项目的 README.md:
|
||||
|
||||
97
backend/Makefile
Normal file
97
backend/Makefile
Normal file
@@ -0,0 +1,97 @@
|
||||
.PHONY: \
|
||||
build run \
|
||||
test test-unit test-integration test-coverage \
|
||||
lint clean \
|
||||
migrate-up migrate-down migrate-status migrate-create \
|
||||
mysql-up mysql-down mysql-test mysql-test-quick
|
||||
|
||||
VERSION := $(shell go run ./cmd/versionctl print)
|
||||
GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || printf 'unknown')
|
||||
BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
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)
|
||||
|
||||
DB_DRIVER ?= sqlite3
|
||||
DB_DSN ?= $(HOME)/.nex/config.db
|
||||
|
||||
ifeq ($(DB_DRIVER),mysql)
|
||||
GOOSE_DIR := migrations/mysql
|
||||
GOOSE_DRIVER := mysql
|
||||
else ifeq ($(DB_DRIVER),sqlite3)
|
||||
GOOSE_DIR := migrations/sqlite
|
||||
GOOSE_DRIVER := sqlite3
|
||||
else
|
||||
$(error unsupported DB_DRIVER '$(DB_DRIVER)', use sqlite3 or mysql)
|
||||
endif
|
||||
|
||||
build:
|
||||
go build -ldflags "$(GO_LDFLAGS)" -o bin/server ./cmd/server
|
||||
|
||||
run:
|
||||
go run -ldflags "$(GO_LDFLAGS)" ./cmd/server
|
||||
|
||||
test:
|
||||
go test ./internal/... ./pkg/... ./tests/... ./cmd/server/... -v
|
||||
|
||||
test-unit:
|
||||
go test ./internal/... ./pkg/... -v
|
||||
|
||||
test-integration:
|
||||
go test ./tests/... -v
|
||||
|
||||
test-coverage:
|
||||
go test ./... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
@printf 'Coverage report generated: backend/coverage.html\n'
|
||||
|
||||
lint:
|
||||
go tool golangci-lint run ./...
|
||||
|
||||
clean:
|
||||
rm -rf bin/ coverage.out coverage.html
|
||||
|
||||
migrate-up:
|
||||
@printf 'Running database migration up...\n'
|
||||
goose -dir $(GOOSE_DIR) $(GOOSE_DRIVER) "$(DB_DSN)" up
|
||||
|
||||
migrate-down:
|
||||
@printf 'Running database migration down...\n'
|
||||
goose -dir $(GOOSE_DIR) $(GOOSE_DRIVER) "$(DB_DSN)" down
|
||||
|
||||
migrate-status:
|
||||
@printf 'Checking database migration status...\n'
|
||||
goose -dir $(GOOSE_DIR) $(GOOSE_DRIVER) "$(DB_DSN)" status
|
||||
|
||||
migrate-create:
|
||||
@printf 'Migration name: '; \
|
||||
read name; \
|
||||
goose -dir migrations/sqlite create $$name sql; \
|
||||
goose -dir migrations/mysql create $$name sql
|
||||
|
||||
mysql-up:
|
||||
@printf 'Starting MySQL test container...\n'
|
||||
cd tests/mysql && docker-compose up -d
|
||||
@printf 'Waiting for MySQL to be ready...\n'
|
||||
@for i in $$(seq 1 30); do \
|
||||
if docker exec nex-mysql-test mysqladmin ping -h localhost -u root -ptestpass --silent 2>/dev/null; then \
|
||||
printf 'MySQL is ready\n'; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
printf 'Waiting... (%s/30)\n' $$i; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
printf 'MySQL failed to start\n'; \
|
||||
exit 1
|
||||
|
||||
mysql-down:
|
||||
@printf 'Stopping MySQL test container...\n'
|
||||
cd tests/mysql && docker-compose down -v
|
||||
|
||||
mysql-test:
|
||||
@set -e; \
|
||||
$(MAKE) mysql-up; \
|
||||
trap '$(MAKE) mysql-down' EXIT; \
|
||||
go test -tags=mysql ./tests/mysql/... -v -count=1
|
||||
|
||||
mysql-test-quick:
|
||||
@printf 'Running MySQL tests without container management...\n'
|
||||
go test -tags=mysql ./tests/mysql/... -v -count=1
|
||||
@@ -437,24 +437,37 @@ docker run -d -e NEX_SERVER_PORT=9000 -e NEX_LOG_LEVEL=info nex-server
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
# 运行 backend 默认测试
|
||||
make test
|
||||
|
||||
# 分类测试
|
||||
make test-unit
|
||||
make test-integration
|
||||
|
||||
# 生成覆盖率报告
|
||||
make test-coverage
|
||||
|
||||
# MySQL 专项测试
|
||||
make mysql-up
|
||||
make mysql-down
|
||||
make mysql-test
|
||||
make mysql-test-quick
|
||||
```
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
```bash
|
||||
# 使用 Makefile
|
||||
make migrate-up DB_PATH=~/.nex/config.db
|
||||
make migrate-down DB_PATH=~/.nex/config.db
|
||||
make migrate-status DB_PATH=~/.nex/config.db
|
||||
make migrate-up DB_DSN=~/.nex/config.db
|
||||
make migrate-down DB_DSN=~/.nex/config.db
|
||||
make migrate-status DB_DSN=~/.nex/config.db
|
||||
|
||||
# 创建新迁移
|
||||
make migrate-create
|
||||
|
||||
# MySQL 迁移
|
||||
make migrate-up DB_DRIVER=mysql DB_DSN='user:pass@tcp(localhost:3306)/nex'
|
||||
|
||||
# 或直接使用 goose
|
||||
goose -dir migrations sqlite3 ~/.nex/config.db up
|
||||
```
|
||||
@@ -571,9 +584,12 @@ GET /anthropic/v1/models
|
||||
## 开发
|
||||
|
||||
```bash
|
||||
make build # 构建
|
||||
make lint # 代码检查
|
||||
make deps # 整理依赖
|
||||
make build # 构建 backend/bin/server
|
||||
make run # 运行后端服务
|
||||
make lint # 代码检查
|
||||
make clean # 清理 backend 构建产物
|
||||
go mod tidy # 整理依赖
|
||||
go generate ./... # 刷新 mock 等生成代码
|
||||
```
|
||||
|
||||
环境要求:Go 1.26 或更高版本
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"nex/backend/internal/provider"
|
||||
"nex/backend/internal/repository"
|
||||
"nex/backend/internal/service"
|
||||
"nex/backend/pkg/buildinfo"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -151,7 +152,11 @@ func main() {
|
||||
shutdownCtx, shutdownCancel = context.WithCancel(context.Background())
|
||||
|
||||
go func() {
|
||||
zapLogger.Info("AI Gateway 启动", zap.String("addr", server.Addr))
|
||||
zapLogger.Info("AI Gateway 启动",
|
||||
zap.String("addr", server.Addr),
|
||||
zap.String("version", buildinfo.Version()),
|
||||
zap.String("commit", buildinfo.Commit()),
|
||||
zap.String("build_time", buildinfo.BuildTime()))
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
zapLogger.Fatal("服务器启动失败", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"nex/backend/internal/provider"
|
||||
"nex/backend/internal/repository"
|
||||
"nex/backend/internal/service"
|
||||
"nex/backend/pkg/buildinfo"
|
||||
pkgLogger "nex/backend/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -111,7 +112,11 @@ func main() {
|
||||
}
|
||||
|
||||
go func() {
|
||||
zapLogger.Info("AI Gateway 启动", zap.String("addr", srv.Addr))
|
||||
zapLogger.Info("AI Gateway 启动",
|
||||
zap.String("addr", srv.Addr),
|
||||
zap.String("version", buildinfo.Version()),
|
||||
zap.String("commit", buildinfo.Commit()),
|
||||
zap.String("build_time", buildinfo.BuildTime()))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
zapLogger.Fatal("服务器启动失败", zap.Error(err))
|
||||
}
|
||||
|
||||
119
backend/cmd/versionctl/main.go
Normal file
119
backend/cmd/versionctl/main.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"nex/backend/pkg/projectversion"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return usageError()
|
||||
}
|
||||
|
||||
root, err := projectversion.FindRepoRoot(mustGetwd())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "print":
|
||||
version, readErr := projectversion.ReadString(root)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
fmt.Println(version)
|
||||
return nil
|
||||
case "sync":
|
||||
return projectversion.Sync(root)
|
||||
case "check":
|
||||
return projectversion.Check(root)
|
||||
case "verify-tag":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("verify-tag 需要一个 tag 参数")
|
||||
}
|
||||
return projectversion.VerifyTag(root, args[1])
|
||||
case "macos-plist":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("macos-plist 需要一个最低系统版本参数")
|
||||
}
|
||||
return printMacOSPlist(root, args[1])
|
||||
case "asset-name":
|
||||
return printAssetName(root, args[1:])
|
||||
default:
|
||||
return usageError()
|
||||
}
|
||||
}
|
||||
|
||||
func printMacOSPlist(root, minMacOSVersion string) error {
|
||||
version, err := projectversion.ReadString(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plist, err := projectversion.DesktopInfoPlist(version, minMacOSVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(plist)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printAssetName(root string, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return fmt.Errorf("asset-name 至少需要 kind 和 platform 参数")
|
||||
}
|
||||
|
||||
version, err := projectversion.ReadString(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case "server":
|
||||
if len(args) != 3 {
|
||||
return fmt.Errorf("server 资产命名需要 platform 和 arch 参数")
|
||||
}
|
||||
name, nameErr := projectversion.ServerAssetName(version, args[1], args[2])
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
fmt.Println(name)
|
||||
return nil
|
||||
case "desktop":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("desktop 资产命名只需要 platform 参数")
|
||||
}
|
||||
name, nameErr := projectversion.DesktopAssetName(version, args[1])
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
fmt.Println(name)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("不支持的资产类型 %q", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetwd() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return wd
|
||||
}
|
||||
|
||||
func usageError() error {
|
||||
return fmt.Errorf("用法: versionctl <print|sync|check|verify-tag|macos-plist|asset-name>")
|
||||
}
|
||||
22
backend/pkg/buildinfo/buildinfo.go
Normal file
22
backend/pkg/buildinfo/buildinfo.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package buildinfo
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "unknown"
|
||||
buildTime = "unknown"
|
||||
)
|
||||
|
||||
// Version 返回构建注入的版本号。
|
||||
func Version() string {
|
||||
return version
|
||||
}
|
||||
|
||||
// Commit 返回构建注入的 git commit。
|
||||
func Commit() string {
|
||||
return commit
|
||||
}
|
||||
|
||||
// BuildTime 返回构建注入的构建时间。
|
||||
func BuildTime() string {
|
||||
return buildTime
|
||||
}
|
||||
17
backend/pkg/buildinfo/buildinfo_test.go
Normal file
17
backend/pkg/buildinfo/buildinfo_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package buildinfo
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaults(t *testing.T) {
|
||||
if Version() == "" {
|
||||
t.Fatal("Version() 不应为空")
|
||||
}
|
||||
|
||||
if Commit() == "" {
|
||||
t.Fatal("Commit() 不应为空")
|
||||
}
|
||||
|
||||
if BuildTime() == "" {
|
||||
t.Fatal("BuildTime() 不应为空")
|
||||
}
|
||||
}
|
||||
342
backend/pkg/projectversion/version.go
Normal file
342
backend/pkg/projectversion/version.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package projectversion
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const versionFileName = "VERSION"
|
||||
|
||||
var (
|
||||
semverRegex = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$`)
|
||||
packageVersionRegex = regexp.MustCompile(`(?m)^(\s*"version"\s*:\s*")([^"]+)(",?)$`)
|
||||
frontendVersionFiles = []string{
|
||||
"frontend/.env.production",
|
||||
"frontend/.env.development",
|
||||
"frontend/.env.desktop",
|
||||
}
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
}
|
||||
|
||||
func Parse(raw string) (Version, error) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
parts := semverRegex.FindStringSubmatch(trimmed)
|
||||
if parts == nil {
|
||||
return Version{}, fmt.Errorf("版本号 %q 不符合 x.y.z 格式", raw)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("解析 major 失败: %w", err)
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("解析 minor 失败: %w", err)
|
||||
}
|
||||
|
||||
patch, err := strconv.Atoi(parts[3])
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("解析 patch 失败: %w", err)
|
||||
}
|
||||
|
||||
return Version{Major: major, Minor: minor, Patch: patch}, nil
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
}
|
||||
|
||||
func FindRepoRoot(start string) (string, error) {
|
||||
current := start
|
||||
for {
|
||||
workspacePath := filepath.Join(current, "go.work")
|
||||
if _, err := os.Stat(workspacePath); err == nil {
|
||||
return current, nil
|
||||
}
|
||||
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return "", errors.New("未找到仓库根目录 go.work")
|
||||
}
|
||||
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
func Read(root string) (Version, error) {
|
||||
content, err := os.ReadFile(filepath.Join(root, versionFileName))
|
||||
if err != nil {
|
||||
return Version{}, fmt.Errorf("读取 VERSION 失败: %w", err)
|
||||
}
|
||||
|
||||
return Parse(string(content))
|
||||
}
|
||||
|
||||
func ReadString(root string) (string, error) {
|
||||
version, err := Read(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return version.String(), nil
|
||||
}
|
||||
|
||||
func Sync(root string) error {
|
||||
version, err := ReadString(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
packageJSONPath := filepath.Join(root, "frontend", "package.json")
|
||||
packageJSONContent, err := os.ReadFile(packageJSONPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 frontend/package.json 失败: %w", err)
|
||||
}
|
||||
|
||||
updatedPackageJSON, err := UpdatePackageJSONVersion(string(packageJSONContent), version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(packageJSONPath, []byte(updatedPackageJSON), 0o644); err != nil {
|
||||
return fmt.Errorf("写入 frontend/package.json 失败: %w", err)
|
||||
}
|
||||
|
||||
for _, relPath := range frontendVersionFiles {
|
||||
fullPath := filepath.Join(root, relPath)
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 %s 失败: %w", relPath, err)
|
||||
}
|
||||
|
||||
updated := UpsertEnvVar(string(content), "VITE_APP_VERSION", version)
|
||||
if err := os.WriteFile(fullPath, []byte(updated), 0o644); err != nil {
|
||||
return fmt.Errorf("写入 %s 失败: %w", relPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Check(root string) error {
|
||||
version, err := ReadString(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
packageJSONPath := filepath.Join(root, "frontend", "package.json")
|
||||
packageJSONContent, err := os.ReadFile(packageJSONPath)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("读取 frontend/package.json 失败: %w", err))
|
||||
} else {
|
||||
actualVersion, readErr := ReadPackageJSONVersion(string(packageJSONContent))
|
||||
if readErr != nil {
|
||||
errs = append(errs, readErr)
|
||||
} else if actualVersion != version {
|
||||
errs = append(errs, fmt.Errorf("frontend/package.json 版本为 %s,期望 %s", actualVersion, version))
|
||||
}
|
||||
}
|
||||
|
||||
for _, relPath := range frontendVersionFiles {
|
||||
fullPath := filepath.Join(root, relPath)
|
||||
content, readErr := os.ReadFile(fullPath)
|
||||
if readErr != nil {
|
||||
errs = append(errs, fmt.Errorf("读取 %s 失败: %w", relPath, readErr))
|
||||
continue
|
||||
}
|
||||
|
||||
actualValue, ok := ReadEnvVar(string(content), "VITE_APP_VERSION")
|
||||
if !ok {
|
||||
errs = append(errs, fmt.Errorf("%s 缺少 VITE_APP_VERSION", relPath))
|
||||
continue
|
||||
}
|
||||
|
||||
if actualValue != version {
|
||||
errs = append(errs, fmt.Errorf("%s 的 VITE_APP_VERSION 为 %s,期望 %s", relPath, actualValue, version))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func VerifyTag(root, tag string) error {
|
||||
version, err := ReadString(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(tag, "v") {
|
||||
return fmt.Errorf("tag %q 必须以 v 开头", tag)
|
||||
}
|
||||
|
||||
if tag[1:] != version {
|
||||
return fmt.Errorf("tag %q 与 VERSION %q 不一致", tag, version)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdatePackageJSONVersion(content, version string) (string, error) {
|
||||
if _, err := Parse(version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !packageVersionRegex.MatchString(content) {
|
||||
return "", errors.New("frontend/package.json 缺少 version 字段")
|
||||
}
|
||||
|
||||
updated := packageVersionRegex.ReplaceAllString(content, `${1}`+version+`${3}`)
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func ReadPackageJSONVersion(content string) (string, error) {
|
||||
parts := packageVersionRegex.FindStringSubmatch(content)
|
||||
if parts == nil {
|
||||
return "", errors.New("frontend/package.json 缺少 version 字段")
|
||||
}
|
||||
|
||||
if _, err := Parse(parts[2]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return parts[2], nil
|
||||
}
|
||||
|
||||
func UpsertEnvVar(content, key, value string) string {
|
||||
lines := strings.Split(strings.TrimRight(content, "\n"), "\n")
|
||||
if len(lines) == 1 && lines[0] == "" {
|
||||
lines = lines[:0]
|
||||
}
|
||||
|
||||
updated := false
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, key+"=") {
|
||||
lines[i] = key + "=" + value
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if !updated {
|
||||
lines = append(lines, key+"="+value)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
func ReadEnvVar(content, key string) (string, bool) {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
if strings.HasPrefix(line, key+"=") {
|
||||
return strings.TrimPrefix(line, key+"="), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func ServerAssetName(version, goos, arch string) (string, error) {
|
||||
if _, err := Parse(version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch goos {
|
||||
case "linux", "windows", "darwin":
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的 server 平台 %q", goos)
|
||||
}
|
||||
|
||||
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) {
|
||||
if _, err := Parse(version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch platform {
|
||||
case "linux":
|
||||
return fmt.Sprintf("Nex_%s_linux_amd64.tar.gz", version), nil
|
||||
case "windows":
|
||||
return fmt.Sprintf("Nex_%s_windows_amd64.zip", version), nil
|
||||
case "macos":
|
||||
return fmt.Sprintf("Nex_%s_macOS_universal.zip", version), nil
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的 desktop 平台 %q", platform)
|
||||
}
|
||||
}
|
||||
|
||||
func DesktopInfoPlist(version, minMacOSVersion string) (string, error) {
|
||||
if _, err := Parse(version); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(minMacOSVersion) == "" {
|
||||
return "", errors.New("min macOS version 不能为空")
|
||||
}
|
||||
|
||||
content := strings.Join([]string{
|
||||
`<?xml version="1.0" encoding="UTF-8"?>`,
|
||||
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
||||
`<plist version="1.0">`,
|
||||
`<dict>`,
|
||||
` <key>CFBundleDevelopmentRegion</key>`,
|
||||
` <string>zh-Hans</string>`,
|
||||
` <key>CFBundleExecutable</key>`,
|
||||
` <string>nex</string>`,
|
||||
` <key>CFBundleIconFile</key>`,
|
||||
` <string>icon</string>`,
|
||||
` <key>CFBundleIdentifier</key>`,
|
||||
` <string>com.lanyuanxiaoyao.nex</string>`,
|
||||
` <key>CFBundleInfoDictionaryVersion</key>`,
|
||||
` <string>6.0</string>`,
|
||||
` <key>LSApplicationCategoryType</key>`,
|
||||
` <string>public.app-category.developer-tools</string>`,
|
||||
` <key>CFBundleName</key>`,
|
||||
` <string>Nex</string>`,
|
||||
` <key>CFBundleDisplayName</key>`,
|
||||
` <string>Nex</string>`,
|
||||
` <key>CFBundlePackageType</key>`,
|
||||
` <string>APPL</string>`,
|
||||
` <key>CFBundleShortVersionString</key>`,
|
||||
` <string>` + version + `</string>`,
|
||||
` <key>CFBundleVersion</key>`,
|
||||
` <string>` + version + `</string>`,
|
||||
` <key>NSHumanReadableCopyright</key>`,
|
||||
` <string>Copyright © 2026 Nex</string>`,
|
||||
` <key>LSMinimumSystemVersion</key>`,
|
||||
` <string>` + minMacOSVersion + `</string>`,
|
||||
` <key>LSUIElement</key>`,
|
||||
` <true/>`,
|
||||
` <key>NSHighResolutionCapable</key>`,
|
||||
` <true/>`,
|
||||
`</dict>`,
|
||||
`</plist>`,
|
||||
}, "\n")
|
||||
|
||||
return content + "\n", nil
|
||||
}
|
||||
113
backend/pkg/projectversion/version_test.go
Normal file
113
backend/pkg/projectversion/version_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package projectversion
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
version, err := Parse("1.2.3")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{Major: 1, Minor: 2, Patch: 3}, version)
|
||||
assert.Equal(t, "1.2.3", version.String())
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
invalidValues := []string{"", "1.2", "1.2.3.4", "v1.2.3", "01.2.3", "1.02.3"}
|
||||
for _, tc := range invalidValues {
|
||||
_, err := Parse(tc)
|
||||
assert.Error(t, err, "%q 应校验失败", tc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdatePackageJSONVersion(t *testing.T) {
|
||||
content := "{\n \"name\": \"frontend\",\n \"version\": \"0.0.0\"\n}\n"
|
||||
updated, err := UpdatePackageJSONVersion(content, "1.2.3")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, updated, `"version": "1.2.3"`)
|
||||
|
||||
version, err := ReadPackageJSONVersion(updated)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "1.2.3", version)
|
||||
}
|
||||
|
||||
func TestUpsertEnvVar(t *testing.T) {
|
||||
updated := UpsertEnvVar("VITE_API_BASE=/api\n", "VITE_APP_VERSION", "1.2.3")
|
||||
assert.Contains(t, updated, "VITE_API_BASE=/api\n")
|
||||
assert.Contains(t, updated, "VITE_APP_VERSION=1.2.3\n")
|
||||
|
||||
updated = UpsertEnvVar(updated, "VITE_APP_VERSION", "2.0.0")
|
||||
value, ok := ReadEnvVar(updated, "VITE_APP_VERSION")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "2.0.0", value)
|
||||
assert.Equal(t, 1, strings.Count(updated, "VITE_APP_VERSION="))
|
||||
}
|
||||
|
||||
func TestSyncAndCheck(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.2.3\n"), 0o644))
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(root, "frontend"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", "package.json"), []byte("{\n \"name\": \"frontend\",\n \"version\": \"0.0.0\"\n}\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", ".env.production"), []byte("VITE_API_BASE=/api\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", ".env.development"), []byte("VITE_API_BASE=\n"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "frontend", ".env.desktop"), []byte("VITE_API_BASE=\n"), 0o644))
|
||||
|
||||
require.NoError(t, Sync(root))
|
||||
require.NoError(t, Check(root))
|
||||
|
||||
packageJSONContent, err := os.ReadFile(filepath.Join(root, "frontend", "package.json"))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(packageJSONContent), `"version": "1.2.3"`)
|
||||
|
||||
for _, relPath := range frontendVersionFiles {
|
||||
content, readErr := os.ReadFile(filepath.Join(root, relPath))
|
||||
require.NoError(t, readErr)
|
||||
assert.Contains(t, string(content), "VITE_APP_VERSION=1.2.3\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTag(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.2.3\n"), 0o644))
|
||||
|
||||
require.NoError(t, VerifyTag(root, "v1.2.3"))
|
||||
assert.Error(t, VerifyTag(root, "1.2.3"))
|
||||
assert.Error(t, VerifyTag(root, "v1.2.4"))
|
||||
}
|
||||
|
||||
func TestAssetNames(t *testing.T) {
|
||||
linuxServer, err := ServerAssetName("1.2.3", "linux", "amd64")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "nex-server_1.2.3_linux_amd64.tar.gz", linuxServer)
|
||||
|
||||
macServer, err := ServerAssetName("1.2.3", "darwin", "arm64")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "nex-server_1.2.3_darwin_arm64.tar.gz", macServer)
|
||||
|
||||
macDesktop, err := DesktopAssetName("1.2.3", "macos")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Nex_1.2.3_macOS_universal.zip", macDesktop)
|
||||
|
||||
_, err = DesktopAssetName("1.2.3", "ios")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDesktopInfoPlist(t *testing.T) {
|
||||
plist, err := DesktopInfoPlist("1.2.3", "13.0")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, plist, "<key>CFBundleShortVersionString</key>\n <string>1.2.3</string>")
|
||||
assert.Contains(t, plist, "<key>CFBundleVersion</key>\n <string>1.2.3</string>")
|
||||
assert.Contains(t, plist, "<key>LSMinimumSystemVersion</key>\n <string>13.0</string>")
|
||||
|
||||
_, err = DesktopInfoPlist("1.2", "13.0")
|
||||
assert.Error(t, err)
|
||||
_, err = DesktopInfoPlist("1.2.3", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE=
|
||||
VITE_APP_VERSION=0.1.0
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE=
|
||||
VITE_APP_VERSION=0.1.0
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_BASE=/api
|
||||
VITE_APP_VERSION=0.1.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -67,11 +67,11 @@
|
||||
|
||||
### Requirement: 迁移命令集成
|
||||
|
||||
迁移 SHALL 集成到 Makefile。
|
||||
迁移 SHALL 集成到 `backend/Makefile`,而不是根目录 `Makefile`。
|
||||
|
||||
#### Scenario: 迁移 up 命令
|
||||
|
||||
- **WHEN** 执行 `make backend-migrate-up`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make migrate-up`
|
||||
- **THEN** SHALL 执行所有待执行的迁移
|
||||
- **THEN** SHALL 使用 `DB_DRIVER` 变量选择方言目录(默认 `sqlite3`)
|
||||
- **THEN** SHALL 使用 `DB_DSN` 变量作为数据库连接串
|
||||
@@ -79,20 +79,20 @@
|
||||
|
||||
#### Scenario: 迁移 down 命令
|
||||
|
||||
- **WHEN** 执行 `make backend-migrate-down`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make migrate-down`
|
||||
- **THEN** SHALL 回滚最后一个迁移
|
||||
- **THEN** SHALL 使用 `DB_DRIVER` 和 `DB_DSN` 变量
|
||||
- **THEN** SHALL 显示回滚进度
|
||||
|
||||
#### Scenario: 迁移状态命令
|
||||
|
||||
- **WHEN** 执行 `make backend-migrate-status`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make migrate-status`
|
||||
- **THEN** SHALL 显示当前迁移状态
|
||||
- **THEN** SHALL 显示已执行和待执行的迁移
|
||||
|
||||
#### Scenario: 创建迁移命令
|
||||
|
||||
- **WHEN** 执行 `make backend-migrate-create`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make migrate-create`
|
||||
- **THEN** SHALL 同时在 `migrations/sqlite/` 和 `migrations/mysql/` 两个目录创建新的迁移文件模板
|
||||
- **THEN** SHALL 使用递增的版本号
|
||||
|
||||
|
||||
@@ -132,38 +132,50 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
||||
|
||||
### Requirement: 跨平台构建
|
||||
|
||||
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,产物文件名 SHALL 使用 `nex-{os}-{arch}[.exe]` 格式。
|
||||
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,中间构建产物文件名 SHALL 保持 `nex-{os}-{arch}[.exe]` 格式,最终桌面发布资产文件名 SHALL 包含统一版本号和平台标识。
|
||||
|
||||
#### Scenario: macOS 构建
|
||||
|
||||
- **WHEN** 执行 `desktop-build-mac` 构建命令
|
||||
- **WHEN** 执行 `desktop-build-mac` 构建命令且当前版本为 `1.2.3`
|
||||
- **THEN** 生成 `nex-mac-arm64` 和 `nex-mac-amd64` 可执行文件
|
||||
- **AND** 可打包为 `.app` bundle
|
||||
- **AND** 系统生成可打包为 `.app` bundle 的 macOS 桌面产物
|
||||
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3` 和 `macOS` 平台标识
|
||||
|
||||
#### Scenario: Windows 构建
|
||||
|
||||
- **WHEN** 执行 `desktop-build-win` 构建命令
|
||||
- **THEN** 生成 `nex-win-amd64.exe` 可执行文件
|
||||
- **WHEN** 执行 `desktop-build-win` 构建命令且当前版本为 `1.2.3`
|
||||
- **THEN** 系统生成 Windows 桌面可执行文件
|
||||
- **AND** 生成 `nex-win-amd64.exe` 可执行文件
|
||||
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
|
||||
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3` 和 `windows` 平台标识
|
||||
|
||||
#### Scenario: Linux 构建
|
||||
|
||||
- **WHEN** 执行 `desktop-build-linux` 构建命令
|
||||
- **THEN** 生成 `nex-linux-amd64` 可执行文件
|
||||
- **WHEN** 执行 `desktop-build-linux` 构建命令且当前版本为 `1.2.3`
|
||||
- **THEN** 系统生成 Linux 桌面可执行文件
|
||||
- **AND** 生成 `nex-linux-amd64` 可执行文件
|
||||
- **AND** 最终发布资产文件名 SHALL 包含 `1.2.3` 和 `linux` 平台标识
|
||||
|
||||
### Requirement: macOS .app 打包
|
||||
|
||||
系统 SHALL 支持打包为 macOS .app bundle。
|
||||
系统 SHALL 支持打包为 macOS `.app` bundle,并使 bundle 元数据中的版本字段来源于统一版本号而非硬编码值。
|
||||
|
||||
#### Scenario: .app 结构
|
||||
|
||||
- **WHEN** 执行打包脚本
|
||||
- **WHEN** 执行 macOS 桌面打包脚本
|
||||
- **THEN** 生成 `Nex.app` 目录结构
|
||||
- **AND** 包含 `Contents/Info.plist` 元数据
|
||||
- **AND** 包含 `Contents/MacOS/nex` 可执行文件
|
||||
- **AND** 包含 `Contents/Resources/icon.icns` 图标
|
||||
- **AND** `Info.plist` 中 `LSUIElement` 为 `true`(不显示 Dock 图标)
|
||||
|
||||
#### Scenario: bundle 版本元数据同步
|
||||
|
||||
- **WHEN** 当前统一版本号为 `1.2.3`
|
||||
- **THEN** `Info.plist` 中 `CFBundleShortVersionString` SHALL 为 `1.2.3`
|
||||
- **AND** `Info.plist` 中 `CFBundleVersion` SHALL 为 `1.2.3`
|
||||
- **AND** 打包流程 SHALL NOT 使用硬编码版本值
|
||||
|
||||
### Requirement: Windows 原生对话框
|
||||
|
||||
系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误对话框,替代 `msg *` 命令。
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
|
||||
### Requirement: MySQL 测试环境可启动
|
||||
|
||||
系统 SHALL 提供 Docker Compose 配置以启动 MySQL 8.0 测试环境。
|
||||
系统 SHALL 提供 Docker Compose 配置和 backend 局部 make 命令以启动 MySQL 8.0 测试环境。
|
||||
|
||||
#### Scenario: 启动 MySQL 测试容器
|
||||
- **WHEN** 执行 `make test-mysql-up`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make mysql-up`
|
||||
- **THEN** 启动 MySQL 8.0 容器,端口 13306
|
||||
- **AND** 创建数据库 `nex_test`
|
||||
- **AND** 容器数据存储在内存盘(tmpfs)
|
||||
|
||||
#### Scenario: 销毁 MySQL 测试容器
|
||||
- **WHEN** 执行 `make test-mysql-down`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make mysql-down`
|
||||
- **THEN** 停止并删除容器
|
||||
- **AND** 所有数据被销毁
|
||||
|
||||
@@ -90,15 +90,15 @@ MySQL 测试 SHALL 验证并发写入不丢失数据。
|
||||
|
||||
### Requirement: MySQL 测试命令完整
|
||||
|
||||
Makefile SHALL 提供完整的 MySQL 测试命令。
|
||||
`backend/Makefile` SHALL 提供完整的 MySQL 测试命令。
|
||||
|
||||
#### Scenario: 完整测试流程
|
||||
- **WHEN** 执行 `make test-mysql`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make mysql-test`
|
||||
- **THEN** 启动 Docker MySQL
|
||||
- **AND** 等待 MySQL 就绪
|
||||
- **AND** 运行所有 MySQL 测试
|
||||
- **AND** 销毁容器
|
||||
|
||||
#### Scenario: 快速测试(容器已运行)
|
||||
- **WHEN** 执行 `make test-mysql-quick`
|
||||
- **WHEN** 在 `backend/` 目录执行 `make mysql-test-quick`
|
||||
- **THEN** 直接运行测试,不管理容器生命周期
|
||||
|
||||
78
openspec/specs/release-pipeline/spec.md
Normal file
78
openspec/specs/release-pipeline/spec.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 发布流水线
|
||||
|
||||
## Purpose
|
||||
|
||||
定义 tag 驱动的发布流程、跨平台构建产物要求与 Draft Release 组装规则,确保发布结果可复现且可审阅。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Tag 驱动发布流水线
|
||||
|
||||
系统 SHALL 仅在符合 `vX.Y.Z` 格式的 Git tag 上触发发布流水线,普通分支 push SHALL NOT 创建发布。
|
||||
|
||||
#### Scenario: 有效发布 tag
|
||||
|
||||
- **WHEN** 仓库收到 `v1.2.3` tag push
|
||||
- **THEN** 发布流水线 SHALL 启动版本校验、构建和 Release 组装步骤
|
||||
|
||||
#### Scenario: 普通分支推送
|
||||
|
||||
- **WHEN** 仓库收到非 tag 的分支 push
|
||||
- **THEN** 系统 SHALL NOT 创建 GitHub Release
|
||||
|
||||
### Requirement: 三平台发布构建
|
||||
|
||||
系统 SHALL 在发布流水线中构建 server 与 desktop 的 Linux、Windows、macOS 三个平台产物。
|
||||
|
||||
#### Scenario: Linux 发布构建
|
||||
|
||||
- **WHEN** 发布流水线执行 Linux 构建 job
|
||||
- **THEN** 系统 SHALL 生成 Linux server 发布资产
|
||||
- **AND** 系统 SHALL 生成 Linux desktop 发布资产
|
||||
|
||||
#### Scenario: Windows 发布构建
|
||||
|
||||
- **WHEN** 发布流水线执行 Windows 构建 job
|
||||
- **THEN** 系统 SHALL 生成 Windows server 发布资产
|
||||
- **AND** 系统 SHALL 生成 Windows desktop 发布资产
|
||||
|
||||
#### Scenario: macOS 发布构建
|
||||
|
||||
- **WHEN** 发布流水线执行 macOS 构建 job
|
||||
- **THEN** 系统 SHALL 生成 darwin-amd64 server 发布资产
|
||||
- **AND** 系统 SHALL 生成 darwin-arm64 server 发布资产
|
||||
- **AND** 系统 SHALL 生成 macOS desktop universal 发布资产
|
||||
|
||||
### Requirement: 版本化发布资产命名
|
||||
|
||||
系统 SHALL 为 server 与 desktop 的发布资产使用包含统一版本号和目标平台信息的文件名,确保 Release 页面可直接区分产物用途与平台。
|
||||
|
||||
#### Scenario: server 资产命名
|
||||
|
||||
- **WHEN** 当前发布版本为 `1.2.3`
|
||||
- **THEN** Linux server 发布资产文件名 SHALL 包含 `1.2.3`、`linux` 和 `amd64`
|
||||
- **AND** Windows server 发布资产文件名 SHALL 包含 `1.2.3`、`windows` 和 `amd64`
|
||||
- **AND** macOS server 发布资产文件名 SHALL 分别包含 `1.2.3`、`darwin`、`amd64` 与 `1.2.3`、`darwin`、`arm64`
|
||||
|
||||
#### Scenario: desktop 资产命名
|
||||
|
||||
- **WHEN** 当前发布版本为 `1.2.3`
|
||||
- **THEN** Linux desktop 发布资产文件名 SHALL 包含 `1.2.3` 和 `linux`
|
||||
- **AND** Windows desktop 发布资产文件名 SHALL 包含 `1.2.3` 和 `windows`
|
||||
- **AND** macOS desktop universal 发布资产文件名 SHALL 包含 `1.2.3` 和 `macOS`
|
||||
|
||||
### Requirement: Draft Release 组装
|
||||
|
||||
系统 SHALL 将发布流水线产物上传到 GitHub Draft Release,由人工确认后再公开发布。
|
||||
|
||||
#### Scenario: 发布成功时创建 Draft Release
|
||||
|
||||
- **WHEN** 版本校验通过且三平台发布资产构建完成
|
||||
- **THEN** 系统 SHALL 创建或更新与该 tag 对应的 GitHub Draft Release
|
||||
- **AND** 系统 SHALL 上传 server 与 desktop 的全部发布资产
|
||||
|
||||
#### Scenario: 构建失败时阻止完成发布
|
||||
|
||||
- **WHEN** 任一平台发布资产构建失败或版本校验失败
|
||||
- **THEN** 发布流水线 SHALL 失败
|
||||
- **AND** 系统 SHALL NOT 产生可直接公开的成功发布结果
|
||||
68
openspec/specs/repository-versioning/spec.md
Normal file
68
openspec/specs/repository-versioning/spec.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 仓库版本管理
|
||||
|
||||
## Purpose
|
||||
|
||||
定义仓库统一版本源、镜像同步、发布一致性校验与构建版本注入规则,确保所有构建消费者使用同一版本语义。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 统一版本源
|
||||
|
||||
系统 SHALL 使用仓库根目录 `VERSION` 文件作为全仓唯一版本源,文件内容 SHALL 仅包含一行 `x.y.z` 格式的语义版本号。
|
||||
|
||||
#### Scenario: 读取有效版本号
|
||||
|
||||
- **WHEN** 仓库中的 `VERSION` 文件内容为 `1.2.3`
|
||||
- **THEN** 本地工具和 CI SHALL 将 `1.2.3` 视为当前仓库版本
|
||||
- **AND** 其他版本消费者 SHALL NOT 覆盖该值作为权威来源
|
||||
|
||||
#### Scenario: 拒绝非法版本格式
|
||||
|
||||
- **WHEN** `VERSION` 文件内容不是 `x.y.z` 格式
|
||||
- **THEN** 版本同步与发布校验流程 SHALL 失败
|
||||
- **AND** 系统 SHALL 输出格式错误信息
|
||||
|
||||
### Requirement: 版本镜像同步
|
||||
|
||||
系统 SHALL 提供仓库内的版本同步入口,将 `VERSION` 中的版本值写入需要镜像版本号的构建消费者。
|
||||
|
||||
#### Scenario: 同步前端镜像字段
|
||||
|
||||
- **WHEN** 执行版本同步流程且 `VERSION` 为 `1.2.3`
|
||||
- **THEN** `frontend/package.json` 中的 `version` 字段 SHALL 被同步为 `1.2.3`
|
||||
|
||||
#### Scenario: 同步构建模板值
|
||||
|
||||
- **WHEN** 执行版本同步流程且存在依赖版本号的构建模板或元数据模板
|
||||
- **THEN** 这些模板消费的版本值 SHALL 与 `VERSION` 保持一致
|
||||
- **AND** 系统 SHALL NOT 要求用户手工修改多个版本文件
|
||||
|
||||
### Requirement: 发布版本一致性校验
|
||||
|
||||
系统 SHALL 在发布前校验仓库版本与 Git tag 一致,确保发布锚点与仓库状态不漂移。
|
||||
|
||||
#### Scenario: tag 与 VERSION 一致
|
||||
|
||||
- **WHEN** 发布使用的 Git tag 为 `v1.2.3` 且 `VERSION` 为 `1.2.3`
|
||||
- **THEN** 发布校验 SHALL 通过
|
||||
|
||||
#### Scenario: tag 与 VERSION 不一致
|
||||
|
||||
- **WHEN** 发布使用的 Git tag 为 `v1.2.4` 但 `VERSION` 为 `1.2.3`
|
||||
- **THEN** 发布校验 SHALL 失败
|
||||
- **AND** 系统 SHALL 阻止后续发布步骤继续执行
|
||||
|
||||
### Requirement: 统一构建版本注入
|
||||
|
||||
系统 SHALL 在构建阶段把统一版本信息注入 frontend、server 和 desktop,而不是在运行时依赖外部发布平台查询版本。
|
||||
|
||||
#### Scenario: Go 二进制注入版本元数据
|
||||
|
||||
- **WHEN** 构建 server 或 desktop 二进制
|
||||
- **THEN** 构建流程 SHALL 注入 `version`、`commit` 和 `buildTime` 元数据
|
||||
|
||||
#### Scenario: 前端注入构建版本
|
||||
|
||||
- **WHEN** 执行前端生产构建
|
||||
- **THEN** 构建流程 SHALL 注入 `VITE_APP_VERSION`
|
||||
- **AND** 该值 SHALL 等于 `VERSION` 中的版本号
|
||||
@@ -157,26 +157,33 @@
|
||||
|
||||
### Requirement: 集成到构建流程
|
||||
|
||||
测试 SHALL 集成到构建流程中。
|
||||
测试 SHALL 同时集成到根目录公共流程和 backend 局部流程中。
|
||||
|
||||
#### Scenario: 运行测试命令
|
||||
|
||||
- **WHEN** 执行 `make test` 命令
|
||||
- **THEN** SHALL 运行所有单元测试和集成测试
|
||||
- **THEN** SHALL 运行 backend 核心测试、frontend 的 Vitest 单元/组件测试和 desktop 专属测试
|
||||
- **THEN** SHALL NOT 运行 MySQL 专项测试和 frontend E2E 测试
|
||||
- **THEN** SHALL 显示测试结果
|
||||
- **THEN** SHALL 在测试失败时返回非零退出码
|
||||
|
||||
#### Scenario: 运行 backend 局部测试命令
|
||||
|
||||
- **WHEN** 在 `backend/` 目录执行 `make test` 命令
|
||||
- **THEN** SHALL 运行 backend 核心测试
|
||||
- **THEN** SHALL NOT 运行 frontend 的 Vitest 单元/组件测试、desktop 专属测试、MySQL 专项测试或 frontend E2E 测试
|
||||
|
||||
#### Scenario: 分类测试命令
|
||||
|
||||
- **WHEN** 执行 `make test-unit` 命令
|
||||
- **WHEN** 在 `backend/` 目录执行 `make test-unit` 命令
|
||||
- **THEN** SHALL 仅运行 `./internal/...` 和 `./pkg/...` 下的单元测试
|
||||
|
||||
- **WHEN** 执行 `make test-integration` 命令
|
||||
- **WHEN** 在 `backend/` 目录执行 `make test-integration` 命令
|
||||
- **THEN** SHALL 仅运行 `./tests/...` 下的集成测试
|
||||
|
||||
#### Scenario: 覆盖率检查命令
|
||||
|
||||
- **WHEN** 执行 `make test-coverage` 命令
|
||||
- **WHEN** 在 `backend/` 目录执行 `make test-coverage` 命令
|
||||
- **THEN** SHALL 运行测试并生成覆盖率报告
|
||||
- **THEN** SHALL 检查覆盖率是否达标
|
||||
- **THEN** SHALL 在覆盖率不足时返回非零退出码
|
||||
|
||||
114
openspec/specs/workspace-command-flows/spec.md
Normal file
114
openspec/specs/workspace-command-flows/spec.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Workspace Command Flows
|
||||
|
||||
## Purpose
|
||||
|
||||
定义根目录 `Makefile` 与 `backend/Makefile` 的公开命令边界,明确全仓命令、产品级命令和 backend 局部维护命令的职责分层。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 根目录公开命令分层
|
||||
|
||||
根目录 `Makefile` SHALL 仅暴露全局命令、版本命令、server 产品命令、desktop 产品命令和 release 命令,不再作为 backend 局部维护命令或内部打包步骤的公开入口。
|
||||
|
||||
#### Scenario: 查看根目录公开命令
|
||||
- **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` 这类公共入口
|
||||
|
||||
#### Scenario: 根目录不暴露局部和内部命令
|
||||
- **WHEN** 开发者查看根目录 `Makefile` 的公开 target
|
||||
- **THEN** SHALL NOT 暴露 `backend-*`、`frontend-*`、数据库迁移命令、MySQL 专项测试命令或 `desktop-prepare-*` 之类内部步骤
|
||||
- **THEN** SHALL NOT 暴露 `dev`、`build`、`all`、`desktop-dev`、`desktop-build` 这类模糊或聚合式公共命令
|
||||
|
||||
### Requirement: 全局质量与清理命令
|
||||
|
||||
根目录 `Makefile` SHALL 提供 `lint`、`test`、`clean` 作为全仓默认入口。
|
||||
|
||||
#### Scenario: 执行全局 lint
|
||||
- **WHEN** 执行 `make lint`
|
||||
- **THEN** SHALL 运行 backend 的 Go lint 检查和 frontend 的 lint / format check
|
||||
- **THEN** SHALL 在任一检查失败时返回非零退出码
|
||||
|
||||
#### Scenario: 执行全局 test
|
||||
- **WHEN** 执行 `make test`
|
||||
- **THEN** SHALL 运行 backend 核心测试、frontend 的 Vitest 单元/组件测试和 desktop 专属测试
|
||||
- **THEN** SHALL NOT 运行 MySQL 专项测试和 frontend E2E 测试
|
||||
- **THEN** SHALL 在任一测试失败时返回非零退出码
|
||||
|
||||
#### Scenario: 执行全局 clean
|
||||
- **WHEN** 执行 `make clean`
|
||||
- **THEN** SHALL 清理 server 与 desktop 相关的构建产物、发布产物和测试报告
|
||||
- **THEN** SHALL NOT 删除 `frontend/node_modules`、Go module cache 或 bun cache 之类依赖目录和全局缓存
|
||||
|
||||
### Requirement: Server 产品命令
|
||||
|
||||
根目录 `Makefile` SHALL 提供面向前后端分离 server 模式的产品级命令。
|
||||
|
||||
#### Scenario: 启动 server 模式联调环境
|
||||
- **WHEN** 执行 `make server-run`
|
||||
- **THEN** SHALL 并行启动 Go 后端服务和前端 Vite 开发服务器
|
||||
- **THEN** SHALL 使前端继续通过现有代理访问本地 backend 服务
|
||||
|
||||
#### Scenario: 构建 server 模式产物
|
||||
- **WHEN** 执行 `make server-build`
|
||||
- **THEN** SHALL 生成 `backend/bin/server`
|
||||
- **AND** SHALL 生成 `frontend/dist`
|
||||
- **AND** SHALL 在构建前校验版本一致性
|
||||
- **AND** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件
|
||||
|
||||
#### Scenario: 执行 server lint
|
||||
- **WHEN** 执行 `make server-lint`
|
||||
- **THEN** SHALL 运行 backend 的 Go lint 检查和 frontend 的 lint / format check
|
||||
|
||||
#### Scenario: 执行 server test
|
||||
- **WHEN** 执行 `make server-test`
|
||||
- **THEN** SHALL 运行 backend 核心测试和 frontend 的 Vitest 单元/组件测试
|
||||
- **THEN** SHALL NOT 运行 MySQL 专项测试、frontend E2E 测试或 desktop 专属测试
|
||||
|
||||
#### Scenario: 执行 server clean
|
||||
- **WHEN** 执行 `make server-clean`
|
||||
- **THEN** SHALL 清理 `backend/bin/server`、`frontend/dist` 以及 server 模式相关测试报告与临时产物
|
||||
|
||||
### Requirement: Desktop 产品命令
|
||||
|
||||
根目录 `Makefile` SHALL 提供按平台拆分的 desktop 构建命令,以及 desktop 产品级 `lint`、`test`、`clean` 命令。
|
||||
|
||||
#### Scenario: 执行按平台拆分的 desktop 构建
|
||||
- **WHEN** 执行 `make desktop-build-mac`、`make desktop-build-win` 或 `make desktop-build-linux`
|
||||
- **THEN** SHALL 分别构建对应平台的 desktop 产物
|
||||
- **THEN** SHALL 在构建前执行版本一致性校验
|
||||
- **THEN** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件
|
||||
- **THEN** SHALL NOT 要求通过额外的 `desktop-build` 聚合命令触发构建
|
||||
|
||||
#### Scenario: 根目录不提供 desktop 开发聚合入口
|
||||
- **WHEN** 开发者查看根目录 `Makefile`
|
||||
- **THEN** SHALL NOT 提供 `desktop-dev` 作为公共命令
|
||||
|
||||
#### Scenario: 执行 desktop lint
|
||||
- **WHEN** 执行 `make desktop-lint`
|
||||
- **THEN** SHALL 运行 backend 的 Go lint 检查和 frontend 的 lint / format check
|
||||
|
||||
#### Scenario: 执行 desktop test
|
||||
- **WHEN** 执行 `make desktop-test`
|
||||
- **THEN** SHALL 运行 desktop 专属测试
|
||||
|
||||
#### Scenario: 执行 desktop clean
|
||||
- **WHEN** 执行 `make desktop-clean`
|
||||
- **THEN** SHALL 清理 desktop 构建目录、嵌入资源目录和 desktop 相关测试/打包产物
|
||||
|
||||
### Requirement: Release 命令沿用根目录入口
|
||||
|
||||
根目录 `Makefile` SHALL 继续提供 `release-assets-*` 作为发布资产入口,并与新的版本校验规则保持一致。
|
||||
|
||||
#### Scenario: 执行 release 资产命令
|
||||
- **WHEN** 执行 `make release-assets-linux`、`make release-assets-windows` 或 `make release-assets-macos`
|
||||
- **THEN** SHALL 在构建发布资产前执行版本一致性校验
|
||||
- **THEN** SHALL NOT 隐式执行 `version-sync` 或修改版本镜像文件
|
||||
|
||||
### Requirement: Backend 局部命令下沉
|
||||
|
||||
数据库迁移、MySQL 专项测试以及其他 backend 局部维护命令 SHALL 由 `backend/Makefile` 提供,而不是由根目录 `Makefile` 公开。
|
||||
|
||||
#### Scenario: 执行 backend 局部维护命令
|
||||
- **WHEN** 开发者需要执行数据库迁移或 MySQL 专项测试
|
||||
- **THEN** SHALL 在 `backend/` 目录使用 backend 局部 make 命令完成操作
|
||||
- **THEN** 根目录 `Makefile` SHALL NOT 提供等价的公共命令别名
|
||||
Reference in New Issue
Block a user