Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/maintain-project-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ description: 维护 grpc-kit CLI 服务模板、project migrate 兼容资产和
始终保持以下约束:

- `TargetCLIVersion` 只表示执行迁移的正式 CLI 版本;pkg 目标版本独立记录。
- 只有首行完整 grpc-kit-cli `DO NOT EDIT` marker 的已有普通文件可进入 Change Plan。
- 只有首行完整 grpc-kit-cli `DO NOT EDIT` marker 的已有普通文件可进入 Change Plan;`scripts/generate.sh` 的 marker 位于第二行(首行是 shebang),0.5.1 之前无 marker 的存量世代凭"路径精确 + 全文 SHA-256 匹配冻结摘要"的内容证据窄例外整文件托管,维护规则见 SOP §3。
- 不自动修改用户代码、`go.mod`、`go.sum`,不创建、删除、重命名或 chmod 项目文件。
- 用户可以另行使用共享技能 `migrate-logrus-to-slog` 完成业务源码迁移;该授权属于 AI 编辑工作流,不得反向扩大 `project migrate` 的 Change Plan。
- 兼容资产只引用来源 fixture 已有的项目内符号;新功能走新建模板或独立 feature 流程。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
4. 新增 pkg 迁移目标时保留旧目标资产和回归测试。不要原地把 `v0.5.0` 资产目录解释成 `v0.6.0`。
5. 用户管理文件中的破坏点进入 `ManualAction`。诊断应提供稳定 code、文件、行号和建议,并排除本次 Change Plan 已整体替换的路径。
6. logrus/slog 用户源码可以推荐使用 grpc-kit 共享技能 `migrate-logrus-to-slog`。技能由用户显式授权 AI 编辑并负责语义迁移、依赖清理和完整项目验证;它不是 CLI 自动写入能力,也不能让未托管文件进入 Change Plan。
7. `scripts/generate.sh` 的 marker 位于第二行(首行是 shebang),自 0.5.1 起服务模板与迁移产物都携带它。在此之前生成的副本没有 marker,属于 marker 规则的唯一内容证据窄例外:托管仅凭"路径精确 + 全文 SHA-256 命中冻结摘要"授权整文件改写,写回内容为按 `{{ .TargetCLIVersion }}` 渲染的最新模板资产(嵌入式资产须与 `template/service/scripts/generate.sh.tmpl` 的渲染结果逐字节一致,有测试门禁),并补上第二行 marker。历史摘要只收纳无 marker 世代(v0.3.8 已冻结;v0.3.9-beta.1 因离线无法核对 tag 内容暂未冻结,此类副本走 `generate_script_modified` ManualAction 兜底)。未知内容不进入 Change Plan,只提示人工核对;文件缺失不创建。后续修改 `generate.sh.tmpl` 时必须同步重冻结嵌入资产并更新 `generateScriptAssetSHA256`;对已携带 marker 的 0.5.1+ 世代,后续版本应把 marker 识别扩展到"shebang + 第二行"走常规托管,不再向 legacy 摘要表积累条目。该规则也在 `internal/projectmigrate/scriptpatch.go` 文件头注释中记录。

## 4. 正式 pkg 兼容矩阵

Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
go-version: 1.25.13
cache-dependency-path: go.sum
- run: make test-compatibility PKG_COMPAT_VERSION=v0.5.0
- run: make test-compatibility PKG_COMPAT_VERSION=v0.5.1

template-compatibility:
runs-on: ubuntu-latest
Expand All @@ -44,8 +45,8 @@ jobs:
make protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.29.0
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.29.0
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.30.0
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.30.0
- name: Install protobuf include sources
run: |
mkdir -p "$(go env GOPATH)/src/github.com/grpc-kit"
Expand All @@ -56,5 +57,5 @@ jobs:
git -C "$(go env GOPATH)/src/github.com/googleapis/googleapis" remote add origin https://github.com/googleapis/googleapis.git
git -C "$(go env GOPATH)/src/github.com/googleapis/googleapis" fetch --depth 1 origin e0d0106516a5c613510533821e4508bc6c943b11
git -C "$(go env GOPATH)/src/github.com/googleapis/googleapis" checkout --detach FETCH_HEAD
git clone --branch v2.29.0 --depth 1 https://github.com/grpc-ecosystem/grpc-gateway.git "$(go env GOPATH)/src/github.com/grpc-ecosystem/grpc-gateway"
git clone --branch v2.30.0 --depth 1 https://github.com/grpc-ecosystem/grpc-gateway.git "$(go env GOPATH)/src/github.com/grpc-ecosystem/grpc-gateway"
- run: make test-template-compatibility
23 changes: 23 additions & 0 deletions CHANGELOG/CHANGELOG-0.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@
| Fixed | 功能的修复 |
| Security | 有关安全问题的修复 |

