feat: 迁移 versionctl 为独立模块并新增 make version-bump 命令
- 将 backend/cmd/versionctl 和 backend/pkg/projectversion 迁移至独立 versionctl/ Go 模块 - 新增 bump 子命令支持 major/minor/patch 和指定版本号,含版本倒退防护 - 新增 make version-bump 编排完整升迁流程(bump + sync + check + commit + tag) - 更新所有引用路径:根 Makefile、backend/Makefile、release.yml、.golangci.yml - 新增 versionctl/.golangci.yml(精简配置)和 Makefile(lint/test/coverage) - 根 Makefile lint/test 集成 versionctl 模块 - 同步 openspec specs:新增 version-bump spec,更新 release-pipeline spec
This commit is contained in:
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -24,13 +24,15 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.work
|
||||
cache-dependency-path: backend/go.sum
|
||||
cache-dependency-path: |
|
||||
backend/go.sum
|
||||
versionctl/go.sum
|
||||
|
||||
- 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}"
|
||||
version=$(go run ./versionctl print)
|
||||
go run ./versionctl verify-tag "${GITHUB_REF_NAME}"
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-linux:
|
||||
@@ -47,7 +49,9 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.work
|
||||
cache-dependency-path: backend/go.sum
|
||||
cache-dependency-path: |
|
||||
backend/go.sum
|
||||
versionctl/go.sum
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -93,7 +97,9 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.work
|
||||
cache-dependency-path: backend/go.sum
|
||||
cache-dependency-path: |
|
||||
backend/go.sum
|
||||
versionctl/go.sum
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -153,7 +159,9 @@ jobs:
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.work
|
||||
cache-dependency-path: backend/go.sum
|
||||
cache-dependency-path: |
|
||||
backend/go.sum
|
||||
versionctl/go.sum
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
45
Makefile
45
Makefile
@@ -1,11 +1,12 @@
|
||||
.PHONY: \
|
||||
lint test clean \
|
||||
version-sync version-check \
|
||||
version-sync version-check version-bump \
|
||||
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 \
|
||||
_versionctl-lint _versionctl-test \
|
||||
_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
|
||||
@@ -13,28 +14,28 @@
|
||||
# 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)))
|
||||
|
||||
VERSION = $(call lazy_shell,_VERSION,go run ./backend/cmd/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')
|
||||
BUILD_TIME ?= $(call lazy_shell,_BUILD_TIME,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 = $(call lazy_shell,_SERVER_LINUX_ASSET,go run ./backend/cmd/versionctl asset-name server linux amd64)
|
||||
SERVER_WINDOWS_ASSET = $(call lazy_shell,_SERVER_WINDOWS_ASSET,go run ./backend/cmd/versionctl asset-name server windows amd64)
|
||||
SERVER_DARWIN_AMD64_ASSET = $(call lazy_shell,_SERVER_DARWIN_AMD64_ASSET,go run ./backend/cmd/versionctl asset-name server darwin amd64)
|
||||
SERVER_DARWIN_ARM64_ASSET = $(call lazy_shell,_SERVER_DARWIN_ARM64_ASSET,go run ./backend/cmd/versionctl asset-name server darwin arm64)
|
||||
DESKTOP_LINUX_ASSET = $(call lazy_shell,_DESKTOP_LINUX_ASSET,go run ./backend/cmd/versionctl asset-name desktop linux)
|
||||
DESKTOP_WINDOWS_ASSET = $(call lazy_shell,_DESKTOP_WINDOWS_ASSET,go run ./backend/cmd/versionctl asset-name desktop windows)
|
||||
DESKTOP_MACOS_ASSET = $(call lazy_shell,_DESKTOP_MACOS_ASSET,go run ./backend/cmd/versionctl asset-name desktop macos)
|
||||
SERVER_LINUX_ASSET = $(call lazy_shell,_SERVER_LINUX_ASSET,go run ./versionctl asset-name server linux amd64)
|
||||
SERVER_WINDOWS_ASSET = $(call lazy_shell,_SERVER_WINDOWS_ASSET,go run ./versionctl asset-name server windows amd64)
|
||||
SERVER_DARWIN_AMD64_ASSET = $(call lazy_shell,_SERVER_DARWIN_AMD64_ASSET,go run ./versionctl asset-name server darwin amd64)
|
||||
SERVER_DARWIN_ARM64_ASSET = $(call lazy_shell,_SERVER_DARWIN_ARM64_ASSET,go run ./versionctl asset-name server darwin arm64)
|
||||
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)
|
||||
DESKTOP_MACOS_ASSET = $(call lazy_shell,_DESKTOP_MACOS_ASSET,go run ./versionctl asset-name desktop macos)
|
||||
|
||||
# ============================================
|
||||
# 全局命令
|
||||
# ============================================
|
||||
|
||||
lint: _backend-lint _frontend-check
|
||||
lint: _backend-lint _frontend-check _versionctl-lint
|
||||
@printf 'Lint complete\n'
|
||||
|
||||
test: _backend-test _frontend-test _desktop-test
|
||||
test: _backend-test _frontend-test _desktop-test _versionctl-test
|
||||
@printf 'All tests passed\n'
|
||||
|
||||
clean: _backend-clean _frontend-clean _desktop-clean
|
||||
@@ -45,10 +46,20 @@ clean: _backend-clean _frontend-clean _desktop-clean
|
||||
# ============================================
|
||||
|
||||
version-sync:
|
||||
go run ./backend/cmd/versionctl sync
|
||||
go run ./versionctl sync
|
||||
|
||||
version-check:
|
||||
go run ./backend/cmd/versionctl check
|
||||
go run ./versionctl check
|
||||
|
||||
version-bump:
|
||||
@test -n "$(BUMP)$(SET_VERSION)" || (printf '用法: make version-bump BUMP=major|minor|patch 或 make version-bump SET_VERSION=x.y.z\n' && exit 1)
|
||||
@git diff --quiet HEAD || (printf '工作区不干净,请先提交或暂存改动\n' && exit 1)
|
||||
$(eval _BUMP_ARG := $(if $(SET_VERSION),$(SET_VERSION),$(BUMP)))
|
||||
$(eval _NEW_VERSION := $(shell go run ./versionctl bump $(_BUMP_ARG)))
|
||||
git add VERSION frontend/
|
||||
git commit -m "chore: 版本升迁 v$(_NEW_VERSION)"
|
||||
git tag "v$(_NEW_VERSION)"
|
||||
@printf '版本升迁完成: v%s\n' "$(_NEW_VERSION)"
|
||||
|
||||
# ============================================
|
||||
# Server 模式
|
||||
@@ -97,7 +108,7 @@ desktop-build-mac: version-check _desktop-prepare-frontend _desktop-prepare-embe
|
||||
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
|
||||
go run ./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'
|
||||
|
||||
@@ -216,6 +227,12 @@ _backend-test:
|
||||
_backend-clean:
|
||||
@$(MAKE) -C backend clean
|
||||
|
||||
_versionctl-lint:
|
||||
@$(MAKE) -C versionctl lint
|
||||
|
||||
_versionctl-test:
|
||||
@$(MAKE) -C versionctl test
|
||||
|
||||
_frontend-install:
|
||||
cd frontend && bun install
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -321,27 +321,24 @@ make desktop-clean # 清理 desktop 产物
|
||||
|
||||
### 本地版本演进
|
||||
|
||||
1. 手工修改根目录 `VERSION` 为新的 `x.y.z`
|
||||
2. 同步镜像文件:
|
||||
```bash
|
||||
# 递增版本(自动 sync + check + commit + tag)
|
||||
make version-bump BUMP=minor
|
||||
|
||||
# 或指定具体版本号
|
||||
make version-bump SET_VERSION=1.0.0
|
||||
|
||||
# 推送到远程
|
||||
git push --follow-tags
|
||||
```
|
||||
|
||||
手动同步和校验:
|
||||
|
||||
```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
|
||||
|
||||
@@ -86,9 +86,6 @@ issues:
|
||||
linters:
|
||||
- gocyclo
|
||||
- gocritic
|
||||
- path: '(internal/provider/client\.go|internal/service/model_service_impl\.go|internal/service/stats_buffer\.go|internal/handler/proxy_handler\.go|cmd/(desktop|server|versionctl)/main\.go)'
|
||||
- path: '(internal/provider/client\.go|internal/service/model_service_impl\.go|internal/service/stats_buffer\.go|internal/handler/proxy_handler\.go|cmd/(desktop|server)/main\.go)'
|
||||
linters:
|
||||
- gocyclo
|
||||
- path: 'cmd/versionctl/'
|
||||
linters:
|
||||
- forbidigo
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
migrate-up migrate-down migrate-status migrate-create \
|
||||
mysql-up mysql-down mysql-test mysql-test-quick
|
||||
|
||||
VERSION := $(shell go run ./cmd/versionctl print)
|
||||
VERSION := $(shell go run ../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)
|
||||
|
||||
@@ -4,8 +4,6 @@ cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQ
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
@@ -14,6 +12,7 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cristalhq/acmd v0.12.0/go.mod h1:LG5oa43pE/BbxtfMoImHCQN++0Su7dzipdgBjMCBVDQ=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
@@ -26,8 +25,6 @@ github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6v
|
||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
@@ -48,6 +45,7 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
@@ -66,8 +64,10 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw=
|
||||
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quasilyte/go-ruleguard/rules v0.0.0-20211022131956-028d6511ab71/go.mod h1:4cgAphtvu7Ftv7vOT2ZOYhC6CvBxZixcasr8qIOTA50=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
|
||||
@@ -8,18 +8,28 @@
|
||||
|
||||
### Requirement: Tag 驱动发布流水线
|
||||
|
||||
系统 SHALL 仅在符合 `vX.Y.Z` 格式的 Git tag 上触发发布流水线,普通分支 push SHALL NOT 创建发布。
|
||||
系统 SHALL 仅在符合 `vX.Y.Z` 格式的 Git tag 上触发发布流水线,普通分支 push SHALL NOT 创建发布。发布流水线 SHALL 使用 `./versionctl` 而非 `./backend/cmd/versionctl` 调用版本管理工具。
|
||||
|
||||
#### Scenario: 有效发布 tag
|
||||
|
||||
- **WHEN** 仓库收到 `v1.2.3` tag push
|
||||
- **THEN** 发布流水线 SHALL 启动版本校验、构建和 Release 组装步骤
|
||||
- **AND** 版本校验步骤 SHALL 使用 `go run ./versionctl print` 和 `go run ./versionctl verify-tag` 获取并验证版本
|
||||
|
||||
#### Scenario: 普通分支推送
|
||||
|
||||
- **WHEN** 仓库收到非 tag 的分支 push
|
||||
- **THEN** 系统 SHALL NOT 创建 GitHub Release
|
||||
|
||||
### Requirement: 发布流水线 Go 模块缓存覆盖
|
||||
|
||||
发布流水线 SHALL 在所有 Go module 的 go.sum 文件存在时正确设置 Go 模块缓存路径,确保新增的 `versionctl` module 依赖也被缓存。
|
||||
|
||||
#### Scenario: CI 缓存覆盖所有 module
|
||||
|
||||
- **WHEN** 发布流水线设置 Go 模块缓存
|
||||
- **THEN** `cache-dependency-path` SHALL 覆盖 `backend/go.sum` 和 `versionctl/go.sum`
|
||||
|
||||
### Requirement: 三平台发布构建
|
||||
|
||||
系统 SHALL 在发布流水线中构建 server 与 desktop 的 Linux、Windows、macOS 三个平台产物。
|
||||
|
||||
113
openspec/specs/version-bump/spec.md
Normal file
113
openspec/specs/version-bump/spec.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 版本升迁
|
||||
|
||||
## Purpose
|
||||
|
||||
定义 `version bump` 子命令的版本号递增、下游文件同步、倒退防护及 Makefile 编排规则,确保版本升迁流程安全可自动化。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 版本号递增
|
||||
|
||||
`version bump` 子命令 SHALL 支持三种递增模式:`major`(major+1, minor=0, patch=0)、`minor`(minor+1, patch=0)、`patch`(patch+1),以及直接指定具体版本号。
|
||||
|
||||
#### Scenario: minor 递增
|
||||
|
||||
- **WHEN** 当前 VERSION 为 `0.1.0`,执行 `version bump minor`
|
||||
- **THEN** VERSION 文件 SHALL 被更新为 `0.2.0`
|
||||
|
||||
#### Scenario: major 递增
|
||||
|
||||
- **WHEN** 当前 VERSION 为 `0.1.0`,执行 `version bump major`
|
||||
- **THEN** VERSION 文件 SHALL 被更新为 `1.0.0`
|
||||
|
||||
#### Scenario: patch 递增
|
||||
|
||||
- **WHEN** 当前 VERSION 为 `0.1.0`,执行 `version bump patch`
|
||||
- **THEN** VERSION 文件 SHALL 被更新为 `0.1.1`
|
||||
|
||||
#### Scenario: 指定具体版本号
|
||||
|
||||
- **WHEN** 当前 VERSION 为 `0.1.0`,执行 `version bump 1.0.0`
|
||||
- **THEN** VERSION 文件 SHALL 被更新为 `1.0.0`
|
||||
|
||||
#### Scenario: 指定版本号等于当前 VERSION
|
||||
|
||||
- **WHEN** 当前 VERSION 为 `0.1.0`,执行 `version bump 0.1.0`
|
||||
- **THEN** 命令 SHALL 正常执行,完成 sync 和 check,输出 `0.1.0`
|
||||
|
||||
#### Scenario: 非法 bump 参数
|
||||
|
||||
- **WHEN** 执行 `version bump` 传入既非 `major|minor|patch` 也非合法 semver 的参数
|
||||
- **THEN** 命令 SHALL 以非零退出码失败并输出错误信息
|
||||
|
||||
### Requirement: bump 自动同步下游文件
|
||||
|
||||
`version bump` 子命令 SHALL 在写回 VERSION 文件后自动执行 sync 和 check,确保 `frontend/package.json` 和所有 `frontend/.env.*` 文件与新版本号一致。
|
||||
|
||||
#### Scenario: bump 自动 sync 和 check
|
||||
|
||||
- **WHEN** 执行 `version bump minor` 且当前 VERSION 为 `0.1.0`
|
||||
- **THEN** 命令 SHALL 自动将新版本号 `0.2.0` 同步到 `frontend/package.json` 的 `version` 字段和所有 `frontend/.env.*` 的 `VITE_APP_VERSION` 变量
|
||||
- **AND** 命令 SHALL 自动验证所有下游文件版本号一致性
|
||||
|
||||
#### Scenario: sync 失败时 bump 中止
|
||||
|
||||
- **WHEN** 执行 `version bump minor` 但下游文件同步失败(如文件缺失)
|
||||
- **THEN** 命令 SHALL 以非零退出码失败
|
||||
|
||||
### Requirement: 版本号倒退防护
|
||||
|
||||
`version bump` 子命令 SHALL 检查新版本号严格大于所有已有 git tag 中的最大版本号,防止版本号倒退。
|
||||
|
||||
#### Scenario: 新版本大于已有 tag
|
||||
|
||||
- **WHEN** 已有 tag `v0.1.0`,执行 `version bump minor`
|
||||
- **THEN** 命令 SHALL 成功将版本更新为 `0.2.0`
|
||||
|
||||
#### Scenario: 新版本等于已有 tag
|
||||
|
||||
- **WHEN** 已有 tag `v0.1.0`,执行 `version bump 0.1.0`
|
||||
- **THEN** 命令 SHALL 以非零退出码失败并提示版本号已存在
|
||||
|
||||
#### Scenario: 新版本小于已有 tag
|
||||
|
||||
- **WHEN** 已有 tag `v0.2.0`,执行 `version bump 0.1.5`
|
||||
- **THEN** 命令 SHALL 以非零退出码失败并提示版本号倒退
|
||||
|
||||
#### Scenario: 无已有 tag
|
||||
|
||||
- **WHEN** 不存在任何 `v*.*.*` 格式的 git tag,执行 `version bump 0.1.0`
|
||||
- **THEN** 命令 SHALL 成功
|
||||
|
||||
### Requirement: bump 输出新版本号
|
||||
|
||||
`version bump` 子命令成功时 SHALL 仅将新版本号(不含 `v` 前缀)输出到 stdout,供 Makefile 等外部工具使用。
|
||||
|
||||
#### Scenario: 输出格式
|
||||
|
||||
- **WHEN** 执行 `version bump minor`,当前版本为 `0.1.0`
|
||||
- **THEN** stdout SHALL 输出 `0.2.0`(换行结尾,无额外内容)
|
||||
|
||||
### Requirement: 版本升迁 Makefile 编排
|
||||
|
||||
`make version-bump` SHALL 编排完整的版本升迁流程:工作区干净检查 → `version bump`(含 sync/check/倒退检查)→ git add → git commit → git tag。
|
||||
|
||||
#### Scenario: 完整升迁流程
|
||||
|
||||
- **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`
|
||||
|
||||
#### Scenario: 工作区不干净
|
||||
|
||||
- **WHEN** 执行 `make version-bump BUMP=minor`,但工作区有未提交的改动
|
||||
- **THEN** Makefile SHALL 以非零退出码失败并提示先提交或暂存改动
|
||||
|
||||
#### Scenario: 支持指定版本号
|
||||
|
||||
- **WHEN** 执行 `make version-bump SET_VERSION=1.0.0`
|
||||
- **THEN** Makefile SHALL 将 `1.0.0` 传递给 `version bump` 子命令
|
||||
|
||||
#### Scenario: 不自动推送
|
||||
|
||||
- **WHEN** `make version-bump` 成功完成
|
||||
- **THEN** commit 和 tag SHALL 仅存在于本地,SHALL NOT 自动 push 到远程
|
||||
52
versionctl/.golangci.yml
Normal file
52
versionctl/.golangci.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- errorlint
|
||||
- errcheck
|
||||
- staticcheck
|
||||
- revive
|
||||
- gocritic
|
||||
- gosec
|
||||
- nilerr
|
||||
- goimports
|
||||
- gocyclo
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-blank: true
|
||||
check-type-assertions: true
|
||||
revive:
|
||||
rules:
|
||||
- name: exported
|
||||
- name: var-naming
|
||||
- name: indent-error-flow
|
||||
- name: error-strings
|
||||
- name: error-return
|
||||
- name: blank-imports
|
||||
goimports:
|
||||
local-prefixes: nex/versionctl
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
|
||||
issues:
|
||||
exclude-generated: true
|
||||
exclude-rules:
|
||||
- path: '_test\.go'
|
||||
linters:
|
||||
- errcheck
|
||||
source: '(^\s*_\s*=|,\s*_)'
|
||||
- path: '_test\.go'
|
||||
linters:
|
||||
- revive
|
||||
text: '^exported:'
|
||||
- path: '_test\.go'
|
||||
linters:
|
||||
- gosec
|
||||
text: 'G(101|401|501)'
|
||||
- path: 'main\.go'
|
||||
linters:
|
||||
- gocyclo
|
||||
16
versionctl/Makefile
Normal file
16
versionctl/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: \
|
||||
lint test test-coverage clean
|
||||
|
||||
lint:
|
||||
go tool golangci-lint run ./...
|
||||
|
||||
test:
|
||||
go test ./... -v
|
||||
|
||||
test-coverage:
|
||||
go test ./... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
@printf 'Coverage report generated: versionctl/coverage.html\n'
|
||||
|
||||
clean:
|
||||
rm -rf coverage.out coverage.html
|
||||
14
versionctl/go.mod
Normal file
14
versionctl/go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module nex/versionctl
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require github.com/stretchr/testify v1.11.1
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
8
versionctl/go.sum
Normal file
8
versionctl/go.sum
Normal file
@@ -0,0 +1,8 @@
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"nex/backend/pkg/projectversion"
|
||||
"nex/versionctl/projectversion"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -41,6 +41,11 @@ func run(args []string) error {
|
||||
return fmt.Errorf("verify-tag 需要一个 tag 参数")
|
||||
}
|
||||
return projectversion.VerifyTag(root, args[1])
|
||||
case "bump":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("bump 需要一个参数: major|minor|patch 或具体版本号")
|
||||
}
|
||||
return runBump(root, args[1])
|
||||
case "macos-plist":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("macos-plist 需要一个最低系统版本参数")
|
||||
@@ -53,6 +58,33 @@ func run(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func runBump(root, arg string) error {
|
||||
newVersion, err := projectversion.Bump(root, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := projectversion.ListGitTags(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := projectversion.CheckNoRegression(newVersion, tags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := projectversion.Sync(root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := projectversion.Check(root); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println(newVersion.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func printMacOSPlist(root, minMacOSVersion string) error {
|
||||
version, err := projectversion.ReadString(root)
|
||||
if err != nil {
|
||||
@@ -115,5 +147,5 @@ func mustGetwd() string {
|
||||
}
|
||||
|
||||
func usageError() error {
|
||||
return fmt.Errorf("用法: versionctl <print|sync|check|verify-tag|macos-plist|asset-name>")
|
||||
return fmt.Errorf("用法: version <print|sync|check|verify-tag|bump|macos-plist|asset-name>")
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -57,6 +58,18 @@ func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
}
|
||||
|
||||
func (v Version) Less(other Version) bool {
|
||||
if v.Major != other.Major {
|
||||
return v.Major < other.Major
|
||||
}
|
||||
|
||||
if v.Minor != other.Minor {
|
||||
return v.Minor < other.Minor
|
||||
}
|
||||
|
||||
return v.Patch < other.Patch
|
||||
}
|
||||
|
||||
func FindRepoRoot(start string) (string, error) {
|
||||
current := start
|
||||
for {
|
||||
@@ -340,3 +353,91 @@ func DesktopInfoPlist(version, minMacOSVersion string) (string, error) {
|
||||
|
||||
return content + "\n", nil
|
||||
}
|
||||
|
||||
var tagRegex = regexp.MustCompile(`^v(\d+\.\d+\.\d+)$`)
|
||||
|
||||
func ListGitTags(root string) ([]string, error) {
|
||||
cmd := exec.Command("git", "-C", root, "tag", "--list", "--merge", "HEAD")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取 git tag 列表失败: %w", err)
|
||||
}
|
||||
|
||||
var tags []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
tags = append(tags, line)
|
||||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func CheckNoRegression(newVersion Version, tags []string) error {
|
||||
var maxVersion Version
|
||||
found := false
|
||||
|
||||
for _, tag := range tags {
|
||||
parts := tagRegex.FindStringSubmatch(tag)
|
||||
if parts == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := Parse(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !found || maxVersion.Less(v) {
|
||||
maxVersion = v
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
if newVersion == maxVersion {
|
||||
return fmt.Errorf("版本号 %s 已存在(tag v%s)", newVersion, maxVersion)
|
||||
}
|
||||
|
||||
if newVersion.Less(maxVersion) {
|
||||
return fmt.Errorf("版本号 %s 小于已有 tag v%s,不允许倒退", newVersion, maxVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Bump(root, arg string) (Version, error) {
|
||||
current, err := Read(root)
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
var newVersion Version
|
||||
|
||||
switch arg {
|
||||
case "major":
|
||||
newVersion = Version{Major: current.Major + 1, Minor: 0, Patch: 0}
|
||||
case "minor":
|
||||
newVersion = Version{Major: current.Major, Minor: current.Minor + 1, Patch: 0}
|
||||
case "patch":
|
||||
newVersion = Version{Major: current.Major, Minor: current.Minor, Patch: current.Patch + 1}
|
||||
default:
|
||||
parsed, parseErr := Parse(arg)
|
||||
if parseErr != nil {
|
||||
return Version{}, fmt.Errorf("参数 %q 既非 major|minor|patch 也非合法版本号: %w", arg, parseErr)
|
||||
}
|
||||
|
||||
newVersion = parsed
|
||||
}
|
||||
|
||||
versionPath := filepath.Join(root, versionFileName)
|
||||
if err := os.WriteFile(versionPath, []byte(newVersion.String()+"\n"), 0o600); err != nil {
|
||||
return Version{}, fmt.Errorf("写入 VERSION 失败: %w", err)
|
||||
}
|
||||
|
||||
return newVersion, nil
|
||||
}
|
||||
@@ -111,3 +111,105 @@ func TestDesktopInfoPlist(t *testing.T) {
|
||||
_, err = DesktopInfoPlist("1.2.3", "")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestLess(t *testing.T) {
|
||||
assert.True(t, Version{1, 0, 0}.Less(Version{2, 0, 0}))
|
||||
assert.True(t, Version{1, 1, 0}.Less(Version{1, 2, 0}))
|
||||
assert.True(t, Version{1, 0, 1}.Less(Version{1, 0, 2}))
|
||||
assert.False(t, Version{2, 0, 0}.Less(Version{1, 0, 0}))
|
||||
assert.False(t, Version{1, 0, 0}.Less(Version{1, 0, 0}))
|
||||
}
|
||||
|
||||
func TestBump(t *testing.T) {
|
||||
setupRoot := func(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(root, "VERSION"), []byte("0.1.0\n"), 0o600))
|
||||
return root
|
||||
}
|
||||
|
||||
t.Run("major", func(t *testing.T) {
|
||||
root := setupRoot(t)
|
||||
v, err := Bump(root, "major")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{1, 0, 0}, v)
|
||||
|
||||
read, readErr := ReadString(root)
|
||||
require.NoError(t, readErr)
|
||||
assert.Equal(t, "1.0.0", read)
|
||||
})
|
||||
|
||||
t.Run("minor", func(t *testing.T) {
|
||||
root := setupRoot(t)
|
||||
v, err := Bump(root, "minor")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{0, 2, 0}, v)
|
||||
|
||||
read, readErr := ReadString(root)
|
||||
require.NoError(t, readErr)
|
||||
assert.Equal(t, "0.2.0", read)
|
||||
})
|
||||
|
||||
t.Run("patch", func(t *testing.T) {
|
||||
root := setupRoot(t)
|
||||
v, err := Bump(root, "patch")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{0, 1, 1}, v)
|
||||
|
||||
read, readErr := ReadString(root)
|
||||
require.NoError(t, readErr)
|
||||
assert.Equal(t, "0.1.1", read)
|
||||
})
|
||||
|
||||
t.Run("specific version", func(t *testing.T) {
|
||||
root := setupRoot(t)
|
||||
v, err := Bump(root, "1.0.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{1, 0, 0}, v)
|
||||
})
|
||||
|
||||
t.Run("same version as current", func(t *testing.T) {
|
||||
root := setupRoot(t)
|
||||
v, err := Bump(root, "0.1.0")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, Version{0, 1, 0}, v)
|
||||
})
|
||||
|
||||
t.Run("invalid argument", func(t *testing.T) {
|
||||
root := setupRoot(t)
|
||||
_, err := Bump(root, "invalid")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckNoRegression(t *testing.T) {
|
||||
t.Run("greater than existing tag", func(t *testing.T) {
|
||||
err := CheckNoRegression(Version{0, 2, 0}, []string{"v0.1.0"})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("equal to existing tag", func(t *testing.T) {
|
||||
err := CheckNoRegression(Version{0, 1, 0}, []string{"v0.1.0"})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("less than existing tag", func(t *testing.T) {
|
||||
err := CheckNoRegression(Version{0, 1, 5}, []string{"v0.2.0"})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("no tags", func(t *testing.T) {
|
||||
err := CheckNoRegression(Version{0, 1, 0}, nil)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("skips non-semver tags", func(t *testing.T) {
|
||||
err := CheckNoRegression(Version{0, 2, 0}, []string{"v0.1.0", "some-other-tag"})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("picks max tag", func(t *testing.T) {
|
||||
err := CheckNoRegression(Version{0, 1, 5}, []string{"v0.1.0", "v0.2.0", "v0.0.5"})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user