## [0.5.1] - 2026-09-17

### Changed

- 将 CLI 与新服务模板使用的 `protoc-gen-grpc-gateway`、`protoc-gen-openapiv2` 和 Docker include 源升级到 `v2.30.0`,生成项目的 grpc-gateway runtime 依赖同步升级到 `v2.30.0` 与 `grpc-kit/pkg v0.5.1`(含 OTel `v1.45.0`、gRPC `v1.83.2`):v2.30.0 要求的 `otelgrpc v0.70.0` 删除了 `pkg v0.5.0` 仍使用的 `StreamClientInterceptor`,runtime 升级以 `pkg v0.5.1` 完成向 stats handler 的迁移为前提;CI 模板门禁的生成工具安装同步对齐 `v2.30.0`。
- `grpc-kit/pkg v0.5.1`:客户端 OpenTelemetry 接入从已被上游移除的 `otelgrpc.StreamClientInterceptor()` 迁移到 `grpc.WithStatsHandler(otelgrpc.NewClientHandler(...))`,装配点统一收敛到 `LocalConfig.GetClientDialOption()`;依赖对齐 `grpc-gateway v2.30.0`、`otelgrpc/otelhttp v0.70.0` 与 OpenTelemetry Go `v1.45.0`,并升级 `google.golang.org/grpc v1.83.2` 修复 GO-2026-6443 server panic(govulncheck 符号级命中 `rpc.StartBackground` 调用链)。

1. 客户端遥测覆盖面扩大:普通客户端 unary RPC 与 grpc-gateway 到 gRPC Server 的内部调用新增 OTel client span/metric,streaming client 改由同一 stats handler 观测;unary/stream 共用 `grpcTracingEnableFilter`,`HealthCheck` 与配置命中的方法在 client 侧同样被过滤;observability 关闭时不装配框架 OTel client handler
2. 调用方自定义 stats handler 与框架 handler 可共存(grpc v1.83.x 的 `WithStatsHandler` 为追加语义),应避免再注册另一个 `otelgrpc.NewClientHandler()` 产生重复 span/metric
3. `GetClientStreamInterceptor()` 保留为通用流拦截器扩展入口且未废弃,但默认不再返回 OTel interceptor;只调用该方法、不使用 `GetClientDialOption()` 的外部项目在 `otelgrpc v0.70.0` 依赖图下无法编译,需改用 `GetClientDialOption()`
4. OTel 依赖族对齐到 v1.45.0/v0.70.0 一代:`contrib/instrumentation/runtime v0.70.0`、otlp exporters(5 个)`v1.45.0`、`stdoutmetric v1.45.0`、`exporters/prometheus v0.67.0`(连带 `prometheus/client_golang v1.24.1`);`go` 指令保持 `1.25.13`
5. govulncheck 符号级漏洞清零;未调用级漏洞随间接依赖升级修复 `x/mod v0.40.0`(GO-2026-6180/6179)、`oras-go v2.6.2`(GO-2026-5880)、`go-ntlmssp v0.1.1`(GO-2026-5543),`x/crypto`(GO-2026-6355/6354,修复版要求 go 1.26)、`etcd/client/pkg`(GO-2026-6107,修复需 v3.5.33)与上游暂无修复版的 GO-2026-5932 留待后续版本处理
- `project migrate` 的迁移目标基线保持 `pkg v0.5.0` 不变:`v0.5.1` 被定义为 `v0.5.0` 的补丁兼容候选,CI 迁移门禁在 `v0.5.0` 基线之外增加 `v0.5.1` 候选验证,不改写历史迁移夹具与冻结兼容资产。
- `project migrate` 将存量项目的 `scripts/generate.sh` 纳入托管同步:服务模板自本版本起在该文件第二行携带 `# Code generated by "grpc-kit-cli/...". DO NOT EDIT.` 生成标记(首行保留 shebang)。存量副本没有该标记,迁移以"路径精确 + 全文 SHA-256 匹配冻结历史版本"的内容证据窄例外授权(与 `public/embed.go` 既有先例同规则),命中 v0.3.8 冻结摘要的副本被整文件改写为最新模板并补上标记;迁移产物与 0.5.1 起新脚手架的 `generate.sh` 从此携带 marker,后续版本可改用 marker 规则托管。本地修改过或内容不明的副本只输出 `generate_script_modified` 人工核对提示,不自动覆盖;文件缺失不创建。

### Fixed

- 修复 d454cd6 工具链固化迁移遗漏:`template/service/scripts/generate.sh.tmpl` 第二个 `protoc` 调用仍单独引用旧安装位置 `/usr/local/include`,在新工具链(`binaries.sh` 安装到 `${GOPATH}/include`)下第二个 `protoc` 调用找不到 well-known types。两个 `protoc` 调用现在统一按 `-I "${GOPATH}"/include/`(优先)+ `-I /usr/local/include/`(旧安装位置兜底)的顺序包含双路径,新旧工具链及存量外部 CI 工作流均无需改动;`generate.sh` 模板同时在第二行新增生成标记。

- 修复服务模板在 `protoc-gen-openapiv2 v2.30.0` 下生成的 4xx/5xx 公共 `ErrorResponse` `$ref` 无法解析的问题:生成脚本启用 `allow_merge`,显式输入 `${GOPATH}/src/github.com/grpc-kit/api/known/status/v1/response.proto`,并将合并后的 `microservice.swagger.json` 直接输出到 `public/openapi/`;新增 `$ref` 完整性与关键公共 schema 回归门禁。
- 该问题的已知版本边界为 grpc-gateway `v2.27.5` 起;`v2.27.4` 仅用于说明问题边界,不作为实施或应急回滚方案,模板生成工具升级并固定使用当前 `v2.30.0`。
- HTTP 路由继续独立维护在 `microservice.gateway.yaml`,运行时错误响应契约不变;v2.30.0 重新生成的 gateway 代码仅包含上游请求体清理顺序调整,并已由生成项目与 gochat 测试覆盖。官方 `protoc-gen-openapiv3` 当前不读取该外部配置,因此 OpenAPI v3 条件生成延后,不随本次修复发布。

## [0.5.0] - 2026-09-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ RUN go mod download \
&& git -C $GOPATH/src/github.com/googleapis/googleapis remote add origin https://github.com/googleapis/googleapis.git \
&& git -C $GOPATH/src/github.com/googleapis/googleapis fetch --depth 1 origin e0d0106516a5c613510533821e4508bc6c943b11 \
&& git -C $GOPATH/src/github.com/googleapis/googleapis checkout --detach FETCH_HEAD \
&& git clone -b v2.29.0 --depth 1 https://github.com/grpc-ecosystem/grpc-gateway.git $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway
&& git clone -b v2.30.0 --depth 1 https://github.com/grpc-ecosystem/grpc-gateway.git $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway

# 用于 go 应用的编译
FROM --platform=$TARGETPLATFORM ${GO_IMAGE}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ grpc-kit-cli project migrate ./service
grpc-kit-cli project migrate ./service --apply
```

命令只会更新首行带有完整 `Code generated by "grpc-kit-cli/...". DO NOT EDIT.` 标记、且存在内嵌兼容资产的已有文件;不会修改 `go.mod`、业务源码或其他用户管理文件,也不会创建、删除或重命名文件。`Manual action` 表示仍需完成的 Go 版本、pkg、logrus/slog 或 context API 迁移,因此“托管文件已迁移”不等于“项目已通过编译验证”。根级 `upgrade`/`update` 不用于项目迁移;未来 CLI 自身更新保留 `self update` 语义。
命令只会更新首行带有完整 `Code generated by "grpc-kit-cli/...". DO NOT EDIT.` 标记、且存在内嵌兼容资产的已有文件;`scripts/generate.sh` 的标记位于第二行(首行是 shebang),0.5.1 之前的存量副本没有标记,迁移凭"路径精确 + 全文 SHA-256 匹配冻结历史版本"的内容证据整文件同步到最新服务模板并补上标记,本地修改过或内容不明的副本只会得到 `generate_script_modified` 人工核对提示而不被覆盖,文件缺失则不创建。命令不会修改 `go.mod`、业务源码或其他用户管理文件,也不会创建、删除或重命名文件。`Manual action` 表示仍需完成的 Go 版本、pkg、logrus/slog 或 context API 迁移,因此“托管文件已迁移”不等于“项目已通过编译验证”。根级 `upgrade`/`update` 不用于项目迁移;未来 CLI 自身更新保留 `self update` 语义。

旧服务的用户源码可使用 grpc-kit 共享技能 `migrate-logrus-to-slog` 进行 AI 辅助迁移;安装共享技能后入口为 `scripts/skills/skills/migrate-logrus-to-slog/SKILL.md`。该技能可以在用户明确授权后修改业务源码和依赖,但不会放宽 `project migrate` 的 marker 写入规则,迁移结果仍需经过 diff 审阅、生成、测试和构建。

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.0
0.5.1
17 changes: 16 additions & 1 deletion cmd/project_migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func TestProjectMigrateApply(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute() error = %v\n%s", err, output.String())
}
if !strings.Contains(output.String(), "Managed files migrated: 1") {
if !strings.Contains(output.String(), "Managed files migrated: 2") {
t.Fatalf("output = %q", output.String())
}
if !strings.Contains(output.String(), "Managed files migrated; project compatibility is not yet verified.") {
Expand All @@ -140,6 +140,13 @@ func TestProjectMigrateApply(t *testing.T) {
if !strings.Contains(string(environment), "CLI_VERSION=0.4.0") {
t.Fatalf("scripts/env was not migrated:\n%s", environment)
}
generateScript, err := os.ReadFile(filepath.Join(root, "scripts", "generate.sh"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(generateScript), `# Code generated by "grpc-kit-cli/0.4.0". DO NOT EDIT.`) {
t.Fatalf("scripts/generate.sh was not migrated with the target marker:\n%s", generateScript)
}
}

func writeCommandTestProject(t *testing.T) string {
Expand All @@ -159,6 +166,14 @@ SERVICE_CODE=${SHORT_NAME}.${API_VERSION}.${PRODUCT_CODE}
if err := os.WriteFile(filepath.Join(root, "scripts", "env"), []byte(environment), 0o600); err != nil {
t.Fatal(err)
}
// 同步 frozen legacy fixture:v0.3.8 脚手架自带的 generate.sh 副本。
generateScript, err := os.ReadFile(filepath.Join("..", "internal", "projectmigrate", "testdata", "legacy-v0.3.8-generate.sh"))
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "scripts", "generate.sh"), generateScript, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module git.lmq.io/kaopuvm/spider\n"), 0o600); err != nil {
t.Fatal(err)
}
Expand Down
59 changes: 59 additions & 0 deletions internal/projectmigrate/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,65 @@ func TestVerifyChangeInputDetectsMutation(t *testing.T) {
}
}

func TestApplyRewritesLegacyGenerateScript(t *testing.T) {
root := writePlanProject(t, true)
writeFile(t, filepath.Join(root, "scripts", "generate.sh"), legacyGenerateScriptFixture, 0o755)
initGitRepository(t, root)
beforeSnapshot := snapshotProject(t, root)

plan, err := BuildPlan(root, "0.4.0")
if err != nil {
t.Fatal(err)
}
if plan.Status != StatusReady || len(plan.ApplyBlockers) != 0 {
t.Fatalf("plan status = %s, blockers = %#v", plan.Status, plan.ApplyBlockers)
}
if got := changePaths(plan.Changes); len(got) != 7 {
t.Fatalf("change paths = %v, want 7 managed files", got)
}
if err := Apply(context.Background(), &plan); err != nil {
t.Fatalf("Apply() error = %v", err)
}
if plan.Status != StatusApplied {
t.Fatalf("Apply() status = %s", plan.Status)
}
afterSnapshot := snapshotProject(t, root)
assertOnlyPlannedChanges(t, beforeSnapshot, afterSnapshot, plan.Changes)
scriptBody, err := os.ReadFile(filepath.Join(root, "scripts", "generate.sh"))
if err != nil {
t.Fatal(err)
}
expected, err := renderGenerateScriptTarget("0.4.0")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(scriptBody, expected) {
t.Fatal("generate.sh was not rewritten to the rendered target content")
}
marker, err := ParseMarkerLine(secondLine(scriptBody))
if err != nil || marker.Version != "0.4.0" || marker.CommentPrefix != "#" {
t.Fatalf("migrated generate.sh marker = %#v, want script marker 0.4.0", marker)
}
info, err := os.Stat(filepath.Join(root, "scripts", "generate.sh"))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o755 {
t.Fatalf("generate.sh mode = %o, want 755", info.Mode().Perm())
}

second, err := BuildPlan(root, "0.4.0")
if err != nil {
t.Fatal(err)
}
if second.Status != StatusManagedUpToDate || len(second.Changes) != 0 {
t.Fatalf("second plan status = %s, changes = %v", second.Status, changePaths(second.Changes))
}
if err := Apply(context.Background(), &second); err != nil {
t.Fatalf("idempotent Apply() error = %v", err)
}
}

func initGitRepository(t *testing.T, root string) {
t.Helper()
commands := [][]string{
Expand Down
64 changes: 64 additions & 0 deletions internal/projectmigrate/assets/v0_5_0/scripts/generate.sh.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
# Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT.

set -e

source scripts/env

function gen_protoc() {
mkdir -p ./public/openapi
rm -f ./public/openapi/microservice.swagger.json
test -f "${GOPATH}"/src/github.com/grpc-kit/api/known/status/v1/response.proto

# 生成 *.pb.go 文件
# 同时兼容新旧工具链的 well-known types 位置:优先 ${GOPATH}/include(binaries.sh 安装,与 protoc 版本配对),回退 /usr/local/include(v0.3.8 旧版安装位置)
protoc \
-I ./ \
-I "${GOPATH}"/include/ \
-I /usr/local/include/ \
-I "${GOPATH}"/src \
-I "${GOPATH}"/src/github.com/grpc-ecosystem/grpc-gateway/ \
-I "${GOPATH}"/src/github.com/googleapis/googleapis/ \
--go_opt paths=source_relative \
--go_out ./ \
--go-grpc_opt paths=source_relative \
--go-grpc_opt require_unimplemented_servers=false \
--go-grpc_out ./ \
./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/*.proto

# 生成 *.pb.gw.go 与 swagger 接口文档
protoc \
-I ./ \
-I "${GOPATH}"/include/ \
-I /usr/local/include/ \
-I "${GOPATH}"/src \
-I "${GOPATH}"/src/github.com/googleapis/googleapis/ \
-I "${GOPATH}"/src/github.com/grpc-ecosystem/grpc-gateway/ \
--grpc-gateway_opt paths=source_relative \
--grpc-gateway_opt grpc_api_configuration=./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.gateway.yaml \
--grpc-gateway_out ./ \
--openapiv2_opt disable_default_errors=true \
--openapiv2_opt disable_service_tags=true \
--openapiv2_opt grpc_api_configuration=./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.gateway.yaml \
--openapiv2_opt openapi_configuration=./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.openapiv2.yaml \
--openapiv2_opt allow_merge=true \
--openapiv2_opt merge_file_name=microservice \
--openapiv2_out=json_names_for_fields=false:./public/openapi \
./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.proto \
"${GOPATH}"/src/github.com/grpc-kit/api/known/status/v1/response.proto

test -s ./public/openapi/microservice.swagger.json
cp ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.gateway.yaml ./public/openapi/
cp ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.openapiv2.yaml ./public/openapi/
}

function gen_ent() {
# 因 "go generate" 会继承 "GOOS" 与 "GOARCH" 变量,如在交叉编译环境则可能无法运行
GOOS=${GOHOSTOS} GOARCH=${GOHOSTARCH} go generate ./modeler/ent/
}

if test -z $1; then
gen_protoc
else
$1
fi
Loading