From 1b6908e5f2de6fbe2b187844195c7922f69a123a Mon Sep 17 00:00:00 2001 From: mingqing Date: Thu, 16 Jan 2025 09:41:48 +0800 Subject: [PATCH 01/57] chore: develop 0.3.9-beta.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 6678432..03fca44 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.8 +0.3.9-beta.1 From 2855938a6a7e33c49ca9dbf1521aa53237a95b9c Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 10 Feb 2025 15:08:16 +0800 Subject: [PATCH 02/57] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=20demo.py=20=E4=BD=9C=E4=B8=BA=E7=BB=9F=E4=B8=80=20py?= =?UTF-8?q?thon=20=E8=A7=84=E8=8C=83=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/modeler/flow/script/demo.py | 50 +++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/template/service/modeler/flow/script/demo.py b/template/service/modeler/flow/script/demo.py index e75154b..d108225 100644 --- a/template/service/modeler/flow/script/demo.py +++ b/template/service/modeler/flow/script/demo.py @@ -1 +1,49 @@ -print("hello world") \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +脚本名称: demo.py +功能描述: 简要描述脚本功能 +作者: Your Name +日期: YYYY-MM-DD +版本: 1.0 +""" + +import sys +import logging +from typing import List + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], + datefmt="%Y-%m-%d %H:%M:%S" +) + +def setup() -> None: + """初始化环境设置""" + logging.info("初始化环境") + +def process_data(data: List[int]) -> List[int]: + """ + 处理数据的示例函数 + + :param data: 输入的整数列表 + :return: 处理后的整数列表 + """ + return [x * 2 for x in data] + +def main() -> None: + """主函数""" + setup() + data = [1, 2, 3, 4, 5] + result = process_data(data) + logging.info(f"处理结果: {result}") + +if __name__ == "__main__": + try: + main() + except Exception as e: + logging.warning("出现异常") + sys.exit(1) From f3e4ca1b69911ca693782f86cdd29f4c39b9bccc Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 11 Feb 2025 11:37:55 +0800 Subject: [PATCH 03/57] =?UTF-8?q?refactor:=20demo.py=20=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=A0=86=E6=A0=88=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/modeler/flow/script/demo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/template/service/modeler/flow/script/demo.py b/template/service/modeler/flow/script/demo.py index d108225..01c1aa8 100644 --- a/template/service/modeler/flow/script/demo.py +++ b/template/service/modeler/flow/script/demo.py @@ -11,6 +11,7 @@ import sys import logging +import traceback from typing import List # 配置日志 @@ -45,5 +46,6 @@ def main() -> None: try: main() except Exception as e: - logging.warning("出现异常") + logging.error("出现异常: %s", e) + logging.error("堆栈信息:\n%s", traceback.format_exc()) sys.exit(1) From 01e3df9548649f620e1d71ba8633fa0c7b553606 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 7 May 2025 10:10:37 +0800 Subject: [PATCH 04/57] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20demo.py=20?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E6=A8=A1=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/modeler/flow/script/demo.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/template/service/modeler/flow/script/demo.py b/template/service/modeler/flow/script/demo.py index 01c1aa8..6e690c4 100644 --- a/template/service/modeler/flow/script/demo.py +++ b/template/service/modeler/flow/script/demo.py @@ -11,20 +11,24 @@ import sys import logging -import traceback +from datetime import datetime, timezone from typing import List # 配置日志 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", - handlers=[logging.StreamHandler(sys.stdout)], - datefmt="%Y-%m-%d %H:%M:%S" + datefmt="%Y-%m-%d %H:%M:%S", + stream=sys.stdout ) +# 全局变量 +update_time = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + def setup() -> None: """初始化环境设置""" logging.info("初始化环境") + logging.info("更新时间: %s", update_time) def process_data(data: List[int]) -> List[int]: """ @@ -33,19 +37,22 @@ def process_data(data: List[int]) -> List[int]: :param data: 输入的整数列表 :return: 处理后的整数列表 """ + return [x * 2 for x in data] def main() -> None: """主函数""" setup() + + # 局部变量 data = [1, 2, 3, 4, 5] result = process_data(data) - logging.info(f"处理结果: {result}") + + logging.info("处理结果: %s", result) if __name__ == "__main__": try: main() except Exception as e: - logging.error("出现异常: %s", e) - logging.error("堆栈信息:\n%s", traceback.format_exc()) + logging.exception("出现异常") sys.exit(1) From 28abec2ca1e679979452559705d592050848891d Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 26 Aug 2025 23:43:39 +0800 Subject: [PATCH 05/57] =?UTF-8?q?refactor:=20=E5=88=86=E7=A6=BB=20independ?= =?UTF-8?q?ent=5Fcfg=20=E5=B1=9E=E6=80=A7=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/modeler/independent_cfg.go.tmpl | 34 -------------- .../modeler/independent_option.go.tmpl | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 34 deletions(-) create mode 100644 template/service/modeler/independent_option.go.tmpl diff --git a/template/service/modeler/independent_cfg.go.tmpl b/template/service/modeler/independent_cfg.go.tmpl index 871eb66..7e7c90f 100644 --- a/template/service/modeler/independent_cfg.go.tmpl +++ b/template/service/modeler/independent_cfg.go.tmpl @@ -4,8 +4,6 @@ import ( "context" "fmt" - entsql "entgo.io/ent/dialect/sql" - "github.com/grpc-kit/pkg/cfg" "github.com/sirupsen/logrus" "{{ .Global.Repository }}/modeler/ent" @@ -21,38 +19,6 @@ type IndependentCfg struct { Name string `mapstructure:"name"` } -// ClientIndependentOption 用户自定义初始化资源选项 -type ClientIndependentOption func(cfg *IndependentCfg) - -// WithLogger 传递日志实例 -func WithLogger(logger *logrus.Entry) ClientIndependentOption { - return func(cfg *IndependentCfg) { - cfg.logger = logger - } -} - -// WithDatabaseEntDriver 传递 ent 数据库实例 -func WithDatabaseEntDriver(driver *entsql.Driver) ClientIndependentOption { - return func(cfg *IndependentCfg) { - client := ent.NewClient(ent.Driver(driver)) - cfg.db = client - } -} - -// WithWorkflow 传递流水线配置 -func WithWorkflow(logger *logrus.Entry, fcc *cfg.FlowClientConfig) ClientIndependentOption { - return func(cfg *IndependentCfg) { - if fcc != nil { - client, err := flow.NewClient(logger, fcc) - if err != nil { - panic(err) - } - - cfg.flow = client - } - } -} - // Init 用于初始化实例 func (i *IndependentCfg) Init(opts ...ClientIndependentOption) error { ctx := context.Background() diff --git a/template/service/modeler/independent_option.go.tmpl b/template/service/modeler/independent_option.go.tmpl new file mode 100644 index 0000000..e195271 --- /dev/null +++ b/template/service/modeler/independent_option.go.tmpl @@ -0,0 +1,44 @@ +// Code generated by "grpc-kit-cli/{{ .Global.ReleaseVersion }}". DO NOT EDIT. + +package modeler + +import ( + entsql "entgo.io/ent/dialect/sql" + "github.com/grpc-kit/pkg/cfg" + "github.com/sirupsen/logrus" + + "{{ .Global.Repository }}/modeler/ent" + "{{ .Global.Repository }}/modeler/flow" +) + +// ClientIndependentOption 用户自定义初始化资源选项 +type ClientIndependentOption func(cfg *IndependentCfg) + +// WithLogger 传递日志实例 +func WithLogger(logger *logrus.Entry) ClientIndependentOption { + return func(cfg *IndependentCfg) { + cfg.logger = logger + } +} + +// WithDatabaseEntDriver 传递 ent 数据库实例 +func WithDatabaseEntDriver(driver *entsql.Driver) ClientIndependentOption { + return func(cfg *IndependentCfg) { + client := ent.NewClient(ent.Driver(driver)) + cfg.db = client + } +} + +// WithWorkflow 传递流水线配置 +func WithWorkflow(logger *logrus.Entry, fcc *cfg.FlowClientConfig) ClientIndependentOption { + return func(cfg *IndependentCfg) { + if fcc != nil { + client, err := flow.NewClient(logger, fcc) + if err != nil { + panic(err) + } + + cfg.flow = client + } + } +} From 7b76b86952c0a606ebc9fa92d69f97c804030e0f Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 26 Aug 2025 23:44:20 +0800 Subject: [PATCH 06/57] =?UTF-8?q?build:=20=E6=9C=80=E4=BD=8E=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=20go=201.24=20=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/go.mod.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/service/go.mod.tmpl b/template/service/go.mod.tmpl index bfc49fb..ee24121 100644 --- a/template/service/go.mod.tmpl +++ b/template/service/go.mod.tmpl @@ -1,6 +1,6 @@ module {{ .Global.Repository }} -go 1.22.7 +go 1.24 require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 From ac5bbb4b726853d73f928d9a9e88d88a813a901a Mon Sep 17 00:00:00 2001 From: mingqing Date: Fri, 10 Oct 2025 15:13:04 +0800 Subject: [PATCH 07/57] =?UTF-8?q?docs(template):=20=E6=B7=BB=E5=8A=A0=20rp?= =?UTF-8?q?c=20=E6=96=B9=E6=B3=95=E5=AE=9A=E4=B9=89=E7=9A=84=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E7=A4=BA=E4=BE=8B=E5=8F=82=E8=80=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加书籍管理参考示例注释,展示标准CRUD操作配置方式 -保留Demo示例作为默认实现参考- 所有新增内容遵循模板变量引用规范,支持动态渲染 --- .../service/v1/microservice.gateway.yaml.tmpl | 26 +++++++++++++++++++ .../service/v1/microservice.proto.tmpl | 14 ++++++++++ 2 files changed, 40 insertions(+) diff --git a/template/service/api/template/service/v1/microservice.gateway.yaml.tmpl b/template/service/api/template/service/v1/microservice.gateway.yaml.tmpl index d4c842f..8e13165 100644 --- a/template/service/api/template/service/v1/microservice.gateway.yaml.tmpl +++ b/template/service/api/template/service/v1/microservice.gateway.yaml.tmpl @@ -3,9 +3,35 @@ config_version: 3 http: rules: + # 内置服务 + ## 健康检查 - selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.HealthCheck get: "/api/healthz" + # 参考示例,书籍管理,实现相同一组任务的方法整合在一起,前后均需空行隔开 + ## 获取书籍详情 + #- selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.GetBook + # get: "/api/publishers/{publisher_id}/books/{book_id}" + ## 获取书籍列表 + #- selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.ListBooks + # get: "/api/publishers/{publisher_id}/books" + ## 创建书籍实体 + #- selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.CreateBook + # post: "/api/publishers/{publisher_id}/books" + # body: "book" + ## 更新书籍实体 + #- selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.UpdateBook + # patch: "/api/publishers/{publisher_id}/books/{book_id}" + # body: "book" + ## 删除书籍实体 + #- selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.DeleteBook + # delete: "/api/publishers/{publisher_id}/books/{book_id}" + ## 归档书籍实体 + #- selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.ArchiveBook + # post: "/api/publishers/{publisher_id}/books/{book_id}:archive" + # body: "*" + + # 示例实现 - selector: {{ .Global.ProtoPackage }}.{{ .Global.ServiceTitle }}.Demo post: "/api/demo" body: "*" diff --git a/template/service/api/template/service/v1/microservice.proto.tmpl b/template/service/api/template/service/v1/microservice.proto.tmpl index be8b49d..ef40409 100644 --- a/template/service/api/template/service/v1/microservice.proto.tmpl +++ b/template/service/api/template/service/v1/microservice.proto.tmpl @@ -12,6 +12,20 @@ import "github.com/grpc-kit/api/known/status/v1/response.proto"; // 该微服务支持的 RPC 方法定义 service {{ title .Global.ServiceTitle }} { + // 内置服务,健康检查 rpc HealthCheck(grpc_kit.api.known.status.v1.HealthCheckRequest) returns (grpc_kit.api.known.status.v1.HealthCheckResponse) {} + + // 参考示例,书籍管理,实现相同一组任务的方法整合在一起,前后均需空行隔开 + // rpc GetBook(GetBookRequest) returns (Book) {} + // rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {} + // rpc CreateBook(CreateBookRequest) returns (Book) {} + // rpc UpdateBook(CreateBookRequest) returns (Book) {} + // rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) {} + // rpc ArchiveBook(ArchiveBookRequest) returns (ArchiveBookResponse) {} + + // 示例实现,可以删除 rpc Demo(DemoRequest) returns (DemoResponse) {} + + // xx 管理,实现相同一组任务的方法整合在一起 + // ... } From ae748aef2f1330cd4c8182126307ad8528b4edd6 Mon Sep 17 00:00:00 2001 From: mingqing Date: Fri, 10 Oct 2025 15:43:29 +0800 Subject: [PATCH 08/57] =?UTF-8?q?feat(proto):=20=E6=9B=B4=E6=96=B0proto?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=AD=E7=9A=84=E5=86=85=E9=83=A8=E5=BC=95?= =?UTF-8?q?=E7=94=A8=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在demo.proto.tmpl中添加对同服务内microservice.common.proto的相对路径引用 - 移除DemoResponse中嵌套的Pong消息定义 - 调整microservice.proto.tmpl中的导入注释及顺序 - 修改rpc_demo.go.tmpl中Pong结构体的引用方式 --- .../service/api/template/service/v1/demo.proto.tmpl | 12 +++--------- .../api/template/service/v1/microservice.proto.tmpl | 1 + template/service/handler/rpc_demo.go.tmpl | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/template/service/api/template/service/v1/demo.proto.tmpl b/template/service/api/template/service/v1/demo.proto.tmpl index 7c42f14..2288f0e 100644 --- a/template/service/api/template/service/v1/demo.proto.tmpl +++ b/template/service/api/template/service/v1/demo.proto.tmpl @@ -11,6 +11,9 @@ import "google/protobuf/empty.proto"; // 引入第三方依赖的 proto 文件 import "github.com/grpc-kit/api/known/example/v1/example.proto"; +// 同服务内的 proto 文件使用相对路径引用,既不包含代码仓库前缀 +import "api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.common.proto"; + // DemoRequest Demo 方法请求可使用的接口参数 message DemoRequest { // UUID 资源编号 @@ -22,15 +25,6 @@ message DemoRequest { // DemoResponse Demo方法响应的具体内容 message DemoResponse { - - message Pong { - // UUID 资源编号 - string uuid = 1; - - // Pong 单个资源响应内容 - grpc_kit.api.known.example.v1.ExampleResponse pong = 2; - } - // Pong 返回创建的资源 Pong pong = 1; diff --git a/template/service/api/template/service/v1/microservice.proto.tmpl b/template/service/api/template/service/v1/microservice.proto.tmpl index ef40409..c59a898 100644 --- a/template/service/api/template/service/v1/microservice.proto.tmpl +++ b/template/service/api/template/service/v1/microservice.proto.tmpl @@ -5,6 +5,7 @@ package {{ .Global.ProtoPackage }}; option go_package = "{{ .Global.Repository }}/api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }};{{ .Global.ShortName }}{{ .Template.Service.APIVersion }}"; // 同服务内的 proto 文件使用相对路径引用,既不包含代码仓库前缀 +//import "api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.common.proto"; import "api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/demo.proto"; // 引入依赖的外部 proto 文件 diff --git a/template/service/handler/rpc_demo.go.tmpl b/template/service/handler/rpc_demo.go.tmpl index a148cc0..97ff1d4 100644 --- a/template/service/handler/rpc_demo.go.tmpl +++ b/template/service/handler/rpc_demo.go.tmpl @@ -25,7 +25,7 @@ func (m *Microservice) Demo(ctx context.Context, req *pb.DemoRequest) (*pb.DemoR Ping: &examplev1.ExampleResponse{}, // POST /api/demo // GET /api/demo/{uuid} - Pong: &pb.DemoResponse_Pong{ + Pong: &pb.Pong{ Uuid: "99feafb5-bed6-4daf-927a-69a2ab80c485", Pong: &examplev1.ExampleResponse{}, }, From 0cd4482d29665d64d0a920df4c5f08552bec1c43 Mon Sep 17 00:00:00 2001 From: mingqing Date: Fri, 10 Oct 2025 15:43:51 +0800 Subject: [PATCH 09/57] =?UTF-8?q?feat(proto):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=BE=AE=E6=9C=8D=E5=8A=A1=E9=80=9A=E7=94=A8=20proto=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=96=87=E4=BB=B6-=20=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=BA=86=E6=96=B0=E7=9A=84=20proto=20=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C=E7=94=A8=E4=BA=8E=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=BE=AE=E6=9C=8D=E5=8A=A1=E7=9A=84=E9=80=9A=E7=94=A8=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=BB=93=E6=9E=84-=20=E5=AE=9A=E4=B9=89=E4=BA=86=20Po?= =?UTF-8?q?ng=20=E6=B6=88=E6=81=AF=E7=B1=BB=E5=9E=8B=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E5=90=AB=20uuid=E3=80=81pong=20=E5=93=8D=E5=BA=94=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E4=BB=A5=E5=8F=8A=E6=97=B6=E6=88=B3=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=20-=20=E5=BC=95=E5=85=A5=E4=BA=86=20google.protobuf.timestamp.?= =?UTF-8?q?proto=20=E5=92=8C=E7=AC=AC=E4=B8=89=E6=96=B9=20example.proto?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=20-=20=E9=85=8D=E7=BD=AE=E4=BA=86=20go=5Fpac?= =?UTF-8?q?kage=20=E9=80=89=E9=A1=B9=EF=BC=8C=E6=8C=87=E5=AE=9A=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=9A=84=20Go=E4=BB=A3=E7=A0=81=E5=8C=85=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=92=8C=E5=90=8D=E7=A7=B0=20-=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=BA=86=20package=20=E5=90=8D=E7=A7=B0=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E7=AC=A6=EF=BC=8C=E6=94=AF=E6=8C=81=E6=A0=B9=E6=8D=AE=E5=85=B7?= =?UTF-8?q?=E4=BD=93=E5=BE=AE=E6=9C=8D=E5=8A=A1=E5=90=8D=E7=A7=B0=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/v1/microservice.common.proto.tmpl | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 template/service/api/template/service/v1/microservice.common.proto.tmpl diff --git a/template/service/api/template/service/v1/microservice.common.proto.tmpl b/template/service/api/template/service/v1/microservice.common.proto.tmpl new file mode 100644 index 0000000..244916f --- /dev/null +++ b/template/service/api/template/service/v1/microservice.common.proto.tmpl @@ -0,0 +1,23 @@ +syntax = "proto3"; + +// 根据具体的微服务名称做更改 +package {{ .Global.ProtoPackage }}; + +option go_package = "{{ .Global.Repository }}/api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }};{{ .Global.ShortName }}{{ .Template.Service.APIVersion }}"; + +// 引入 google 公共类型 +import "google/protobuf/timestamp.proto"; + +// 引入第三方依赖的 proto 文件 +import "github.com/grpc-kit/api/known/example/v1/example.proto"; + +message Pong { + // UUID 资源编号 + string uuid = 1; + + // Pong 单个资源响应内容 + grpc_kit.api.known.example.v1.ExampleResponse pong = 2; + + google.protobuf.Timestamp created_at = 8; + google.protobuf.Timestamp updated_at = 9; +} \ No newline at end of file From f18cc5ec88c37c76935f99455a2f056e9bd4d870 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 14 Jan 2026 11:55:19 +0800 Subject: [PATCH 10/57] =?UTF-8?q?refactor(flow):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/modeler/flow/README.md | 27 ++++++++++++++ template/service/modeler/flow/flow.go.tmpl | 36 ++++++++++++++++++- .../service/modeler/flow/script/embed.go.tmpl | 4 +-- .../modeler/flow/script/{ => example}/demo.py | 4 +++ .../modeler/flow/script/{ => example}/demo.sh | 0 .../modeler/flow/template/demo.go.tmpl | 1 - .../flow/template/example/demo.go.tmpl | 1 + 7 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 template/service/modeler/flow/README.md rename template/service/modeler/flow/script/{ => example}/demo.py (93%) rename template/service/modeler/flow/script/{ => example}/demo.sh (100%) delete mode 100644 template/service/modeler/flow/template/demo.go.tmpl create mode 100644 template/service/modeler/flow/template/example/demo.go.tmpl diff --git a/template/service/modeler/flow/README.md b/template/service/modeler/flow/README.md new file mode 100644 index 0000000..781d8f4 --- /dev/null +++ b/template/service/modeler/flow/README.md @@ -0,0 +1,27 @@ +## 工作流 + +规范: + +```text +# 用于定义 argo workflow 流程编排 +template/ + +# 用于实现工作流脚本编码 +script/ +``` + +每个工作流根据类型分组在各自文件夹下,如: + +```text +template/example/ +script/example/ +``` + +当创建新目录时,需要在 `embed.go` 中添加对应目录的文件引用,如: + +```text +//go:embed example/*.sh +//go:embed example/*.py +``` + +所有脚本大小尽量避免超过 100kb 否则有可能无法运行成功。 diff --git a/template/service/modeler/flow/flow.go.tmpl b/template/service/modeler/flow/flow.go.tmpl index 7062aad..335db24 100644 --- a/template/service/modeler/flow/flow.go.tmpl +++ b/template/service/modeler/flow/flow.go.tmpl @@ -32,4 +32,38 @@ func NewClient(logger *logrus.Entry, fcc *cfg.FlowClientConfig) (*Client, error) // Create xx func (w *Client) Create(ctx context.Context) error { return nil -} \ No newline at end of file +} + +/* +func (w *Client) sync(ctx context.Context, templates ...*wfv1.WorkflowTemplate) error { + for _, tmpl := range templates { + un := &unstructured.Unstructured{} + + rawBody, err := json.Marshal(tmpl) + if err != nil { + return err + } + + if err = un.UnmarshalJSON(rawBody); err != nil { + return err + } + + gvk := un.GroupVersionKind() + res := schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + Resource: fmt.Sprintf("%v%v", strings.ToLower(gvk.Kind), "s"), + } + + _, err = w.dynamicset. + Resource(res). + Namespace(un.GetNamespace()). + Apply(ctx, un.GetName(), un, metav1.ApplyOptions{Force: true, FieldManager: w.config.Appname}) + if err != nil { + return err + } + } + + return nil +} +*/ \ No newline at end of file diff --git a/template/service/modeler/flow/script/embed.go.tmpl b/template/service/modeler/flow/script/embed.go.tmpl index 19731e6..e0d99dc 100644 --- a/template/service/modeler/flow/script/embed.go.tmpl +++ b/template/service/modeler/flow/script/embed.go.tmpl @@ -4,6 +4,6 @@ import ( "embed" ) -//go:embed *.sh -//go:embed *.py +//go:embed example/*.sh +//go:embed example/*.py var Assets embed.FS \ No newline at end of file diff --git a/template/service/modeler/flow/script/demo.py b/template/service/modeler/flow/script/example/demo.py similarity index 93% rename from template/service/modeler/flow/script/demo.py rename to template/service/modeler/flow/script/example/demo.py index 6e690c4..81726d5 100644 --- a/template/service/modeler/flow/script/demo.py +++ b/template/service/modeler/flow/script/example/demo.py @@ -51,6 +51,10 @@ def main() -> None: logging.info("处理结果: %s", result) if __name__ == "__main__": + # 全局变量定义,使用大写 + API_ENDPOINT = "" + API_TOKEN = "" + try: main() except Exception as e: diff --git a/template/service/modeler/flow/script/demo.sh b/template/service/modeler/flow/script/example/demo.sh similarity index 100% rename from template/service/modeler/flow/script/demo.sh rename to template/service/modeler/flow/script/example/demo.sh diff --git a/template/service/modeler/flow/template/demo.go.tmpl b/template/service/modeler/flow/template/demo.go.tmpl deleted file mode 100644 index 38cdfe4..0000000 --- a/template/service/modeler/flow/template/demo.go.tmpl +++ /dev/null @@ -1 +0,0 @@ -package template diff --git a/template/service/modeler/flow/template/example/demo.go.tmpl b/template/service/modeler/flow/template/example/demo.go.tmpl new file mode 100644 index 0000000..f7ec372 --- /dev/null +++ b/template/service/modeler/flow/template/example/demo.go.tmpl @@ -0,0 +1 @@ +package example From aab727d986cd4027eae9283b374ea52cdf33d7cc Mon Sep 17 00:00:00 2001 From: mingqing Date: Fri, 16 Jan 2026 15:29:53 +0800 Subject: [PATCH 11/57] =?UTF-8?q?refactor(flow):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E6=B5=81=E6=B0=B4=E7=BA=BF=E7=BC=96=E6=8E=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/modeler/flow/flow.go.tmpl | 11 ++- .../flow/template/example/demo.go.tmpl | 1 - .../flow/template/example/python.go.tmpl | 97 +++++++++++++++++++ .../flow/template/example/shell.go.tmpl | 97 +++++++++++++++++++ 4 files changed, 204 insertions(+), 2 deletions(-) delete mode 100644 template/service/modeler/flow/template/example/demo.go.tmpl create mode 100644 template/service/modeler/flow/template/example/python.go.tmpl create mode 100644 template/service/modeler/flow/template/example/shell.go.tmpl diff --git a/template/service/modeler/flow/flow.go.tmpl b/template/service/modeler/flow/flow.go.tmpl index 335db24..c72172f 100644 --- a/template/service/modeler/flow/flow.go.tmpl +++ b/template/service/modeler/flow/flow.go.tmpl @@ -31,6 +31,15 @@ func NewClient(logger *logrus.Entry, fcc *cfg.FlowClientConfig) (*Client, error) // Create xx func (w *Client) Create(ctx context.Context) error { + /* + if err := w.sync(ctx, + example.NewExampleShell(w.config).Template(), + example.NewExamplePython(w.config).Template(), + ); err != nil { + return err + } + */ + return nil } @@ -66,4 +75,4 @@ func (w *Client) sync(ctx context.Context, templates ...*wfv1.WorkflowTemplate) return nil } -*/ \ No newline at end of file +*/ diff --git a/template/service/modeler/flow/template/example/demo.go.tmpl b/template/service/modeler/flow/template/example/demo.go.tmpl deleted file mode 100644 index f7ec372..0000000 --- a/template/service/modeler/flow/template/example/demo.go.tmpl +++ /dev/null @@ -1 +0,0 @@ -package example diff --git a/template/service/modeler/flow/template/example/python.go.tmpl b/template/service/modeler/flow/template/example/python.go.tmpl new file mode 100644 index 0000000..8aab1e5 --- /dev/null +++ b/template/service/modeler/flow/template/example/python.go.tmpl @@ -0,0 +1,97 @@ +package example + +/* +import ( + "fmt" + + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/grpc-kit/pkg/cfg" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "{{ .Global.Repository }}/modeler/flow/script" +) + +// ExamplePython xx +type ExamplePython struct { + config *cfg.FlowClientConfig + template *wfv1.WorkflowTemplate +} + +// NewExampleShell xx +func NewExamplePython(config *cfg.FlowClientConfig) *ExamplePython { + t := &wfv1.WorkflowTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "WorkflowTemplate", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%v-example-python", config.Appname), + Namespace: config.Namespace, + }, + Spec: wfv1.WorkflowSpec{ + Entrypoint: "main", + ServiceAccountName: "executor", + Volumes: []corev1.Volume{ + { + Name: "global-tmp", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + Templates: []wfv1.Template{}, + ActiveDeadlineSeconds: ptr.To[int64](3600), + ArchiveLogs: ptr.To[bool](false), + }, + } + + d := &ExamplePython{config: config, template: t} + d.initTemplates() + + return d +} + +// Template xx +func (d *ExamplePython) Template() *wfv1.WorkflowTemplate { + return d.template +} + +func (d *ExamplePython) initTemplates() { + d.template.Spec.Templates = append(d.template.Spec.Templates, d.mainTemplate()) + d.template.Spec.Templates = append(d.template.Spec.Templates, d.scriptDemo()) +} + +func (d *ExamplePython) mainTemplate() wfv1.Template { + return wfv1.Template{ + Name: "main", + DAG: &wfv1.DAGTemplate{ + Tasks: []wfv1.DAGTask{ + { + Name: "demo", + Template: "demo", + }, + }, + }, + } +} + +func (d *ExamplePython) scriptDemo() wfv1.Template { + sourceRaw, err := script.Assets.ReadFile("example/demo.py") + if err != nil { + panic(err) + } + + return wfv1.Template{ + Name: "demo", + Script: &wfv1.ScriptTemplate{ + Container: corev1.Container{ + Image: "ccr.ccs.tencentyun.com/opsaid/python:alpine3.6", + Command: []string{"python"}, + }, + Source: string(sourceRaw), + }, + } +} +*/ diff --git a/template/service/modeler/flow/template/example/shell.go.tmpl b/template/service/modeler/flow/template/example/shell.go.tmpl new file mode 100644 index 0000000..e91af4b --- /dev/null +++ b/template/service/modeler/flow/template/example/shell.go.tmpl @@ -0,0 +1,97 @@ +package example + +/* +import ( + "fmt" + + wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" + "github.com/grpc-kit/pkg/cfg" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + {{ .Global.Repository }}/modeler/flow/script +) + +// ExampleShell xx +type ExampleShell struct { + config *cfg.FlowClientConfig + template *wfv1.WorkflowTemplate +} + +// NewExampleShell xx +func NewExampleShell(config *cfg.FlowClientConfig) *ExampleShell { + t := &wfv1.WorkflowTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "WorkflowTemplate", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%v-example-shell", config.Appname), + Namespace: config.Namespace, + }, + Spec: wfv1.WorkflowSpec{ + Entrypoint: "main", + ServiceAccountName: "executor", + Volumes: []corev1.Volume{ + { + Name: "global-tmp", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + Templates: []wfv1.Template{}, + ActiveDeadlineSeconds: ptr.To[int64](3600), + ArchiveLogs: ptr.To[bool](false), + }, + } + + d := &ExampleShell{config: config, template: t} + d.initTemplates() + + return d +} + +// Template xx +func (d *ExampleShell) Template() *wfv1.WorkflowTemplate { + return d.template +} + +func (d *ExampleShell) initTemplates() { + d.template.Spec.Templates = append(d.template.Spec.Templates, d.mainTemplate()) + d.template.Spec.Templates = append(d.template.Spec.Templates, d.scriptDemo()) +} + +func (d *ExampleShell) mainTemplate() wfv1.Template { + return wfv1.Template{ + Name: "main", + DAG: &wfv1.DAGTemplate{ + Tasks: []wfv1.DAGTask{ + { + Name: "demo", + Template: "demo", + }, + }, + }, + } +} + +func (d *ExampleShell) scriptDemo() wfv1.Template { + sourceRaw, err := script.Assets.ReadFile("example/demo.sh") + if err != nil { + panic(err) + } + + return wfv1.Template{ + Name: "demo", + Script: &wfv1.ScriptTemplate{ + Container: corev1.Container{ + Image: "ccr.ccs.tencentyun.com/opsaid/minideb:bullseye", + Command: []string{"sh"}, + }, + Source: string(sourceRaw), + }, + } +} +*/ From 76b2c7b7bdffdeca2ef5e207b630042559f70b3b Mon Sep 17 00:00:00 2001 From: mingqing Date: Thu, 12 Mar 2026 22:55:25 +0800 Subject: [PATCH 12/57] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20vite=20antd?= =?UTF-8?q?=20=E6=9C=80=E5=B0=8F=E5=8C=96=E4=B8=9A=E5=8A=A1=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=A8=A1=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/web/admin/README.md | 147 ++++++++++++++++++ template/web/admin/eslint.config.js | 28 ++++ template/web/admin/index.html | 13 ++ template/web/admin/package.json | 37 +++++ template/web/admin/src/main.tsx | 7 + template/web/admin/src/pages/example/demo.tsx | 48 ++++++ template/web/admin/src/routes.tsx | 6 + template/web/admin/src/vite-env.d.ts | 1 + template/web/admin/tsconfig.app.json | 31 ++++ template/web/admin/tsconfig.json | 14 ++ template/web/admin/tsconfig.node.json | 24 +++ template/web/admin/vite.config.ts | 32 ++++ 12 files changed, 388 insertions(+) create mode 100644 template/web/admin/README.md create mode 100644 template/web/admin/eslint.config.js create mode 100644 template/web/admin/index.html create mode 100644 template/web/admin/package.json create mode 100644 template/web/admin/src/main.tsx create mode 100644 template/web/admin/src/pages/example/demo.tsx create mode 100644 template/web/admin/src/routes.tsx create mode 100644 template/web/admin/src/vite-env.d.ts create mode 100644 template/web/admin/tsconfig.app.json create mode 100644 template/web/admin/tsconfig.json create mode 100644 template/web/admin/tsconfig.node.json create mode 100644 template/web/admin/vite.config.ts diff --git a/template/web/admin/README.md b/template/web/admin/README.md new file mode 100644 index 0000000..515180b --- /dev/null +++ b/template/web/admin/README.md @@ -0,0 +1,147 @@ +# 业务后台模版 + +基于 `@grpc-kit/adm` 的最小化业务后台骨架,用于新服务初始化管理后台。 + +## 初始化步骤 + +1. 将本目录复制到目标服务的 `web/admin/` 下: + +```bash +cp -r template/web/admin /path/to/your-service/web/admin +``` + +2. 修改 `package.json`: + +- `name` 改为你的服务名,如 `"myproduct-myservice-admin"` +- `@grpc-kit/adm` 的路径改为实际的模版包路径: + +```json +"@grpc-kit/adm": "file:/absolute/path/to/github.com/grpc-kit/adm/vite-antd" +``` + +3. 修改 `index.html` 中的 `` 和 `src/main.tsx` 中的 `title` 为你的服务名称。 + +4. 安装依赖并启动: + +```bash +cd web/admin +npm install +npm run dev +``` + +5. 构建(产物输出到 `../../public/admin`): + +```bash +npm run build +``` + +## 目录结构 + +``` +web/admin/ + package.json # 项目依赖与脚本 + vite.config.ts # 构建配置、alias、代理 + index.html # HTML 入口 + src/ + main.tsx # 应用入口,调用 createApp + routes.tsx # 业务路由清单(集中注册) + vite-env.d.ts # Vite 类型声明 + pages/ # 业务页面(按领域分目录) + example/ + demo.tsx + api/ # 业务 API 客户端(按 proto 自动生成) + {product}/{service}/v1/ + client.ts + types.ts + index.ts + components/ # 业务自定义组件(按需创建) +``` + +## 核心约定 + +### 路由注册 + +所有业务路由统一在 `src/routes.tsx` 中注册,不得分散到其他文件: + +```tsx +import type { RouteItem } from '@grpc-kit/adm'; +import MyPage from './pages/my-domain/my-page'; + +export const businessRoutes: RouteItem[] = [ + { path: '/my-domain/my-page', element: <MyPage /> }, +]; +``` + +### API 客户端 + +业务 API 客户端统一使用模版导出的 httpClient: + +```ts +import { httpClient } from '@grpc-kit/adm'; +``` + +API 客户端文件放在 `src/api/{productCode}/{shortName}/v1/`,通常由构建工具从 `microservice.gateway.yaml` 自动生成。 + +### 路径别名 + +Vite 和 TypeScript 中已配置以下别名: + +| 别名 | 指向 | 用途 | +|------|------|------| +| `@/` | `src/` | 本地源码 | +| `@api/` | `src/api/` | 本地业务 API | +| `@api/known/` | 模版包内置 API | 登录、用户、权限等通用接口 | + +### 模版扩展 + +通过 `createApp` 的配置参数扩展模版能力: + +```tsx +import { createApp } from '@grpc-kit/adm'; +import { businessRoutes } from './routes'; + +createApp({ + title: 'My Service Admin', // 浏览器侧边栏标题 + routes: businessRoutes, // 业务路由 + menuIcons: { // 额外图标映射(可选) + 'MyIcon': <MyIconComponent />, + }, +}); +``` + +## 新增业务页面 + +1. 在 `src/pages/` 下按领域创建目录和页面文件: + +``` +src/pages/device/ + list.tsx +``` + +2. 在 `src/routes.tsx` 中注册路由: + +```tsx +import DeviceList from './pages/device/list'; + +export const businessRoutes: RouteItem[] = [ + // ...existing routes + { path: '/device/list', element: <DeviceList /> }, +]; +``` + +3. 验证: + +```bash +npx tsc --noEmit +npm run build +``` + +## 质量门禁 + +提交前必须通过: + +```bash +npx tsc --noEmit # 类型检查 +npm run build # 构建 +npm run lint # ESLint(建议) +``` diff --git a/template/web/admin/eslint.config.js b/template/web/admin/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/template/web/admin/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/template/web/admin/index.html b/template/web/admin/index.html new file mode 100644 index 0000000..d3d9c9b --- /dev/null +++ b/template/web/admin/index.html @@ -0,0 +1,13 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Admin + + +
+ + + diff --git a/template/web/admin/package.json b/template/web/admin/package.json new file mode 100644 index 0000000..b1b17ff --- /dev/null +++ b/template/web/admin/package.json @@ -0,0 +1,37 @@ +{ + "name": "your-service-admin", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "node ./node_modules/typescript/lib/tsc.js -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@grpc-kit/adm": "file:../../path/to/grpc-kit/adm/vite-antd", + "@ant-design/icons": "^5.6.1", + "@ant-design/pro-components": "^2.8.6", + "antd": "^5.24.2", + "axios": "^1.8.1", + "jwt-decode": "^4.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/node": "^22.13.10", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.4.1" + } +} diff --git a/template/web/admin/src/main.tsx b/template/web/admin/src/main.tsx new file mode 100644 index 0000000..b1d5c9f --- /dev/null +++ b/template/web/admin/src/main.tsx @@ -0,0 +1,7 @@ +import { createApp } from '@grpc-kit/adm'; +import { businessRoutes } from './routes'; + +createApp({ + title: 'Your Service Admin', + routes: businessRoutes, +}); diff --git a/template/web/admin/src/pages/example/demo.tsx b/template/web/admin/src/pages/example/demo.tsx new file mode 100644 index 0000000..2d8663e --- /dev/null +++ b/template/web/admin/src/pages/example/demo.tsx @@ -0,0 +1,48 @@ +import { ProCard } from '@ant-design/pro-components'; +import { Button, Space, Table, Tag } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; + +const columns = [ + { + title: '名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (status: string) => ( + {status} + ), + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + key: 'updatedAt', + }, +]; + +const mockData = [ + { key: '1', name: '示例条目 A', status: 'running', updatedAt: '2026-01-01 12:00:00' }, + { key: '2', name: '示例条目 B', status: 'stopped', updatedAt: '2026-01-02 08:30:00' }, +]; + +function ExampleDemo() { + return ( + }> + 新增 + + } + > + + + + + ); +} + +export default ExampleDemo; diff --git a/template/web/admin/src/routes.tsx b/template/web/admin/src/routes.tsx new file mode 100644 index 0000000..aad16fc --- /dev/null +++ b/template/web/admin/src/routes.tsx @@ -0,0 +1,6 @@ +import type { RouteItem } from '@grpc-kit/adm'; +import ExampleDemo from './pages/example/demo'; + +export const businessRoutes: RouteItem[] = [ + { path: '/example/demo', element: }, +]; diff --git a/template/web/admin/src/vite-env.d.ts b/template/web/admin/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/template/web/admin/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/template/web/admin/tsconfig.app.json b/template/web/admin/tsconfig.app.json new file mode 100644 index 0000000..c03a215 --- /dev/null +++ b/template/web/admin/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + + "paths": { + "@/*": ["./src/*"], + "@api/*": ["./src/api/*"] + } + }, + "include": ["src"] +} diff --git a/template/web/admin/tsconfig.json b/template/web/admin/tsconfig.json new file mode 100644 index 0000000..647f194 --- /dev/null +++ b/template/web/admin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@api/*": ["./src/api/*"] + } + } +} diff --git a/template/web/admin/tsconfig.node.json b/template/web/admin/tsconfig.node.json new file mode 100644 index 0000000..db0becc --- /dev/null +++ b/template/web/admin/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/template/web/admin/vite.config.ts b/template/web/admin/vite.config.ts new file mode 100644 index 0000000..20b04f1 --- /dev/null +++ b/template/web/admin/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from "path"; + +export default defineConfig({ + base: '/admin/', + build: { + outDir: '../../public/admin', + }, + plugins: [react()], + resolve: { + alias: [ + { find: "@api/known", replacement: path.resolve(__dirname, "./node_modules/@grpc-kit/adm/src/api/known") }, + { find: "@api", replacement: path.resolve(__dirname, "./src/api") }, + { find: "@", replacement: path.resolve(__dirname, "./src") }, + ], + }, + server: { + proxy: { + '/builtin': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/builtin/, '/builtin'), + }, + '/api': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '/api'), + }, + }, + }, +}) From 444e52c779fb6f36e9d36e2f9277a41bd8177fec Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 16 Mar 2026 16:52:10 +0800 Subject: [PATCH 13/57] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20grpc-kit=20?= =?UTF-8?q?=E5=85=AC=E5=85=B1=20skills=20=E6=8A=80=E8=83=BD=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/.gitmodules | 3 +++ template/service/Makefile.tmpl | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 template/service/.gitmodules diff --git a/template/service/.gitmodules b/template/service/.gitmodules new file mode 100644 index 0000000..227eadb --- /dev/null +++ b/template/service/.gitmodules @@ -0,0 +1,3 @@ +[submodule "scripts/skills"] + path = scripts/skills + url = https://github.com/grpc-kit/skills.git diff --git a/template/service/Makefile.tmpl b/template/service/Makefile.tmpl index 608298f..abdd8df 100644 --- a/template/service/Makefile.tmpl +++ b/template/service/Makefile.tmpl @@ -66,6 +66,23 @@ test: generate ## Run unit tests. @${GO} mod tidy @${GO} test ./... -timeout 3m -v -gcflags=-l -cover=true +.PHONY: skills +skills: ## Install or update grpc-kit common skills. + @if [ ! -d ".git" ]; then \ + echo "==> Initializing git repository..."; \ + git init; \ + fi + @if [ ! -e "scripts/skills/.git" ]; then \ + echo "==> scripts/skills not found or not a submodule. Initializing..."; \ + if [ -d "scripts/skills" ]; then rm -rf scripts/skills; fi; \ + git submodule add -f https://github.com/grpc-kit/skills.git scripts/skills; \ + git submodule update --init --recursive; \ + else \ + echo "==> scripts/skills exists. Syncing with remote (main)..."; \ + git submodule update --remote --rebase scripts/skills; \ + fi + @echo "==> Skills are up-to-date." + ##@ Build .PHONY: build From 54bfdfddcd08ac3c973d271c20ef35b3c89f78b2 Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 16 Mar 2026 17:21:56 +0800 Subject: [PATCH 14/57] feat: add AGENTS.md --- template/service/AGENTS.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 template/service/AGENTS.md diff --git a/template/service/AGENTS.md b/template/service/AGENTS.md new file mode 100644 index 0000000..0059195 --- /dev/null +++ b/template/service/AGENTS.md @@ -0,0 +1,38 @@ +# AGENTS.md + +This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, etc.) when working with code in this repository. + +## Project Overview + +- Primary Language: Go (see `go.mod` for version requirement) +- Main Branch: `main` +- Current Version: See `VERSION` + +## Repository Structure + +```text +api/ +cmd/ +config/ +...... +``` + +**Key Files:** + +- `go.mod` - Go version and dependencies +- `Makefile` - Build system (see `make help`) + +## Build System + +```shell +make build +``` + +**Key Makefile Targets:** `make help` for full list + +## Testing + +## Configuration + +## Development Patterns + From 37f88b51c44273858fcee2b6f840621eb9b1d6c6 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 18 Mar 2026 19:05:30 +0800 Subject: [PATCH 15/57] refactor: AGENTS.md to AGENTS.md.tmpl --- template/service/AGENTS.md | 38 -------------- template/service/AGENTS.md.tmpl | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 38 deletions(-) delete mode 100644 template/service/AGENTS.md create mode 100644 template/service/AGENTS.md.tmpl diff --git a/template/service/AGENTS.md b/template/service/AGENTS.md deleted file mode 100644 index 0059195..0000000 --- a/template/service/AGENTS.md +++ /dev/null @@ -1,38 +0,0 @@ -# AGENTS.md - -This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, etc.) when working with code in this repository. - -## Project Overview - -- Primary Language: Go (see `go.mod` for version requirement) -- Main Branch: `main` -- Current Version: See `VERSION` - -## Repository Structure - -```text -api/ -cmd/ -config/ -...... -``` - -**Key Files:** - -- `go.mod` - Go version and dependencies -- `Makefile` - Build system (see `make help`) - -## Build System - -```shell -make build -``` - -**Key Makefile Targets:** `make help` for full list - -## Testing - -## Configuration - -## Development Patterns - diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl new file mode 100644 index 0000000..f86c905 --- /dev/null +++ b/template/service/AGENTS.md.tmpl @@ -0,0 +1,90 @@ +# AGENTS.md + +This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, etc.) when working with code in this repository. + +## Project Overview + +- Primary Language: Go (see `go.mod` for version requirement) +- Main Branch: `main` +- Current Version: See `VERSION` + +## Repository Structure + +```text +api/ +cmd/ +config/ +handler/ +modeler/ +scripts/ +docs/ +web/admin/ +deploy/ +``` + +**Key Files:** + +- `go.mod` - Go version and dependencies +- `Makefile` - Build system (see `make help`) +- `scripts/generate.sh` - Proto/Ent code generation entrypoint +- `scripts/env` - Build and generation metadata (`PRODUCT_CODE`, `SHORT_NAME`, `API_VERSION`) + +**Directory Responsibilities:** + +- `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}` - API contracts (`*.proto`) and generated RPC bindings (`*.pb.go`, `*_grpc.pb.go`, `*.pb.gw.go`) +- `cmd/server/` - service bootstrap entrypoint (generated by grpc-kit scaffolding) +- `handler/` - RPC registration and service handlers +- `modeler/` - data models, flow logic, and ent schema/runtime +- `config/` - runtime configuration files for local/minimal environments +- `scripts/` - generation, environment checks, packaging, and helper automation +- `docs/` - design, migration, and feature documentation +- `web/admin/` - frontend admin application + +## Build System + +```shell +make build +``` + +**Key Makefile Targets:** `make help` for full list + +**Recommended Workflow (source of truth: `Makefile`):** + +```shell +make precheck +make generate +make test +make build +``` + +- Use `make run` for local service startup. +- Avoid bypassing Makefile targets when adding or changing API/model code. + +## Testing + +- Default validation target is `make test` (includes `make generate` and `go test ./...`). +- Use `make lint` to run formatting and vet checks before submitting larger changes. +- If API contracts or ent schema change, run generation first and include generated artifacts in the same change. + +## Configuration + +- Local runtime config is passed via `-c`, defaulting to `./config/app.yaml` in `cmd/server/main.go`. +- For local development, use `make run`, which starts with `config/app-dev-local.yaml`. +- Keep environment-specific config in `config/`; do not hardcode runtime values into handlers/modelers. + +## Development Patterns + +- API design in the `api/` directory must comply with the [Google API Improvement Proposals (AIP)](https://google.aip.dev/). +- Keep API versioned under `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/`; register cross-domain RPCs through `microservice.proto`. +- For API changes: update `*.proto` plus gateway/openapi configs first, then run `make generate`. + +## Generated Files Policy + +- Do not manually edit files containing generated headers (for example: `cmd/server/main.go`, `api/${{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/*.pb.go`, `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.pb.gw.go`). +- Generated OpenAPI output is produced by `scripts/generate.sh` and moved to `public/openapi/`. +- If generated output is incorrect, fix source definitions (`*.proto`, gateway/openapi config, ent schema) and regenerate. + +## Environment and Dependency Notes + +- `GOPATH` must be configured before running Make targets (`scripts/precheck.sh` enforces this). +- Keep `go.mod` `replace` entries intact unless the user explicitly asks to change local dependency mapping. From 5236c74bf322ee89c92867cb3b51fd84873327d0 Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 27 Apr 2026 11:07:48 +0800 Subject: [PATCH 16/57] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Git=20?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E8=A7=84=E8=8C=83=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/AGENTS.md.tmpl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl index f86c905..916daf1 100644 --- a/template/service/AGENTS.md.tmpl +++ b/template/service/AGENTS.md.tmpl @@ -78,6 +78,23 @@ make build - Keep API versioned under `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/`; register cross-domain RPCs through `microservice.proto`. - For API changes: update `*.proto` plus gateway/openapi configs first, then run `make generate`. +## Git Commit Convention + +- All commit messages must follow [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/). +- Use the format: `type(scope)!: short summary` +- Common types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`. +- Use `!` (or a `BREAKING CHANGE:` footer) for breaking changes. +- Keep the subject concise and imperative; avoid trailing periods. + +**Examples:** + +- `feat(handler): add batch ipam allocation endpoint` +- `fix(modeler): handle empty opensearch result safely` +- `docs(readme): clarify local run prerequisites` +- `refactor(rpc_search)!: rename query fields for aip compliance` + +When a change affects generated artifacts, include source and generated files in the same commit message scope when possible. + ## Generated Files Policy - Do not manually edit files containing generated headers (for example: `cmd/server/main.go`, `api/${{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/*.pb.go`, `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.pb.gw.go`). From ad08028a428cdbd92591833f6d8f053e6652747b Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 27 Apr 2026 23:22:53 +0800 Subject: [PATCH 17/57] =?UTF-8?q?feat:=20=E5=9C=A8=E7=94=9F=E6=88=90?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=B8=AD=E6=B7=BB=E5=8A=A0=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=20microservice.gateway.yaml=20=E6=96=87=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/scripts/generate.sh.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/template/service/scripts/generate.sh.tmpl b/template/service/scripts/generate.sh.tmpl index 60437b6..0a59b0b 100755 --- a/template/service/scripts/generate.sh.tmpl +++ b/template/service/scripts/generate.sh.tmpl @@ -36,6 +36,7 @@ function gen_protoc() { # 移动生成的 microservice.swagger.json 文件 if test -f ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.swagger.json; then + cp ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.gateway.yaml ./public/openapi/ mv ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.swagger.json ./public/openapi/ fi } From 6a74c48bc66a33e80feaaa989f3e79c5f282bd26 Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 28 Apr 2026 18:34:45 +0800 Subject: [PATCH 18/57] feat: add functionality to copy microservice.openapiv2.yaml in generate script --- template/service/scripts/generate.sh.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/template/service/scripts/generate.sh.tmpl b/template/service/scripts/generate.sh.tmpl index 0a59b0b..6599304 100755 --- a/template/service/scripts/generate.sh.tmpl +++ b/template/service/scripts/generate.sh.tmpl @@ -37,6 +37,7 @@ function gen_protoc() { # 移动生成的 microservice.swagger.json 文件 if test -f ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.swagger.json; then 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/ mv ./api/${PRODUCT_CODE}/${SHORT_NAME}/${API_VERSION}/microservice.swagger.json ./public/openapi/ fi } From 9c228087006a91984c1c392c96de076faf6b03cb Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 8 Jul 2026 10:24:11 +0800 Subject: [PATCH 19/57] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20CHANGELOG-0.?= =?UTF-8?q?4=20=E6=96=87=E4=BB=B6=EF=BC=8C=E8=AE=B0=E5=BD=95=E6=96=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E3=80=81=E5=8F=98=E6=9B=B4=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=8F=8A=E5=AE=89=E5=85=A8=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 339 +++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 CHANGELOG/CHANGELOG-0.4.md diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md new file mode 100644 index 0000000..49834ca --- /dev/null +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -0,0 +1,339 @@ +# CHANGELOG-0.4 + +| 名称 | 说明 | +|------------|------------------------| +| Added | 添加新功能 | +| Changed | 功能的变更 | +| Deprecated | 未来会删除 | +| Removed | 之前为Deprecated状态,此版本被移除 | +| Fixed | 功能的修复 | +| Security | 有关安全问题的修复 | + +## [Unreleased] + +## [0.4.0] - 2026-07-08 + +### Added + +#### grpc-kit/api 模块 + +- 新增 `KnownAdmin` 内置管理服务完整 Proto 定义 + + 在 `known/admin/v1/` 目录下新增以下 Proto 文件,定义了完整的后台管理 API 接口: + + 1. `admin.proto` — `KnownAdmin` 服务入口,聚合所有管理 RPC 方法 + 2. `admin.common.proto` — 公共消息定义(AuthProvider、User、Role、Department、Group、Menu、Policy、OAuth2Client 等) + 3. `admin.gateway.yaml` — grpc-gateway 路由配置 + 4. `admin.openapiv2.yaml` / `admin.swagger.json` — OpenAPI / Swagger 文档 + 5. `auth.proto` — 登录认证、MFA 多因素认证、认证提供方管理、登录选项列表 + 6. `code.proto` — Code 校验规则定义 + 7. `config.proto` — 本地配置管理、全局设置管理 + 8. `database.proto` — 数据库初始化接口 + 9. `department.proto` — 部门管理及成员关系 + 10. `group.proto` — 群组管理及成员关系 + 11. `menu.proto` — 菜单管理 + 12. `oauth2.proto` — OAuth2 客户端管理 + 13. `policy.proto` — 策略管理 + 14. `roles.proto` — 角色管理(含成员、菜单、策略关联) + 15. `security.proto` — 安全密钥与 OAuth2 Discovery/JWKS/Userinfo + 16. `service.proto` — 服务字典与 Action 管理 + 17. `setting.proto` — 全局设置管理 + 18. `user.proto` — 用户管理 + +- 新增多因素认证(MFA)接口 + + 支持基于 TOTP 的多因素认证,包含: + 1. 登录时 MFA 二步验证(`VerifyAuthMFA`) + 2. 登录态首次配置 MFA(`StartAuthMFASetup` / `ConfirmAuthMFASetup`) + 3. 用户级 MFA 管理(`SetupUserMFA` / `ConfirmUserMFA` / `DisableUserMFA`) + +- 新增 OAuth2 客户端管理接口 + + 支持 OAuth2 客户端的完整 CRUD 操作,包含客户端密钥的自动生成与 bcrypt 哈希存储。 + +- 新增登录页认证提供方列表接口 + + `ListLoginOptions` 接口无需认证,返回最小字段集供登录页展示可用认证方式。 + +- 新增全局设置管理接口 + + 支持按分类获取和更新全局配置(`GetGlobalSettings` / `ListGlobalSettings` / `UpdateGlobalSettings`)。 + +- 新增本地配置查询接口 + + 支持获取本地配置列表及按模块获取配置(`ListLocalConfigs` / `GetLocalConfigs`)。 + +- 新增数据库初始化接口 + + `CreateDatabaseInitialize` 支持通过密码哈希初始化管理员账户。 + +- 新增服务字典与 Action 管理接口 + + `ListServices` / `ListServiceActions` 用于查询已注册的微服务及其操作定义。 + +- 新增角色策略关联接口 + + 支持角色与策略的绑定管理(`ListRolePolicies` / `CreateRolePolicies` / `UpdateRolePolicies` / `DeleteRolePolicy`)。 + +- 新增角色菜单关联接口 + + 支持角色与菜单的绑定管理(`ListRoleMenus` / `CreateRoleMenus` / `UpdateRoleMenus` / `DeleteRoleMenu`)。 + +- 引入通用可见性枚举与 `protected` 字段 + + 为部门、角色、认证提供方等实体添加 `protected` 字段,防止误删内置数据。 + +#### grpc-kit/pkg 模块 + +- 新增 Admin 管理服务完整实现 + + 在 `admin/` 目录下实现了 `KnownAdmin` 服务的所有 RPC 方法,涵盖: + 1. 认证登录(`rpc_auth_login.go`)— 支持本地静态用户、LDAP、OIDC 回调登录 + 2. MFA 多因素认证(`rpc_auth_mfa.go`、`mfa_challenge.go`、`mfa_gate.go`)— TOTP 验证、恢复码机制 + 3. 认证回调(`rpc_auth_callback.go`)— OIDC 回调用户信息入库 + 4. 用户管理(`rpc_users.go`、`social_users.go`、`static_users.go`) + 5. 角色管理(`rpc_roles.go`、`principal_role_helpers.go`) + 6. 部门管理(`rpc_departments.go`) + 7. 群组管理(`rpc_groups.go`) + 8. 菜单管理(`rpc_menus.go`) + 9. 策略管理(`rpc_policies.go`、`role_policy_helpers.go`) + 10. 角色策略关联(`rpc_role_policies.go`) + 11. OAuth2 客户端管理(`rpc_oauth2_clients.go`) + 12. 安全密钥(`rpc_security_keys.go`)— JWT 签名证书与 JWKS 管理 + 13. 全局设置(`rpc_global_settings.go`) + 14. 本地配置(`rpc_local_config.go`) + 15. 服务字典(`rpc_services.go`)— 解析 `admin.gateway.yaml` 并支持 Swagger 集成 + 16. 数据库初始化(`rpc_database.go`)— 内置菜单、部门、管理员账户种子数据 + 17. 微信开放平台登录(`wechat_open.go`) + 18. 内置种子代码管理(`well_known_seed_code.go`) + +- 新增 Lion ORM 数据模型层 + + 基于 [ent](https://entgo.io/) 框架实现完整的数据持久化层,包含以下实体模型: + 1. `users` — 用户 + 2. `useridentities` — 用户身份(本地密码、社交登录) + 3. `usermemberships` — 用户成员关系 + 4. `userprofiles` — 用户属性扩展 + 5. `authproviders` — 认证提供方 + 6. `credentials` — 凭证(安全密钥) + 7. `departments` — 部门 + 8. `groups` — 群组 + 9. `menus` — 菜单 + 10. `roles` — 角色 + 11. `rolemenus` — 角色-菜单关联 + 12. `rolepolicies` — 角色-策略关联 + 13. `principalroles` — 主体-角色关联 + 14. `policies` — 策略 + 15. `globalsettings` — 全局设置 + 16. `oauth2clients` — OAuth2 客户端 + 17. `oauth2codes` — OAuth2 授权码 + +- 新增 OPA 权限策略引擎集成 + + 集成 [Open Policy Agent (OPA)](https://www.openpolicyagent.org/) 作为权限策略验证引擎: + 1. 支持内置 Rego 规则与 RBAC 数据 + 2. 支持 `opa_envoy_plugin` 外部鉴权服务 + 3. 支持动态 data provider 配置 + 4. 支持获取本地 RBAC 数据转化为 Envoy RBAC 结构 + 5. 为自定义 HTTP handler 复用鉴权配置 + +- 新增基于 CloudEvents 的审计日志模块 + + 实现完整的 gRPC 审计拦截器,支持: + 1. Unary 与 Stream 方法的审计事件记录 + 2. 基于 [CloudEvents](https://cloudevents.io/) 规范的事件封装 + 3. 通过 Kafka(IBM/sarama)传输审计事件 + 4. 审计事件推送失败指标(`grpc_kit.audit_event.send_errors`) + 5. 支持 JSON 序列化配置 + 6. 忽略 RPC 健康检查的审计事件 + +- 新增缓存模块 + + 1. 支持 Redis 缓存(含 Cluster / Sentinel 模式) + 2. 实现内存 LRU 缓存 + 3. 支持 TTL 过期策略 + 4. Redis 操作集成 OpenTelemetry 链路与指标 + 5. 使用 gob 序列化缓存至 Redis + 6. 自动添加内部缓存键前缀 + 7. 统一缓存操作返回 bool 类型 + +- 新增 GRN(通用资源名称)解析与匹配 + + 借鉴 AWS ARN 设计 6 段固定结构的资源标识符,支持通配符匹配,与 Rego 端 `grn_match` 函数行为一致。 + +- 新增 Automations 流程编排配置 + + 支持工作流配置参数获取与脚本内容获取。 + +- 新增配置快照(Config Snapshot)功能 + +- 新增前端静态文件托管增强 + + 1. 支持后台前端 history fallback 效果 + 2. 内嵌 Swagger UI 提供 OpenAPI 文档展示 + 3. 解析 `admin.gateway.yaml` 微服务网关配置 + +- 新增加密工具 + + 1. `crypto/bcrypt.go` — bcrypt 哈希工具 + 2. `crypto/aes.go` — ent 字段 AES 加解密 + 3. `crypto/sha.go` — 公共 SHA256 计算函数 + +- 新增 LDAP 登录支持 + + 支持 LDAP 用户登录流程,包含属性同步功能。 + +- 新增静态用户登录认证 + + 支持基于本地配置文件的静态用户登录,静态用户加入 `user_id` 属性。 + +- 新增 `/debug/` 端点内部网络访问限制 + +- 新增返回 `x-` 开头 HTTP 请求头的支持 + +- 新增更丰富的错误信息自定义能力 + +### Changed + +#### grpc-kit/api 模块 + +- 统一 ID 字段类型为 `int64` + + 原先部分实体 ID 使用 `int32`,统一更改为 `int64` 以支持更大范围的标识符。 + +- 统一名称字段为系统标识符(`code`)与展示名称(`display_name`) + + 将 `name` 字段更改为 `code`,仅支持字母编码,用于系统标识。 + +- 重命名 `expired_at` 为 `expires_at` + + 统一过期时间字段命名。 + +- 重命名 `order_weight` 为 `sort_order` + + 统一排序字段命名。 + +- 重命名用户关系模型 + + 1. `UserGroup` → `GroupMember` + 2. `UserDepartment` → `DepartmentMember` + +- 统一列表 RPC 方法使用 `List` 前缀 + +- 重构成员关系模型以统一管理群组和部门成员 + +- 重构国际化名称结构,移除 `I18NName` 中的 `default_language` 字段 + +- 更新分页请求消息描述以增强清晰度和一致性 + +- 更改手机号格式以兼容国际化 + +- 更改 `idcard` 为 `national_id` + +- 统一接口前缀 + +#### grpc-kit/pkg 模块 + +- 升级 OpenTelemetry 至 v1.43.0 + + 1. 移除 grpc stream otelgrpc,改用 grpc stats 实现 + 2. 降低指标 bucket 边界数 + 3. 改进 otel grpc 使用方式 + 4. 未获取到 trace id 时使用 UUID 填充审计 ID + +- 升级 Kafka SDK:Shopify/sarama → IBM/sarama + +- 升级 OPA 至 v1.x 版本 + +- 升级 grpc-gateway 至 v2.28.0 + +- 统一 ID 字段类型从 `int32` 更改为 `int64` + +- 将 `name` 更改为 `code`,仅支持字母编码 + +- 重命名 `expired_at` 为 `expires_at` + +- 注册至配置中心使用 JSON 代替 YAML 格式 + +- 为 admin 等自定义 HTTP handler 添加鉴权中间件 + +- 优化 Redis 与 Memory 缓存实现 + +- 统一参数设置使用 `WithXXX` 风格 + +- 更改 Envoy 请求 URL 函数 + +- 移除 URL-scheme 定义,添加 scheme metadata 传递 + +- 对初始化函数 `InitX` 更改以避免不必要暴露 + +- 独立 gRPC 审计拦截器至单独模块 + +- 对 ctx 中获取的数据独立至 `rpc` 模块 + +- 减少 user 认证相关对外可视方法 + +- 统一缓存操作返回 bool 类型 + +- 对 JSON marshal 提取为公共配置 + +- 标准化本地配置快照中的服务名称 + +- 统一成员处理逻辑(跨部门和群组) + +### Removed + +#### grpc-kit/api 模块 + +- 移除已废弃的 Proto 文件 + + 移除 `policy_attachments`、`policy_statements`、`resources`、`resource_types` 等 Proto 定义,精简 API 结构。 + +- 移除 `Scope` 消息,更新 `Action` 消息结构 + +- 移除 `surface_mask` 字段 + +- 移除与资源和角色权限相关的未使用消息 + +- 移除 ent demo schema 及代码生成文件(`ent/`、`ent/schema/demo.go`、`ent/entc.go`、`ent/generate.go`) + +#### grpc-kit/pkg 模块 + +- 移除已废弃的 `policy_statement.proto` 文件及其生成代码 + +- 移除 `surface_mask` 字段及菜单相关功能 + +- 移除 Services 实体管理代码 + +- 移除未使用的 `Scopes` 和 `PermissionBindings` 事务客户端 + +- 移除 `lion_user_roles` 边及关联功能 + +- 移除不再使用的 SQLite3 依赖 + +- 移除未使用的 schema 文件及关联事务客户端 + +### Fixed + +#### grpc-kit/pkg 模块 + +- 修复审计拦截器日志记录方法(使用 `Warn` 替代 `Warnf`) +- 修复推送审计日志 response 时的空指针错误 +- 修复认证关闭下 Security Authorization 的空指针问题 +- 修复 HTTP 与 gRPC 之间链路请求 ID 传递丢失 +- 修复 JWT 为 HS256 签名时缺失 ctx 设置 groups +- 修复对象存储默认上传分块大小为 64M +- 修复配置项 `independent` 中 `time.Duration` 类型无法解析 +- 修复用户 ID 类型转换问题(int → int64) +- 修复 JWT 令牌验证逻辑 +- 修复部门成员类型默认值处理 +- 修复认证提供商类型设置问题 +- 修复 HTTP basic auth 认证无法识别 `password_hash` +- 修复用户名为空时使用 subject 作为用户名 +- 修复 URL 由 `certs` 至 `jwks` +- 修复共享实例不能存储 RPC method 变化的内容 +- 修复遗漏 `hide_children_in_menu` 后缀 +- 修复默认 RBAC 示例属性 principals 编写错误 +- 修复自定义 HTTP handler 仅在开启鉴权时输出 input 日志 +- 修复对 RBAC 数据添加包头 +- 修复 Open Policy Agent 相关包导入路径至 v1 版本 From 06ce6fcfeff2f68eaeae261fa80e86e84b9ea6a6 Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 20 Jul 2026 15:38:41 +0800 Subject: [PATCH 20/57] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=20AGENTS.md=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=EF=BC=8C=E7=BB=86=E5=8C=96=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=88=86=E7=B1=BB=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/AGENTS.md.tmpl | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl index 916daf1..f9fd8d1 100644 --- a/template/service/AGENTS.md.tmpl +++ b/template/service/AGENTS.md.tmpl @@ -22,14 +22,14 @@ web/admin/ deploy/ ``` -**Key Files:** +Key Files: - `go.mod` - Go version and dependencies - `Makefile` - Build system (see `make help`) - `scripts/generate.sh` - Proto/Ent code generation entrypoint - `scripts/env` - Build and generation metadata (`PRODUCT_CODE`, `SHORT_NAME`, `API_VERSION`) -**Directory Responsibilities:** +Directory Responsibilities: - `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}` - API contracts (`*.proto`) and generated RPC bindings (`*.pb.go`, `*_grpc.pb.go`, `*.pb.gw.go`) - `cmd/server/` - service bootstrap entrypoint (generated by grpc-kit scaffolding) @@ -46,9 +46,9 @@ deploy/ make build ``` -**Key Makefile Targets:** `make help` for full list +Key Makefile Targets: `make help` for full list -**Recommended Workflow (source of truth: `Makefile`):** +Recommended Workflow (source of truth: `Makefile`): ```shell make precheck @@ -86,7 +86,7 @@ make build - Use `!` (or a `BREAKING CHANGE:` footer) for breaking changes. - Keep the subject concise and imperative; avoid trailing periods. -**Examples:** +Examples: - `feat(handler): add batch ipam allocation endpoint` - `fix(modeler): handle empty opensearch result safely` @@ -97,9 +97,16 @@ When a change affects generated artifacts, include source and generated files in ## Generated Files Policy -- Do not manually edit files containing generated headers (for example: `cmd/server/main.go`, `api/${{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/*.pb.go`, `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.pb.gw.go`). +Files in this project fall into two categories, distinguished by whether the first line contains `// Code generated by "grpc-kit-cli/...". DO NOT EDIT.`: + +- Framework-managed files (with `DO NOT EDIT` header): Managed by grpc-kit CLI, overwritten on `grpc-kit-cli update`. Do not modify. Examples: `cmd/server/main.go`, `handler/register.go`, `handler/microservice.go`, `api/${{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/*.pb.go`, `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.pb.gw.go`. +- Business-editable files (no `DO NOT EDIT` header): Preserved on upgrade, free to modify. Examples: `handler/private.go`, `handler/rpc_demo.go`, `modeler/mcp/registrar.go`, `modeler/mcp/registrar_test.go`, `config/app-dev-local.yaml`. + +Additional rules: + +- Do not manually edit framework-managed files. If generated output is incorrect, fix source definitions (`*.proto`, gateway/openapi config, ent schema) and regenerate via `scripts/generate.sh`. - Generated OpenAPI output is produced by `scripts/generate.sh` and moved to `public/openapi/`. -- If generated output is incorrect, fix source definitions (`*.proto`, gateway/openapi config, ent schema) and regenerate. +- Place custom business logic (HTTP routes, MCP resource registration, RPC handler implementations) in business-editable files (e.g., `handler/private.go`). Framework-managed files provide extension hooks such as `privateHTTPHandle(mux)` and `privateMCPHandle()`. ## Environment and Dependency Notes From 1f51e68f39064f83f8bdff3760e991e2ee2cb1cf Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 20 Jul 2026 15:41:51 +0800 Subject: [PATCH 21/57] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20MCP=20?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E8=B5=84=E6=BA=90=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=99=A8=E6=A8=A1=E6=9D=BF=EF=BC=8C=E6=94=AF=E6=8C=81=20Tools/?= =?UTF-8?q?Resources/Prompts=20=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/config/app-dev-local.yaml.tmpl | 13 ++ template/service/handler/private.go.tmpl | 38 ++++ template/service/handler/register.go.tmpl | 6 + .../service/modeler/mcp/registrar.go.tmpl | 187 ++++++++++++++++++ .../modeler/mcp/registrar_test.go.tmpl | 29 +++ 5 files changed, 273 insertions(+) create mode 100644 template/service/modeler/mcp/registrar.go.tmpl create mode 100644 template/service/modeler/mcp/registrar_test.go.tmpl diff --git a/template/service/config/app-dev-local.yaml.tmpl b/template/service/config/app-dev-local.yaml.tmpl index 5687cf9..f3b22bf 100644 --- a/template/service/config/app-dev-local.yaml.tmpl +++ b/template/service/config/app-dev-local.yaml.tmpl @@ -52,6 +52,19 @@ debugger: # otlp: # endpoint: http://127.0.0.1:4317 +# AI 连接器配置(MCP Server) +# 启用后可在 modeler/mcp/ 下编写自定义原生 MCP 资源(Tools / Resources / Prompts), +# 并在 handler/private.go 的 privateMCPHandle() 中注册到 MCP Server。 +# 详见:https://grpc-kit.com/docs/roadmap/mcp/07-phase7-custom-tool-extension +aiconnector: + enable: false + mcp_server: + enable: false + path: /mcp + transport: streamable_http + allowed_tags: + - mcp + # 应用私有配置 independent: name: grpc-kit diff --git a/template/service/handler/private.go.tmpl b/template/service/handler/private.go.tmpl index 80f7505..1c4b153 100644 --- a/template/service/handler/private.go.tmpl +++ b/template/service/handler/private.go.tmpl @@ -7,6 +7,7 @@ import ( "google.golang.org/grpc" "{{ .Global.Repository }}/modeler" + "{{ .Global.Repository }}/modeler/mcp" ) func (m *Microservice) privateExtended() ([]modeler.ClientIndependentOption, error) { @@ -58,3 +59,40 @@ func (m *Microservice) privateHTTPHandle(mux *http.ServeMux) error { return nil } + +// privateMCPHandle 注册自定义原生 MCP 资源(Tools / Resources / Prompts)。 +// 在 HTTPHandlerFrontend(AutoBridge 已同步完成)之后、StartBackground 之前调用, +// 确保 HTTP Server 启动时所有 MCP 资源(AutoBridge Tool + 自定义)均已就绪。 +// MCP 未启用时 wrapper 为 nil,直接跳过。 +func (m *Microservice) privateMCPHandle() error { + wrapper := m.baseCfg.MCPServerInstance() + if wrapper == nil { + return nil // MCP 未启用,跳过 + } + + // 按需收集依赖(与 privateExtended() 装配 IndependentCfg 的模式一致) + var opts []mcp.RegistrarOption + + // 数据库(未配置时 GetEntClient 返回 error,静默跳过) + if db, err := m.thisCfg.GetEntClient(); err == nil && db != nil { + opts = append(opts, mcp.WithDatabase(db)) + } + + // 工作流(未配置时 GetFlowClient 返回 error,静默跳过) + if fc, err := m.thisCfg.GetFlowClient(); err == nil && fc != nil { + opts = append(opts, mcp.WithWorkflow(fc)) + } + + // 日志 + opts = append(opts, mcp.WithLogger(m.logger)) + + // 业务开发者可在此追加自定义 Option,例如: + // opts = append(opts, mcp.WithRPCClient(m.client)) + + registrar := mcp.NewRegistrar(opts...) + if regErr := registrar.Register(wrapper.MCPServer()); regErr != nil { + m.logger.Warnf("register custom mcp resources: %v", regErr) + } + + return nil +} diff --git a/template/service/handler/register.go.tmpl b/template/service/handler/register.go.tmpl index daca9ac..7783f70 100644 --- a/template/service/handler/register.go.tmpl +++ b/template/service/handler/register.go.tmpl @@ -38,6 +38,12 @@ func (m *Microservice) Register(ctx context.Context) error { return err } + // 注册自定义 MCP 资源(Tools/Resources/Prompts,在 private.go 中实现) + // 此时 HTTPHandlerFrontend 已同步完成 AutoBridge,自定义资源与 AutoBridge Tool 共存 + if err := m.privateMCPHandle(); err != nil { + return err + } + // 注册HTTP网关 if err := m.server.RegisterGateway(mux); err != nil { return err diff --git a/template/service/modeler/mcp/registrar.go.tmpl b/template/service/modeler/mcp/registrar.go.tmpl new file mode 100644 index 0000000..ce6aaf7 --- /dev/null +++ b/template/service/modeler/mcp/registrar.go.tmpl @@ -0,0 +1,187 @@ +package mcp + +import ( + "context" + "encoding/json" + + logrus "github.com/sirupsen/logrus" + + mcpserver "github.com/grpc-kit/pkg/mcp" + sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" + + "{{ .Global.Repository }}/modeler/ent" + "{{ .Global.Repository }}/modeler/flow" +) + +// 保留 mcpserver 导入以演示「Authorization header 透传」用法(见各 handler 注释示例)。 +// 业务开发者在 handler 中实际调用 mcpserver.AuthHeaderFromContext(ctx) 后可删除此行。 +var _ = mcpserver.AuthHeaderFromContext + +// Registrar 持有业务依赖,实现自定义 MCP 资源注册(Tools / Resources / Prompts)。 +// 业务开发者可在此添加 LLM client、RPC client 等依赖字段及对应 With* 选项。 +type Registrar struct { + logger *logrus.Entry + db *ent.Client + flow *flow.Client +} + +// RegistrarOption 是 Registrar 的构造选项(Functional Options 模式), +// 与 modeler.ClientIndependentOption 风格一致。 +type RegistrarOption func(*Registrar) + +// NewRegistrar 构造自定义 MCP 资源注册器。 +// 零参数时所有依赖为零值(适用于不依赖 db/flow 的纯计算型资源); +// 通过 With* 选项按需注入依赖。 +func NewRegistrar(opts ...RegistrarOption) *Registrar { + r := &Registrar{} + for _, opt := range opts { + opt(r) + } + return r +} + +// WithDatabase 注入 ent 数据库客户端 +func WithDatabase(db *ent.Client) RegistrarOption { + return func(r *Registrar) { r.db = db } +} + +// WithWorkflow 注入 flow 工作流客户端 +func WithWorkflow(flow *flow.Client) RegistrarOption { + return func(r *Registrar) { r.flow = flow } +} + +// WithLogger 注入日志实例 +func WithLogger(logger *logrus.Entry) RegistrarOption { + return func(r *Registrar) { r.logger = logger } +} + +// Register 向 SDK MCP Server 注册业务原生 MCP 资源。 +// server 参数为 wrapper.MCPServer() 返回的 SDK 实例, +// 其上提供 AddTool / AddResource / AddResourceTemplate / AddPrompt 四个方法,按需调用。 +func (r *Registrar) Register(server *sdkmcp.Server) error { + // ── Tools ────────────────────────────────────────── + // 示例 Tool:echo(业务开发者可替换为实际 Tool) + server.AddTool(&sdkmcp.Tool{ + Name: "{{ .Global.ShortName }}_echo", + Description: "回显输入文本(自定义 MCP Tool 示例)", + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "text": map[string]any{ + "type": "string", + "description": "待回显的文本", + }, + }, + "required": []string{"text"}, + }, + }, r.handleEcho) + + // ── Resources ────────────────────────────────────── + // 示例 Resource:服务配置信息(业务开发者可替换为实际 Resource) + server.AddResource(&sdkmcp.Resource{ + URI: "{{ .Global.ShortName }}://config", + Name: "config", + Description: "服务配置信息(自定义 MCP Resource 示例)", + MIMEType: "application/json", + }, r.handleReadConfig) + + // ── Prompts ──────────────────────────────────────── + // 示例 Prompt:问候模板(业务开发者可替换为实际 Prompt) + server.AddPrompt(&sdkmcp.Prompt{ + Name: "{{ .Global.ShortName }}_greeting", + Description: "生成问候语(自定义 MCP Prompt 示例)", + Arguments: []*sdkmcp.PromptArgument{ + { + Name: "name", + Description: "被问候者的名称", + Required: true, + }, + }, + }, r.handleGreeting) + + return nil +} + +// ── Tool Handler ────────────────────────────────────── +// ToolHandler 签名: func(ctx, *CallToolRequest) (*CallToolResult, error) +// 客户端调用 tools/call 时触发。注意:req.Params.Arguments 是 json.RawMessage, +// 需在 handler 内自行 unmarshal(Server.AddTool 不自动解码)。 + +func (r *Registrar) handleEcho( + ctx context.Context, req *sdkmcp.CallToolRequest, +) (*sdkmcp.CallToolResult, error) { + // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): + // authHeader := mcpserver.AuthHeaderFromContext(ctx) + + var args struct { + Text string `json:"text"` + } + if len(req.Params.Arguments) > 0 { + if err := json.Unmarshal(req.Params.Arguments, &args); err != nil { + return &sdkmcp.CallToolResult{ + IsError: true, + Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: `{"error":"invalid arguments"}`}}, + }, nil + } + } + + if r.logger != nil { + r.logger.Debugf("mcp echo tool called: text=%q", args.Text) + } + + payload, _ := json.Marshal(map[string]any{"echo": args.Text}) + return &sdkmcp.CallToolResult{ + Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: string(payload)}}, + }, nil +} + +// ── Resource Handler ────────────────────────────────── +// ResourceHandler 签名: func(ctx, *ReadResourceRequest) (*ReadResourceResult, error) +// 客户端调用 resources/read 时触发,通过 req.Params.URI 获取请求的资源 URI。 + +func (r *Registrar) handleReadConfig( + ctx context.Context, req *sdkmcp.ReadResourceRequest, +) (*sdkmcp.ReadResourceResult, error) { + // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): + // authHeader := mcpserver.AuthHeaderFromContext(ctx) + + config := map[string]any{ + "service": "{{ .Global.ShortName }}", + "version": "{{ .Global.ReleaseVersion }}", + } + payload, _ := json.Marshal(config) + return &sdkmcp.ReadResourceResult{ + Contents: []*sdkmcp.ResourceContents{ + { + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(payload), + }, + }, + }, nil +} + +// ── Prompt Handler ──────────────────────────────────── +// PromptHandler 签名: func(ctx, *GetPromptRequest) (*GetPromptResult, error) +// 客户端调用 prompts/get 时触发,通过 req.Params.Arguments 获取模板参数(map[string]string)。 + +func (r *Registrar) handleGreeting( + ctx context.Context, req *sdkmcp.GetPromptRequest, +) (*sdkmcp.GetPromptResult, error) { + // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): + // authHeader := mcpserver.AuthHeaderFromContext(ctx) + + name := req.Params.Arguments["name"] + return &sdkmcp.GetPromptResult{ + Description: "问候语模板", + Messages: []*sdkmcp.PromptMessage{ + { + // MCP Role 为字符串类型("user" | "assistant"),SDK 未导出常量 + Role: "user", + Content: &sdkmcp.TextContent{ + Text: "你好," + name + "!欢迎使用 {{ .Global.ServiceTitle }} 服务。", + }, + }, + }, + }, nil +} diff --git a/template/service/modeler/mcp/registrar_test.go.tmpl b/template/service/modeler/mcp/registrar_test.go.tmpl new file mode 100644 index 0000000..a362911 --- /dev/null +++ b/template/service/modeler/mcp/registrar_test.go.tmpl @@ -0,0 +1,29 @@ +package mcp + +import ( + "testing" + + mcpserver "github.com/grpc-kit/pkg/mcp" +) + +// TestRegistrar_AllResources 验证示例 Tool / Resource / Prompt 能成功注册到 MCP Server。 +// 示例资源不依赖 db/flow,零参数构造 Registrar。 +func TestRegistrar_AllResources(t *testing.T) { + wrapper, err := mcpserver.NewServer(true, "streamable_http") + if err != nil { + t.Fatalf("NewServer: %v", err) + } + if wrapper == nil { + t.Fatal("wrapper is nil") + } + + r := NewRegistrar() + if err := r.Register(wrapper.MCPServer()); err != nil { + t.Fatalf("Register: %v", err) + } + + // TODO: 通过 SDK session 端到端调用验证(参考 pkg/mcp/integration_test.go): + // - tools/call {{ .Global.ShortName }}_echo + // - resources/read {{ .Global.ShortName }}://config + // - prompts/get {{ .Global.ShortName }}_greeting +} From 45f618b9730b04e5858ae308d37c032cb26469cc Mon Sep 17 00:00:00 2001 From: mingqing Date: Sat, 25 Jul 2026 16:23:57 +0800 Subject: [PATCH 22/57] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20MCP=20?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=B3=A8=E5=86=8C=E5=99=A8=E7=9A=84=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=92=8C=E8=B5=84=E6=BA=90=E5=A4=84=E7=90=86=E5=99=A8?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/modeler/mcp/handler.go.tmpl | 92 ++++++++++++++++ template/service/modeler/mcp/option.go.tmpl | 23 ++++ .../service/modeler/mcp/registrar.go.tmpl | 102 ------------------ 3 files changed, 115 insertions(+), 102 deletions(-) create mode 100644 template/service/modeler/mcp/handler.go.tmpl create mode 100644 template/service/modeler/mcp/option.go.tmpl diff --git a/template/service/modeler/mcp/handler.go.tmpl b/template/service/modeler/mcp/handler.go.tmpl new file mode 100644 index 0000000..f0fa037 --- /dev/null +++ b/template/service/modeler/mcp/handler.go.tmpl @@ -0,0 +1,92 @@ +package mcp + +import ( + "context" + "encoding/json" + + sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// ── Tool Handler ────────────────────────────────────── +// ToolHandler 签名: func(ctx, *CallToolRequest) (*CallToolResult, error) +// 客户端调用 tools/call 时触发。注意:req.Params.Arguments 是 json.RawMessage, +// 需在 handler 内自行 unmarshal(Server.AddTool 不自动解码)。 + +func (r *Registrar) handleEcho( + ctx context.Context, req *sdkmcp.CallToolRequest, +) (*sdkmcp.CallToolResult, error) { + // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): + // authHeader := mcpserver.AuthHeaderFromContext(ctx) + + var args struct { + Text string `json:"text"` + } + if len(req.Params.Arguments) > 0 { + if err := json.Unmarshal(req.Params.Arguments, &args); err != nil { + return &sdkmcp.CallToolResult{ + IsError: true, + Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: `{"error":"invalid arguments"}`}}, + }, nil + } + } + + if r.logger != nil { + r.logger.Debugf("mcp echo tool called: text=%q", args.Text) + } + + payload, _ := json.Marshal(map[string]any{"echo": args.Text}) + return &sdkmcp.CallToolResult{ + Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: string(payload)}}, + }, nil +} + +// ── Resource Handler ────────────────────────────────── +// ResourceHandler 签名: func(ctx, *ReadResourceRequest) (*ReadResourceResult, error) +// 客户端调用 resources/read 时触发,通过 req.Params.URI 获取请求的资源 URI。 + +func (r *Registrar) handleReadConfig( + ctx context.Context, req *sdkmcp.ReadResourceRequest, +) (*sdkmcp.ReadResourceResult, error) { + // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): + // authHeader := mcpserver.AuthHeaderFromContext(ctx) + + config := map[string]any{ + "service": "{{ .Global.ShortName }}", + "version": "{{ .Global.ReleaseVersion }}", + } + payload, _ := json.Marshal(config) + return &sdkmcp.ReadResourceResult{ + Contents: []*sdkmcp.ResourceContents{ + { + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(payload), + }, + }, + }, nil +} + +// ── Prompt Handler ──────────────────────────────────── +// PromptHandler 签名: func(ctx, *GetPromptRequest) (*GetPromptResult, error) +// 客户端调用 prompts/get 时触发,通过 req.Params.Arguments 获取模板参数(map[string]string)。 + +func (r *Registrar) handleGreeting( + ctx context.Context, req *sdkmcp.GetPromptRequest, +) (*sdkmcp.GetPromptResult, error) { + // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): + // authHeader := mcpserver.AuthHeaderFromContext(ctx) + + name := req.Params.Arguments["name"] + return &sdkmcp.GetPromptResult{ + Description: "问候语模板", + Messages: []*sdkmcp.PromptMessage{ + { + // MCP Role 为字符串类型("user" | "assistant"),SDK 未导出常量 + Role: "user", + Content: &sdkmcp.TextContent{ + Text: "你好," + name + "!欢迎使用 {{ .Global.ServiceTitle }} 服务。", + }, + }, + }, + }, nil +} diff --git a/template/service/modeler/mcp/option.go.tmpl b/template/service/modeler/mcp/option.go.tmpl new file mode 100644 index 0000000..4bdd865 --- /dev/null +++ b/template/service/modeler/mcp/option.go.tmpl @@ -0,0 +1,23 @@ +package mcp + +import ( + logrus "github.com/sirupsen/logrus" + + "{{ .Global.Repository }}/modeler/ent" + "{{ .Global.Repository }}/modeler/flow" +) + +// WithDatabase 注入 ent 数据库客户端 +func WithDatabase(db *ent.Client) RegistrarOption { + return func(r *Registrar) { r.db = db } +} + +// WithWorkflow 注入 flow 工作流客户端 +func WithWorkflow(flow *flow.Client) RegistrarOption { + return func(r *Registrar) { r.flow = flow } +} + +// WithLogger 注入日志实例 +func WithLogger(logger *logrus.Entry) RegistrarOption { + return func(r *Registrar) { r.logger = logger } +} diff --git a/template/service/modeler/mcp/registrar.go.tmpl b/template/service/modeler/mcp/registrar.go.tmpl index ce6aaf7..72f3983 100644 --- a/template/service/modeler/mcp/registrar.go.tmpl +++ b/template/service/modeler/mcp/registrar.go.tmpl @@ -1,9 +1,6 @@ package mcp import ( - "context" - "encoding/json" - logrus "github.com/sirupsen/logrus" mcpserver "github.com/grpc-kit/pkg/mcp" @@ -40,21 +37,6 @@ func NewRegistrar(opts ...RegistrarOption) *Registrar { return r } -// WithDatabase 注入 ent 数据库客户端 -func WithDatabase(db *ent.Client) RegistrarOption { - return func(r *Registrar) { r.db = db } -} - -// WithWorkflow 注入 flow 工作流客户端 -func WithWorkflow(flow *flow.Client) RegistrarOption { - return func(r *Registrar) { r.flow = flow } -} - -// WithLogger 注入日志实例 -func WithLogger(logger *logrus.Entry) RegistrarOption { - return func(r *Registrar) { r.logger = logger } -} - // Register 向 SDK MCP Server 注册业务原生 MCP 资源。 // server 参数为 wrapper.MCPServer() 返回的 SDK 实例, // 其上提供 AddTool / AddResource / AddResourceTemplate / AddPrompt 四个方法,按需调用。 @@ -101,87 +83,3 @@ func (r *Registrar) Register(server *sdkmcp.Server) error { return nil } - -// ── Tool Handler ────────────────────────────────────── -// ToolHandler 签名: func(ctx, *CallToolRequest) (*CallToolResult, error) -// 客户端调用 tools/call 时触发。注意:req.Params.Arguments 是 json.RawMessage, -// 需在 handler 内自行 unmarshal(Server.AddTool 不自动解码)。 - -func (r *Registrar) handleEcho( - ctx context.Context, req *sdkmcp.CallToolRequest, -) (*sdkmcp.CallToolResult, error) { - // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): - // authHeader := mcpserver.AuthHeaderFromContext(ctx) - - var args struct { - Text string `json:"text"` - } - if len(req.Params.Arguments) > 0 { - if err := json.Unmarshal(req.Params.Arguments, &args); err != nil { - return &sdkmcp.CallToolResult{ - IsError: true, - Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: `{"error":"invalid arguments"}`}}, - }, nil - } - } - - if r.logger != nil { - r.logger.Debugf("mcp echo tool called: text=%q", args.Text) - } - - payload, _ := json.Marshal(map[string]any{"echo": args.Text}) - return &sdkmcp.CallToolResult{ - Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: string(payload)}}, - }, nil -} - -// ── Resource Handler ────────────────────────────────── -// ResourceHandler 签名: func(ctx, *ReadResourceRequest) (*ReadResourceResult, error) -// 客户端调用 resources/read 时触发,通过 req.Params.URI 获取请求的资源 URI。 - -func (r *Registrar) handleReadConfig( - ctx context.Context, req *sdkmcp.ReadResourceRequest, -) (*sdkmcp.ReadResourceResult, error) { - // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): - // authHeader := mcpserver.AuthHeaderFromContext(ctx) - - config := map[string]any{ - "service": "{{ .Global.ShortName }}", - "version": "{{ .Global.ReleaseVersion }}", - } - payload, _ := json.Marshal(config) - return &sdkmcp.ReadResourceResult{ - Contents: []*sdkmcp.ResourceContents{ - { - URI: req.Params.URI, - MIMEType: "application/json", - Text: string(payload), - }, - }, - }, nil -} - -// ── Prompt Handler ──────────────────────────────────── -// PromptHandler 签名: func(ctx, *GetPromptRequest) (*GetPromptResult, error) -// 客户端调用 prompts/get 时触发,通过 req.Params.Arguments 获取模板参数(map[string]string)。 - -func (r *Registrar) handleGreeting( - ctx context.Context, req *sdkmcp.GetPromptRequest, -) (*sdkmcp.GetPromptResult, error) { - // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): - // authHeader := mcpserver.AuthHeaderFromContext(ctx) - - name := req.Params.Arguments["name"] - return &sdkmcp.GetPromptResult{ - Description: "问候语模板", - Messages: []*sdkmcp.PromptMessage{ - { - // MCP Role 为字符串类型("user" | "assistant"),SDK 未导出常量 - Role: "user", - Content: &sdkmcp.TextContent{ - Text: "你好," + name + "!欢迎使用 {{ .Global.ServiceTitle }} 服务。", - }, - }, - }, - }, nil -} From 09e0edd8e77ac406aa38ddd67d9c1b60e69a0798 Mon Sep 17 00:00:00 2001 From: mingqing Date: Sat, 25 Jul 2026 17:45:30 +0800 Subject: [PATCH 23/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20CHANGELOG-0.?= =?UTF-8?q?4=EF=BC=8C=E6=B7=BB=E5=8A=A0=200.4.1=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=E3=80=81=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E5=92=8C=E7=A7=BB=E9=99=A4=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index 49834ca..d3d273a 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -11,6 +11,87 @@ ## [Unreleased] +## [0.4.1] - 2026-07-25 + +### Added + +#### grpc-kit/api 模块 + +- 新增企业级凭证(Credential)管理完整 API 定义 + + 在 `known/admin/v1/` 下新增凭证管理的完整接口,支持 API Key、对称密钥、非对称密钥对、X.509 证书、软件许可证、通用密文六类凭证: + + 1. 新增 `Credential` 核心消息(`admin.common.proto`),采用分区设计(标识区 -> 展示区 -> 分类区 -> 管理区 -> 密钥材料区 -> 生命周期区 -> 扩展区 -> 审计区),含 `Type`/`Algorithm`/`Usage`/`Status`/`Source` 嵌套枚举,密钥材料以 `oneof key_material` 表达 + 2. 新增 6 个 RPC 方法(`admin.proto`):`CreateCredential` / `ListCredentials` / `GetCredential` / `UpdateCredential` / `DeleteCredential` / `RevealCredentialSecret` + 3. 新增 `security.proto` 请求/响应消息,支持 cursor/offset 双分页、AIP-160 `filter`、`order_by`、`FieldMask` 局部更新 + 4. `RevealCredentialSecret` 支持用户密码 SHA256 二次验证后按类型揭示敏感字段 + 5. 新增 `CredentialCode` 种子枚举(`code.proto`),含 `CREDENTIAL_CODE_JWT_SIGNING_V1`(DB code: `jwt-signing-v1`) + 6. 新增 gateway 路由(`admin.gateway.yaml`,前缀 `/builtin/admin/api/v1/credentials`)及 OpenAPI/Swagger 文档 + +#### grpc-kit/pkg 模块 + +- 新增凭证(Credential)管理服务实现 + + 在 `admin/rpc_security_keys.go` 实现 `KnownAdmin` 服务的全部凭证 RPC 方法: + + 1. 敏感字段(`api_secret`/`private_key`/`passphrase`/`license_key`/`symmetric_key`)使用 AES-GCM 加密存储,`credentialToProto` 永不映射 `*_encrypted` 字段 + 2. `computeFingerprint` 按凭证类型计算 SHA-256 摘要(64 字符 hex),用于幂等去重;`SECRET` 类型 Token 持久化(`type=SECRET + usage=AUTH + source=USER`)跳过指纹检查 + 3. `RevealCredentialSecret` 校验当前用户密码 SHA256 后解密返回敏感字段 + 4. `CreateCredential` 空 `code` 时自动生成 12 位随机码(复用 `schema.EnsureCode`) + 5. `UpdateCredential` 通过 `FieldMask` 支持局部更新,`code`/`type`/`algorithm`/`key_material`/`fingerprint` 不可修改 + 6. `DeleteCredential` 软删除,受保护凭证(`protected=true`)禁止删除 + +- 新增 Lion ORM `credentials` 实体模型 + + 在 `lion/credentials/` 新增基于 ent 的凭证实体,覆盖 API Key、密钥对、X.509 证书、许可证、对称密钥等全部字段类型及审计字段。 + +- 新增凭证种子代码管理 + + `admin/well_known_seed_code.go` 新增 `seedCredentialCode()`,将 `CREDENTIAL_CODE_JWT_SIGNING_V1` 映射为 DB code `jwt-signing-v1`,与既有 `DepartmentCode`/`RoleCode`/`AuthProviderCode`/`BootstrapUsername` 风格统一。 + +- 新增 MCP Server 优雅关闭 + + `mcp/mcp.go` 新增 `Server.Close()` 方法,采用 best-effort 策略遍历关闭所有活跃 sessions,收集首个错误但继续关闭剩余 session。 + +- 新增 MCP 内置 Resources 与 Prompt + + 1. `mcp/tools/resources.go` 新增 `RegisterBuiltinResources()`,注册 `grpc-kit://version`、`grpc-kit://openapi-spec/microservice`、`grpc-kit://openapi-spec/admin` 三个内置资源,镜像已公开的 `/version`、`/openapi-spec` HTTP 端点 + 2. `mcp/tools/prompts.go` 新增 `RegisterGettingStartedPrompt()` 内置 Prompt `getting_started`,服务名取自 microservice swagger 的 `info.title` + +- 新增 AutoBridge HTTP 方法语义标注 + + `mcp/tools/bridge.go` 新增 `buildToolAnnotations()`,按 HTTP method 推断 MCP `ToolAnnotations`:GET/HEAD/OPTIONS 只读+幂等、PUT 幂等写入、DELETE 幂等破坏性、POST/PATCH 非幂等;`DestructiveHint` 显式置 `false` 以覆盖 SDK 默认值。 + +- 新增 MFA 自服务校验 + + `admin/rpc_auth_mfa.go` 为 `SetupUserMFA` / `DisableUserMFA` 增加自服务门:用户只能管理自己的 MFA,防止已认证但无角色用户越权操作他人账户。 + +### Changed + +#### grpc-kit/pkg 模块 + +- 调整 AutoBridge 工具命名格式 + + `mcp/tools/bridge.go` 工具名称仅取方法名(snake_case),移除服务名前缀;多 service 场景下 method 重名由 `uniqueToolName` 追加 `_2`/`_3` 兜底。 + +- 更新凭证种子代码为 JWT 签名版本 + + `admin/well_known_seed_code.go` 凭证种子代码调整为 JWT 签名版本,同步调整 `rpc_security_keys_test.go` 相关测试用例。 + +### Removed + +#### grpc-kit/api 模块 + +- 移除 `CredentialSeedCode` 枚举 + + 移除旧的 `CredentialSeedCode` 枚举,统一为 `CredentialCode` 种子枚举,与既有种子代码体系风格一致。 + +#### grpc-kit/pkg 模块 + +- 移除 MCP 内部运行配置暴露工具 + + 移除早期 `get_config` 等内部运行配置暴露工具(见 ADR-009),改为通过内置 Resources 镜像公开 HTTP 端点,不引入新安全面。 + ## [0.4.0] - 2026-07-08 ### Added From 4b87c3b7a4e58bea0d6250a201c7258f0f27093f Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 5 Aug 2026 10:50:21 +0800 Subject: [PATCH 24/57] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20CHANGELOG-0.?= =?UTF-8?q?4=EF=BC=8C=E6=B7=BB=E5=8A=A0=200.4.2=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=E3=80=81=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E3=80=81=E4=BF=AE=E5=A4=8D=E5=92=8C=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index d3d273a..cc45df2 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -11,6 +11,89 @@ ## [Unreleased] +## [0.4.2] - 2026-08-05 + +### Added + +#### grpc-kit/pkg 模块 + +- 新增标准化 Access Token Claims 与统一签发器 + + 1. `auth/token.go` 新增 `CommonClaims` 与 `AccessTokenClaims`,分离 OIDC ID Token 和 OAuth 2.0 Access Token 的语义 + 2. Access Token 新增 `client_id` / `scope` / `roles` 声明,并统一携带 `sub` / `iat` / `exp` / `jti` / `tenant` / `groups` + 3. 新增 `BuildAccessTokenClaims` 以及 HS256 / RS256 签名辅助函数;Access Token 的 JWT `typ` 为 `at+jwt`,ID Token 为 `JWT` + 4. 统一本地静态用户、LDAP、OIDC、OAuth2 与 MFA 验证后的令牌签发流程 + +- 新增基于 `roles` 的授权配置与 Context API + + 1. `security.authorization` 新增 `allowed_roles`,`http_users` 新增 `roles` + 2. `cfg.LocalConfig` 新增 `AccessTokenFrom()` / `RolesFrom()` + 3. `rpc` 新增 `ContextWithTokenClaims()` / `GetTokenClaimsFromContext()` 与 roles Context 存取方法 + 4. 旧的 `allowed_groups` 继续兼容;`IDTokenFrom()` 和 ID Token Context 方法保留一个兼容周期 + +- 新增 JWKS 凭证生命周期保护 + + 1. 创建 KEY_PAIR / X.509 凭证时,支持将 PEM 或 DER 公钥、私钥和证书统一归一化为 DER 存储,PKCS#8 RSA 私钥转换为 PKCS#1 DER + 2. 禁止禁用或删除最后一个活跃 JWKS 凭证,确保系统始终保留可用签名密钥 + 3. JWKS 端点同时返回 `ACTIVE` 和 `EXPIRED` 公钥,便于密钥轮换后继续验证尚未过期的历史令牌 + +- OAuth2 Userinfo 新增 HTTP Basic Auth 支持 + + `/builtin/admin/api/v1/oauth2/userinfo` 现可使用 Bearer Access Token 或 `app.yaml` 中配置的 HTTP Basic 用户访问;在未配置数据库时也可返回最小身份信息。 + +### Changed + +#### grpc-kit/pkg 模块 + +- 分离角色与群组语义 + + `roles` 专用于授权,`groups` 仅表示用户的群组成员关系;权限前置门和 `allowed_roles` 均仅检查角色。`allowed_groups` 仅作为旧配置名兼容,其值仍按 role code 解释;同时配置两者时必须保持集合一致,否则拒绝授权。 + +- 调整 JWT / OIDC 声明模型 + + 1. 新签发令牌使用 OIDC 标准 `preferred_username`,仍兼容读取旧 `username` 声明 + 2. 新 Access Token 不再签发历史 `appid` / `username` 声明,也不再生成 `@localhost` 占位邮箱 + 3. `IDTokenClaims` 补充 `nonce` / `azp` / `auth_time` / `acr` / `amr` / `at_hash` / `c_hash` 等 OIDC 标准声明 + +- 改进角色与群组的有效性计算 + + 签发令牌时分别计算 role code 与 group code,并过滤未激活、已过期或已删除的成员关系、群组、部门和角色。 + +- 调整 MFA Challenge 令牌签发上下文 + + MFA 挑战保留 `client_id` / `scope` / `tenant` / TTL,确保完成 MFA 后签发的 Access Token 与原始登录请求一致;Challenge 读写改为深拷贝和显式更新暂存密钥。 + +- 升级依赖 + + 1. `github.com/golang-jwt/jwt` 从 v4.5.2 升级至 v5.3.1 + 2. `github.com/modelcontextprotocol/go-sdk` 从 v1.6.1 升级至 v1.7.0 + +- 升级注意 + + 1. 仅包含 `groups` 而不包含 `roles` 的旧 Access Token 不再获得角色权限,升级后应让用户重新登录以换取新令牌 + 2. 配置项建议从 `allowed_groups` 迁移至 `allowed_roles`;迁移期间同时配置时,两者的 role code 必须一致 + 3. 直接构造 `auth.IDTokenClaims` 的调用方需适配内嵌的 `CommonClaims`;业务 Access Token 请改用 `AccessTokenClaims`、`AccessTokenFrom()` 与新 Token Claims Context API + +### Fixed + +#### grpc-kit/pkg 模块 + +- 修复 `SetEmail()` 条件反转导致非空邮箱未被写入的问题,并按上游 `email_verified` 声明设置 GitHub / OAuth2 用户的邮箱验证状态。 + +- 修复角色创建和更新时遗漏 `status` 字段的问题;创建时未指定状态默认为 `ACTIVE`。 + +- 修复受保护凭证无法更新 `status` 的问题,保留 `display_name` / `description` / `status` 三个可更新字段。 + +- 修复 HS256 验证在启用 `skip_expiry_check` 时的过期校验处理,改用 jwt/v5 `WithoutClaimsValidation` 跳过声明校验,但仍保留签名验证。 + +### Security + +#### grpc-kit/pkg 模块 + +- OIDC 登录改为通过 Provider Discovery 获取 Verifier,并校验上游 `id_token` 的签名、Issuer、Audience 与有效期后再读取 Claims。 + +- 授权不再将 `groups` 声明回退解释为角色,避免群组成员关系与权限角色混用造成误授权。 + ## [0.4.1] - 2026-07-25 ### Added From 2e9d6509f4100d313187051102d7ac1f700e6e5f Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 9 Aug 2026 17:45:22 +0800 Subject: [PATCH 25/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20go.mod=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=EF=BC=8C=E5=8D=87=E7=BA=A7=20Go=20=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=87=B3=201.25.0=20=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/go.mod.tmpl | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/template/service/go.mod.tmpl b/template/service/go.mod.tmpl index ee24121..40da9cf 100644 --- a/template/service/go.mod.tmpl +++ b/template/service/go.mod.tmpl @@ -1,16 +1,19 @@ module {{ .Global.Repository }} -go 1.24 +go 1.25.0 require ( - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 - github.com/grpc-kit/pkg v0.3.8 - github.com/sirupsen/logrus v1.9.3 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.18.2 - go.opentelemetry.io/otel v1.33.0 - go.opentelemetry.io/otel/metric v1.33.0 - go.opentelemetry.io/otel/trace v1.33.0 - google.golang.org/grpc v1.68.1 - google.golang.org/protobuf v1.35.2 + entgo.io/ent v0.14.5 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 + github.com/grpc-kit/pkg v0.4.2 + github.com/modelcontextprotocol/go-sdk v1.7.0 + github.com/sirupsen/logrus v1.9.4 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + google.golang.org/grpc v1.81.1 + google.golang.org/protobuf v1.36.11 + k8s.io/client-go v0.34.1 ) From 335f6b2d6a3560ee6dcc3e2706bd5b0b23e96866 Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 9 Aug 2026 17:45:49 +0800 Subject: [PATCH 26/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20Makefile=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=EF=BC=8C=E5=9C=A8=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=90=8E=E5=90=8C=E6=AD=A5=20Go=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/Makefile.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/template/service/Makefile.tmpl b/template/service/Makefile.tmpl index abdd8df..3d002c0 100644 --- a/template/service/Makefile.tmpl +++ b/template/service/Makefile.tmpl @@ -52,6 +52,8 @@ generate: precheck ## Generate code from proto files. @./scripts/version.sh update @echo ">> generation code from proto files" @./scripts/generate.sh + @echo ">> synchronize Go module dependencies" + @${GO} mod tidy @echo ">> generation database schema" @./scripts/generate.sh gen_ent From e67b21f2bfb85a25474e5abbd388565a3e8f0f45 Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 9 Aug 2026 17:46:38 +0800 Subject: [PATCH 27/57] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=20MCP=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=A8=A1=E6=9D=BF=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E4=B8=8E=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/handler/private.go.tmpl | 2 +- template/service/modeler/mcp/handler.go.tmpl | 15 +++++++++++++-- template/service/modeler/mcp/registrar.go.tmpl | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/template/service/handler/private.go.tmpl b/template/service/handler/private.go.tmpl index 1c4b153..cf2ddb0 100644 --- a/template/service/handler/private.go.tmpl +++ b/template/service/handler/private.go.tmpl @@ -91,7 +91,7 @@ func (m *Microservice) privateMCPHandle() error { registrar := mcp.NewRegistrar(opts...) if regErr := registrar.Register(wrapper.MCPServer()); regErr != nil { - m.logger.Warnf("register custom mcp resources: %v", regErr) + return fmt.Errorf("register custom mcp resources: %w", regErr) } return nil diff --git a/template/service/modeler/mcp/handler.go.tmpl b/template/service/modeler/mcp/handler.go.tmpl index f0fa037..af79fd8 100644 --- a/template/service/modeler/mcp/handler.go.tmpl +++ b/template/service/modeler/mcp/handler.go.tmpl @@ -3,6 +3,8 @@ package mcp import ( "context" "encoding/json" + "fmt" + "strings" sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -29,9 +31,15 @@ func (r *Registrar) handleEcho( }, nil } } + if strings.TrimSpace(args.Text) == "" { + return &sdkmcp.CallToolResult{ + IsError: true, + Content: []sdkmcp.Content{&sdkmcp.TextContent{Text: `{"error":"text is required"}`}}, + }, nil + } if r.logger != nil { - r.logger.Debugf("mcp echo tool called: text=%q", args.Text) + r.logger.Debugf("mcp echo tool called: text_length=%d", len(args.Text)) } payload, _ := json.Marshal(map[string]any{"echo": args.Text}) @@ -76,7 +84,10 @@ func (r *Registrar) handleGreeting( // 取出框架注入的 Authorization header(如需透传给后端 gRPC 调用): // authHeader := mcpserver.AuthHeaderFromContext(ctx) - name := req.Params.Arguments["name"] + name := strings.TrimSpace(req.Params.Arguments["name"]) + if name == "" { + return nil, fmt.Errorf("name is required") + } return &sdkmcp.GetPromptResult{ Description: "问候语模板", Messages: []*sdkmcp.PromptMessage{ diff --git a/template/service/modeler/mcp/registrar.go.tmpl b/template/service/modeler/mcp/registrar.go.tmpl index 72f3983..b11a3fc 100644 --- a/template/service/modeler/mcp/registrar.go.tmpl +++ b/template/service/modeler/mcp/registrar.go.tmpl @@ -1,6 +1,8 @@ package mcp import ( + "fmt" + logrus "github.com/sirupsen/logrus" mcpserver "github.com/grpc-kit/pkg/mcp" @@ -41,6 +43,10 @@ func NewRegistrar(opts ...RegistrarOption) *Registrar { // server 参数为 wrapper.MCPServer() 返回的 SDK 实例, // 其上提供 AddTool / AddResource / AddResourceTemplate / AddPrompt 四个方法,按需调用。 func (r *Registrar) Register(server *sdkmcp.Server) error { + if server == nil { + return fmt.Errorf("register custom MCP resources: server is nil") + } + // ── Tools ────────────────────────────────────────── // 示例 Tool:echo(业务开发者可替换为实际 Tool) server.AddTool(&sdkmcp.Tool{ From 588025c190dec6867d9341ab224ca5df6f51e4b6 Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 9 Aug 2026 17:46:52 +0800 Subject: [PATCH 28/57] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=20MCP=20?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=99=A8=E7=AB=AF=E5=88=B0=E7=AB=AF=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=9C=8D=E5=8A=A1=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=89=A9=E5=B1=95=E7=82=B9=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modeler/mcp/registrar_test.go.tmpl | 48 ++++++++- template/service_test.go | 99 +++++++++++++++++++ 2 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 template/service_test.go diff --git a/template/service/modeler/mcp/registrar_test.go.tmpl b/template/service/modeler/mcp/registrar_test.go.tmpl index a362911..cf17cfc 100644 --- a/template/service/modeler/mcp/registrar_test.go.tmpl +++ b/template/service/modeler/mcp/registrar_test.go.tmpl @@ -1,9 +1,13 @@ package mcp import ( + "context" + "net/http/httptest" + "strings" "testing" mcpserver "github.com/grpc-kit/pkg/mcp" + sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" ) // TestRegistrar_AllResources 验证示例 Tool / Resource / Prompt 能成功注册到 MCP Server。 @@ -22,8 +26,44 @@ func TestRegistrar_AllResources(t *testing.T) { t.Fatalf("Register: %v", err) } - // TODO: 通过 SDK session 端到端调用验证(参考 pkg/mcp/integration_test.go): - // - tools/call {{ .Global.ShortName }}_echo - // - resources/read {{ .Global.ShortName }}://config - // - prompts/get {{ .Global.ShortName }}_greeting + httpServer := httptest.NewServer(wrapper.Handler()) + defer httpServer.Close() + client := sdkmcp.NewClient(&sdkmcp.Implementation{Name: "registrar-test", Version: "0.0.1"}, nil) + session, err := client.Connect(context.Background(), &sdkmcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil) + if err != nil { + t.Fatalf("Connect: %v", err) + } + defer session.Close() + + toolResult, err := session.CallTool(context.Background(), &sdkmcp.CallToolParams{ + Name: "{{ .Global.ShortName }}_echo", Arguments: map[string]any{"text": "hello"}, + }) + if err != nil || toolResult.IsError || len(toolResult.Content) != 1 { + t.Fatalf("CallTool: result=%v err=%v", toolResult, err) + } + if text, ok := toolResult.Content[0].(*sdkmcp.TextContent); !ok || !strings.Contains(text.Text, `"echo":"hello"`) { + t.Fatalf("CallTool content = %#v", toolResult.Content) + } + + resourceResult, err := session.ReadResource(context.Background(), &sdkmcp.ReadResourceParams{URI: "{{ .Global.ShortName }}://config"}) + if err != nil || len(resourceResult.Contents) != 1 || !strings.Contains(resourceResult.Contents[0].Text, `"service":"{{ .Global.ShortName }}"`) { + t.Fatalf("ReadResource: result=%v err=%v", resourceResult, err) + } + + promptResult, err := session.GetPrompt(context.Background(), &sdkmcp.GetPromptParams{ + Name: "{{ .Global.ShortName }}_greeting", Arguments: map[string]string{"name": "Codex"}, + }) + if err != nil || len(promptResult.Messages) != 1 { + t.Fatalf("GetPrompt: result=%v err=%v", promptResult, err) + } + text, ok := promptResult.Messages[0].Content.(*sdkmcp.TextContent) + if !ok || !strings.Contains(text.Text, "Codex") { + t.Fatalf("GetPrompt content = %#v", promptResult.Messages) + } +} + +func TestRegistrarRejectsNilServer(t *testing.T) { + if err := NewRegistrar().Register(nil); err == nil { + t.Fatal("Register(nil) should fail") + } } diff --git a/template/service_test.go b/template/service_test.go new file mode 100644 index 0000000..daedac0 --- /dev/null +++ b/template/service_test.go @@ -0,0 +1,99 @@ +package template + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/grpc-kit/cli/config" +) + +// TestServiceTemplateRendersMCPExtension 防止 MCP 扩展点在模板变更时被遗漏, +// 同时验证生成的 Go 文件均可被 parser 解析。 +func TestServiceTemplateRendersMCPExtension(t *testing.T) { + previousDir, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd: %v", err) + } + tempDir := t.TempDir() + if err := os.Chdir(tempDir); err != nil { + t.Fatalf("Chdir(%s): %v", tempDir, err) + } + t.Cleanup(func() { + if err := os.Chdir(previousDir); err != nil { + t.Errorf("restore working directory: %v", err) + } + }) + + generator, err := New(config.Config{ + Global: config.GlobalConfig{ + Type: TypeService, + APIEndpoint: "api", + ProductCode: "demo", + ShortName: "echo", + ReleaseVersion: "0.0.1-test", + Repository: "example.com/acme/echo", + Organization: "acme", + Appname: "echo", + ServiceCode: "demo.echo", + ProtoPackage: "demo.echo.v1", + ServiceTitle: "Echo", + }, + Template: config.TemplateConfig{Service: config.TemplateService{APIVersion: "v1"}}, + }) + if err != nil { + t.Fatalf("New: %v", err) + } + if err := generator.Generate(); err != nil { + t.Fatalf("Generate: %v", err) + } + + root := filepath.Join(tempDir, "echo") + assertContains := func(relativePath string, expected ...string) { + t.Helper() + content, err := os.ReadFile(filepath.Join(root, relativePath)) + if err != nil { + t.Fatalf("ReadFile(%s): %v", relativePath, err) + } + for _, value := range expected { + if !strings.Contains(string(content), value) { + t.Errorf("%s does not contain %q", relativePath, value) + } + } + } + assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()") + assertContains("handler/private.go", "func (m *Microservice) privateMCPHandle() error", "return fmt.Errorf") + assertContains("modeler/mcp/registrar.go", "func (r *Registrar) Register", "server is nil") + assertContains("modeler/mcp/registrar_test.go", "session.CallTool", "session.ReadResource", "session.GetPrompt") + assertContains("config/app-dev-local.yaml", "aiconnector:", "mcp_server:") + assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.4.2", "github.com/modelcontextprotocol/go-sdk v1.7.0") + assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") + + for _, relativePath := range []string{"handler/private.go", "modeler/mcp/registrar.go", "modeler/mcp/handler.go"} { + content, err := os.ReadFile(filepath.Join(root, relativePath)) + if err != nil { + t.Fatalf("ReadFile(%s): %v", relativePath, err) + } + if strings.Contains(string(content), "DO NOT EDIT") { + t.Errorf("business-editable file %s is incorrectly marked generated", relativePath) + } + } + + fset := token.NewFileSet() + err = filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || filepath.Ext(path) != ".go" { + return nil + } + _, parseErr := parser.ParseFile(fset, path, nil, parser.AllErrors) + return parseErr + }) + if err != nil { + t.Fatalf("parse generated Go files: %v", err) + } +} From 90447ae089b8e9890d82bc9c4f0f8acf6b39c54c Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 11 Aug 2026 21:28:58 +0800 Subject: [PATCH 29/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20AGENTS.md=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E6=8A=80=E8=83=BD=E9=83=A8=E5=88=86=E4=BB=A5=E6=8C=87=E5=AF=BC?= =?UTF-8?q?=20AI=20=E7=BC=96=E7=A0=81=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/AGENTS.md.tmpl | 7 +++++++ template/service_test.go | 1 + 2 files changed, 8 insertions(+) diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl index f9fd8d1..72870d4 100644 --- a/template/service/AGENTS.md.tmpl +++ b/template/service/AGENTS.md.tmpl @@ -8,6 +8,13 @@ This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, e - Main Branch: `main` - Current Version: See `VERSION` +## Shared Skills + +- Run `make skills` to install or update grpc-kit shared skills under `scripts/skills/skills`. +- When a task matches a shared skill, read its complete `SKILL.md` before acting and follow any referenced resources it requires. +- For release notes, version summaries, or CHANGELOG updates, use `scripts/skills/skills/generate-release-changelog/SKILL.md`. +- If the shared skill directory is unavailable, report that it has not been initialized before falling back to repository conventions. + ## Repository Structure ```text diff --git a/template/service_test.go b/template/service_test.go index daedac0..3feef60 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -71,6 +71,7 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { assertContains("config/app-dev-local.yaml", "aiconnector:", "mcp_server:") assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.4.2", "github.com/modelcontextprotocol/go-sdk v1.7.0") assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") + assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") for _, relativePath := range []string{"handler/private.go", "modeler/mcp/registrar.go", "modeler/mcp/handler.go"} { content, err := os.ReadFile(filepath.Join(root, relativePath)) From 935cec43bd8a76b57b03f2828e3e74e8e828b9cc Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 11 Aug 2026 21:29:04 +0800 Subject: [PATCH 30/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20CHANGELOG?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=200.4.3=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=8C=85=E5=90=AB=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E8=87=AA=E6=9C=8D=E5=8A=A1=20API=20=E5=92=8C?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E6=A8=A1=E6=9D=BF=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 77 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index cc45df2..050db9b 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -11,6 +11,83 @@ ## [Unreleased] +## [0.4.3] - 2026-08-11 + +### Added + +#### grpc-kit/api 模块 + +- 新增个人中心自服务 API + + 1. 新增 `GetCurrentUser`,返回当前认证用户的最小化个人资料、MFA 状态、是否支持修改本地密码以及稳定的 `ETag` + 2. 新增 `UpdateCurrentUser`,通过 `FieldMask` 更新当前用户可编辑的个人资料字段 + 3. 新增 `ChangeCurrentUserPassword`,支持当前用户修改本地密码 + 4. 新增 `DeleteUserMFA`,供已获授权的管理员重置指定用户的 MFA 配置 + +#### grpc-kit/pkg 模块 + +- 新增个人中心自服务实现 + + `GetCurrentUser` / `UpdateCurrentUser` / `ChangeCurrentUserPassword` 基于认证上下文操作当前用户;个人资料响应不暴露审计、身份凭据和成员关系等管理字段,并设置 `Cache-Control: private, no-store`。 + +- 新增普通管理员、用户和访客内置角色,以及 `unassigned` 内置部门代码。 + +- 微信登录新增 UnionID 持久化与冲突检测,避免同一 UnionID 关联到多个本地用户。 + +#### grpc-kit/cli 模块 + +- 服务模板新增 MCP 注册器端到端测试,覆盖 Tool 调用、Resource 读取和 Prompt 获取,并验证生成的 MCP 扩展点及 Go 源码可解析。 + +- 服务模板的 `generate` 目标在代码生成后自动执行 `go mod tidy`。 + +### Changed + +#### grpc-kit/api 模块 + +- 调整 MFA 自服务 API 为当前用户语义 + + `SetupUserMFA` / `ConfirmUserMFA` / `DisableUserMFA` 重命名为 `SetupCurrentUserMFA` / `ConfirmCurrentUserMFA` / `DisableCurrentUserMFA`,并移除请求中的 `user_id`;管理员重置其他用户 MFA 请改用 `DeleteUserMFA`。 + +- 重构 `CreateAuthTokenRequest` 为 OAuth2 Access Token 签发模型 + + 移除 `appid`、`phone_number` 和 `password_hash` 等身份凭证输入;新增 `client_id`、`scope`、`tenant`、`roles`、`groups`、`email_verified` 与 `subject` 声明字段,支持超级管理员委托签发。 + +- `UpdateCredentialRequest` 对不可修改字段(`code`、`type`、`algorithm`、`key_material`、`fingerprint`)的变更请求,统一返回参数错误,不再静默忽略。 + +#### grpc-kit/pkg 模块 + +- `CreateAuthToken` 改为必须认证;仅超级管理员可委托为其他用户签发令牌,非超级管理员只能在自身角色、群组与租户范围内签发。 + +- 令牌签发不再回退写入默认租户;身份与授权声明按请求和授权结果按需写入。 + +- MCP 初始化与前端管理开关解耦;个人中心自服务方法不受管理 API allow-list 限制,并允许 `Cache-Control` 响应头透传。 + +- 内置菜单调整:移除已并入“本地配置 > 认证鉴权”的“全局设置”菜单,将凭证管理更名为“凭证与令牌”;未注册策略分类返回空分类,以支持前端空状态展示。 + +#### grpc-kit/cli 模块 + +- 服务模板升级至 Go 1.25,并更新 grpc-gateway、gRPC、OpenTelemetry、MCP SDK 等依赖;模板使用 `github.com/grpc-kit/pkg v0.4.2`。 + +- MCP 模板加强运行时校验:注册器拒绝空 Server,`echo` Tool 拒绝空文本,问候 Prompt 要求非空名称;自定义 MCP 资源注册失败将直接返回错误。 + +### Fixed + +#### grpc-kit/pkg 模块 + +- MFA challenge 绑定发起时的 Access Token 会话,防止跨会话确认或关闭 MFA;用户管理新增细粒度权限检查,并禁止更新服务端托管字段。 + +- 内置角色初始化改为幂等收敛:历史数据的可修正属性与种子不一致时自动补齐,不再导致初始化失败。 + +- 受保护菜单仅在字段实际变化时才视为修改,并补充 `credential_source` 解析支持。 + +### Security + +#### grpc-kit/pkg 模块 + +- 个人中心和 MFA 自服务操作仅作用于认证上下文中的当前主体;返回的个人资料排除管理员审计、身份凭据和成员关系信息。 + +- 收紧令牌签发与用户管理授权边界,避免未认证签发、越权委托签发及客户端篡改服务端托管用户字段。 + ## [0.4.2] - 2026-08-05 ### Added From 394e0f500ef266c85f561c23afb2916805ecf331 Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 18 Aug 2026 15:32:10 +0800 Subject: [PATCH 31/57] =?UTF-8?q?feat:=20=E6=9C=8D=E5=8A=A1=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=96=B0=E5=A2=9E=E9=BB=91=E7=9B=92=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=20E2E=20=E6=B5=8B=E8=AF=95=E6=A8=A1=E7=89=88=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 test/e2e/ 目录:client(仅依赖标准库的最小 HTTP 客户端,支持 Basic/Bearer 鉴权与网关统一错误体解码)、fixture(探活 guard、环境变量覆盖与 runID 数据隔离、鉴权夹具)、demo 示范用例(覆盖健康检查、Demo 各 HTTP 绑定投影与鉴权负例) - 新增 test/e2e/README.md 目录规范,含黑盒纯度约束(禁止 import 业务包)与环境变量覆盖说明 - Makefile 新增 make test-e2e 目标,lint 增加 go vet -tags=e2e - GitLab CI 新增手动触发的 e2e-tests 作业(自动构建并后台拉起服务),Jenkins 提供注释版接入示例 - AGENTS.md 补充 test/e2e/ 目录职责与测试约定 - 模板测试验证 E2E 文件可解析并强制黑盒纯度约束 --- CHANGELOG/CHANGELOG-0.4.md | 13 ++ .../.gitlab/workflows/grpc-kit.yml.tmpl | 41 ++++ .../.jenkins/workflows/Jenkinsfile.tmpl | 23 ++ template/service/AGENTS.md.tmpl | 13 +- template/service/Makefile.tmpl | 5 + template/service/test/e2e/README.md | 98 +++++++++ template/service/test/e2e/client/client.go | 182 ++++++++++++++++ .../test/e2e/demo/suite_demo_test.go.tmpl | 205 ++++++++++++++++++ .../test/e2e/demo/testmain_test.go.tmpl | 13 ++ .../service/test/e2e/fixture/auth.go.tmpl | 91 ++++++++ template/service/test/e2e/fixture/env.go.tmpl | 78 +++++++ template/service/test/e2e/fixture/main.go | 54 +++++ template/service_test.go | 46 +++- 13 files changed, 858 insertions(+), 4 deletions(-) create mode 100644 template/service/test/e2e/README.md create mode 100644 template/service/test/e2e/client/client.go create mode 100644 template/service/test/e2e/demo/suite_demo_test.go.tmpl create mode 100644 template/service/test/e2e/demo/testmain_test.go.tmpl create mode 100644 template/service/test/e2e/fixture/auth.go.tmpl create mode 100644 template/service/test/e2e/fixture/env.go.tmpl create mode 100644 template/service/test/e2e/fixture/main.go diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index 050db9b..6dded43 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -11,6 +11,19 @@ ## [Unreleased] +### Added + +#### grpc-kit/cli 模块 + +- 服务模板新增黑盒接口 E2E 测试模版框架 + + 1. 新增 `test/e2e/`,全部文件带 `//go:build e2e` 构建约束,仅在显式启用该标签时参与编译 + 2. 新增 `test/e2e/client`(仅依赖标准库的最小 HTTP 客户端,支持 Basic / Bearer 鉴权,并把网关统一错误体 `{"error":{...}}` 解码为可断言的状态码) + 3. 新增 `test/e2e/fixture`(`Main` 统一探活入口、`Env` 环境变量覆盖与 `runID` 数据隔离、`Authed` 鉴权夹具) + 4. 新增 `test/e2e/demo` 示范用例,覆盖健康检查、Demo 接口的 `response_body` 投影与未认证访问 + 5. 新增 `test/e2e/README.md` 目录规范,含黑盒纯度约束(`test/e2e/` 禁止 import `api/`、`handler/`、`internal/`、`modeler/`)与环境变量覆盖说明;被测服务的运行配置由环境提供(默认 `config/app-dev-local.yaml`),E2E 自身不携带配置 + 6. 新增 `make test-e2e` 目标,`make lint` 增加 `go vet -tags=e2e ./test/e2e/...`,GitLab CI 增加手动触发的 `e2e-tests` 作业(配置路径由 `E2E_CONFIG_FILE` 指定) + ## [0.4.3] - 2026-08-11 ### Added diff --git a/template/service/.gitlab/workflows/grpc-kit.yml.tmpl b/template/service/.gitlab/workflows/grpc-kit.yml.tmpl index c6d34ec..49e26ae 100644 --- a/template/service/.gitlab/workflows/grpc-kit.yml.tmpl +++ b/template/service/.gitlab/workflows/grpc-kit.yml.tmpl @@ -52,6 +52,47 @@ unit-tests: script: - make test +# 黑盒接口 E2E 测试,需先在后台拉起服务 +# TODO; 默认手工触发且不阻断流水线,确认 runner 环境(端口、依赖组件)可用后再改为自动执行 +e2e-tests: + stage: test + needs: + - unit-tests + variables: + # 被测服务的运行配置,需 runner 环境内可读(如挂载或由前置阶段下发) + E2E_CONFIG_FILE: config/app-dev-local.yaml + # 需与 ${E2E_CONFIG_FILE} 里的 services.http_address 对应 + E2E_BASE_URL: http://127.0.0.1:8080 + # 需与 ${E2E_CONFIG_FILE} 里的 security.authentication.http_users 对应 + E2E_BASIC_USERNAME: user1 + E2E_BASIC_PASSWORD: grpc-kit-cli + script: + - source scripts/env + - test -f "${E2E_CONFIG_FILE}" || { echo "missing ${E2E_CONFIG_FILE}"; exit 1; } + - make build + - ./build/service -c "${E2E_CONFIG_FILE}" > e2e-service.log 2>&1 & + - | + for i in $(seq 1 30); do + if curl -sf "${E2E_BASE_URL}/api/healthz?service=${SERVICE_CODE}" > /dev/null; then + echo "service is ready" + break + fi + if test "${i}" = "30"; then + echo "service not ready, log:" + cat e2e-service.log + exit 1 + fi + sleep 1 + done + - make test-e2e + artifacts: + when: always + paths: + - e2e-service.log + expire_in: 24h + when: manual + allow_failure: true + # 代码覆盖率 coverage: stage: test diff --git a/template/service/.jenkins/workflows/Jenkinsfile.tmpl b/template/service/.jenkins/workflows/Jenkinsfile.tmpl index bca6357..49a6bf1 100644 --- a/template/service/.jenkins/workflows/Jenkinsfile.tmpl +++ b/template/service/.jenkins/workflows/Jenkinsfile.tmpl @@ -80,6 +80,29 @@ pipeline { cd source make test ''' + + // TODO; 如需接入黑盒接口 E2E(test/e2e/),放开以下步骤 + // 注意:E2E 需要服务先在后台运行,并在结束时回收进程; + // E2E_CONFIG_FILE 指向本环境可读的运行配置,凭据需与其 http_users 一致 + // sh ''' + // cd source + // export E2E_CONFIG_FILE=config/app-dev-local.yaml + // export E2E_BASE_URL=http://127.0.0.1:8080 + // export E2E_BASIC_USERNAME=user1 + // export E2E_BASIC_PASSWORD=grpc-kit-cli + // test -f "${E2E_CONFIG_FILE}" || { echo "missing ${E2E_CONFIG_FILE}"; exit 1; } + // make build + // ./build/service -c "${E2E_CONFIG_FILE}" > e2e-service.log 2>&1 & + // echo $! > e2e-service.pid + // source scripts/env + // for i in $(seq 1 30); do + // curl -sf "${E2E_BASE_URL}/api/healthz?service=${SERVICE_CODE}" > /dev/null && break + // test "${i}" = "30" && { cat e2e-service.log; exit 1; } + // sleep 1 + // done + // make test-e2e || { kill $(cat e2e-service.pid); exit 1; } + // kill $(cat e2e-service.pid) + // ''' } } diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl index 72870d4..be52b77 100644 --- a/template/service/AGENTS.md.tmpl +++ b/template/service/AGENTS.md.tmpl @@ -24,6 +24,7 @@ config/ handler/ modeler/ scripts/ +test/e2e/ docs/ web/admin/ deploy/ @@ -44,6 +45,7 @@ Directory Responsibilities: - `modeler/` - data models, flow logic, and ent schema/runtime - `config/` - runtime configuration files for local/minimal environments - `scripts/` - generation, environment checks, packaging, and helper automation +- `test/e2e/` - black-box API end-to-end tests (`//go:build e2e`), see `test/e2e/README.md` - `docs/` - design, migration, and feature documentation - `web/admin/` - frontend admin application @@ -72,6 +74,15 @@ make build - Default validation target is `make test` (includes `make generate` and `go test ./...`). - Use `make lint` to run formatting and vet checks before submitting larger changes. - If API contracts or ent schema change, run generation first and include generated artifacts in the same change. +- Unit and integration tests stay colocated with the source they cover; never move them into `test/`. +- Black-box API e2e tests live in per-domain packages under `test/e2e/`, are all guarded by + `//go:build e2e`, and run with `make test-e2e` against a locally running server (`make run`). + Directory conventions, naming rules, and environment variables: `test/e2e/README.md`. +- Purity constraint for `test/e2e/`: import only the standard library, `test/e2e/client`, and + `test/e2e/fixture`. Importing `api/`, `handler/`, `internal/`, or `modeler/` is forbidden — + e2e asserts the external HTTP contract, not internal implementation. +- `test/e2e/demo/` covers the built-in sample endpoints. Delete it together with + `handler/rpc_demo.go` and `api/**/demo.proto` when the sample API is removed. ## Configuration @@ -107,7 +118,7 @@ When a change affects generated artifacts, include source and generated files in Files in this project fall into two categories, distinguished by whether the first line contains `// Code generated by "grpc-kit-cli/...". DO NOT EDIT.`: - Framework-managed files (with `DO NOT EDIT` header): Managed by grpc-kit CLI, overwritten on `grpc-kit-cli update`. Do not modify. Examples: `cmd/server/main.go`, `handler/register.go`, `handler/microservice.go`, `api/${{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/*.pb.go`, `api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.pb.gw.go`. -- Business-editable files (no `DO NOT EDIT` header): Preserved on upgrade, free to modify. Examples: `handler/private.go`, `handler/rpc_demo.go`, `modeler/mcp/registrar.go`, `modeler/mcp/registrar_test.go`, `config/app-dev-local.yaml`. +- Business-editable files (no `DO NOT EDIT` header): Preserved on upgrade, free to modify. Examples: `handler/private.go`, `handler/rpc_demo.go`, `modeler/mcp/registrar.go`, `modeler/mcp/registrar_test.go`, `config/app-dev-local.yaml`, `test/e2e/**`. Additional rules: diff --git a/template/service/Makefile.tmpl b/template/service/Makefile.tmpl index 3d002c0..8a44898 100644 --- a/template/service/Makefile.tmpl +++ b/template/service/Makefile.tmpl @@ -62,12 +62,17 @@ lint: generate ## Run go fmt and vet against code. @${GO} mod tidy @${GO} fmt ./... @${GO} vet ./... + @${GO} vet -tags=e2e ./test/e2e/... .PHONY: test test: generate ## Run unit tests. @${GO} mod tidy @${GO} test ./... -timeout 3m -v -gcflags=-l -cover=true +.PHONY: test-e2e +test-e2e: ## Run black-box API e2e tests against a running local server (make run). + @${GO} test -tags=e2e -p 1 -count=1 ./test/e2e/... -timeout 5m -v + .PHONY: skills skills: ## Install or update grpc-kit common skills. @if [ ! -d ".git" ]; then \ diff --git a/template/service/test/e2e/README.md b/template/service/test/e2e/README.md new file mode 100644 index 0000000..d757f7e --- /dev/null +++ b/template/service/test/e2e/README.md @@ -0,0 +1,98 @@ +# test/ 目录规划规范 + +参考基准:Kubernetes 测试组织方式 + Go 官方测试惯例。 + +## 1. 测试类型归属 + +| 测试类型 | 位置 | 隔离方式 | 运行入口 | +| --- | --- | --- | --- | +| 白盒/灰盒单测 | 与源码同目录(colocated,`*_test.go`) | 无 build tag | `make test`(`go test ./...`) | +| 依赖真实外部后端的集成测试 | 与源码同目录 | 环境变量门控 + `t.Skip` | `make test` + 设置环境变量 | +| 黑盒 E2E | `test/e2e/` | `//go:build e2e` | `make test-e2e`(前置 `make run`) | + +**黑盒纯度约束**:`test/e2e/` 下代码只允许 import 标准库、`test/e2e/client`、 +`test/e2e/fixture`,禁止导入本服务的业务包(`api/`、`handler/`、`internal/`、`modeler/`) +—— 保证测的是对外 HTTP 契约而非内部实现。 + +这条约束还带来一个实用副作用:`go vet -tags=e2e ./test/e2e/...` 只依赖标准库, +不需要先跑 `make generate`,因此在没有 protoc 工具链的环境里也能做编译检查。 + +## 2. 目录结构(按业务域分包) + +```text +test/ +└── e2e/ + ├── client/ # 薄 HTTP 客户端:鉴权注入、统一错误体解码(google.rpc.Status) + ├── fixture/ # 环境引导:env(BaseURL/ServiceCode/runID/前缀)、 + │ # main(Main(m) 探活 guard)、auth(调用主体与资源工厂挂载点) + └── demo/ # 内置示例接口域:HealthCheck 与 Demo 的全部 HTTP 绑定(D1–D8) +``` + +- **目录创建时机**:域目录随该域首个 suite 落地时创建,不预建空目录。 +- **`demo/` 的处置**:它测的是模版内置的示例接口。删除 `handler/rpc_demo.go` 与 + `api/**/demo.proto` 时,请一并删除 `test/e2e/demo/`。 +- **Go 语言约束**(域分包的既定代价与对策): + 1. `TestMain` 是 per-package 的:各域包 `testmain_test.go` 一行 + `func TestMain(m *testing.M) { fixture.Main(m) }` 接入统一引导; + 2. 每个域包是独立测试进程:runID 按进程生成(隔离性更好); + 3. Makefile `test-e2e` 固定 `-p 1` 串行化包执行,防共享环境互踩。 + +## 3. 命名与隔离约定 + +- suite 文件:`suite___test.go`; +- 包名 = 域名(如 `package demo`); +- 文件头注释引用被测文档章节与用例 ID 表,便于 grep 对照; +- Test 函数 `Test`,子测试名以用例 ID 开头(如 `"D5 ..."`); +- helper 归属:域内私有 helper 留在 suite 文件;跨域共享的一律 export 进 `fixture/`; +- 有状态资源一律「创建即注册 `t.Cleanup`」,清理失败只 `t.Logf`,不中断其他清理; +- 测试数据带 `env.Prefix()`(含 runID)前缀,避免并发与历史残留互踩; +- `test/e2e/` 下所有 `.go` 文件(含 `client`/`fixture` 的非 `_test` 文件)统一 + `//go:build e2e`; +- 单测、集成测试留在源码旁,禁止迁入 `test/`。 + +## 4. 运行方式 + +```shell +make run # 终端 1:本地起服务(config/app-dev-local.yaml) +make test-e2e # 终端 2:全量 E2E(-p 1 串行) + +# 按域/用例过滤 +go test -tags=e2e -p 1 ./test/e2e/demo/... -run TestDemo -v +``` + +`.gitignore` 忽略了 `config/app-dev-*.yaml`,因此 CI 或全新环境里没有那份配置。 +E2E 不自带配置,被测服务的运行配置由环境提供(挂载、密钥管理或前置阶段下发), +CI 里通过 `E2E_CONFIG_FILE` 指定要读哪一份: + +```shell +make build +./build/service -c config/app-dev-local.yaml # 终端 1,或 CI 里的 ${E2E_CONFIG_FILE} +make test-e2e # 终端 2 +``` + +用例只依赖该配置的三处内容——`services.http_address`、`services.service_code`、 +`security.authentication.http_users`。与默认值不一致时用下面的环境变量覆盖,不要改用例: + +| 变量 | 默认值 | 对应配置项 | +| --- | --- | --- | +| `E2E_BASE_URL` | `http://127.0.0.1:8080` | `services.http_address` | +| `E2E_SERVICE_CODE` | 由模版生成 | `services.service_code`(健康检查的必填入参) | +| `E2E_BASIC_USERNAME` | `user1` | `security.authentication.http_users[].username` | +| `E2E_BASIC_PASSWORD` | `grpc-kit-cli` | `security.authentication.http_users[].password` | + +若项目改用 OIDC / Bearer 认证,请在 `fixture/auth.go` 的 `RequireAuthed` 里换取 +access_token 并用 `client.WithToken` 构造,把失败收敛成一次 `t.Fatalf`。 + +常见故障: + +| 现象 | 原因与处理 | +| --- | --- | +| `e2e: 探活失败 ...: connection refused` | 服务没起来,先 `make run` | +| 探活返回 `501 Unimplemented` | 端口被另一个 grpc-kit 服务占用(网关收到请求但后端没有本服务),换端口或用 `E2E_BASE_URL` 指向正确实例 | +| 探活返回 `404 NotFound` | `E2E_SERVICE_CODE` 与服务配置的 `service_code` 不一致 | +| 用例返回 `401 Unauthenticated` | 配置里的 `http_users` 与 `E2E_BASIC_USERNAME` / `E2E_BASIC_PASSWORD` 不匹配 | + +## 5. testdata 约定 + +E2E 静态数据(golden file、OpenAPI 样本等)放 `test/e2e/<域>/testdata/`, +Go 工具链自动忽略该目录名。 diff --git a/template/service/test/e2e/client/client.go b/template/service/test/e2e/client/client.go new file mode 100644 index 0000000..2c56ed5 --- /dev/null +++ b/template/service/test/e2e/client/client.go @@ -0,0 +1,182 @@ +//go:build e2e + +// Package client 提供黑盒 E2E 测试使用的最小 HTTP 客户端:统一注入鉴权头、编码请求 JSON, +// 并把网关统一错误体 {"error":{...}} 解码为 gRPC 状态码,供用例按错误语义断言。 +// +// 该包只依赖标准库。E2E 测的是对外 HTTP 契约,禁止 import 本服务的业务包 +// (api/、handler/、internal/、modeler/),详见 test/e2e/README.md。 +package client + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// Client 是面向单个调用主体的 HTTP 客户端;凭据变更时通过 WithToken/WithBasicAuth 派生新实例。 +type Client struct { + baseURL string + http *http.Client + // authorization 为完整的 Authorization 头值,如 "Basic dXNlcjE6..." 或 "Bearer xxx";空则匿名。 + authorization string +} + +// Option 是 Client 的构造选项。 +type Option func(*Client) + +// WithBaseURL 设置服务基础地址,如 http://127.0.0.1:8080。 +func WithBaseURL(baseURL string) Option { + return func(c *Client) { c.baseURL = strings.TrimRight(baseURL, "/") } +} + +// WithBasicAuth 设置 HTTP Basic 凭据,对应配置中的 security.authentication.http_users。 +func WithBasicAuth(username, password string) Option { + return func(c *Client) { c.authorization = basicHeader(username, password) } +} + +// WithToken 设置 Bearer 访问令牌,对应配置中的 security.authentication.oidc_provider。 +func WithToken(token string) Option { + return func(c *Client) { c.authorization = "Bearer " + token } +} + +// WithTimeout 设置单次请求超时,默认 30 秒。 +func WithTimeout(d time.Duration) Option { + return func(c *Client) { c.http.Timeout = d } +} + +// New 构造 Client,默认 30 秒超时。 +func New(opts ...Option) *Client { + c := &Client{http: &http.Client{Timeout: 30 * time.Second}} + for _, opt := range opts { + opt(c) + } + return c +} + +// WithBasicAuth 返回携带新 Basic 凭据的 Client 副本,用于多主体用例。 +func (c *Client) WithBasicAuth(username, password string) *Client { + next := *c + next.authorization = basicHeader(username, password) + return &next +} + +// WithToken 返回携带新令牌的 Client 副本,用于多主体用例。 +func (c *Client) WithToken(token string) *Client { + next := *c + next.authorization = "Bearer " + token + return &next +} + +// Anonymous 返回去掉全部凭据的 Client 副本,用于鉴权负例。 +func (c *Client) Anonymous() *Client { + next := *c + next.authorization = "" + return &next +} + +// BaseURL 返回该客户端的服务基础地址。 +func (c *Client) BaseURL() string { return c.baseURL } + +func basicHeader(username, password string) string { + return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)) +} + +// Response 保留原始状态码、响应头与响应体;用例按需解码,不强制结构, +// 以便覆盖「零值字段可能省略」这类契约宽容性。 +type Response struct { + StatusCode int + Header http.Header + Body []byte +} + +// errorBody 对应网关统一错误结构 {"error":{"code","status","message","details"}}。 +type errorBody struct { + Error struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + } `json:"error"` +} + +// Decode 把响应体解码到 v。 +func (r *Response) Decode(v any) error { + return json.Unmarshal(r.Body, v) +} + +// IsOK 报告响应是否为 2xx。 +func (r *Response) IsOK() bool { return r.StatusCode >= 200 && r.StatusCode < 300 } + +// RPCCode 返回错误体中的 gRPC 状态字符串(如 "NotFound");成功响应返回空串。 +func (r *Response) RPCCode() string { + if r.IsOK() { + return "" + } + var eb errorBody + if err := json.Unmarshal(r.Body, &eb); err != nil { + return "" + } + return eb.Error.Status +} + +// RPCMessage 返回错误体中的 message;解码失败时返回原始响应体,便于失败诊断。 +func (r *Response) RPCMessage() string { + if r.IsOK() { + return "" + } + var eb errorBody + if err := json.Unmarshal(r.Body, &eb); err != nil { + return string(r.Body) + } + return eb.Error.Message +} + +// HasRPCCode 按 gRPC 状态码断言比较(大小写与分隔符不敏感, +// 兼容 "NotFound" 与 "NOT_FOUND" 两种表示)。 +func (r *Response) HasRPCCode(code string) bool { + return normalizeCode(r.RPCCode()) == normalizeCode(code) +} + +func normalizeCode(code string) string { + replacer := strings.NewReplacer("_", "", "-", "", " ", "") + return strings.ToLower(replacer.Replace(code)) +} + +// Do 发送 JSON 请求并返回原始响应。body 为 nil 时不携带请求体。 +func (c *Client) Do(ctx context.Context, method, path string, body any) (*Response, error) { + var reader io.Reader + if body != nil { + raw, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshal request body: %w", err) + } + reader = bytes.NewReader(raw) + } + + req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader) + if err != nil { + return nil, fmt.Errorf("build request %s %s: %w", method, path, err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + if c.authorization != "" { + req.Header.Set("Authorization", c.authorization) + } + + resp, err := c.http.Do(req) + if err != nil { + return nil, fmt.Errorf("do %s %s: %w", method, path, err) + } + defer func() { _ = resp.Body.Close() }() + + raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("read response of %s %s: %w", method, path, err) + } + return &Response{StatusCode: resp.StatusCode, Header: resp.Header, Body: raw}, nil +} diff --git a/template/service/test/e2e/demo/suite_demo_test.go.tmpl b/template/service/test/e2e/demo/suite_demo_test.go.tmpl new file mode 100644 index 0000000..f1dda83 --- /dev/null +++ b/template/service/test/e2e/demo/suite_demo_test.go.tmpl @@ -0,0 +1,205 @@ +//go:build e2e + +// demo 域·内置示例接口套件,覆盖 +// api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}/microservice.gateway.yaml +// 中 HealthCheck 与 Demo 的全部 HTTP 绑定,外加一条鉴权负例。 +// +// 用例编号 D1–D8,与子测试名一一对应,便于与设计文档 grep 对照。 +// 这是 E2E 的最小示范:业务接口请按域另建包(test/e2e//), +// 示例接口删除后本文件也应一并删除,详见 test/e2e/README.md。 +package demo + +import ( + "context" + "net/http" + "net/url" + "testing" + + "{{ .Global.Repository }}/test/e2e/fixture" +) + +// demoFixedUUID 是 handler/rpc_demo.go 里写死的示例资源编号: +// 请求 uuid 等于它时,响应的 pong.pong.name 被固定为 "grpc-kit"。 +const demoFixedUUID = "99feafb5-bed6-4daf-927a-69a2ab80c485" + +// exampleResponse 对应 grpc_kit.api.known.example.v1.ExampleResponse。 +// 字段用指针以区分「字段省略」与「零值」—— 网关是否省略零值字段属于实现细节, +// 用例不应依赖,只断言语义。 +type exampleResponse struct { + Name *string `json:"name"` +} + +// TestHealthCheck 覆盖 GET /api/healthz。该方法在 config/app-dev-local.yaml 的 +// security.authentication.insecure_rpcs 内,匿名可访问。 +func TestHealthCheck(t *testing.T) { + env := fixture.Init() + ctx := context.Background() + anonymous := fixture.Anonymous() + + t.Run("D1 匿名访问健康检查返回 SERVING", func(t *testing.T) { + resp, err := anonymous.Do(ctx, http.MethodGet, + "/api/healthz?service="+url.QueryEscape(env.ServiceCode), nil) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if !resp.IsOK() { + t.Fatalf("期望 HTTP 200,得到 %d: %s", resp.StatusCode, resp.RPCMessage()) + } + + var body struct { + Status *string `json:"status"` + } + if err := resp.Decode(&body); err != nil { + t.Fatalf("解码响应失败: %v(原始响应 %s)", err, resp.Body) + } + if body.Status == nil || *body.Status != "SERVING" { + t.Errorf("期望 status=SERVING,得到 %v(原始响应 %s)", body.Status, resp.Body) + } + }) + + t.Run("D2 未知服务代码返回 404 NotFound", func(t *testing.T) { + resp, err := anonymous.Do(ctx, http.MethodGet, + "/api/healthz?service="+url.QueryEscape("unknown."+env.RunID), nil) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("期望 HTTP 404,得到 %d: %s", resp.StatusCode, resp.Body) + } + if !resp.HasRPCCode("NotFound") { + t.Errorf("期望 gRPC 状态 NotFound,得到 %q(原始响应 %s)", resp.RPCCode(), resp.Body) + } + }) +} + +// TestDemo 覆盖 Demo 方法的 5 个 HTTP 绑定与鉴权负例。 +// 这些绑定各自配置了不同的 response_body 投影,是网关规则最容易配错的地方。 +func TestDemo(t *testing.T) { + authed := fixture.RequireAuthed(t) + env := authed.Env + ctx := context.Background() + + t.Run("D3 POST /api/demo 返回 pong 投影并回显 ping.name", func(t *testing.T) { + // 用非固定 uuid,走 handler 里「pong.pong.name = req.ping.name」的分支。 + wantName := env.Name("post") + resp, err := authed.Client.Do(ctx, http.MethodPost, "/api/demo", map[string]any{ + "uuid": env.Prefix() + "post", + "ping": map[string]any{"name": wantName}, + }) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if !resp.IsOK() { + t.Fatalf("期望 HTTP 200,得到 %d: %s", resp.StatusCode, resp.RPCMessage()) + } + + // response_body: pong —— 顶层是 Pong 而非 DemoResponse。 + var body struct { + UUID *string `json:"uuid"` + Pong *exampleResponse `json:"pong"` + } + if err := resp.Decode(&body); err != nil { + t.Fatalf("解码响应失败: %v(原始响应 %s)", err, resp.Body) + } + if body.UUID == nil { + t.Errorf("期望响应顶层含 uuid 字段(response_body: pong 投影),原始响应 %s", resp.Body) + } + if body.Pong == nil || body.Pong.Name == nil || *body.Pong.Name != wantName { + t.Errorf("期望 pong.name=%q,得到 %v(原始响应 %s)", wantName, body.Pong, resp.Body) + } + }) + + t.Run("D4 GET /api/demo 返回完整响应体", func(t *testing.T) { + resp, err := authed.Client.Do(ctx, http.MethodGet, "/api/demo", nil) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if !resp.IsOK() { + t.Fatalf("期望 HTTP 200,得到 %d: %s", resp.StatusCode, resp.RPCMessage()) + } + + // 该绑定未配置 response_body,顶层是完整的 DemoResponse。 + var body struct { + Content []exampleResponse `json:"content"` + Pong *struct { + UUID *string `json:"uuid"` + } `json:"pong"` + } + if err := resp.Decode(&body); err != nil { + t.Fatalf("解码响应失败: %v(原始响应 %s)", err, resp.Body) + } + if len(body.Content) == 0 { + t.Fatalf("期望 content 非空,原始响应 %s", resp.Body) + } + if body.Pong == nil { + t.Errorf("期望完整响应体含 pong 字段,原始响应 %s", resp.Body) + } + }) + + t.Run("D5 GET /api/demo/{uuid} 返回 pong.pong 投影", func(t *testing.T) { + resp, err := authed.Client.Do(ctx, http.MethodGet, "/api/demo/"+demoFixedUUID, nil) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if !resp.IsOK() { + t.Fatalf("期望 HTTP 200,得到 %d: %s", resp.StatusCode, resp.RPCMessage()) + } + + // response_body: pong.pong —— 顶层直接是 ExampleResponse。 + var body exampleResponse + if err := resp.Decode(&body); err != nil { + t.Fatalf("解码响应失败: %v(原始响应 %s)", err, resp.Body) + } + if body.Name == nil || *body.Name != "grpc-kit" { + t.Errorf("请求固定 uuid 时期望 name=grpc-kit,得到 %v(原始响应 %s)", body.Name, resp.Body) + } + }) + + t.Run("D6 PUT /api/demo/{uuid} 回显 ping 投影", func(t *testing.T) { + wantName := env.Name("put") + // body: "ping" —— 请求体本身就是 ExampleRequest。 + resp, err := authed.Client.Do(ctx, http.MethodPut, "/api/demo/"+demoFixedUUID, + map[string]any{"name": wantName, "value": 1, "age": 2}) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if !resp.IsOK() { + t.Fatalf("期望 HTTP 200,得到 %d: %s", resp.StatusCode, resp.RPCMessage()) + } + + // response_body: ping —— 顶层直接是 ExampleResponse。 + var body exampleResponse + if err := resp.Decode(&body); err != nil { + t.Fatalf("解码响应失败: %v(原始响应 %s)", err, resp.Body) + } + if body.Name == nil || *body.Name != wantName { + t.Errorf("期望回显 name=%q,得到 %v(原始响应 %s)", wantName, body.Name, resp.Body) + } + }) + + t.Run("D7 DELETE /api/demo/{uuid} 返回 empty 投影", func(t *testing.T) { + resp, err := authed.Client.Do(ctx, http.MethodDelete, "/api/demo/"+demoFixedUUID, nil) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + // response_body: empty,而 handler 未给 Empty 赋值, + // 因此只断言调用成功,不约束响应体形状。 + if !resp.IsOK() { + t.Fatalf("期望 HTTP 2xx,得到 %d: %s", resp.StatusCode, resp.RPCMessage()) + } + }) + + t.Run("D8 匿名访问受保护接口返回 401 Unauthenticated", func(t *testing.T) { + // Demo 不在 insecure_rpcs 列表内,匿名调用应被认证拦截。 + resp, err := fixture.Anonymous().Do(ctx, http.MethodGet, "/api/demo", nil) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != http.StatusUnauthorized { + t.Fatalf("期望 HTTP 401,得到 %d: %s", resp.StatusCode, resp.Body) + } + if !resp.HasRPCCode("Unauthenticated") { + t.Errorf("期望 gRPC 状态 Unauthenticated,得到 %q(原始响应 %s)", resp.RPCCode(), resp.Body) + } + }) +} diff --git a/template/service/test/e2e/demo/testmain_test.go.tmpl b/template/service/test/e2e/demo/testmain_test.go.tmpl new file mode 100644 index 0000000..18fe709 --- /dev/null +++ b/template/service/test/e2e/demo/testmain_test.go.tmpl @@ -0,0 +1,13 @@ +//go:build e2e + +// demo 域包入口:接入 fixture.Main(探活 guard)。 +// 运行前提:本地服务已启动(make run);运行方式:make test-e2e。 +package demo + +import ( + "testing" + + "{{ .Global.Repository }}/test/e2e/fixture" +) + +func TestMain(m *testing.M) { fixture.Main(m) } diff --git a/template/service/test/e2e/fixture/auth.go.tmpl b/template/service/test/e2e/fixture/auth.go.tmpl new file mode 100644 index 0000000..5ebe0b7 --- /dev/null +++ b/template/service/test/e2e/fixture/auth.go.tmpl @@ -0,0 +1,91 @@ +//go:build e2e + +// 调用主体引导:模版默认认证方式为 HTTP Basic(config/app-dev-local.yaml 的 +// security.authentication.http_users),用例通过 RequireAuthed(t) 复用同一份凭据; +// 该文件同时是业务测试资源工厂的挂载点。 +package fixture + +import ( + "sync" + "testing" + + "{{ .Global.Repository }}/test/e2e/client" +) + +// 开发环境(config/app-dev-local.yaml)的 HTTP Basic 账号。 +// 可用环境变量覆盖:E2E_BASIC_USERNAME / E2E_BASIC_PASSWORD。 +const ( + defaultBasicUsername = "user1" + defaultBasicPassword = "grpc-kit-cli" +) + +// AuthedFixture 持有已认证的客户端,并作为业务测试资源工厂的挂载点。 +type AuthedFixture struct { + Env *Env + Client *client.Client +} + +var ( + authedOnce sync.Once + authedFix *AuthedFixture +) + +// Authed 返回进程级已认证 fixture(首次调用时构造)。 +func Authed() *AuthedFixture { + env := Init() + authedOnce.Do(func() { + authedFix = &AuthedFixture{ + Env: env, + Client: client.New( + client.WithBaseURL(env.BaseURL), + client.WithBasicAuth( + getenv("E2E_BASIC_USERNAME", defaultBasicUsername), + getenv("E2E_BASIC_PASSWORD", defaultBasicPassword), + ), + ), + } + }) + return authedFix +} + +// RequireAuthed 返回已认证 fixture。 +// +// Basic 凭据无需与服务交互即可构造,因此当前不会失败;保留 *testing.T 入参是为了 +// 给「需要先换取令牌」的项目留统一扩展点 —— 例如接入 OIDC 后在此登录取 access_token, +// 用 client.WithToken 构造,并把失败收敛成一次 t.Fatalf,而不是让每个用例各自报 401。 +func RequireAuthed(t *testing.T) *AuthedFixture { + t.Helper() + return Authed() +} + +// Anonymous 返回不带任何凭据的客户端,用于鉴权负例。 +func Anonymous() *client.Client { + return client.New(client.WithBaseURL(Init().BaseURL)) +} + +// 业务资源工厂挂载点:新增有状态资源请仿照以下形态 —— 创建即注册 t.Cleanup, +// 保证用例失败也能回收;清理失败只 t.Logf,不中断其他清理。 +// 建议按域拆到独立文件(如 fixture/order.go),避免本文件膨胀。 +// +// func (a *AuthedFixture) CreateOrder(t *testing.T, code string) Order { +// t.Helper() +// ctx := context.Background() +// resp, err := a.Client.Do(ctx, http.MethodPost, "/api/orders", +// map[string]any{"order": map[string]string{"code": code}}) +// if err != nil { +// t.Fatalf("创建订单 %s 失败: %v", code, err) +// } +// if !resp.IsOK() { +// t.Fatalf("创建订单 %s 失败: HTTP %d %s", code, resp.StatusCode, resp.RPCMessage()) +// } +// var order Order +// if err := resp.Decode(&order); err != nil { +// t.Fatalf("解码订单响应失败: %v(%s)", err, resp.Body) +// } +// t.Cleanup(func() { +// if _, err := a.Client.Do(context.Background(), http.MethodDelete, "/api/orders/"+code, nil); err != nil { +// t.Logf("清理订单 %s 失败: %v", code, err) +// } +// }) +// return order +// } diff --git a/template/service/test/e2e/fixture/env.go.tmpl b/template/service/test/e2e/fixture/env.go.tmpl new file mode 100644 index 0000000..af20023 --- /dev/null +++ b/template/service/test/e2e/fixture/env.go.tmpl @@ -0,0 +1,78 @@ +//go:build e2e + +// Package fixture 提供 E2E 测试的环境引导:服务地址、调用凭据与本次运行的唯一数据前缀。 +// 建议所有测试数据(名称、编码等)都带 RunID 前缀,便于清理与并发隔离。 +package fixture + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "os" + "time" +) + +// 被测服务的默认地址与服务代码,取自 config/app-dev-local.yaml。 +// 可用环境变量覆盖:E2E_BASE_URL / E2E_SERVICE_CODE。 +const ( + defaultBaseURL = "http://127.0.0.1:8080" + defaultServiceCode = "{{ .Global.ShortName }}.{{ .Template.Service.APIVersion }}.{{ .Global.ProductCode }}" +) + +// Env 汇总 E2E 运行所需的环境配置。 +type Env struct { + // BaseURL 是被测服务地址,默认 http://127.0.0.1:8080。 + BaseURL string + // ServiceCode 是服务代码,格式「应用短名.接口版本.产品代码」,健康检查需要它作为入参。 + ServiceCode string + // RunID 是本次运行的唯一短标识,嵌入测试数据前缀。 + RunID string +} + +// env 返回进程级单例(TestMain 前初始化一次)。 +var env *Env + +// Init 初始化环境配置;重复调用返回已初始化的实例。 +func Init() *Env { + if env != nil { + return env + } + env = &Env{ + BaseURL: getenv("E2E_BASE_URL", defaultBaseURL), + ServiceCode: getenv("E2E_SERVICE_CODE", defaultServiceCode), + RunID: newRunID(), + } + return env +} + +// newRunID 生成短随机标识:时间戳后 6 位数字 + 2 字节随机十六进制。 +// 保持简短以适配各类标识常见的长度约束。 +func newRunID() string { + buf := make([]byte, 2) + if _, err := rand.Read(buf); err != nil { + panic(fmt.Sprintf("fixture: generate run id: %v", err)) + } + return fmt.Sprintf("%s%s", time.Now().Format("150405"), hex.EncodeToString(buf)) +} + +// getenv 读取环境变量,空则返回默认值。 +func getenv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +// Prefix 返回测试数据前缀,如 "e2e153012ab12-"。 +func (e *Env) Prefix() string { return "e2e" + e.RunID + "-" } + +// Name 生成一个带本次运行前缀的名称,超过 32 字符则截断。 +// 各业务标识(用户名、邮箱、资源编码等)的字符集与长度规则各不相同, +// 请按需在此追加专用生成器,如 Username(hint) / Email(hint)。 +func (e *Env) Name(hint string) string { + name := e.Prefix() + hint + if len(name) > 32 { + name = name[:32] + } + return name +} diff --git a/template/service/test/e2e/fixture/main.go b/template/service/test/e2e/fixture/main.go new file mode 100644 index 0000000..586288b --- /dev/null +++ b/template/service/test/e2e/fixture/main.go @@ -0,0 +1,54 @@ +//go:build e2e + +// Main 供各业务域包的 TestMain 复用:探活 guard 与进程退出码收敛于此。 +// 各域包(test/e2e/)一行接入:func TestMain(m *testing.M) { fixture.Main(m) }。 +package fixture + +import ( + "fmt" + "net/http" + "net/url" + "os" + "testing" + "time" +) + +// Main 是各业务域包 TestMain 的统一入口:探活 guard + m.Run。 +func Main(m *testing.M) { + env := Init() + + // 前置 guard:确认被测服务可达,失败时给出可操作的提示而不是一排连接错误。 + if err := probe(env); err != nil { + fmt.Fprintf(os.Stderr, "e2e: 探活失败 %s(%v);请先在另一终端执行 make run\n", + env.BaseURL, err) + os.Exit(1) + } + fmt.Printf("e2e: 目标服务 %s,serviceCode=%s,runID=%s\n", + env.BaseURL, env.ServiceCode, env.RunID) + + os.Exit(m.Run()) +} + +// probe 用免认证的健康检查接口确认服务在线。 +// /api/healthz 对应 HealthCheck 方法,它在 config/app-dev-local.yaml 的 +// security.authentication.insecure_rpcs 列表内,匿名可访问; +// 该方法要求 service 入参等于本服务的 service_code,否则返回 404 NotFound。 +func probe(env *Env) error { + target := env.BaseURL + "/api/healthz?service=" + url.QueryEscape(env.ServiceCode) + + probeClient := &http.Client{Timeout: 3 * time.Second} + resp, err := probeClient.Get(target) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + // 常见原因:E2E_SERVICE_CODE 与服务配置的 service_code 不一致, + // 或该端口上跑的是另一个服务(此时网关会返回 501 Unimplemented)。 + return fmt.Errorf("GET /api/healthz?service=%s 返回 HTTP %d(期望 200);"+ + "请确认端口未被其他服务占用,且 service_code 一致", + env.ServiceCode, resp.StatusCode) + } + return nil +} diff --git a/template/service_test.go b/template/service_test.go index 3feef60..c87ab65 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -73,7 +73,28 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") - for _, relativePath := range []string{"handler/private.go", "modeler/mcp/registrar.go", "modeler/mcp/handler.go"} { + // 黑盒接口 E2E 模版框架,详见 test/e2e/README.md + assertContains("test/e2e/README.md", "//go:build e2e", "make test-e2e") + assertContains("test/e2e/client/client.go", "//go:build e2e", "func (r *Response) HasRPCCode(") + assertContains("test/e2e/fixture/main.go", "//go:build e2e", "func Main(m *testing.M)") + assertContains("test/e2e/fixture/env.go", "//go:build e2e", `defaultServiceCode = "echo.v1.demo"`) + assertContains("test/e2e/fixture/auth.go", "//go:build e2e", "example.com/acme/echo/test/e2e/client") + assertContains("test/e2e/demo/testmain_test.go", "//go:build e2e", "fixture.Main(m)") + assertContains("test/e2e/demo/suite_demo_test.go", "//go:build e2e", "/api/healthz", "/api/demo", "Unauthenticated") + assertContains("Makefile", "test-e2e:", "vet -tags=e2e") + assertContains("AGENTS.md", "test/e2e/") + + for _, relativePath := range []string{ + "handler/private.go", + "modeler/mcp/registrar.go", + "modeler/mcp/handler.go", + "test/e2e/client/client.go", + "test/e2e/fixture/env.go", + "test/e2e/fixture/main.go", + "test/e2e/fixture/auth.go", + "test/e2e/demo/testmain_test.go", + "test/e2e/demo/suite_demo_test.go", + } { content, err := os.ReadFile(filepath.Join(root, relativePath)) if err != nil { t.Fatalf("ReadFile(%s): %v", relativePath, err) @@ -84,6 +105,7 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { } fset := token.NewFileSet() + e2eRoot := filepath.Join(root, "test", "e2e") err = filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { if walkErr != nil { return walkErr @@ -91,8 +113,26 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { if entry.IsDir() || filepath.Ext(path) != ".go" { return nil } - _, parseErr := parser.ParseFile(fset, path, nil, parser.AllErrors) - return parseErr + file, parseErr := parser.ParseFile(fset, path, nil, parser.AllErrors) + if parseErr != nil { + return parseErr + } + if !strings.HasPrefix(path, e2eRoot+string(filepath.Separator)) { + return nil + } + // 黑盒纯度约束:test/e2e/ 只允许依赖标准库与 test/e2e/ 内部包, + // 禁止 import 本服务的业务包(api/、handler/、internal/、modeler/)。 + for _, spec := range file.Imports { + importPath := strings.Trim(spec.Path.Value, `"`) + if !strings.HasPrefix(importPath, "example.com/acme/echo") { + continue + } + if !strings.HasPrefix(importPath, "example.com/acme/echo/test/e2e/") { + relativePath, _ := filepath.Rel(root, path) + t.Errorf("e2e file %s violates black-box purity by importing %q", relativePath, importPath) + } + } + return nil }) if err != nil { t.Fatalf("parse generated Go files: %v", err) From efab25d96257b3038add397f1039434f61da78c2 Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 1 Sep 2026 21:50:25 +0800 Subject: [PATCH 32/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20CHANGELOG?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=200.4.4=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=8C=85=E5=90=AB=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E3=80=81=E7=BE=A4=E7=BB=84=E4=B8=8E=E9=83=A8=E9=97=A8=E5=9B=9E?= =?UTF-8?q?=E6=94=B6=E7=AB=99=E6=8E=A5=E5=8F=A3=E5=8F=8A=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 110 +++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index 6dded43..52cf2e8 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -24,6 +24,116 @@ 5. 新增 `test/e2e/README.md` 目录规范,含黑盒纯度约束(`test/e2e/` 禁止 import `api/`、`handler/`、`internal/`、`modeler/`)与环境变量覆盖说明;被测服务的运行配置由环境提供(默认 `config/app-dev-local.yaml`),E2E 自身不携带配置 6. 新增 `make test-e2e` 目标,`make lint` 增加 `go vet -tags=e2e ./test/e2e/...`,GitLab CI 增加手动触发的 `e2e-tests` 作业(配置路径由 `E2E_CONFIG_FILE` 指定) +## [0.4.4] - 2026-09-01 + +### Added + +#### grpc-kit/api 模块 + +- 新增用户、群组与部门回收站(软删除)接口 + + 1. 新增 `DeleteUser` / `UndeleteUser` / `ExpungeUser`:用户软删除进入回收站、从回收站恢复(恢复后为 `DISABLED` 状态)与永久删除;`ListUsers` 新增 `show_deleted` 回收站查询参数,`filter` 支持 `deleted_at != null` 过滤语义 + 2. 新增 `DeleteGroup` / `UndeleteGroup` / `ExpungeGroup`:群组软删除、恢复与永久删除;`Group` 新增 `protected` 字段,标识系统内置/受保护群组 + 3. 新增 `UndeleteDepartment` / `ExpungeDepartment`:部门软删除恢复与永久删除;`ListDepartments` 新增 `show_deleted` 回收站查询参数,回收站查询需同时设置 `show_deleted=true` 与 `filter="deleted_at != null"` + 4. `DeleteUser` / `DeleteGroup` / `DeleteDepartment` 统一改为返回对应实体消息,不再返回 `Empty` + +- 新增 `ListUserAuthBindings` 用户认证身份绑定查询接口 + + 返回指定用户的认证身份与提供方摘要,包含 `provider_user_id`、`provider_union_id`、`credential_configured`(该 Provider 所需的用户级凭据是否已配置,当前仅 LOCAL 身份填充)等字段。 + +- `LdapConfig` 新增 `user_id_attribute` 字段 + + 支持从已认证的 LDAP Entry 属性生成 `provider_user_id`;新建 Provider 未提供时默认 `uid`,历史配置缺失时由服务端继续按 `dn` 兼容。 + +- 新增智能连接(AI Connector)本地配置消息 + + 1. 新增 `AIConnectorConfig` 与 `MCPServerConfig` 配置消息 + 2. `LocalConfigs` oneof 新增 `aiconnector` 字段(编号 12),`independent` 字段编号由 12 调整为 13 + +- 新增群组配置消息 + + `Group` 新增 `oneof config` 群组配置:`DepartmentConfig`(部门)、`RoleConfig`(角色)、`DynamicConfig`(动态用户过滤)、`SystemConfig`(系统种子过滤),并新增 `parent_id` 层级字段与 `deleted_at`。 + +- 新增自定义全局设置接口 + + 新增 `CreateGlobalSetting` / `DeleteGlobalSetting` 方法与 `GlobalSettingInput` 消息,支持按分类创建、删除自定义全局配置项,更新经既有 `UpdateGlobalSettings` 完成。 + +#### grpc-kit/pkg 模块 + +- 新增用户、群组与部门回收站(软删除)生命周期实现 + + 1. 用户:`DeleteUser` 将用户移入回收站,`UndeleteUser` 恢复为 `DISABLED` 状态,`ExpungeUser` 永久删除;认证、MFA 与个人中心自助路径统一拒绝非 `ACTIVE` 或已删除用户;`ListUsers` 支持 `show_deleted` 与 `deleted_at != null` 回收站过滤;删除受最后一名活跃超级管理员保护 + 2. 群组:`DeleteGroup` / `UndeleteGroup` / `ExpungeGroup` 实现软删除、恢复与永久删除;`protected` 群组禁止删除或永久删除 + 3. 部门:删除校验受保护部门、子部门、群组引用及最后一名活跃超级管理员;`UpdateDepartment` 基于 `update_mask` 重写为全字段更新并拒绝已删除部门;部门群组引用须指向未删除的 `ACTIVE` 部门;部门 `code` 增加唯一约束 + +- `UpdateUser` 支持人工置位 `email_verified` / `phone_verified` 验证状态;联系方式变更且未显式指定时自动重置为未验证。 + +- 新增 `DeleteUserMFA` 管理接口实现 + + 重置指定用户的 MFA 密钥、恢复码与启用状态,并清除待处理挑战。 + +- 新增 `ListUserAuthBindings` 用户认证身份绑定查询实现。 + +- 新增外部身份自动关联与 LDAP 用户标识解析 + + 1. 外部登录时通过已验证标识符(规范化邮箱 / E.164 手机号)自动关联本地用户,受全局设置开关控制 + 2. 用户身份提供商与用户映射添加唯一索引,防止重复绑定 + 3. LDAP 支持 `user_id_attribute` 配置,从已认证 Entry 生成 `provider_user_id`(新建默认 `uid`,历史配置按 `dn` 兼容) + +- 新增智能连接本地配置支持及 MCP 服务端配置定义,配置快照纳入 `aiconnector` 模块。 + +- 内置菜单增强:新增可观测性菜单及其子项;建立菜单排序区间契约,业务菜单排序限定于框架菜单之间。 + +- 增强全局设置:支持创建、获取、更新与删除自定义全局配置项(需超级管理员权限),并提供布尔、整数、浮点数、持续时间、字符串、字符串数组与 JSON 多种数据类型的读取与校验。 + +- 应用样例配置文件新增对象存储与流程编排配置示例。 + +### Changed + +#### grpc-kit/api 模块 + +- 调整 `Group` 字段结构 + + 移除 `ref_id` / `ref_expr`,改为 `oneof config` 表达群组配置;新增 `parent_id` 层级字段(普通 API 当前只能写 0)。字段编号整体重排(如 `protected` 由 18 调整为 16、审计字段后移),按字段编号编码的旧消息不兼容,调用方需重新生成代码。 + +- `LocalConfigs.independent` 字段编号调整为 13,`aiconnector` 占用编号 12;直接依赖字段编号的调用方需重新生成代码。 + +#### grpc-kit/pkg 模块 + +- 重构群组与成员管理模型 + + 1. 移除部门与用户组的 `lion_groups` 关联边,群组改为 `oneof config` 配置(部门/角色/动态/系统)驱动的成员规则解析 + 2. 组 claim 与成员查询改用关联 `EXISTS` 谓词,并统一 cursor 分页 + 3. 强制组更新显式携带 `update_mask`,统一成员解析路由与过滤器缓存 + +### Fixed + +#### grpc-kit/api 模块 + +- `PrincipalType` 枚举与 Swagger 描述明确 GROUP 主体角色绑定限制:仅允许 `type=SYSTEM` 的群组。 + +- 修正用户资料接口 Swagger 描述,移除错误的 `reason` 信息。 + +#### grpc-kit/pkg 模块 + +- 修复组 claim 查询过早 `LIMIT` 截断成员结果,并处理内置组 `code` 被软删行占用的情况。 + +- 外部身份关联失败时返回具体错误原因,替代笼统的失败信息。 + +- 调整配置管理子菜单排序,与样例配置文件大类顺序保持一致。 + +### Security + +#### grpc-kit/pkg 模块 + +- MCP 服务端启用同源保护:基于标准库 `CrossOriginProtection` 拒绝不可信跨站请求,并对 SSE / Streamable HTTP 的 GET 长连接同样执行严格同源检查;无 `Origin` 的非浏览器 MCP Client 保持兼容。 + +- 本地配置查询(`ListLocalConfigs` / `GetLocalConfigs`)收紧为仅超级管理员可访问。 + +- 配置快照新增敏感信息掩码,按敏感键名递归掩码嵌套配置值,避免敏感数据外泄。 + +- 外部身份自动关联仅在全部已验证标识符收敛于同一活跃本地用户时执行,防止标识符冲突导致误关联。 + ## [0.4.3] - 2026-08-11 ### Added From cb7d3af31dfa9bbf7e5464d4fdf7670835e01aba Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 6 Sep 2026 23:15:43 +0800 Subject: [PATCH 33/57] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=8A=80=E8=83=BD=E6=96=87=E6=A1=A3=E5=92=8C=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=EF=BC=8C=E6=9B=B4=E6=96=B0=20AGENTS.md=20=E4=BB=A5?= =?UTF-8?q?=E5=8C=85=E5=90=AB=E6=96=B0=E6=8A=80=E8=83=BD=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/.agents/skills/README.md | 28 ++++ .../.agents/skills/add-api-domain/SKILL.md | 16 ++ template/service/AGENTS.md.tmpl | 9 ++ template/service/CLAUDE.md | 1 + template/service_test.go | 152 +++++++++++++++++- template/template.go | 2 +- 6 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 template/service/.agents/skills/README.md create mode 100644 template/service/.agents/skills/add-api-domain/SKILL.md create mode 100644 template/service/CLAUDE.md diff --git a/template/service/.agents/skills/README.md b/template/service/.agents/skills/README.md new file mode 100644 index 0000000..2537361 --- /dev/null +++ b/template/service/.agents/skills/README.md @@ -0,0 +1,28 @@ +# 服务技能 + +本目录保存随服务仓库一起维护的项目级技能。任务与某个技能匹配时,先完整阅读对应的 `SKILL.md`,再按需读取其引用的资源。 + +## 创建技能 + +每个技能使用独立目录: + +```text +.agents/skills//SKILL.md +``` + +`SKILL.md` 必须以 YAML frontmatter 开头,并包含: + +- `name`:与父目录同名,仅使用小写字母、数字和单连字符。 +- `description`:简要说明技能完成什么工作,以及应在什么情况下使用。 + +正文应提供本服务可直接执行的步骤。只有确有需要时才增加 `scripts/`、`references/` 或 `assets/`;从 `SKILL.md` 出发使用相对路径引用这些资源。 + +## 技能范围 + +服务技能与 `scripts/skills` 中的共享技能相互独立。使用服务技能不需要运行 `make skills`,也不应自动修改共享技能 submodule。 + +部分客户端会原生发现 `.agents/skills/`,其他客户端则通过 `AGENTS.md` 中的索引按文件读取。新增或删除技能时,请同步维护 `AGENTS.md` 的简短索引。避免在不同作用域创建同名技能;发生重名时,应明确使用仓库内的具体文件路径。 + +## 模板打包限制 + +本目录同时是 grpc-kit CLI 的嵌入式模板资源。不要在模板技能子树中添加隐藏文件、以下划线开头的文件、符号链接或独立 `go.mod`,否则文件可能无法可靠地进入发布产物。生成服务后由业务维护者新增的资源不受此模板打包限制。 diff --git a/template/service/.agents/skills/add-api-domain/SKILL.md b/template/service/.agents/skills/add-api-domain/SKILL.md new file mode 100644 index 0000000..a5f9c5f --- /dev/null +++ b/template/service/.agents/skills/add-api-domain/SKILL.md @@ -0,0 +1,16 @@ +--- +name: add-api-domain +description: 为 grpc-kit 服务新增 API 领域,包括 proto 契约、HTTP 映射、处理器和 API 测试。适用于新增领域或相关 RPC。 +--- + +# 新增 API 领域 + +在生成服务的根目录执行任务。修改文件前,先阅读 `AGENTS.md`、`scripts/env`、`Makefile` 和 `test/e2e/README.md`。检查一个已有 API 领域,确认当前 package 和文件约定,不要假设模板占位路径仍然适用。 + +1. 在现有 `api/` package 下定义带版本的消息和服务方法,并遵循仓库现有的 AIP 约定。在 `microservice.proto` 中增加 import 和 RPC,然后更新对应的 gateway 与 OpenAPI YAML。只有需求确实涉及持久化时才修改 Ent Schema。 +2. 需要生成绑定代码来实现 handler 时运行 `make generate`,并检查生成差异。不要修改带有 `DO NOT EDIT` 标记的文件,也不要把代码生成描述成能够完成业务 handler 实现。 +3. 在允许业务编辑的 handler 文件中实现 RPC,并使用已有的注册扩展点。传输层映射和领域行为应与相邻 handler 保持一致。 +4. 在实现代码旁增加有实际断言的单元测试。当 API 契约可从外部观察时,在 `test/e2e//` 下增加黑盒测试。复用 `test/e2e/client` 和 `test/e2e/fixture`,添加 `e2e` build tag 和每个 package 的 `TestMain`,只断言外部 HTTP 契约。不能用空测试、无条件跳过或没有断言的 stub 充当已完成测试。 +5. 运行 `make test`,完成代码生成和单元测试;该命令不会运行 E2E 测试。如果本地已有配置正确的服务,再运行 `make test-e2e`。否则可在条件允许时使用 `go vet -tags=e2e ./test/e2e/...` 做编译和静态检查,并明确说明没有执行运行时 E2E。 + +完成后列出修改过的契约文件、生成产物、handler 和测试,并报告准确的验证结果以及因环境条件未执行的检查。 diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl index be52b77..997aee3 100644 --- a/template/service/AGENTS.md.tmpl +++ b/template/service/AGENTS.md.tmpl @@ -15,6 +15,15 @@ This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, e - For release notes, version summaries, or CHANGELOG updates, use `scripts/skills/skills/generate-release-changelog/SKILL.md`. - If the shared skill directory is unavailable, report that it has not been initialized before falling back to repository conventions. +## Service Skills + +- Service-specific skills live in `.agents/skills//SKILL.md`. When a task matches one, read its complete `SKILL.md` and only the supporting resources relevant to the task before acting. +- Available service skill: `add-api-domain` — add a versioned API domain, HTTP mappings, handler implementation, and API tests. Read `.agents/skills/add-api-domain/SKILL.md` for that work. +- See `.agents/skills/README.md` for the format and maintenance rules. Native support and refresh behavior vary by client; agents without native discovery can read the indexed file directly. +- Service skills are independent of shared skills. Do not run `make skills`, access the network, or modify the `scripts/skills` submodule merely to use a service skill. +- Give service skills distinct names. If a name collides with another scope, use the explicit repository path rather than assuming a client-specific precedence rule. +- These files are business-editable. When adding or removing a service skill, update the compact index above. + ## Repository Structure ```text diff --git a/template/service/CLAUDE.md b/template/service/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/template/service/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/template/service_test.go b/template/service_test.go index c87ab65..ace3b2b 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -3,17 +3,20 @@ package template import ( "go/parser" "go/token" + "io/fs" "os" "path/filepath" + "regexp" + "sort" "strings" "testing" "github.com/grpc-kit/cli/config" ) -// TestServiceTemplateRendersMCPExtension 防止 MCP 扩展点在模板变更时被遗漏, +// TestServiceTemplateRendersExtensions 防止服务扩展点在模板变更时被遗漏, // 同时验证生成的 Go 文件均可被 parser 解析。 -func TestServiceTemplateRendersMCPExtension(t *testing.T) { +func TestServiceTemplateRendersExtensions(t *testing.T) { previousDir, err := os.Getwd() if err != nil { t.Fatalf("Getwd: %v", err) @@ -72,6 +75,55 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.4.2", "github.com/modelcontextprotocol/go-sdk v1.7.0") assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") + assertContains("AGENTS.md", "## Service Skills", ".agents/skills/add-api-domain/SKILL.md") + assertContains(".agents/skills/README.md", "# 服务技能", "模板打包限制") + assertContains(".agents/skills/add-api-domain/SKILL.md", "name: add-api-domain", "make test-e2e") + for _, relativePath := range []string{"AGENTS.md", "Makefile", filepath.Join("scripts", "env"), filepath.Join("test", "e2e", "README.md")} { + if _, err := os.Stat(filepath.Join(root, relativePath)); err != nil { + t.Errorf("skill repository reference %s is unavailable: %v", relativePath, err) + } + } + skillContent, err := os.ReadFile(filepath.Join(root, ".agents", "skills", "add-api-domain", "SKILL.md")) + if err != nil { + t.Fatalf("ReadFile(SKILL.md): %v", err) + } + for _, forbidden := range []string{"template/service/v1", "example.com/acme/echo"} { + if strings.Contains(string(skillContent), forbidden) { + t.Errorf("SKILL.md contains template-specific path %q", forbidden) + } + } + + claudeContent, err := os.ReadFile(filepath.Join(root, "CLAUDE.md")) + if err != nil { + t.Fatalf("ReadFile(CLAUDE.md): %v", err) + } + if got, want := string(claudeContent), "@AGENTS.md\n"; got != want { + t.Errorf("CLAUDE.md = %q, want %q", got, want) + } + if _, err := os.Stat(filepath.Join(root, "AGENTS.md")); err != nil { + t.Errorf("CLAUDE.md import target AGENTS.md is unavailable: %v", err) + } + if _, err := os.Stat(filepath.Join(root, ".claude", "skills")); !os.IsNotExist(err) { + t.Errorf("unexpected .claude/skills bridge: %v", err) + } + for _, relativePath := range []string{ + "CLAUDE.md.tmpl", + filepath.Join(".agents", "skills", "add-api-domain", "SKILL.md.tmpl"), + } { + if _, err := os.Stat(filepath.Join(root, relativePath)); !os.IsNotExist(err) { + t.Errorf("unexpected template suffix in generated service %s: %v", relativePath, err) + } + } + makefile, err := os.ReadFile(filepath.Join(root, "Makefile")) + if err != nil { + t.Fatalf("ReadFile(Makefile): %v", err) + } + if strings.Contains(string(makefile), "agents-sync:") { + t.Error("unexpected agents-sync target") + } + + validateGeneratedSkill(t, filepath.Join(root, ".agents", "skills", "add-api-domain")) + compareTemplateSkillAssets(t, previousDir) // 黑盒接口 E2E 模版框架,详见 test/e2e/README.md assertContains("test/e2e/README.md", "//go:build e2e", "make test-e2e") @@ -85,6 +137,9 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { assertContains("AGENTS.md", "test/e2e/") for _, relativePath := range []string{ + filepath.Join(".agents", "skills", "README.md"), + filepath.Join(".agents", "skills", "add-api-domain", "SKILL.md"), + "CLAUDE.md", "handler/private.go", "modeler/mcp/registrar.go", "modeler/mcp/handler.go", @@ -99,7 +154,7 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { if err != nil { t.Fatalf("ReadFile(%s): %v", relativePath, err) } - if strings.Contains(string(content), "DO NOT EDIT") { + if strings.HasPrefix(string(content), "// Code generated by") { t.Errorf("business-editable file %s is incorrectly marked generated", relativePath) } } @@ -138,3 +193,94 @@ func TestServiceTemplateRendersMCPExtension(t *testing.T) { t.Fatalf("parse generated Go files: %v", err) } } + +func validateGeneratedSkill(t *testing.T, skillDir string) { + t.Helper() + + content, err := os.ReadFile(filepath.Join(skillDir, "SKILL.md")) + if err != nil { + t.Fatalf("ReadFile(SKILL.md): %v", err) + } + parts := strings.SplitN(string(content), "---", 3) + if len(parts) != 3 || parts[0] != "" { + t.Fatal("SKILL.md must start with YAML frontmatter") + } + fields := make(map[string]string) + for _, line := range strings.Split(parts[1], "\n") { + key, value, ok := strings.Cut(line, ":") + if ok { + fields[strings.TrimSpace(key)] = strings.TrimSpace(value) + } + } + name := fields["name"] + description := fields["description"] + if name != filepath.Base(skillDir) { + t.Errorf("skill name %q does not match directory %q", name, filepath.Base(skillDir)) + } + if valid := regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`).MatchString(name); !valid || len(name) > 64 { + t.Errorf("invalid skill name %q", name) + } + if description == "" || len(description) > 1024 { + t.Errorf("invalid skill description length %d", len(description)) + } + body := strings.TrimSpace(parts[2]) + if body == "" || strings.Contains(body, "TODO") { + t.Error("skill body is empty or contains an unfinished placeholder") + } + if strings.HasPrefix(string(content), "// Code generated by") { + t.Error("service skill is incorrectly marked generated") + } +} + +func compareTemplateSkillAssets(t *testing.T, templateDir string) { + t.Helper() + + const root = "service/.agents/skills" + diskFiles := make([]string, 0) + err := filepath.WalkDir(filepath.Join(templateDir, "service", ".agents", "skills"), func(filePath string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if filePath != filepath.Join(templateDir, "service", ".agents", "skills") && + (strings.HasPrefix(entry.Name(), ".") || strings.HasPrefix(entry.Name(), "_")) { + return &fs.PathError{Op: "embed", Path: filePath, Err: fs.ErrInvalid} + } + if entry.Type()&os.ModeSymlink != 0 { + return &fs.PathError{Op: "embed", Path: filePath, Err: fs.ErrInvalid} + } + if entry.Name() == "go.mod" { + return &fs.PathError{Op: "embed", Path: filePath, Err: fs.ErrInvalid} + } + if entry.IsDir() { + return nil + } + relativePath, err := filepath.Rel(templateDir, filePath) + if err != nil { + return err + } + diskFiles = append(diskFiles, filepath.ToSlash(relativePath)) + return nil + }) + if err != nil { + t.Fatalf("walk disk skill template: %v", err) + } + + embeddedFiles := make([]string, 0) + err = fs.WalkDir(Assets, root, func(filePath string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if !entry.IsDir() { + embeddedFiles = append(embeddedFiles, filePath) + } + return nil + }) + if err != nil { + t.Fatalf("walk embedded skill template: %v", err) + } + sort.Strings(diskFiles) + sort.Strings(embeddedFiles) + if strings.Join(diskFiles, "\x00") != strings.Join(embeddedFiles, "\x00") { + t.Errorf("embedded skill files = %v, disk skill files = %v", embeddedFiles, diskFiles) + } +} diff --git a/template/template.go b/template/template.go index 2fb9557..fd6ef94 100644 --- a/template/template.go +++ b/template/template.go @@ -7,7 +7,7 @@ import ( "github.com/grpc-kit/cli/config" ) -//go:embed service/* +//go:embed service/* all:service/.agents var Assets embed.FS const ( From a4c85e91f2e3731cf64e0e8c005699c94a91ad4b Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 13 Sep 2026 12:40:47 +0800 Subject: [PATCH 34/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E6=9D=BF=E6=B5=8B=E8=AF=95=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=97=A5=E5=BF=97=E7=9B=B8=E5=85=B3=E7=9A=84=E6=96=AD?= =?UTF-8?q?=E8=A8=80=E4=BB=A5=E6=94=AF=E6=8C=81=E9=BB=91=E7=9B=92=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=20E2E=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/template/service_test.go b/template/service_test.go index ace3b2b..90ac426 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -69,10 +69,15 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { } assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()") assertContains("handler/private.go", "func (m *Microservice) privateMCPHandle() error", "return fmt.Errorf") + // Phase 0 slog migration baseline: keep the legacy generated logger surface + // explicit until the new template switches to slog in a later phase. + assertContains("handler/microservice.go", "*logrus.Entry", "lc.GetLogger()") + assertContains("modeler/independent_option.go", "func WithLogger(logger *logrus.Entry)", "func WithWorkflow(logger *logrus.Entry") + assertContains("modeler/mcp/option.go", "func WithLogger(logger *logrus.Entry)") assertContains("modeler/mcp/registrar.go", "func (r *Registrar) Register", "server is nil") assertContains("modeler/mcp/registrar_test.go", "session.CallTool", "session.ReadResource", "session.GetPrompt") assertContains("config/app-dev-local.yaml", "aiconnector:", "mcp_server:") - assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.4.2", "github.com/modelcontextprotocol/go-sdk v1.7.0") + assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.4.2", "github.com/modelcontextprotocol/go-sdk v1.7.0", "github.com/sirupsen/logrus v1.9.4") assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") assertContains("AGENTS.md", "## Service Skills", ".agents/skills/add-api-domain/SKILL.md") From 2f20d0879567da5ad4cea83d4f206fbe494f275c Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 13 Sep 2026 22:02:30 +0800 Subject: [PATCH 35/57] =?UTF-8?q?feat:=20=E6=9C=8D=E5=8A=A1=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=97=A5=E5=BF=97=E4=BB=8E=20logrus=20=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E8=87=B3=E6=A0=87=E5=87=86=E5=BA=93=20slog=EF=BC=8C?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E4=B8=8E=E5=85=B3=E9=97=AD=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E5=A2=9E=E5=8A=A0=20context=20=E4=BC=A0=E9=80=92?= =?UTF-8?q?=EF=BC=8C=E5=8D=87=E7=BA=A7=20grpc-kit/pkg=20=E8=87=B3=20v0.5.0?= =?UTF-8?q?=20=E5=B9=B6=E5=90=8C=E6=AD=A5=E5=8D=87=E7=BA=A7=20CLI=20Go=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E4=B8=8E=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 33 +- go.sum | 1102 ++--------------- template/service/cmd/server/main.go.tmpl | 2 +- template/service/go.mod.tmpl | 3 +- template/service/handler/microservice.go.tmpl | 10 +- .../service/handler/microservice_test.go.tmpl | 3 +- template/service/handler/register.go.tmpl | 2 +- template/service/handler/rpc_demo.go.tmpl | 3 +- .../service/handler/rpc_demo_test.go.tmpl | 3 +- template/service/handler/rpc_internal_test.go | 3 +- template/service/handler/shutdown.go.tmpl | 6 +- template/service/modeler/flow/flow.go.tmpl | 6 +- .../service/modeler/independent_cfg.go.tmpl | 11 +- .../service/modeler/independent_cfg_test.go | 2 +- .../modeler/independent_option.go.tmpl | 7 +- template/service/modeler/mcp/handler.go.tmpl | 2 +- template/service/modeler/mcp/option.go.tmpl | 4 +- .../service/modeler/mcp/registrar.go.tmpl | 5 +- .../modeler/mcp/registrar_test.go.tmpl | 9 +- template/service_test.go | 31 +- 20 files changed, 183 insertions(+), 1064 deletions(-) diff --git a/go.mod b/go.mod index 274fa5e..6a63b7a 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,37 @@ module github.com/grpc-kit/cli -go 1.16 +go 1.24.6 + +toolchain go1.24.7 require ( + entgo.io/ent v0.14.5 github.com/grpc-kit/pkg v0.2.4 github.com/mitchellh/go-homedir v1.1.0 - github.com/spf13/cobra v1.2.1 - github.com/spf13/viper v1.10.1 + github.com/spf13/cobra v1.10.1 + github.com/spf13/viper v1.21.0 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index 754fc84..0770174 100644 --- a/go.sum +++ b/go.sum @@ -1,1026 +1,94 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/sarama v1.25.0/go.mod h1:y/CFFTO9eaMTNriwu/Q+W4eioLqiDMGkA1W+gmdfj8w= -github.com/Shopify/sarama v1.32.0/go.mod h1:+EmJJKZWVT/faR9RcOxJerP+LId4iWdQPBGLy1Y1Njs= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/Shopify/toxiproxy/v2 v2.3.0/go.mod h1:KvQTtB6RjCJY4zqNJn7C7JDFgsG5uoHYDirfUfpIm0c= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.8.0/go.mod h1:m41mqM/Pa9pzPPNrvWwY3M7llCuzciKk5tqH0m6Rz9I= -github.com/cloudevents/sdk-go/v2 v2.8.0/go.mod h1:GpCBmUj7DIRiDhVvsK5d6WCbgTWs8DxAWTRtAwQmIXs= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4= +entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/grpc-kit/pkg v0.2.4 h1:tHNB1iVDX6e0T+8mKDshpV+BW4r1fmcDGLEqALOZdII= github.com/grpc-kit/pkg v0.2.4/go.mod h1:qkK9TaJyPXMFGZ1DrEuO1DibGayu3YOTVTNTMYyWWeU= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= -github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= -github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= -github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= -github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.0/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.etcd.io/etcd/client/v3 v3.5.2/go.mod h1:kOOaWFFgHygyT0WlSmL8TJiXmMysO/nNUlEsSsN6W4o= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff h1:8Zg5TdmcbU8A7CXGjGXF1Slqu/nIFCRaR3S5gT2plIA= +google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:dbWfpVPvW/RqafStmRWBUpMN14puDezDMHxNYiRfQu0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= -gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= -gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= -gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= -gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/template/service/cmd/server/main.go.tmpl b/template/service/cmd/server/main.go.tmpl index 688cb33..c8cb98d 100644 --- a/template/service/cmd/server/main.go.tmpl +++ b/template/service/cmd/server/main.go.tmpl @@ -64,7 +64,7 @@ func startServer(ctx context.Context) (*handler.Microservice, error) { return nil, err } - m, err := handler.NewMicroservice(lc) + m, err := handler.NewMicroservice(ctx, lc) if err != nil { return nil, err } diff --git a/template/service/go.mod.tmpl b/template/service/go.mod.tmpl index 40da9cf..7962ecd 100644 --- a/template/service/go.mod.tmpl +++ b/template/service/go.mod.tmpl @@ -5,9 +5,8 @@ go 1.25.0 require ( entgo.io/ent v0.14.5 github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 - github.com/grpc-kit/pkg v0.4.2 + github.com/grpc-kit/pkg v0.5.0 github.com/modelcontextprotocol/go-sdk v1.7.0 - github.com/sirupsen/logrus v1.9.4 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 go.opentelemetry.io/otel v1.44.0 diff --git a/template/service/handler/microservice.go.tmpl b/template/service/handler/microservice.go.tmpl index fe42cfa..c3cb88c 100644 --- a/template/service/handler/microservice.go.tmpl +++ b/template/service/handler/microservice.go.tmpl @@ -3,9 +3,11 @@ package handler import ( + "context" + "log/slog" + "github.com/grpc-kit/pkg/cfg" "github.com/grpc-kit/pkg/rpc" - "github.com/sirupsen/logrus" "{{ .Global.Repository }}/modeler" ) @@ -15,13 +17,13 @@ type Microservice struct { code string // 服务代码 server *rpc.Server // 服务定义 client *rpc.Client // 服务调用 - logger *logrus.Entry // 全局日志 + logger *slog.Logger // 全局日志 baseCfg *cfg.LocalConfig // 基础配置 thisCfg *modeler.IndependentCfg // 个性配置 } // NewMicroservice 全局只实例化一次 -func NewMicroservice(lc *cfg.LocalConfig) (*Microservice, error) { +func NewMicroservice(ctx context.Context, lc *cfg.LocalConfig) (*Microservice, error) { // 基础配置初始化 if err := lc.Init(); err != nil { return nil, err @@ -58,7 +60,7 @@ func NewMicroservice(lc *cfg.LocalConfig) (*Microservice, error) { } // 自定义配置初始化 - if err = m.thisCfg.Init(opts...); err != nil { + if err = m.thisCfg.Init(ctx, opts...); err != nil { return m, err } diff --git a/template/service/handler/microservice_test.go.tmpl b/template/service/handler/microservice_test.go.tmpl index 86c24fd..6bf4682 100644 --- a/template/service/handler/microservice_test.go.tmpl +++ b/template/service/handler/microservice_test.go.tmpl @@ -1,6 +1,7 @@ package handler import ( + "context" "testing" "github.com/grpc-kit/pkg/cfg" @@ -20,7 +21,7 @@ func init() { Independent: modeler.IndependentCfg{}, } - s, err := NewMicroservice(lc) + s, err := NewMicroservice(context.Background(), lc) if err != nil { panic(err) } diff --git a/template/service/handler/register.go.tmpl b/template/service/handler/register.go.tmpl index 7783f70..f8d5553 100644 --- a/template/service/handler/register.go.tmpl +++ b/template/service/handler/register.go.tmpl @@ -29,7 +29,7 @@ func (m *Microservice) Register(ctx context.Context) error { } // 注册前端托管静态数据 - if err = m.baseCfg.HTTPHandlerFrontend(mux, public.Assets); err != nil { + if err = m.baseCfg.HTTPHandlerFrontendContext(ctx, mux, public.Assets); err != nil { return err } diff --git a/template/service/handler/rpc_demo.go.tmpl b/template/service/handler/rpc_demo.go.tmpl index 97ff1d4..07c4d56 100644 --- a/template/service/handler/rpc_demo.go.tmpl +++ b/template/service/handler/rpc_demo.go.tmpl @@ -2,6 +2,7 @@ package handler import ( "context" + "fmt" examplev1 "github.com/grpc-kit/pkg/api/known/example/v1" @@ -10,7 +11,7 @@ import ( // Demo test func (m *Microservice) Demo(ctx context.Context, req *pb.DemoRequest) (*pb.DemoResponse, error) { - m.logger.Warnf("test demo warn: %v", "func Demo") + m.logger.WarnContext(ctx, fmt.Sprintf("test demo warn: %v", "func Demo")) result := &pb.DemoResponse{ // GET /api/demo diff --git a/template/service/handler/rpc_demo_test.go.tmpl b/template/service/handler/rpc_demo_test.go.tmpl index e7146f3..68ad688 100644 --- a/template/service/handler/rpc_demo_test.go.tmpl +++ b/template/service/handler/rpc_demo_test.go.tmpl @@ -1,7 +1,6 @@ package handler import ( - "context" "testing" pb "{{ .Global.Repository }}/api/{{ .Global.ProductCode }}/{{ .Global.ShortName }}/{{ .Template.Service.APIVersion }}" @@ -12,7 +11,7 @@ func TestDemo(t *testing.T) { Uuid: "99feafb5-bed6-4daf-927a-69a2ab80c485", } - resp, err := m.Demo(context.TODO(), req) + resp, err := m.Demo(t.Context(), req) if err != nil { t.Errorf("Demo test fail: %v", err) } diff --git a/template/service/handler/rpc_internal_test.go b/template/service/handler/rpc_internal_test.go index 0caab19..80f2e47 100644 --- a/template/service/handler/rpc_internal_test.go +++ b/template/service/handler/rpc_internal_test.go @@ -1,7 +1,6 @@ package handler import ( - "context" "testing" statusv1 "github.com/grpc-kit/pkg/api/known/status/v1" @@ -12,7 +11,7 @@ func TestInternal(t *testing.T) { Service: m.baseCfg.Services.ServiceCode, } - _, err := m.HealthCheck(context.TODO(), req) + _, err := m.HealthCheck(t.Context(), req) if err != nil { t.Errorf("HealthCheck test fail: %v", err) } diff --git a/template/service/handler/shutdown.go.tmpl b/template/service/handler/shutdown.go.tmpl index 31d8f39..4feb9f5 100644 --- a/template/service/handler/shutdown.go.tmpl +++ b/template/service/handler/shutdown.go.tmpl @@ -9,9 +9,9 @@ import ( // Shutdown 优雅关闭gRPC与HTTP服务 func (m *Microservice) Shutdown(ctx context.Context) error { - m.logger.Warnf("Shutdown server begin") + m.logger.WarnContext(ctx, "Shutdown server begin") - if err := m.baseCfg.Deregister(); err != nil { + if err := m.baseCfg.DeregisterContext(ctx); err != nil { return err } @@ -24,6 +24,6 @@ func (m *Microservice) Shutdown(ctx context.Context) error { return err } - m.logger.Warnf("Shutdown server end") + m.logger.WarnContext(ctx, "Shutdown server end") return nil } diff --git a/template/service/modeler/flow/flow.go.tmpl b/template/service/modeler/flow/flow.go.tmpl index c72172f..3110fe5 100644 --- a/template/service/modeler/flow/flow.go.tmpl +++ b/template/service/modeler/flow/flow.go.tmpl @@ -2,21 +2,21 @@ package flow import ( "context" + "log/slog" "github.com/grpc-kit/pkg/cfg" - "github.com/sirupsen/logrus" "k8s.io/client-go/dynamic" ) // Client xx type Client struct { - logger *logrus.Entry + logger *slog.Logger config *cfg.FlowClientConfig dynamicset *dynamic.DynamicClient } // NewClient returns a new Workflow -func NewClient(logger *logrus.Entry, fcc *cfg.FlowClientConfig) (*Client, error) { +func NewClient(logger *slog.Logger, fcc *cfg.FlowClientConfig) (*Client, error) { dynamicset, err := dynamic.NewForConfig(fcc.Config) if err != nil { return nil, err diff --git a/template/service/modeler/independent_cfg.go.tmpl b/template/service/modeler/independent_cfg.go.tmpl index 7e7c90f..fc8d1ef 100644 --- a/template/service/modeler/independent_cfg.go.tmpl +++ b/template/service/modeler/independent_cfg.go.tmpl @@ -3,8 +3,7 @@ package modeler import ( "context" "fmt" - - "github.com/sirupsen/logrus" + "log/slog" "{{ .Global.Repository }}/modeler/ent" "{{ .Global.Repository }}/modeler/flow" @@ -12,7 +11,7 @@ import ( // IndependentCfg 个性配置 type IndependentCfg struct { - logger *logrus.Entry + logger *slog.Logger db *ent.Client flow *flow.Client @@ -20,9 +19,7 @@ type IndependentCfg struct { } // Init 用于初始化实例 -func (i *IndependentCfg) Init(opts ...ClientIndependentOption) error { - ctx := context.Background() - +func (i *IndependentCfg) Init(ctx context.Context, opts ...ClientIndependentOption) error { for _, opt := range opts { opt(i) } @@ -59,4 +56,4 @@ func (i *IndependentCfg) GetFlowClient() (*flow.Client, error) { } return i.flow, nil -} \ No newline at end of file +} diff --git a/template/service/modeler/independent_cfg_test.go b/template/service/modeler/independent_cfg_test.go index 81233da..4b66f45 100644 --- a/template/service/modeler/independent_cfg_test.go +++ b/template/service/modeler/independent_cfg_test.go @@ -4,7 +4,7 @@ import "testing" func TestIndependentCfg(t *testing.T) { i := &IndependentCfg{} - if err := i.Init(); err != nil { + if err := i.Init(t.Context()); err != nil { t.Error(err) } } diff --git a/template/service/modeler/independent_option.go.tmpl b/template/service/modeler/independent_option.go.tmpl index e195271..596a776 100644 --- a/template/service/modeler/independent_option.go.tmpl +++ b/template/service/modeler/independent_option.go.tmpl @@ -3,9 +3,10 @@ package modeler import ( + "log/slog" + entsql "entgo.io/ent/dialect/sql" "github.com/grpc-kit/pkg/cfg" - "github.com/sirupsen/logrus" "{{ .Global.Repository }}/modeler/ent" "{{ .Global.Repository }}/modeler/flow" @@ -15,7 +16,7 @@ import ( type ClientIndependentOption func(cfg *IndependentCfg) // WithLogger 传递日志实例 -func WithLogger(logger *logrus.Entry) ClientIndependentOption { +func WithLogger(logger *slog.Logger) ClientIndependentOption { return func(cfg *IndependentCfg) { cfg.logger = logger } @@ -30,7 +31,7 @@ func WithDatabaseEntDriver(driver *entsql.Driver) ClientIndependentOption { } // WithWorkflow 传递流水线配置 -func WithWorkflow(logger *logrus.Entry, fcc *cfg.FlowClientConfig) ClientIndependentOption { +func WithWorkflow(logger *slog.Logger, fcc *cfg.FlowClientConfig) ClientIndependentOption { return func(cfg *IndependentCfg) { if fcc != nil { client, err := flow.NewClient(logger, fcc) diff --git a/template/service/modeler/mcp/handler.go.tmpl b/template/service/modeler/mcp/handler.go.tmpl index af79fd8..4eba623 100644 --- a/template/service/modeler/mcp/handler.go.tmpl +++ b/template/service/modeler/mcp/handler.go.tmpl @@ -39,7 +39,7 @@ func (r *Registrar) handleEcho( } if r.logger != nil { - r.logger.Debugf("mcp echo tool called: text_length=%d", len(args.Text)) + r.logger.DebugContext(ctx, fmt.Sprintf("mcp echo tool called: text_length=%d", len(args.Text))) } payload, _ := json.Marshal(map[string]any{"echo": args.Text}) diff --git a/template/service/modeler/mcp/option.go.tmpl b/template/service/modeler/mcp/option.go.tmpl index 4bdd865..0be8ea3 100644 --- a/template/service/modeler/mcp/option.go.tmpl +++ b/template/service/modeler/mcp/option.go.tmpl @@ -1,7 +1,7 @@ package mcp import ( - logrus "github.com/sirupsen/logrus" + "log/slog" "{{ .Global.Repository }}/modeler/ent" "{{ .Global.Repository }}/modeler/flow" @@ -18,6 +18,6 @@ func WithWorkflow(flow *flow.Client) RegistrarOption { } // WithLogger 注入日志实例 -func WithLogger(logger *logrus.Entry) RegistrarOption { +func WithLogger(logger *slog.Logger) RegistrarOption { return func(r *Registrar) { r.logger = logger } } diff --git a/template/service/modeler/mcp/registrar.go.tmpl b/template/service/modeler/mcp/registrar.go.tmpl index b11a3fc..de77323 100644 --- a/template/service/modeler/mcp/registrar.go.tmpl +++ b/template/service/modeler/mcp/registrar.go.tmpl @@ -2,8 +2,7 @@ package mcp import ( "fmt" - - logrus "github.com/sirupsen/logrus" + "log/slog" mcpserver "github.com/grpc-kit/pkg/mcp" sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" @@ -19,7 +18,7 @@ var _ = mcpserver.AuthHeaderFromContext // Registrar 持有业务依赖,实现自定义 MCP 资源注册(Tools / Resources / Prompts)。 // 业务开发者可在此添加 LLM client、RPC client 等依赖字段及对应 With* 选项。 type Registrar struct { - logger *logrus.Entry + logger *slog.Logger db *ent.Client flow *flow.Client } diff --git a/template/service/modeler/mcp/registrar_test.go.tmpl b/template/service/modeler/mcp/registrar_test.go.tmpl index cf17cfc..67a0a97 100644 --- a/template/service/modeler/mcp/registrar_test.go.tmpl +++ b/template/service/modeler/mcp/registrar_test.go.tmpl @@ -1,7 +1,6 @@ package mcp import ( - "context" "net/http/httptest" "strings" "testing" @@ -29,13 +28,13 @@ func TestRegistrar_AllResources(t *testing.T) { httpServer := httptest.NewServer(wrapper.Handler()) defer httpServer.Close() client := sdkmcp.NewClient(&sdkmcp.Implementation{Name: "registrar-test", Version: "0.0.1"}, nil) - session, err := client.Connect(context.Background(), &sdkmcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil) + session, err := client.Connect(t.Context(), &sdkmcp.StreamableClientTransport{Endpoint: httpServer.URL}, nil) if err != nil { t.Fatalf("Connect: %v", err) } defer session.Close() - toolResult, err := session.CallTool(context.Background(), &sdkmcp.CallToolParams{ + toolResult, err := session.CallTool(t.Context(), &sdkmcp.CallToolParams{ Name: "{{ .Global.ShortName }}_echo", Arguments: map[string]any{"text": "hello"}, }) if err != nil || toolResult.IsError || len(toolResult.Content) != 1 { @@ -45,12 +44,12 @@ func TestRegistrar_AllResources(t *testing.T) { t.Fatalf("CallTool content = %#v", toolResult.Content) } - resourceResult, err := session.ReadResource(context.Background(), &sdkmcp.ReadResourceParams{URI: "{{ .Global.ShortName }}://config"}) + resourceResult, err := session.ReadResource(t.Context(), &sdkmcp.ReadResourceParams{URI: "{{ .Global.ShortName }}://config"}) if err != nil || len(resourceResult.Contents) != 1 || !strings.Contains(resourceResult.Contents[0].Text, `"service":"{{ .Global.ShortName }}"`) { t.Fatalf("ReadResource: result=%v err=%v", resourceResult, err) } - promptResult, err := session.GetPrompt(context.Background(), &sdkmcp.GetPromptParams{ + promptResult, err := session.GetPrompt(t.Context(), &sdkmcp.GetPromptParams{ Name: "{{ .Global.ShortName }}_greeting", Arguments: map[string]string{"name": "Codex"}, }) if err != nil || len(promptResult.Messages) != 1 { diff --git a/template/service_test.go b/template/service_test.go index ace3b2b..3a3026f 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -67,12 +67,31 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { } } } - assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()") + assertNotContains := func(relativePath string, forbidden ...string) { + t.Helper() + content, err := os.ReadFile(filepath.Join(root, relativePath)) + if err != nil { + t.Fatalf("ReadFile(%s): %v", relativePath, err) + } + for _, value := range forbidden { + if strings.Contains(string(content), value) { + t.Errorf("%s unexpectedly contains %q", relativePath, value) + } + } + } + assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()", "HTTPHandlerFrontendContext(ctx") assertContains("handler/private.go", "func (m *Microservice) privateMCPHandle() error", "return fmt.Errorf") + assertContains("handler/microservice.go", `"log/slog"`, "logger *slog.Logger", "NewMicroservice(ctx context.Context") + assertContains("handler/rpc_demo.go", "WarnContext(ctx") + assertContains("handler/shutdown.go", "WarnContext(ctx", "DeregisterContext(ctx)") + assertContains("modeler/independent_cfg.go", "Init(ctx context.Context") + assertContains("modeler/mcp/handler.go", "DebugContext(ctx") + assertContains("modeler/mcp/option.go", "WithLogger(logger *slog.Logger)") assertContains("modeler/mcp/registrar.go", "func (r *Registrar) Register", "server is nil") assertContains("modeler/mcp/registrar_test.go", "session.CallTool", "session.ReadResource", "session.GetPrompt") assertContains("config/app-dev-local.yaml", "aiconnector:", "mcp_server:") - assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.4.2", "github.com/modelcontextprotocol/go-sdk v1.7.0") + assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.5.0", "github.com/modelcontextprotocol/go-sdk v1.7.0") + assertNotContains("go.mod", "github.com/sirupsen/logrus") assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") assertContains("AGENTS.md", "## Service Skills", ".agents/skills/add-api-domain/SKILL.md") @@ -168,6 +187,14 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { if entry.IsDir() || filepath.Ext(path) != ".go" { return nil } + content, readErr := os.ReadFile(path) + if readErr != nil { + return readErr + } + if strings.Contains(string(content), "github.com/sirupsen/logrus") || strings.Contains(string(content), "*logrus.Entry") { + relativePath, _ := filepath.Rel(root, path) + t.Errorf("generated Go file %s still references logrus", relativePath) + } file, parseErr := parser.ParseFile(fset, path, nil, parser.AllErrors) if parseErr != nil { return parseErr From e54514c19f4bbf6038fa03b574b997369906ecb8 Mon Sep 17 00:00:00 2001 From: mingqing Date: Sun, 13 Sep 2026 22:02:57 +0800 Subject: [PATCH 36/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20CHANGELOG?= =?UTF-8?q?=EF=BC=8CUnreleased=20=E6=96=B0=E5=A2=9E=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=97=A5=E5=BF=97=E4=BB=8E=20logrus=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E8=87=B3=E6=A0=87=E5=87=86=E5=BA=93=20slog?= =?UTF-8?q?=20=E7=9A=84=20Breaking=20=E5=8F=98=E6=9B=B4=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index 52cf2e8..5383cdc 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -24,6 +24,17 @@ 5. 新增 `test/e2e/README.md` 目录规范,含黑盒纯度约束(`test/e2e/` 禁止 import `api/`、`handler/`、`internal/`、`modeler/`)与环境变量覆盖说明;被测服务的运行配置由环境提供(默认 `config/app-dev-local.yaml`),E2E 自身不携带配置 6. 新增 `make test-e2e` 目标,`make lint` 增加 `go vet -tags=e2e ./test/e2e/...`,GitLab CI 增加手动触发的 `e2e-tests` 作业(配置路径由 `E2E_CONFIG_FILE` 指定) +### Changed + +#### grpc-kit/cli 模块 + +- **Breaking(v0.5.0)**:服务模板日志从 logrus 切换为 Go 标准库 `log/slog`。 + + 1. `Microservice`、`IndependentCfg`、flow client 与 MCP registrar 的 logger 类型改为 `*slog.Logger`;`GetLogger`、`WithLogger`、`WithWorkflow`、`flow.NewClient` 名称保持不变 + 2. RPC、MCP 和 shutdown 日志改用 slog Context API;模板改调 `HTTPHandlerFrontendContext`、`DeregisterContext`,调用链已有的 ctx 会继续向下传递 + 3. `NewMicroservice` 和 `IndependentCfg.Init` 新增 ctx 参数;旧生成项目可按迁移文档机械修改 + 4. 新模板固定 `github.com/grpc-kit/pkg v0.5.0` 并移除 logrus 直接依赖 + ## [0.4.4] - 2026-09-01 ### Added From e698256b396993b049929b9918852f1dd3b14a19 Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 14 Sep 2026 21:41:30 +0800 Subject: [PATCH 37/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=BE=AE?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=BC=A0=E9=80=92=E4=B8=8A=E4=B8=8B=E6=96=87=E8=87=B3=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E5=92=8C=E5=85=B3=E9=97=AD=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/handler/microservice.go.tmpl | 2 +- template/service/handler/register.go.tmpl | 2 +- template/service/handler/shutdown.go.tmpl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/template/service/handler/microservice.go.tmpl b/template/service/handler/microservice.go.tmpl index c3cb88c..cbf69bf 100644 --- a/template/service/handler/microservice.go.tmpl +++ b/template/service/handler/microservice.go.tmpl @@ -25,7 +25,7 @@ type Microservice struct { // NewMicroservice 全局只实例化一次 func NewMicroservice(ctx context.Context, lc *cfg.LocalConfig) (*Microservice, error) { // 基础配置初始化 - if err := lc.Init(); err != nil { + if err := lc.Init(ctx); err != nil { return nil, err } diff --git a/template/service/handler/register.go.tmpl b/template/service/handler/register.go.tmpl index f8d5553..4d9f92f 100644 --- a/template/service/handler/register.go.tmpl +++ b/template/service/handler/register.go.tmpl @@ -29,7 +29,7 @@ func (m *Microservice) Register(ctx context.Context) error { } // 注册前端托管静态数据 - if err = m.baseCfg.HTTPHandlerFrontendContext(ctx, mux, public.Assets); err != nil { + if err = m.baseCfg.HTTPHandlerFrontend(ctx, mux, public.Assets); err != nil { return err } diff --git a/template/service/handler/shutdown.go.tmpl b/template/service/handler/shutdown.go.tmpl index 4feb9f5..0385c0f 100644 --- a/template/service/handler/shutdown.go.tmpl +++ b/template/service/handler/shutdown.go.tmpl @@ -11,7 +11,7 @@ import ( func (m *Microservice) Shutdown(ctx context.Context) error { m.logger.WarnContext(ctx, "Shutdown server begin") - if err := m.baseCfg.DeregisterContext(ctx); err != nil { + if err := m.baseCfg.Deregister(ctx); err != nil { return err } From dd3ef000e32a9cf6e6fd7c961f63777002882497 Mon Sep 17 00:00:00 2001 From: mingqing Date: Mon, 14 Sep 2026 21:41:35 +0800 Subject: [PATCH 38/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E6=9D=BF=E6=B5=8B=E8=AF=95=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E6=97=A5=E5=BF=97=E7=9B=B8=E5=85=B3=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=9A=84=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E4=BC=A0=E9=80=92=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/template/service_test.go b/template/service_test.go index 3a3026f..c0426b3 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -79,11 +79,14 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { } } } - assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()", "HTTPHandlerFrontendContext(ctx") + assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()", "HTTPHandlerFrontend(ctx") + assertNotContains("handler/register.go", "HTTPHandlerFrontendContext(ctx") assertContains("handler/private.go", "func (m *Microservice) privateMCPHandle() error", "return fmt.Errorf") - assertContains("handler/microservice.go", `"log/slog"`, "logger *slog.Logger", "NewMicroservice(ctx context.Context") + assertContains("handler/microservice.go", `"log/slog"`, "logger *slog.Logger", "NewMicroservice(ctx context.Context", "lc.Init(ctx)") + assertNotContains("handler/microservice.go", "lc.InitContext(ctx)") assertContains("handler/rpc_demo.go", "WarnContext(ctx") - assertContains("handler/shutdown.go", "WarnContext(ctx", "DeregisterContext(ctx)") + assertContains("handler/shutdown.go", "WarnContext(ctx", "Deregister(ctx)") + assertNotContains("handler/shutdown.go", "DeregisterContext(ctx)") assertContains("modeler/independent_cfg.go", "Init(ctx context.Context") assertContains("modeler/mcp/handler.go", "DebugContext(ctx") assertContains("modeler/mcp/option.go", "WithLogger(logger *slog.Logger)") From cd52caccbb7660d8731408dc75f284fbacef3bb5 Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 15 Sep 2026 18:27:08 +0800 Subject: [PATCH 39/57] =?UTF-8?q?feat:=20=E5=9C=A8=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E5=92=8C=E6=BC=94=E7=A4=BA=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E4=B8=AD=E5=A2=9E=E5=8A=A0=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BC=A0?= =?UTF-8?q?=E9=80=92=EF=BC=8C=E7=AE=80=E5=8C=96=E6=97=A5=E5=BF=97=E8=BE=93?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/handler/register.go.tmpl | 2 +- template/service/handler/rpc_demo.go.tmpl | 3 +-- template/service/modeler/mcp/handler.go.tmpl | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/template/service/handler/register.go.tmpl b/template/service/handler/register.go.tmpl index 4d9f92f..3ab36dc 100644 --- a/template/service/handler/register.go.tmpl +++ b/template/service/handler/register.go.tmpl @@ -50,7 +50,7 @@ func (m *Microservice) Register(ctx context.Context) error { } // 开启gRPC与HTTP服务 - if err := m.server.StartBackground(); err != nil { + if err := m.server.StartBackground(ctx); err != nil { return err } diff --git a/template/service/handler/rpc_demo.go.tmpl b/template/service/handler/rpc_demo.go.tmpl index 07c4d56..7c1088f 100644 --- a/template/service/handler/rpc_demo.go.tmpl +++ b/template/service/handler/rpc_demo.go.tmpl @@ -2,7 +2,6 @@ package handler import ( "context" - "fmt" examplev1 "github.com/grpc-kit/pkg/api/known/example/v1" @@ -11,7 +10,7 @@ import ( // Demo test func (m *Microservice) Demo(ctx context.Context, req *pb.DemoRequest) (*pb.DemoResponse, error) { - m.logger.WarnContext(ctx, fmt.Sprintf("test demo warn: %v", "func Demo")) + m.logger.WarnContext(ctx, "test demo warn: func Demo") result := &pb.DemoResponse{ // GET /api/demo diff --git a/template/service/modeler/mcp/handler.go.tmpl b/template/service/modeler/mcp/handler.go.tmpl index 4eba623..72d6def 100644 --- a/template/service/modeler/mcp/handler.go.tmpl +++ b/template/service/modeler/mcp/handler.go.tmpl @@ -39,7 +39,7 @@ func (r *Registrar) handleEcho( } if r.logger != nil { - r.logger.DebugContext(ctx, fmt.Sprintf("mcp echo tool called: text_length=%d", len(args.Text))) + r.logger.DebugContext(ctx, "MCP echo tool called") } payload, _ := json.Marshal(map[string]any{"echo": args.Text}) From 2bd6812f92405dec86e9f3e4c7d1ef2e6cfa9571 Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 15 Sep 2026 18:27:24 +0800 Subject: [PATCH 40/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E6=9D=BF=E6=B5=8B=E8=AF=95=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=AF=B9=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BC=A0=E9=80=92?= =?UTF-8?q?=E7=9A=84=E6=96=AD=E8=A8=80=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/template/service_test.go b/template/service_test.go index c0426b3..b9fa9e3 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -79,16 +79,18 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { } } } - assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()", "HTTPHandlerFrontend(ctx") - assertNotContains("handler/register.go", "HTTPHandlerFrontendContext(ctx") + assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()", "HTTPHandlerFrontend(ctx", "StartBackground(ctx)") + assertNotContains("handler/register.go", "HTTPHandlerFrontendContext(ctx", "StartBackground()") assertContains("handler/private.go", "func (m *Microservice) privateMCPHandle() error", "return fmt.Errorf") assertContains("handler/microservice.go", `"log/slog"`, "logger *slog.Logger", "NewMicroservice(ctx context.Context", "lc.Init(ctx)") assertNotContains("handler/microservice.go", "lc.InitContext(ctx)") - assertContains("handler/rpc_demo.go", "WarnContext(ctx") + assertContains("handler/rpc_demo.go", `WarnContext(ctx, "test demo warn: func Demo")`) + assertNotContains("handler/rpc_demo.go", "fmt.Sprintf", `"fmt"`) assertContains("handler/shutdown.go", "WarnContext(ctx", "Deregister(ctx)") assertNotContains("handler/shutdown.go", "DeregisterContext(ctx)") assertContains("modeler/independent_cfg.go", "Init(ctx context.Context") - assertContains("modeler/mcp/handler.go", "DebugContext(ctx") + assertContains("modeler/mcp/handler.go", `DebugContext(ctx, "MCP echo tool called")`) + assertNotContains("modeler/mcp/handler.go", "text_length", "fmt.Sprintf(\"mcp echo tool called") assertContains("modeler/mcp/option.go", "WithLogger(logger *slog.Logger)") assertContains("modeler/mcp/registrar.go", "func (r *Registrar) Register", "server is nil") assertContains("modeler/mcp/registrar_test.go", "session.CallTool", "session.ReadResource", "session.GetPrompt") From 096ff9530c115ecb0b549f44d49f8f18ef80a22f Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 15 Sep 2026 18:27:43 +0800 Subject: [PATCH 41/57] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E6=9D=BF=E6=97=A5=E5=BF=97=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8C=E5=88=87=E6=8D=A2=E4=B8=BA=20Go=20=E6=A0=87=E5=87=86?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E5=A2=9E=E5=BC=BA=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E4=BC=A0=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index 5383cdc..3e5378d 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -31,9 +31,11 @@ - **Breaking(v0.5.0)**:服务模板日志从 logrus 切换为 Go 标准库 `log/slog`。 1. `Microservice`、`IndependentCfg`、flow client 与 MCP registrar 的 logger 类型改为 `*slog.Logger`;`GetLogger`、`WithLogger`、`WithWorkflow`、`flow.NewClient` 名称保持不变 - 2. RPC、MCP 和 shutdown 日志改用 slog Context API;模板改调 `HTTPHandlerFrontendContext`、`DeregisterContext`,调用链已有的 ctx 会继续向下传递 - 3. `NewMicroservice` 和 `IndependentCfg.Init` 新增 ctx 参数;旧生成项目可按迁移文档机械修改 - 4. 新模板固定 `github.com/grpc-kit/pkg v0.5.0` 并移除 logrus 直接依赖 + 2. RPC、MCP 和 shutdown 日志改用 slog Context API;`LocalConfig.HTTPHandlerFrontend`、`Deregister` 直接接收 ctx,调用链已有的 ctx 会继续向下传递 + 3. `NewMicroservice` 和 `IndependentCfg.Init` 新增 ctx 参数,基础配置初始化改调 `LocalConfig.Init(ctx)`;服务注册经 `sd.Register(ctx, ...)` 继续传递启动 ctx + 4. **Breaking(v0.5.0)**:移除临时的 `InitContext`、`DeregisterContext`、`HTTPHandlerFrontendContext` 和 `sd.RegisterContext` 双入口,旧生成项目需为原方法调用补充 ctx;直接实现 `sd.Registry` 的代码需迁移到 `Deregister(ctx)` + 5. **Breaking(v0.5.0)**:`errs.Status.WithLogger` 直接增加 ctx 首参并删除 `WithLoggerContext`;自定义业务调用需迁移为 `WithLogger(ctx, logger, format, err)` + 6. 新模板固定 `github.com/grpc-kit/pkg v0.5.0` 并移除 logrus 直接依赖 ## [0.4.4] - 2026-09-01 From 94d29a48d129139b676dbe769014453feb2e0e6c Mon Sep 17 00:00:00 2001 From: mingqing Date: Tue, 15 Sep 2026 20:56:26 +0800 Subject: [PATCH 42/57] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20CHANGELOG-0.?= =?UTF-8?q?5=EF=BC=8C=E8=AE=B0=E5=BD=95=E6=9C=8D=E5=8A=A1=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E6=97=A5=E5=BF=97=E5=A4=84=E7=90=86=E5=92=8C=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E4=BC=A0=E9=80=92=E7=9A=84=E9=87=8D=E5=A4=A7?= =?UTF-8?q?=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.4.md | 13 ------------- CHANGELOG/CHANGELOG-0.5.md | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 CHANGELOG/CHANGELOG-0.5.md diff --git a/CHANGELOG/CHANGELOG-0.4.md b/CHANGELOG/CHANGELOG-0.4.md index 3e5378d..52cf2e8 100644 --- a/CHANGELOG/CHANGELOG-0.4.md +++ b/CHANGELOG/CHANGELOG-0.4.md @@ -24,19 +24,6 @@ 5. 新增 `test/e2e/README.md` 目录规范,含黑盒纯度约束(`test/e2e/` 禁止 import `api/`、`handler/`、`internal/`、`modeler/`)与环境变量覆盖说明;被测服务的运行配置由环境提供(默认 `config/app-dev-local.yaml`),E2E 自身不携带配置 6. 新增 `make test-e2e` 目标,`make lint` 增加 `go vet -tags=e2e ./test/e2e/...`,GitLab CI 增加手动触发的 `e2e-tests` 作业(配置路径由 `E2E_CONFIG_FILE` 指定) -### Changed - -#### grpc-kit/cli 模块 - -- **Breaking(v0.5.0)**:服务模板日志从 logrus 切换为 Go 标准库 `log/slog`。 - - 1. `Microservice`、`IndependentCfg`、flow client 与 MCP registrar 的 logger 类型改为 `*slog.Logger`;`GetLogger`、`WithLogger`、`WithWorkflow`、`flow.NewClient` 名称保持不变 - 2. RPC、MCP 和 shutdown 日志改用 slog Context API;`LocalConfig.HTTPHandlerFrontend`、`Deregister` 直接接收 ctx,调用链已有的 ctx 会继续向下传递 - 3. `NewMicroservice` 和 `IndependentCfg.Init` 新增 ctx 参数,基础配置初始化改调 `LocalConfig.Init(ctx)`;服务注册经 `sd.Register(ctx, ...)` 继续传递启动 ctx - 4. **Breaking(v0.5.0)**:移除临时的 `InitContext`、`DeregisterContext`、`HTTPHandlerFrontendContext` 和 `sd.RegisterContext` 双入口,旧生成项目需为原方法调用补充 ctx;直接实现 `sd.Registry` 的代码需迁移到 `Deregister(ctx)` - 5. **Breaking(v0.5.0)**:`errs.Status.WithLogger` 直接增加 ctx 首参并删除 `WithLoggerContext`;自定义业务调用需迁移为 `WithLogger(ctx, logger, format, err)` - 6. 新模板固定 `github.com/grpc-kit/pkg v0.5.0` 并移除 logrus 直接依赖 - ## [0.4.4] - 2026-09-01 ### Added diff --git a/CHANGELOG/CHANGELOG-0.5.md b/CHANGELOG/CHANGELOG-0.5.md new file mode 100644 index 0000000..7bb8123 --- /dev/null +++ b/CHANGELOG/CHANGELOG-0.5.md @@ -0,0 +1,40 @@ +# CHANGELOG-0.5 + +| 名称 | 说明 | +|------------|------------------------| +| Added | 添加新功能 | +| Changed | 功能的变更 | +| Deprecated | 未来会删除 | +| Removed | 之前为Deprecated状态,此版本被移除 | +| Fixed | 功能的修复 | +| Security | 有关安全问题的修复 | + +## [Unreleased] + +### Changed + +#### grpc-kit/cli 模块 + +- **Breaking**:服务模板日志从 logrus 切换为 Go 标准库 `log/slog`。 + + 1. `Microservice`、`IndependentCfg`、flow client 与 MCP registrar 的 logger 类型改为 `*slog.Logger`;`GetLogger`、`WithLogger`、`WithWorkflow`、`flow.NewClient` 等 API 名称保持不变 + 2. RPC、MCP 和 shutdown 日志改用 slog Context API;新模板不再直接依赖 `github.com/sirupsen/logrus` + 3. 新模板固定使用 `github.com/grpc-kit/pkg v0.5.0` + +- **Breaking**:初始化、注册、注销和服务启动链路统一直接传递 `context.Context`。 + + 1. `NewMicroservice(ctx, ...)`、`IndependentCfg.Init(ctx, ...)` 和 `LocalConfig.Init(ctx)` 直接接收启动上下文 + 2. `LocalConfig.HTTPHandlerFrontend(ctx, ...)`、`sd.Register(ctx, ...)`、`LocalConfig.Deregister(ctx)` 与 `sd.Registry.Deregister(ctx)` 直接接收调用方上下文 + 3. `rpc.Server.StartBackground(ctx)` 直接接收启动上下文,使启动等待可以响应取消;旧生成项目中的 `StartBackground()` 调用需补充 ctx + 4. 移除临时的 `InitContext`、`DeregisterContext`、`HTTPHandlerFrontendContext` 和 `sd.RegisterContext` 双入口,不保留无 ctx 兼容包装 + +- **Breaking**:`errs.Status.WithLogger` 增加 ctx 首参并删除 `WithLoggerContext`;自定义业务调用需迁移为 `WithLogger(ctx, logger, format, err)`。 + +#### 旧生成项目迁移 + +升级到 `github.com/grpc-kit/pkg v0.5.0` 时,旧项目需要同步完成以下机械迁移: + +1. 将 logger 类型和构造逻辑从 logrus 改为 `*slog.Logger` +2. 为 `NewMicroservice`、`Init`、`HTTPHandlerFrontend`、`sd.Register`、`Deregister`、`StartBackground` 和 `WithLogger` 调用补充已有 ctx +3. 删除对临时 `*Context` 方法及 logrus 兼容入口的调用 +4. 运行代码生成、单元测试与构建,确认自定义 handler、注册流程和关闭流程均已适配新签名 From cc1eba2cc738c7c89a86ea0e323ff37841297cba Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 00:04:04 +0800 Subject: [PATCH 43/57] =?UTF-8?q?feat:=20=E6=9C=8D=E5=8A=A1=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=20go=20=E6=8C=87=E4=BB=A4=E6=8F=90=E5=8D=87=E8=87=B3?= =?UTF-8?q?=201.25.13=20=E5=B9=B6=E5=90=8C=E6=AD=A5=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pkg v0.5.0 的 go 指令已随安全补丁提升至 1.25.13,模板必须同步, 否则生成项目无法解析 pkg 依赖;模板测试断言与 CHANGELOG-0.5 同步更新。 Co-Authored-By: Claude Code --- CHANGELOG/CHANGELOG-0.5.md | 2 ++ template/service/go.mod.tmpl | 2 +- template/service_test.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG/CHANGELOG-0.5.md b/CHANGELOG/CHANGELOG-0.5.md index 7bb8123..acba32b 100644 --- a/CHANGELOG/CHANGELOG-0.5.md +++ b/CHANGELOG/CHANGELOG-0.5.md @@ -30,6 +30,8 @@ - **Breaking**:`errs.Status.WithLogger` 增加 ctx 首参并删除 `WithLoggerContext`;自定义业务调用需迁移为 `WithLogger(ctx, logger, format, err)`。 +- **Changed**:服务模板与 `pkg v0.5.0` 的 `go` 指令提升至 `1.25.13`(随安全补丁升级 grpc v1.83.1、x/net v0.56.0、x/text v0.39.0 并修复 Go 标准库已知漏洞);生成与升级项目需使用 Go 1.25.13 及以上版本构建。 + #### 旧生成项目迁移 升级到 `github.com/grpc-kit/pkg v0.5.0` 时,旧项目需要同步完成以下机械迁移: diff --git a/template/service/go.mod.tmpl b/template/service/go.mod.tmpl index 7962ecd..2e00731 100644 --- a/template/service/go.mod.tmpl +++ b/template/service/go.mod.tmpl @@ -1,6 +1,6 @@ module {{ .Global.Repository }} -go 1.25.0 +go 1.25.13 require ( entgo.io/ent v0.14.5 diff --git a/template/service_test.go b/template/service_test.go index b9fa9e3..ceea9bc 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -95,7 +95,7 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { assertContains("modeler/mcp/registrar.go", "func (r *Registrar) Register", "server is nil") assertContains("modeler/mcp/registrar_test.go", "session.CallTool", "session.ReadResource", "session.GetPrompt") assertContains("config/app-dev-local.yaml", "aiconnector:", "mcp_server:") - assertContains("go.mod", "go 1.25.0", "github.com/grpc-kit/pkg v0.5.0", "github.com/modelcontextprotocol/go-sdk v1.7.0") + assertContains("go.mod", "go 1.25.13", "github.com/grpc-kit/pkg v0.5.0", "github.com/modelcontextprotocol/go-sdk v1.7.0") assertNotContains("go.mod", "github.com/sirupsen/logrus") assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") From 6ec2c9a32aef39572a86cc318c91846e9194e239 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 11:59:23 +0800 Subject: [PATCH 44/57] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A8=A1=E6=9D=BF=20public/embed.go=20=E9=9D=99?= =?UTF-8?q?=E6=80=81=E8=B5=84=E6=BA=90=E5=B5=8C=E5=85=A5=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E5=85=85=E6=B8=B2=E6=9F=93=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- template/service/public/{embed.go => embed.go.tmpl} | 2 +- template/service_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) rename template/service/public/{embed.go => embed.go.tmpl} (94%) diff --git a/template/service/public/embed.go b/template/service/public/embed.go.tmpl similarity index 94% rename from template/service/public/embed.go rename to template/service/public/embed.go.tmpl index de6caab..858f731 100644 --- a/template/service/public/embed.go +++ b/template/service/public/embed.go.tmpl @@ -3,7 +3,7 @@ package public import ( - "embed" + "embed" ) //go:embed admin/* diff --git a/template/service_test.go b/template/service_test.go index ceea9bc..7e8886d 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -81,6 +81,8 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { } assertContains("handler/register.go", "DO NOT EDIT", "privateMCPHandle()", "HTTPHandlerFrontend(ctx", "StartBackground(ctx)") assertNotContains("handler/register.go", "HTTPHandlerFrontendContext(ctx", "StartBackground()") + assertContains("public/embed.go", `// Code generated by "grpc-kit-cli/0.0.1-test". DO NOT EDIT.`, "var Assets embed.FS") + assertNotContains("public/embed.go", "{{", "}}", ".Global.ReleaseVersion") assertContains("handler/private.go", "func (m *Microservice) privateMCPHandle() error", "return fmt.Errorf") assertContains("handler/microservice.go", `"log/slog"`, "logger *slog.Logger", "NewMicroservice(ctx context.Context", "lc.Init(ctx)") assertNotContains("handler/microservice.go", "lc.InitContext(ctx)") From 9a94ff97724298c414bc2edef01f319f08933894 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 16:56:31 +0800 Subject: [PATCH 45/57] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20project=20mi?= =?UTF-8?q?grate=20=E5=91=BD=E4=BB=A4=E5=AE=9E=E7=8E=B0=E5=AD=98=E9=87=8F?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=89=98=E7=AE=A1=E6=96=87=E4=BB=B6=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 internal/projectmigrate 包:迁移计划构建、diff 预览、原子写入与失败回滚、手动操作诊断及冻结兼容模板资产 - 重构 cmd 层为构造函数注册模式,Execute 返回错误以支持非零退出码,并按需加载用户配置 - 移除废弃的 update 命令 --- cmd/new.go | 27 +- cmd/project.go | 28 ++ cmd/project_migrate.go | 79 +++++ cmd/project_migrate_test.go | 166 ++++++++++ cmd/root.go | 88 ++++-- cmd/root_test.go | 72 +++++ cmd/update.go | 49 --- internal/projectmigrate/apply.go | 184 +++++++++++ internal/projectmigrate/apply_test.go | 183 +++++++++++ internal/projectmigrate/assets.go | 132 ++++++++ .../v0_5_0/v0_3_8/cmd/server/main.go.tmpl | 77 +++++ .../v0_3_8/handler/microservice.go.tmpl | 68 +++++ .../v0_5_0/v0_3_8/handler/register.go.tmpl | 52 ++++ .../v0_3_8/handler/rpc_internal.go.tmpl | 21 ++ .../v0_5_0/v0_3_8/handler/shutdown.go.tmpl | 29 ++ .../assets/v0_5_0/v0_3_8/scripts/env.tmpl | 21 ++ internal/projectmigrate/assets_test.go | 126 ++++++++ internal/projectmigrate/compatibility_test.go | 176 +++++++++++ internal/projectmigrate/diagnostics.go | 274 +++++++++++++++++ internal/projectmigrate/diagnostics_test.go | 119 ++++++++ internal/projectmigrate/git.go | 67 ++++ internal/projectmigrate/marker.go | 75 +++++ internal/projectmigrate/marker_test.go | 58 ++++ internal/projectmigrate/output.go | 84 ++++++ internal/projectmigrate/plan.go | 285 ++++++++++++++++++ internal/projectmigrate/plan_test.go | 156 ++++++++++ internal/projectmigrate/project.go | 223 ++++++++++++++ internal/projectmigrate/project_test.go | 114 +++++++ internal/projectmigrate/source_fixture.go | 94 ++++++ .../projectmigrate/source_fixture_test.go | 51 ++++ internal/projectmigrate/target.go | 22 ++ internal/projectmigrate/version.go | 68 +++++ internal/projectmigrate/version_test.go | 32 ++ 33 files changed, 3210 insertions(+), 90 deletions(-) create mode 100644 cmd/project.go create mode 100644 cmd/project_migrate.go create mode 100644 cmd/project_migrate_test.go create mode 100644 cmd/root_test.go delete mode 100644 cmd/update.go create mode 100644 internal/projectmigrate/apply.go create mode 100644 internal/projectmigrate/apply_test.go create mode 100644 internal/projectmigrate/assets.go create mode 100644 internal/projectmigrate/assets/v0_5_0/v0_3_8/cmd/server/main.go.tmpl create mode 100644 internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/microservice.go.tmpl create mode 100644 internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/register.go.tmpl create mode 100644 internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/rpc_internal.go.tmpl create mode 100644 internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/shutdown.go.tmpl create mode 100644 internal/projectmigrate/assets/v0_5_0/v0_3_8/scripts/env.tmpl create mode 100644 internal/projectmigrate/assets_test.go create mode 100644 internal/projectmigrate/compatibility_test.go create mode 100644 internal/projectmigrate/diagnostics.go create mode 100644 internal/projectmigrate/diagnostics_test.go create mode 100644 internal/projectmigrate/git.go create mode 100644 internal/projectmigrate/marker.go create mode 100644 internal/projectmigrate/marker_test.go create mode 100644 internal/projectmigrate/output.go create mode 100644 internal/projectmigrate/plan.go create mode 100644 internal/projectmigrate/plan_test.go create mode 100644 internal/projectmigrate/project.go create mode 100644 internal/projectmigrate/project_test.go create mode 100644 internal/projectmigrate/source_fixture.go create mode 100644 internal/projectmigrate/source_fixture_test.go create mode 100644 internal/projectmigrate/target.go create mode 100644 internal/projectmigrate/version.go create mode 100644 internal/projectmigrate/version_test.go diff --git a/cmd/new.go b/cmd/new.go index 8e80dfc..c979377 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -24,28 +24,27 @@ import ( "github.com/spf13/cobra" ) -// newCmd represents the new command -var newCmd = &cobra.Command{ - Use: "new", - Short: "Create a new code templates for your product", - Long: `Create a new code templates for your product. It will only be used when +func newNewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "new", + Short: "Create a new code templates for your product", + Long: `Create a new code templates for your product. It will only be used when it is newly created. For example: ./grpc-kit-cli new -t service -o default -p opsaid -s test1 `, - RunE: runFuncNew, - SilenceUsage: true, - SilenceErrors: false, -} - -func init() { - rootCmd.AddCommand(newCmd) + RunE: runFuncNew, + SilenceUsage: true, + SilenceErrors: false, + } // 只在该command下生效的参数 - newCmd.Flags().StringVar(&cfgType.Template.Service.APIVersion, + cmd.Flags().StringVar(&cfgType.Template.Service.APIVersion, "api-version", "v1", "api version, like: v1alpha1, v1beta1, v1") - newCmd.Flags().StringVarP(&cfgType.Template.Service.Organization, + cmd.Flags().StringVarP(&cfgType.Template.Service.Organization, "organization", "o", "grpc-kit", "the company or department where the product is located") + + return cmd } func runFuncNew(cmd *cobra.Command, args []string) error { diff --git a/cmd/project.go b/cmd/project.go new file mode 100644 index 0000000..5edba64 --- /dev/null +++ b/cmd/project.go @@ -0,0 +1,28 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import "github.com/spf13/cobra" + +func newProjectCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "project", + Short: "Inspect and maintain an existing grpc-kit project", + Args: cobra.NoArgs, + Annotations: map[string]string{skipUserConfigAnnotation: "true"}, + } + cmd.AddCommand(newProjectMigrateCommand()) + return cmd +} diff --git a/cmd/project_migrate.go b/cmd/project_migrate.go new file mode 100644 index 0000000..ae64968 --- /dev/null +++ b/cmd/project_migrate.go @@ -0,0 +1,79 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/grpc-kit/cli/internal/projectmigrate" + "github.com/grpc-kit/pkg/vars" + "github.com/spf13/cobra" +) + +type projectMigrateOptions struct { + apply bool +} + +func newProjectMigrateCommand() *cobra.Command { + options := projectMigrateOptions{} + cmd := &cobra.Command{ + Use: "migrate [path]", + Short: "Preview or apply managed-file migrations for an existing project", + Args: cobra.MaximumNArgs(1), + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + projectPath := "." + if len(args) == 1 { + projectPath = args[0] + } + + if options.apply { + if _, err := projectmigrate.ValidateTargetCLIVersion(vars.ReleaseVersion); err != nil { + return fmt.Errorf("refuse --apply: %w", err) + } + } + + return runFuncProjectMigrate(cmd, projectPath, options) + }, + } + cmd.Flags().BoolVar(&options.apply, "apply", false, "apply the managed-file migration plan") + return cmd +} + +func runFuncProjectMigrate(cmd *cobra.Command, projectPath string, options projectMigrateOptions) error { + plan, err := projectmigrate.BuildPlan(projectPath, vars.ReleaseVersion) + if err != nil { + return err + } + if err := projectmigrate.WritePlan(cmd.OutOrStdout(), plan); err != nil { + return fmt.Errorf("write migration preview: %w", err) + } + if plan.Blocked() { + return fmt.Errorf("migration plan is %s", plan.Status) + } + if !options.apply { + return nil + } + if err := projectmigrate.Apply(cmd.Context(), &plan); err != nil { + return err + } + if _, err = fmt.Fprintf(cmd.OutOrStdout(), "Managed files migrated: %d\n", len(plan.Changes)); err != nil { + return err + } + if len(plan.ManualActions) != 0 { + _, err = fmt.Fprintln(cmd.OutOrStdout(), "Managed files migrated; project compatibility is not yet verified.") + } + return err +} diff --git a/cmd/project_migrate_test.go b/cmd/project_migrate_test.go new file mode 100644 index 0000000..63125b0 --- /dev/null +++ b/cmd/project_migrate_test.go @@ -0,0 +1,166 @@ +package cmd + +import ( + "bytes" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/grpc-kit/pkg/vars" +) + +func TestProjectMigrateRejectsExtraArguments(t *testing.T) { + cmd := newRootCommand(func() error { return nil }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", "one", "two"}) + + if err := cmd.Execute(); err == nil { + t.Fatal("Execute() error = nil, want an argument validation error") + } +} + +func TestProjectMigratePreviewAllowsPrereleaseVersion(t *testing.T) { + previous := vars.ReleaseVersion + vars.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { vars.ReleaseVersion = previous }) + + cmd := newRootCommand(func() error { return nil }) + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", writeCommandTestProject(t)}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if !strings.Contains(output.String(), "Status: ready") || !strings.Contains(output.String(), "Apply blocked [release_version]") { + t.Fatalf("output = %q", output.String()) + } +} + +func TestProjectMigrateRejectsInvalidProject(t *testing.T) { + previous := vars.ReleaseVersion + vars.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { vars.ReleaseVersion = previous }) + + cmd := newRootCommand(func() error { return nil }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", t.TempDir()}) + + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), "invalid project") { + t.Fatalf("Execute() error = %v, want invalid project", err) + } +} + +func TestProjectMigrateRejectsUnsupportedSource(t *testing.T) { + previous := vars.ReleaseVersion + vars.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { vars.ReleaseVersion = previous }) + + root := writeCommandTestProject(t) + environmentPath := filepath.Join(root, "scripts", "env") + body, err := os.ReadFile(environmentPath) + if err != nil { + t.Fatal(err) + } + body = []byte(strings.ReplaceAll(string(body), "0.3.9-beta.1", "0.4.0")) + if err := os.WriteFile(environmentPath, body, 0o600); err != nil { + t.Fatal(err) + } + + cmd := newRootCommand(func() error { return nil }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", root}) + + err = cmd.Execute() + if err == nil || !strings.Contains(err.Error(), "migration plan is unsupported") { + t.Fatalf("Execute() error = %v, want unsupported source", err) + } +} + +func TestProjectMigrateApplyRejectsPrereleaseBuild(t *testing.T) { + previous := vars.ReleaseVersion + vars.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { vars.ReleaseVersion = previous }) + + cmd := newRootCommand(func() error { return nil }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", "--apply"}) + + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), "refuse --apply") { + t.Fatalf("Execute() error = %v, want prerelease apply refusal", err) + } +} + +func TestProjectMigrateApply(t *testing.T) { + previous := vars.ReleaseVersion + vars.ReleaseVersion = "0.4.0" + t.Cleanup(func() { vars.ReleaseVersion = previous }) + + root := writeCommandTestProject(t) + for _, args := range [][]string{ + {"init"}, + {"add", "."}, + {"-c", "user.name=grpc-kit-test", "-c", "user.email=test@grpc-kit.invalid", "commit", "-m", "fixture"}, + } { + command := exec.Command("git", append([]string{"-C", root}, args...)...) + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", args, err, output) + } + } + + cmd := newRootCommand(func() error { return nil }) + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", root, "--apply"}) + + 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") { + t.Fatalf("output = %q", output.String()) + } + if !strings.Contains(output.String(), "Managed files migrated; project compatibility is not yet verified.") { + t.Fatalf("output does not explain compatibility status: %q", output.String()) + } + environment, err := os.ReadFile(filepath.Join(root, "scripts", "env")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(environment), "CLI_VERSION=0.4.0") { + t.Fatalf("scripts/env was not migrated:\n%s", environment) + } +} + +func writeCommandTestProject(t *testing.T) string { + t.Helper() + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "scripts"), 0o755); err != nil { + t.Fatal(err) + } + environment := `# Code generated by "grpc-kit-cli/0.3.9-beta.1". DO NOT EDIT. +CLI_VERSION=0.3.9-beta.1 +PRODUCT_CODE=oneops +SHORT_NAME=spider +API_VERSION=v1 +APPNAME=${PRODUCT_CODE}-${SHORT_NAME}-${API_VERSION} +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) + } + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module git.lmq.io/kaopuvm/spider\n"), 0o600); err != nil { + t.Fatal(err) + } + return root +} diff --git a/cmd/root.go b/cmd/root.go index 337003f..b7b5979 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,7 +16,6 @@ package cmd import ( "fmt" - "os" "github.com/grpc-kit/cli/config" homedir "github.com/mitchellh/go-homedir" @@ -27,32 +26,42 @@ import ( var cfgFile string var cfgType config.Config -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "grpc-kit-cli", - Short: "grpc-kit-cli is the main command, used to build your product easy.", - Long: `gRPC Kit Cli is used to quickly generate code templates for the same product -to comply with the same specifications. +const skipUserConfigAnnotation = "grpc-kit-cli/skip-user-config" -Complete documentation is available at https://grpc-kit.com/. -`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, +// Execute runs the CLI and returns command errors to main so they produce a +// non-zero process exit code. +func Execute() error { + return NewRootCommand().Execute() } -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - // fmt.Println(err) - // os.Exit(1) - } +// NewRootCommand constructs an isolated command tree for execution and tests. +func NewRootCommand() *cobra.Command { + cfgFile = "" + cfgType = config.Config{} + viper.Reset() + + return newRootCommand(loadUserConfig) } -func init() { - // 注册方法,可在command的Execute方法执行前(preRun)调用 - cobra.OnInitialize(initConfig, unmarshalConfig) +func newRootCommand(loadConfig func() error) *cobra.Command { + rootCmd := &cobra.Command{ + Use: "grpc-kit-cli", + Short: "grpc-kit-cli is the main command, used to build your product easy.", + Long: `gRPC Kit Cli is used to quickly generate code templates for the same product +to comply with the same specifications. + +Complete documentation is available at https://grpc-kit.com/. +`, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if skipsUserConfig(cmd) { + return nil + } + return loadConfig() + }, + } // 全局参数设置,也就是可在其他command中生效 rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file, priority higher than flags (default \"$HOME/.grpc-kit-cli.yaml\")") @@ -62,10 +71,32 @@ func init() { rootCmd.PersistentFlags().StringVarP(&cfgType.Global.ProductCode, "product-code", "p", "", "the product code (must match regex \"^([a-z0-9]){4,}$\")") rootCmd.PersistentFlags().StringVarP(&cfgType.Global.ShortName, "short-name", "s", "", "application name under the product (must match regex \"^([a-z0-9]){4,}$\")") rootCmd.PersistentFlags().StringVarP(&cfgType.Global.Repository, "repository", "r", "git-domain/product-code/short-name", "name to use for go module (e.g., github.com/user/repo)") + + rootCmd.AddCommand(newNewCommand()) + rootCmd.AddCommand(newVersionCommand()) + rootCmd.AddCommand(newProjectCommand()) + + return rootCmd +} + +func skipsUserConfig(cmd *cobra.Command) bool { + for current := cmd; current != nil; current = current.Parent() { + if current.Annotations[skipUserConfigAnnotation] == "true" { + return true + } + } + return false +} + +func loadUserConfig() error { + if err := initConfig(); err != nil { + return err + } + return unmarshalConfig() } // initConfig reads in config file and ENV variables if set. -func initConfig() { +func initConfig() error { if cfgFile != "" { // Use config file from the flag. viper.SetConfigFile(cfgFile) @@ -73,8 +104,7 @@ func initConfig() { // Find home directory. home, err := homedir.Dir() if err != nil { - fmt.Println(err) - os.Exit(1) + return fmt.Errorf("find home directory: %w", err) } // Search config in home directory with name ".cli" (without extension). @@ -88,17 +118,21 @@ func initConfig() { if err := viper.ReadInConfig(); err == nil { fmt.Println("Using config file:", viper.ConfigFileUsed()) } + + return nil } -func unmarshalConfig() { +func unmarshalConfig() error { // 优先级以用户在 cli 上指定的值最高 productCode := cfgType.Global.ProductCode if err := viper.Unmarshal(&cfgType); err != nil { - // return err + return fmt.Errorf("unmarshal config: %w", err) } if productCode != "" { cfgType.Global.ProductCode = productCode } + + return nil } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..61c9520 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "errors" + "io" + "testing" + + "github.com/grpc-kit/pkg/vars" +) + +func TestRootCommandReturnsCommandErrors(t *testing.T) { + cmd := newRootCommand(func() error { return nil }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"does-not-exist"}) + + if err := cmd.Execute(); err == nil { + t.Fatal("Execute() error = nil, want an unknown-command error") + } +} + +func TestProjectCommandsSkipUserConfig(t *testing.T) { + previous := vars.ReleaseVersion + vars.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { vars.ReleaseVersion = previous }) + + configErr := errors.New("user config must not be loaded") + loaded := false + cmd := newRootCommand(func() error { + loaded = true + return configErr + }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"project", "migrate", writeCommandTestProject(t)}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if loaded { + t.Fatal("project migrate loaded user generation config") + } +} + +func TestGenerationCommandsLoadUserConfig(t *testing.T) { + configErr := errors.New("config sentinel") + loaded := false + cmd := newRootCommand(func() error { + loaded = true + return configErr + }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"new"}) + + err := cmd.Execute() + if !errors.Is(err, configErr) { + t.Fatalf("Execute() error = %v, want %v", err, configErr) + } + if !loaded { + t.Fatal("new command did not load user generation config") + } +} + +func TestRootCommandDoesNotRegisterAmbiguousUpgradeAliases(t *testing.T) { + cmd := newRootCommand(func() error { return nil }) + for _, child := range cmd.Commands() { + if child.Name() == "update" || child.Name() == "upgrade" { + t.Fatalf("unexpected root command %q", child.Name()) + } + } +} diff --git a/cmd/update.go b/cmd/update.go deleted file mode 100644 index cadeb09..0000000 --- a/cmd/update.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright © 2024 The gRPC Kit Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "bytes" - "fmt" - "os" - - "github.com/spf13/cobra" -) - -// updateCmd represents the version command -var updateCmd = &cobra.Command{ - Use: "update", - Short: "Update the application code template", - Long: `Updating the application code template generated by grpc-kit-cli to the current version.`, - RunE: runFuncUpdate, -} - -func init() { - // rootCmd.AddCommand(updateCmd) -} - -func runFuncUpdate(cmd *cobra.Command, args []string) error { - // 遍历当前目录所有文件 - // 获取当前目录下的 "scripts/env" 文件 - content, err := os.ReadFile("./scripts/env") - if err != nil { - return err - } - - lines := bytes.Split(content, []byte("\n")) - fmt.Println(string(lines[0])) - - return nil -} diff --git a/internal/projectmigrate/apply.go b/internal/projectmigrate/apply.go new file mode 100644 index 0000000..cf814c5 --- /dev/null +++ b/internal/projectmigrate/apply.go @@ -0,0 +1,184 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "bytes" + "context" + "crypto/sha256" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" +) + +type atomicWriteFunc func(path string, body []byte, mode fs.FileMode) error + +// Apply writes exactly the changes in plan after revalidating Git state and +// every input digest. A failure rolls back files written by this invocation. +func Apply(ctx context.Context, plan *Plan) error { + if plan == nil { + return fmt.Errorf("plan is nil") + } + if plan.Blocked() { + return fmt.Errorf("migration plan is blocked by conflicts") + } + if len(plan.Changes) == 0 { + plan.Status = StatusManagedUpToDate + return nil + } + if _, err := ValidateTargetCLIVersion(plan.TargetCLIVersion); err != nil { + return fmt.Errorf("refuse apply: %w", err) + } + if err := ensureGitWorktree(ctx, plan.Project.Root); err != nil { + return err + } + status, err := gitOutput(ctx, plan.Project.Root, "status", "--porcelain", "--untracked-files=normal", "--", ".") + if err != nil { + return err + } + if len(bytes.TrimSpace(status)) != 0 { + return fmt.Errorf("project worktree is not clean") + } + for _, change := range plan.Changes { + if err := ensureGitTracked(ctx, plan.Project.Root, change.Path); err != nil { + return err + } + if err := verifyChangeInput(plan.Project.Root, change); err != nil { + return err + } + } + if err := applyChanges(plan.Project.Root, plan.Changes, writeFileAtomic); err != nil { + return err + } + plan.Status = StatusApplied + return nil +} + +func applyChanges(root string, changes []Change, writer atomicWriteFunc) error { + applied := make([]Change, 0, len(changes)) + for _, change := range changes { + absolutePath, err := projectFilePath(root, change.Path) + if err != nil { + return rollbackChanges(root, applied, writer, err) + } + if err := writer(absolutePath, change.After, change.Mode); err != nil { + return rollbackChanges(root, applied, writer, fmt.Errorf("write %s: %w", change.Path, err)) + } + applied = append(applied, change) + written, err := readRegularFile(absolutePath, maxManagedFileSize) + if err != nil || !bytes.Equal(written, change.After) { + if err == nil { + err = fmt.Errorf("written content did not match the plan") + } + return rollbackChanges(root, applied, writer, fmt.Errorf("verify written file %s: %w", change.Path, err)) + } + } + return nil +} + +func rollbackChanges(root string, applied []Change, writer atomicWriteFunc, cause error) error { + var rollbackErrs []error + for index := len(applied) - 1; index >= 0; index-- { + change := applied[index] + absolutePath, err := projectFilePath(root, change.Path) + if err == nil { + err = writer(absolutePath, change.Before, change.Mode) + } + if err != nil { + rollbackErrs = append(rollbackErrs, fmt.Errorf("rollback %s: %w", change.Path, err)) + } + } + if len(rollbackErrs) == 0 { + return cause + } + return errors.Join(append([]error{cause}, rollbackErrs...)...) +} + +func verifyChangeInput(root string, change Change) error { + absolutePath, err := projectFilePath(root, change.Path) + if err != nil { + return err + } + body, err := readRegularFile(absolutePath, maxManagedFileSize) + if err != nil { + return fmt.Errorf("revalidate %s: %w", change.Path, err) + } + if sha256.Sum256(body) != change.BeforeSHA256 { + return fmt.Errorf("revalidate %s: content changed after planning", change.Path) + } + info, err := os.Lstat(absolutePath) + if err != nil { + return err + } + if info.Mode() != change.Mode { + return fmt.Errorf("revalidate %s: file mode changed after planning", change.Path) + } + return nil +} + +func projectFilePath(root, relativePath string) (string, error) { + if relativePath == "" || filepath.IsAbs(relativePath) { + return "", fmt.Errorf("unsafe project path %q", relativePath) + } + cleanRelative := filepath.Clean(filepath.FromSlash(relativePath)) + if cleanRelative == "." || cleanRelative == ".." || len(cleanRelative) >= 3 && cleanRelative[:3] == ".."+string(filepath.Separator) { + return "", fmt.Errorf("unsafe project path %q", relativePath) + } + absolutePath := filepath.Join(root, cleanRelative) + withinRoot, err := filepath.Rel(root, absolutePath) + if err != nil || withinRoot == ".." || len(withinRoot) >= 3 && withinRoot[:3] == ".."+string(filepath.Separator) { + return "", fmt.Errorf("unsafe project path %q", relativePath) + } + return absolutePath, nil +} + +func writeFileAtomic(path string, body []byte, mode fs.FileMode) (returnErr error) { + directory := filepath.Dir(path) + temporary, err := os.CreateTemp(directory, ".grpc-kit-migrate-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + closed := false + defer func() { + if !closed { + if closeErr := temporary.Close(); returnErr == nil && closeErr != nil { + returnErr = closeErr + } + } + if removeErr := os.Remove(temporaryPath); returnErr == nil && removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) { + returnErr = removeErr + } + }() + if err := temporary.Chmod(mode.Perm()); err != nil { + return err + } + if _, err := temporary.Write(body); err != nil { + return err + } + if err := temporary.Sync(); err != nil { + return err + } + if err := temporary.Close(); err != nil { + return err + } + closed = true + if err := os.Rename(temporaryPath, path); err != nil { + return err + } + return nil +} diff --git a/internal/projectmigrate/apply_test.go b/internal/projectmigrate/apply_test.go new file mode 100644 index 0000000..cf05ac3 --- /dev/null +++ b/internal/projectmigrate/apply_test.go @@ -0,0 +1,183 @@ +package projectmigrate + +import ( + "bytes" + "context" + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestApplyAndReplanAreIdempotent(t *testing.T) { + root := writePlanProject(t, true) + 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 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) + registerBody, err := os.ReadFile(filepath.Join(root, "handler", "register.go")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(registerBody), "privateMCPHandle") || !strings.Contains(string(registerBody), "StartBackground(ctx)") { + t.Fatalf("unexpected register.go:\n%s", registerBody) + } + info, err := os.Stat(filepath.Join(root, "handler", "register.go")) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o640 { + t.Fatalf("register.go mode = %o, want 640", 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 !hasDiagnostic(second.ApplyBlockers, "dirty_worktree", "") { + t.Fatalf("second plan blockers = %#v", second.ApplyBlockers) + } + if err := Apply(context.Background(), &second); err != nil { + t.Fatalf("idempotent Apply() error = %v", err) + } +} + +type snapshotEntry struct { + body []byte + mode os.FileMode +} + +func snapshotProject(t *testing.T, root string) map[string]snapshotEntry { + t.Helper() + snapshot := make(map[string]snapshotEntry) + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relativePath, err := filepath.Rel(root, path) + if err != nil { + return err + } + if entry.IsDir() { + if entry.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + body, err := os.ReadFile(path) + if err != nil { + return err + } + info, err := entry.Info() + if err != nil { + return err + } + snapshot[filepath.ToSlash(relativePath)] = snapshotEntry{body: body, mode: info.Mode()} + return nil + }) + if err != nil { + t.Fatal(err) + } + return snapshot +} + +func assertOnlyPlannedChanges(t *testing.T, before, after map[string]snapshotEntry, changes []Change) { + t.Helper() + if len(before) != len(after) { + t.Fatalf("file count changed: before=%d after=%d", len(before), len(after)) + } + changeSet := make(map[string]Change, len(changes)) + for _, change := range changes { + changeSet[change.Path] = change + } + for path, beforeEntry := range before { + afterEntry, exists := after[path] + if !exists { + t.Fatalf("file %s was deleted", path) + } + change, planned := changeSet[path] + if planned { + if !bytes.Equal(afterEntry.body, change.After) { + t.Errorf("file %s does not equal planned output", path) + } + } else if !bytes.Equal(beforeEntry.body, afterEntry.body) { + t.Errorf("unplanned file %s changed", path) + } + if beforeEntry.mode != afterEntry.mode { + t.Errorf("file %s mode changed from %v to %v", path, beforeEntry.mode, afterEntry.mode) + } + } +} + +func TestApplyChangesRollsBackOnFailure(t *testing.T) { + root := t.TempDir() + firstPath := filepath.Join(root, "first.txt") + secondPath := filepath.Join(root, "second.txt") + writeFile(t, firstPath, []byte("first-before"), 0o600) + writeFile(t, secondPath, []byte("second-before"), 0o600) + changes := []Change{ + {Path: "first.txt", Before: []byte("first-before"), After: []byte("first-after"), Mode: 0o600}, + {Path: "second.txt", Before: []byte("second-before"), After: []byte("second-after"), Mode: 0o600}, + } + calls := 0 + writer := func(path string, body []byte, mode os.FileMode) error { + calls++ + if calls == 2 { + return errors.New("injected failure") + } + return os.WriteFile(path, body, mode) + } + if err := applyChanges(root, changes, writer); err == nil || !strings.Contains(err.Error(), "injected failure") { + t.Fatalf("applyChanges() error = %v", err) + } + first, _ := os.ReadFile(firstPath) + second, _ := os.ReadFile(secondPath) + if string(first) != "first-before" || string(second) != "second-before" { + t.Fatalf("rollback result first=%q second=%q", first, second) + } +} + +func TestVerifyChangeInputDetectsMutation(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "managed.go") + writeFile(t, path, []byte("before"), 0o600) + change := Change{Path: "managed.go", Before: []byte("before"), Mode: 0o600} + change.BeforeSHA256 = [32]byte{1} + if err := verifyChangeInput(root, change); err == nil || !strings.Contains(err.Error(), "content changed") { + t.Fatalf("verifyChangeInput() error = %v", err) + } +} + +func initGitRepository(t *testing.T, root string) { + t.Helper() + commands := [][]string{ + {"init"}, + {"add", "."}, + {"-c", "user.name=grpc-kit-test", "-c", "user.email=test@grpc-kit.invalid", "commit", "-m", "fixture"}, + } + for _, args := range commands { + cmd := exec.Command("git", append([]string{"-C", root}, args...)...) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", args, err, output) + } + } +} diff --git a/internal/projectmigrate/assets.go b/internal/projectmigrate/assets.go new file mode 100644 index 0000000..ad35087 --- /dev/null +++ b/internal/projectmigrate/assets.go @@ -0,0 +1,132 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "bytes" + "embed" + "fmt" + "go/parser" + "go/token" + "path" + "strings" + "text/template" +) + +const compatibilityAssetRoot = "assets/v0_5_0/v0_3_8" + +var compatibilityAssetPaths = []string{ + "cmd/server/main.go", + "handler/microservice.go", + "handler/register.go", + "handler/rpc_internal.go", + "handler/shutdown.go", + "scripts/env", +} + +//go:embed assets/v0_5_0/v0_3_8 +var compatibilityAssets embed.FS + +type assetData struct { + TargetCLIVersion string + Repository string + ProductCode string + ShortName string + APIVersion string + ServiceTitle string +} + +// CompatibilityAssetPaths returns the existing paths that the selected source +// family can map to pkg v0.5.0-compatible managed content. +func CompatibilityAssetPaths(sourceFamily string) ([]string, error) { + if !hasCompatibilityAssets(sourceFamily) { + return nil, fmt.Errorf("source family %q has no frozen compatibility assets", sourceFamily) + } + return append([]string(nil), compatibilityAssetPaths...), nil +} + +// RenderCompatibilityAsset renders one frozen asset entirely in memory. +func RenderCompatibilityAsset(project Project, relativePath, targetCLIVersion string) ([]byte, error) { + if !hasCompatibilityAssets(project.SourceFamily) { + return nil, fmt.Errorf("source family %q has no frozen compatibility assets", project.SourceFamily) + } + if !containsAssetPath(relativePath) { + return nil, fmt.Errorf("path %q has no compatibility asset", relativePath) + } + version, err := normalizeTargetCLIVersion(targetCLIVersion) + if err != nil { + return nil, fmt.Errorf("target CLI version: %w", err) + } + templatePath := path.Join(compatibilityAssetRoot, relativePath+".tmpl") + body, err := compatibilityAssets.ReadFile(templatePath) + if err != nil { + return nil, fmt.Errorf("read compatibility asset %s: %w", relativePath, err) + } + tmpl, err := template.New(relativePath).Option("missingkey=error").Parse(string(body)) + if err != nil { + return nil, fmt.Errorf("parse compatibility asset %s: %w", relativePath, err) + } + data := assetData{ + TargetCLIVersion: version, + Repository: project.ModulePath, + ProductCode: project.ProductCode, + ShortName: project.ShortName, + APIVersion: project.APIVersion, + ServiceTitle: identifierTitle(project.ProductCode) + identifierTitle(project.ShortName), + } + var rendered bytes.Buffer + if err := tmpl.Execute(&rendered, data); err != nil { + return nil, fmt.Errorf("render compatibility asset %s: %w", relativePath, err) + } + result := rendered.Bytes() + if bytes.Contains(result, []byte("{{")) || bytes.Contains(result, []byte("}}")) { + return nil, fmt.Errorf("render compatibility asset %s: unresolved template expression", relativePath) + } + marker, err := ParseMarkerLine(firstLine(result)) + if err != nil || marker.Version != version { + return nil, fmt.Errorf("render compatibility asset %s: invalid target marker", relativePath) + } + if strings.HasSuffix(relativePath, ".go") { + if _, err := parser.ParseFile(token.NewFileSet(), relativePath, result, parser.AllErrors); err != nil { + return nil, fmt.Errorf("render compatibility asset %s: invalid Go syntax: %w", relativePath, err) + } + } + return result, nil +} + +func hasCompatibilityAssets(sourceFamily string) bool { + return sourceFamily == "v0.3.8" || sourceFamily == "v0.3.9-beta.1" +} + +func containsAssetPath(relativePath string) bool { + for _, candidate := range compatibilityAssetPaths { + if candidate == relativePath { + return true + } + } + return false +} + +func firstLine(body []byte) string { + line, _, _ := bytes.Cut(body, []byte("\n")) + return string(line) +} + +func identifierTitle(value string) string { + if value == "" { + return "" + } + return strings.ToUpper(value[:1]) + value[1:] +} diff --git a/internal/projectmigrate/assets/v0_5_0/v0_3_8/cmd/server/main.go.tmpl b/internal/projectmigrate/assets/v0_5_0/v0_3_8/cmd/server/main.go.tmpl new file mode 100644 index 0000000..a6f5f1f --- /dev/null +++ b/internal/projectmigrate/assets/v0_5_0/v0_3_8/cmd/server/main.go.tmpl @@ -0,0 +1,77 @@ +// Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT. + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/grpc-kit/pkg/cfg" + "github.com/grpc-kit/pkg/signal" + "github.com/grpc-kit/pkg/vars" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + "{{ .Repository }}/handler" +) + +var ( + flagCfgFile *string + flagVersion *bool +) + +func init() { + flagCfgFile = flag.StringP("config", "c", "./config/app.yaml", "config file") + flagVersion = flag.BoolP("version", "v", false, "print version and exit") + flag.Parse() + + if *flagVersion { + fmt.Println(vars.GetVersion()) + os.Exit(0) + } +} + +func main() { + viper.SetConfigFile(*flagCfgFile) + + if err := viper.ReadInConfig(); err == nil { + fmt.Printf("Using config file: %v\n", *flagCfgFile) + } else { + fmt.Printf("Load config file: %v, err: %v\n", *flagCfgFile, err) + os.Exit(1) + } + + ctx := context.Background() + + m, err := startServer(ctx) + if err != nil { + fmt.Printf("Start server err: %v\n", err) + os.Exit(1) + } + + signal.WaitQuit() + + if err := m.Shutdown(ctx); err != nil { + fmt.Printf("Shutdown server err: %v\n", err) + os.Exit(1) + } +} + +func startServer(ctx context.Context) (*handler.Microservice, error) { + lc, err := cfg.New(viper.GetViper()) + if err != nil { + return nil, err + } + + m, err := handler.NewMicroservice(ctx, lc) + if err != nil { + return nil, err + } + + if err := m.Register(ctx); err != nil { + return nil, err + } + + return m, nil +} diff --git a/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/microservice.go.tmpl b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/microservice.go.tmpl new file mode 100644 index 0000000..bf5062d --- /dev/null +++ b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/microservice.go.tmpl @@ -0,0 +1,68 @@ +// Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT. + +package handler + +import ( + "context" + "log/slog" + + "github.com/grpc-kit/pkg/cfg" + "github.com/grpc-kit/pkg/rpc" + + "{{ .Repository }}/modeler" +) + +// Microservice 该微服务的结构 +type Microservice struct { + code string // 服务代码 + server *rpc.Server // 服务定义 + client *rpc.Client // 服务调用 + logger *slog.Logger // 全局日志 + baseCfg *cfg.LocalConfig // 基础配置 + thisCfg *modeler.IndependentCfg // 个性配置 +} + +// NewMicroservice 全局只实例化一次 +func NewMicroservice(ctx context.Context, lc *cfg.LocalConfig) (*Microservice, error) { + // 基础配置初始化 + if err := lc.Init(ctx); err != nil { + return nil, err + } + + m := &Microservice{ + code: lc.Services.ServiceCode, + logger: lc.GetLogger(), + baseCfg: lc, + thisCfg: &modeler.IndependentCfg{}, + } + + // 自定义配置实例化 + if err := lc.GetIndependent(m.thisCfg); err != nil { + return m, err + } + + // RPC客户端服务端 + client, err := lc.GetRPCClient() + if err != nil { + return m, err + } + server, err := lc.GetRPCServer() + if err != nil { + return m, err + } + m.client = client + m.server = server + + // 其他个性扩展逻辑 + opts, err := m.privateExtended() + if err != nil { + return m, err + } + + // 自定义配置初始化 + if err = m.thisCfg.Init(ctx, opts...); err != nil { + return m, err + } + + return m, nil +} diff --git a/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/register.go.tmpl b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/register.go.tmpl new file mode 100644 index 0000000..406dd1d --- /dev/null +++ b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/register.go.tmpl @@ -0,0 +1,52 @@ +// Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT. + +package handler + +import ( + "context" + + pb "{{ .Repository }}/api/{{ .ProductCode }}/{{ .ShortName }}/{{ .APIVersion }}" + "{{ .Repository }}/internal/security" + "{{ .Repository }}/public" + "google.golang.org/grpc/health" + "google.golang.org/grpc/health/grpc_health_v1" +) + +// Register 用于服务启动前环境准备 +func (m *Microservice) Register(ctx context.Context) error { + pb.Register{{ .ServiceTitle }}Server(m.server.Server(), m) + grpc_health_v1.RegisterHealthServer(m.server.Server(), health.NewServer()) + + // 注册服务信息 + mux, err := m.baseCfg.Register(ctx, pb.Register{{ .ServiceTitle }}HandlerFromEndpoint) + if err != nil { + return err + } + + // 注册访问安全策略 + if err = m.baseCfg.SecurityPolicyLoad(ctx, security.Assets); err != nil { + return err + } + + // 注册前端托管静态数据 + if err = m.baseCfg.HTTPHandlerFrontend(ctx, mux, public.Assets); err != nil { + return err + } + + // 这里添加其他自定义实现 + if err := m.privateHTTPHandle(mux); err != nil { + return err + } + + // 注册HTTP网关 + if err := m.server.RegisterGateway(mux); err != nil { + return err + } + + // 开启gRPC与HTTP服务 + if err := m.server.StartBackground(ctx); err != nil { + return err + } + + return nil +} diff --git a/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/rpc_internal.go.tmpl b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/rpc_internal.go.tmpl new file mode 100644 index 0000000..00081a4 --- /dev/null +++ b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/rpc_internal.go.tmpl @@ -0,0 +1,21 @@ +// Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT. + +package handler + +import ( + "context" + + statusv1 "github.com/grpc-kit/pkg/api/known/status/v1" + "github.com/grpc-kit/pkg/errs" +) + +// HealthCheck 用于健康检测 +func (m *Microservice) HealthCheck(ctx context.Context, req *statusv1.HealthCheckRequest) (*statusv1.HealthCheckResponse, error) { + if req.Service == m.code { + return &statusv1.HealthCheckResponse{ + Status: statusv1.HealthCheckResponse_SERVING, + }, nil + } + + return nil, errs.NotFound(ctx).WithMessage("unknown service").Err() +} diff --git a/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/shutdown.go.tmpl b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/shutdown.go.tmpl new file mode 100644 index 0000000..f60dff2 --- /dev/null +++ b/internal/projectmigrate/assets/v0_5_0/v0_3_8/handler/shutdown.go.tmpl @@ -0,0 +1,29 @@ +// Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT. + +package handler + +import ( + "context" + "time" +) + +// Shutdown 优雅关闭gRPC与HTTP服务 +func (m *Microservice) Shutdown(ctx context.Context) error { + m.logger.WarnContext(ctx, "Shutdown server begin") + + if err := m.baseCfg.Deregister(ctx); err != nil { + return err + } + + // 最长等待关闭的时间,例如超过30秒则强制关闭gateway + ctx, cancel := context.WithTimeout(ctx, time.Second*30) + defer cancel() + + // 阻塞等待,直到所有连接正常或超时退出 + if err := m.server.Shutdown(ctx); err != nil { + return err + } + + m.logger.WarnContext(ctx, "Shutdown server end") + return nil +} diff --git a/internal/projectmigrate/assets/v0_5_0/v0_3_8/scripts/env.tmpl b/internal/projectmigrate/assets/v0_5_0/v0_3_8/scripts/env.tmpl new file mode 100644 index 0000000..3678543 --- /dev/null +++ b/internal/projectmigrate/assets/v0_5_0/v0_3_8/scripts/env.tmpl @@ -0,0 +1,21 @@ +# Code generated by "grpc-kit-cli/{{ .TargetCLIVersion }}". DO NOT EDIT. +# +# https://grpc-kit.com/docs/spec-api/key-terms/ + +# 工具版本:生成该模版所使用的 cli 版本 +CLI_VERSION={{ .TargetCLIVersion }} + +# 产品代码:同一产品使用相同代码,使用单个词 +PRODUCT_CODE={{ .ProductCode }} + +# 应用短名:同一产品使用相同代码,使用单个词 +SHORT_NAME={{ .ShortName }} + +# 接口版本:全局接口版本 +API_VERSION={{ .APIVersion }} + +# 应用名称:用于生成文件的命名 +APPNAME=${PRODUCT_CODE}-${SHORT_NAME}-${API_VERSION} + +# 服务代码:用于 grpc 服务之间调用 +SERVICE_CODE=${SHORT_NAME}.${API_VERSION}.${PRODUCT_CODE} diff --git a/internal/projectmigrate/assets_test.go b/internal/projectmigrate/assets_test.go new file mode 100644 index 0000000..b1d370f --- /dev/null +++ b/internal/projectmigrate/assets_test.go @@ -0,0 +1,126 @@ +package projectmigrate + +import ( + "bytes" + "strings" + "testing" +) + +func TestRenderCompatibilityAssets(t *testing.T) { + project := Project{ + ModulePath: "git.lmq.io/kaopuvm/spider", + SourceFamily: "v0.3.9-beta.1", + ProductCode: "oneops", + ShortName: "spider", + APIVersion: "v1", + } + paths, err := CompatibilityAssetPaths(project.SourceFamily) + if err != nil { + t.Fatal(err) + } + if len(paths) != 6 { + t.Fatalf("CompatibilityAssetPaths() returned %d paths, want 6", len(paths)) + } + + rendered := make(map[string][]byte, len(paths)) + for _, relativePath := range paths { + body, err := RenderCompatibilityAsset(project, relativePath, "0.4.0") + if err != nil { + t.Fatalf("RenderCompatibilityAsset(%q) error = %v", relativePath, err) + } + if bytes.Contains(body, []byte("{{")) || bytes.Contains(body, []byte("}}")) { + t.Fatalf("RenderCompatibilityAsset(%q) left a template expression", relativePath) + } + if !bytes.HasPrefix(body, []byte(`// Code generated by "grpc-kit-cli/0.4.0". DO NOT EDIT.`)) && + !bytes.HasPrefix(body, []byte(`# Code generated by "grpc-kit-cli/0.4.0". DO NOT EDIT.`)) { + t.Fatalf("RenderCompatibilityAsset(%q) has an unexpected marker", relativePath) + } + rendered[relativePath] = body + } + + assertAssetContains(t, rendered, "cmd/server/main.go", "handler.NewMicroservice(ctx, lc)") + assertAssetContains(t, rendered, "handler/microservice.go", `"log/slog"`, "lc.Init(ctx)", "m.thisCfg.Init(ctx, opts...)") + assertAssetContains(t, rendered, "handler/register.go", "HTTPHandlerFrontend(ctx, mux", "StartBackground(ctx)", "RegisterOneopsSpiderServer") + assertAssetNotContains(t, rendered, "handler/register.go", "privateMCPHandle", "modeler/mcp") + assertAssetContains(t, rendered, "handler/shutdown.go", "WarnContext(ctx", "Deregister(ctx)") + assertAssetContains(t, rendered, "scripts/env", "CLI_VERSION=0.4.0", "PRODUCT_CODE=oneops", "SHORT_NAME=spider") +} + +func TestRenderCompatibilityAssetRejectsUnsupportedInputs(t *testing.T) { + project := Project{ + ModulePath: "example.com/acme/service", + SourceFamily: "v0.3.8", + ProductCode: "acme", + ShortName: "demo", + APIVersion: "v1", + } + tests := []struct { + name string + project Project + path string + version string + }{ + {name: "unknown path", project: project, path: "handler/private.go", version: "0.4.0"}, + {name: "development target", project: project, path: "handler/shutdown.go", version: "0.0.0"}, + {name: "unknown source family", project: func() Project { p := project; p.SourceFamily = "v0.4.0"; return p }(), path: "handler/shutdown.go", version: "0.4.0"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := RenderCompatibilityAsset(test.project, test.path, test.version); err == nil { + t.Fatal("RenderCompatibilityAsset() error = nil") + } + }) + } +} + +func TestRenderCompatibilityAssetAllowsPrereleasePreview(t *testing.T) { + project := Project{ + ModulePath: "example.com/acme/service", + SourceFamily: "v0.3.8", + ProductCode: "acme", + ShortName: "demo", + APIVersion: "v1", + } + body, err := RenderCompatibilityAsset(project, "handler/shutdown.go", "0.4.0-beta.1") + if err != nil { + t.Fatalf("RenderCompatibilityAsset() error = %v", err) + } + if !bytes.HasPrefix(body, []byte(`// Code generated by "grpc-kit-cli/0.4.0-beta.1". DO NOT EDIT.`)) { + t.Fatalf("RenderCompatibilityAsset() marker = %q", firstLine(body)) + } +} + +func TestCompatibilityAssetPathsReturnsCopy(t *testing.T) { + first, err := CompatibilityAssetPaths("v0.3.8") + if err != nil { + t.Fatal(err) + } + first[0] = "changed" + second, err := CompatibilityAssetPaths("v0.3.8") + if err != nil { + t.Fatal(err) + } + if second[0] == "changed" { + t.Fatal("CompatibilityAssetPaths returned mutable package state") + } +} + +func assertAssetContains(t *testing.T, assets map[string][]byte, relativePath string, values ...string) { + t.Helper() + body := string(assets[relativePath]) + for _, value := range values { + if !strings.Contains(body, value) { + t.Errorf("%s does not contain %q", relativePath, value) + } + } +} + +func assertAssetNotContains(t *testing.T, assets map[string][]byte, relativePath string, values ...string) { + t.Helper() + body := string(assets[relativePath]) + for _, value := range values { + if strings.Contains(body, value) { + t.Errorf("%s unexpectedly contains %q", relativePath, value) + } + } +} diff --git a/internal/projectmigrate/compatibility_test.go b/internal/projectmigrate/compatibility_test.go new file mode 100644 index 0000000..c65a958 --- /dev/null +++ b/internal/projectmigrate/compatibility_test.go @@ -0,0 +1,176 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestCompatibilityAssetsCompileAgainstReleasedPkg is intentionally opt-in: +// it invokes the Go toolchain and may populate module/build caches. It +// validates the frozen upgrade assets against a released module, without a +// local replace directive. GRPC_KIT_COMPAT_PKG_VERSION defaults to v0.5.0 and +// can select another released version for a compatibility matrix: +// +// GRPC_KIT_RUN_COMPATIBILITY_TEST=1 GRPC_KIT_COMPAT_PKG_VERSION=v0.5.8 \ +// go test ./internal/projectmigrate \ +// -run TestCompatibilityAssetsCompileAgainstReleasedPkg -count=1 +func TestCompatibilityAssetsCompileAgainstReleasedPkg(t *testing.T) { + if os.Getenv("GRPC_KIT_RUN_COMPATIBILITY_TEST") != "1" { + t.Skip("set GRPC_KIT_RUN_COMPATIBILITY_TEST=1 to compile the fixture against a released pkg version") + } + pkgVersion := os.Getenv("GRPC_KIT_COMPAT_PKG_VERSION") + if pkgVersion == "" { + pkgVersion = currentTargetPkgVersion + } + normalizedPkgVersion, err := normalizeVersion(pkgVersion) + if err != nil || strings.Contains(normalizedPkgVersion, "-") { + t.Fatalf("GRPC_KIT_COMPAT_PKG_VERSION must be a stable canonical semantic version: %q", pkgVersion) + } + pkgVersion = "v" + normalizedPkgVersion + + root := t.TempDir() + project := Project{ + Root: root, + ModulePath: "example.com/oneops/spider", + ProductCode: "oneops", + ShortName: "spider", + APIVersion: "v1", + SourceFamily: "v0.3.8", + } + for _, relativePath := range compatibilityAssetPaths { + body, err := RenderCompatibilityAsset(project, relativePath, "0.4.0") + if err != nil { + t.Fatalf("render %s: %v", relativePath, err) + } + writeCompatibilityFixtureFile(t, root, relativePath, body) + } + + fixtureFiles := map[string]string{ + "go.mod": `module example.com/oneops/spider + +go ` + currentTargetGoVersion + ` + +require ( + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 + github.com/grpc-kit/pkg ` + pkgVersion + ` + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + google.golang.org/grpc v1.83.1 +) +`, + "modeler/independent_cfg.go": `package modeler + +import ( + "context" + "log/slog" +) + +type ClientIndependentOption func(*IndependentCfg) + +type IndependentCfg struct { + logger *slog.Logger +} + +func (c *IndependentCfg) Init(ctx context.Context, opts ...ClientIndependentOption) error { + _ = ctx + for _, opt := range opts { + opt(c) + } + return nil +} +`, + "handler/private.go": `package handler + +import ( + "net/http" + + "example.com/oneops/spider/modeler" +) + +func (m *Microservice) privateExtended() ([]modeler.ClientIndependentOption, error) { + return nil, nil +} + +func (m *Microservice) privateHTTPHandle(*http.ServeMux) error { + return nil +} +`, + "api/oneops/spider/v1/service.go": `package v1 + +import ( + "context" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" +) + +func RegisterOneopsSpiderServer(grpc.ServiceRegistrar, any) {} + +func RegisterOneopsSpiderHandlerFromEndpoint(context.Context, *runtime.ServeMux, string, []grpc.DialOption) error { + return nil +} +`, + "internal/security/embed.go": `package security + +import "embed" + +//go:embed auth.rego +var Assets embed.FS +`, + "internal/security/auth.rego": "package oneops.spider.v1\n", + "public/embed.go": `package public + +import "embed" + +//go:embed gateway.yaml +var Assets embed.FS +`, + "public/gateway.yaml": "swagger: '2.0'\n", + } + for relativePath, body := range fixtureFiles { + writeCompatibilityFixtureFile(t, root, relativePath, []byte(body)) + } + + runFixtureGoCommand(t, root, "mod", "tidy") + runFixtureGoCommand(t, root, "test", "./handler") + runFixtureGoCommand(t, root, "build", "./...") +} + +func writeCompatibilityFixtureFile(t *testing.T, root, relativePath string, body []byte) { + t.Helper() + path := filepath.Join(root, filepath.FromSlash(relativePath)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, body, 0o600); err != nil { + t.Fatal(err) + } +} + +func runFixtureGoCommand(t *testing.T, root string, arguments ...string) { + t.Helper() + command := exec.Command("go", arguments...) + command.Dir = root + command.Env = append(os.Environ(), "GOWORK=off", "GOTOOLCHAIN=auto") + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("go %s: %v\n%s", strings.Join(arguments, " "), err, output) + } +} diff --git a/internal/projectmigrate/diagnostics.go b/internal/projectmigrate/diagnostics.go new file mode 100644 index 0000000..fffd027 --- /dev/null +++ b/internal/projectmigrate/diagnostics.go @@ -0,0 +1,274 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/mod/modfile" + "golang.org/x/mod/semver" +) + +const maxDiagnosticGoFileSize = 8 << 20 + +var printfLoggerMethods = map[string]struct{}{ + "Tracef": {}, "Debugf": {}, "Infof": {}, "Printf": {}, "Warnf": {}, "Warningf": {}, "Errorf": {}, "Fatalf": {}, "Panicf": {}, +} + +// ScanManualActions reports known pkg v0.5.0 incompatibilities without +// modifying source. Paths already replaced by the managed plan are excluded. +func ScanManualActions(project Project, changedPaths map[string]struct{}) ([]Diagnostic, error) { + actions, err := scanGoModActions(project) + if err != nil { + return nil, err + } + dedup := make(map[string]struct{}) + for _, action := range actions { + dedup[diagnosticKey(action)] = struct{}{} + } + err = filepath.WalkDir(project.Root, func(currentPath string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if entry.Name() == ".git" || entry.Name() == "vendor" { + return filepath.SkipDir + } + return nil + } + if entry.Type()&fs.ModeSymlink != 0 || !entry.Type().IsRegular() || !strings.HasSuffix(entry.Name(), ".go") { + return nil + } + relativePath, err := filepath.Rel(project.Root, currentPath) + if err != nil { + return err + } + relativePath = filepath.ToSlash(relativePath) + if _, excluded := changedPaths[relativePath]; excluded { + return nil + } + body, err := readRegularFile(currentPath, maxDiagnosticGoFileSize) + if err != nil { + return fmt.Errorf("read %s: %w", relativePath, err) + } + fileSet := token.NewFileSet() + parsed, err := parser.ParseFile(fileSet, relativePath, body, parser.SkipObjectResolution) + if err != nil { + appendDiagnostic(&actions, dedup, Diagnostic{Code: "inspect_source", Path: relativePath, Message: "fix Go syntax before migration diagnostics: " + err.Error()}) + return nil + } + scanGoFile(fileSet, parsed, relativePath, &actions, dedup) + return nil + }) + if err != nil { + return nil, fmt.Errorf("scan manual actions: %w", err) + } + sortDiagnostics(actions) + return actions, nil +} + +func scanGoModActions(project Project) ([]Diagnostic, error) { + path := filepath.Join(project.Root, "go.mod") + body, err := readRegularFile(path, maxGoModSize) + if err != nil { + return nil, err + } + parsed, err := modfile.Parse("go.mod", body, nil) + if err != nil { + return nil, err + } + var actions []Diagnostic + goVersion := "" + if parsed.Go != nil { + goVersion = parsed.Go.Version + } + if goVersion == "" || semver.Compare("v"+goVersion, "v"+currentTargetGoVersion) < 0 { + actions = append(actions, Diagnostic{Code: "go_version", Path: "go.mod", Line: lineContaining(body, "go "), Message: fmt.Sprintf("set the Go directive to at least %s (current %q)", currentTargetGoVersion, goVersion)}) + } + pkgVersion := "" + for _, requirement := range parsed.Require { + switch requirement.Mod.Path { + case "github.com/grpc-kit/pkg": + pkgVersion = requirement.Mod.Version + case "github.com/sirupsen/logrus": + actions = append(actions, Diagnostic{ + Code: "logrus_dependency", + Path: "go.mod", + Line: lineContaining(body, "github.com/sirupsen/logrus"), + Message: "remove the direct logrus requirement after migrating source usage, then run go mod tidy", + }) + } + } + if pkgVersion != currentTargetPkgVersion { + actions = append(actions, Diagnostic{Code: "pkg_version", Path: "go.mod", Line: lineContaining(body, "github.com/grpc-kit/pkg"), Message: fmt.Sprintf("require github.com/grpc-kit/pkg %s (current %q)", currentTargetPkgVersion, pkgVersion)}) + } + return actions, nil +} + +func scanGoFile(fileSet *token.FileSet, file *ast.File, relativePath string, actions *[]Diagnostic, dedup map[string]struct{}) { + logrusAliases := make(map[string]struct{}) + sdAliases := make(map[string]struct{}) + for _, imported := range file.Imports { + importPath, err := strconv.Unquote(imported.Path.Value) + if err != nil { + continue + } + alias := filepath.Base(importPath) + if imported.Name != nil { + alias = imported.Name.Name + } + switch importPath { + case "github.com/sirupsen/logrus": + logrusAliases[alias] = struct{}{} + appendDiagnostic(actions, dedup, Diagnostic{Code: "logrus_import", Path: relativePath, Line: fileSet.Position(imported.Pos()).Line, Message: "replace direct logrus usage with log/slog"}) + case "github.com/grpc-kit/pkg/sd": + sdAliases[alias] = struct{}{} + } + } + + ast.Inspect(file, func(node ast.Node) bool { + switch current := node.(type) { + case *ast.StarExpr: + if selector, ok := current.X.(*ast.SelectorExpr); ok && selector.Sel.Name == "Entry" && isAlias(selector.X, logrusAliases) { + appendDiagnostic(actions, dedup, Diagnostic{Code: "logrus_type", Path: relativePath, Line: fileSet.Position(current.Pos()).Line, Message: "replace *logrus.Entry with *slog.Logger"}) + } + case *ast.SelectorExpr: + if isAlias(current.X, logrusAliases) && (current.Sel.Name == "Hook" || current.Sel.Name == "Formatter" || current.Sel.Name == "Fields") { + appendDiagnostic(actions, dedup, Diagnostic{Code: "logrus_custom", Path: relativePath, Line: fileSet.Position(current.Pos()).Line, Message: "manually redesign logrus Hook/Formatter/Fields behavior for slog"}) + } + if current.Sel.Name == "Data" && looksLikeLoggerExpression(current.X) { + appendDiagnostic(actions, dedup, Diagnostic{Code: "logrus_custom", Path: relativePath, Line: fileSet.Position(current.Pos()).Line, Message: "replace direct logrus Entry.Data access with explicit slog attributes"}) + } + case *ast.CallExpr: + scanCall(fileSet, current, relativePath, logrusAliases, sdAliases, actions, dedup) + case *ast.FuncDecl: + if current.Name.Name == "Init" && receiverTypeName(current) == "IndependentCfg" && !firstParameterIsContext(current.Type.Params) { + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_signature", Path: relativePath, Line: fileSet.Position(current.Pos()).Line, Message: "change IndependentCfg.Init to accept context.Context as its first parameter"}) + } + } + return true + }) +} + +func scanCall(fileSet *token.FileSet, call *ast.CallExpr, relativePath string, logrusAliases, sdAliases map[string]struct{}, actions *[]Diagnostic, dedup map[string]struct{}) { + selector, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return + } + line := fileSet.Position(call.Pos()).Line + name := selector.Sel.Name + if _, ok := printfLoggerMethods[name]; ok && looksLikeLoggerExpression(selector.X) { + appendDiagnostic(actions, dedup, Diagnostic{Code: "printf_logger", Path: relativePath, Line: line, Message: fmt.Sprintf("replace %s with a context-aware slog call and structured attributes", name)}) + } + if (name == "WithFields" || name == "WithField" || name == "WithError") && (looksLikeLoggerExpression(selector.X) || isAlias(selector.X, logrusAliases)) { + appendDiagnostic(actions, dedup, Diagnostic{Code: "logrus_custom", Path: relativePath, Line: line, Message: fmt.Sprintf("manually translate %s to slog attributes", name)}) + } + switch { + case name == "NewMicroservice" && len(call.Args) == 1: + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_call", Path: relativePath, Line: line, Message: "pass context.Context as the first NewMicroservice argument"}) + case name == "Init" && len(call.Args) == 0: + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_call", Path: relativePath, Line: line, Message: "verify this Init call and pass context.Context when it targets grpc-kit configuration"}) + case name == "Deregister" && len(call.Args) == 0: + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_call", Path: relativePath, Line: line, Message: "pass context.Context to Deregister"}) + case name == "Register" && isAlias(selector.X, sdAliases) && len(call.Args) == 5: + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_call", Path: relativePath, Line: line, Message: "pass context.Context as the first sd.Register argument"}) + case name == "HTTPHandlerFrontend" && len(call.Args) == 2: + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_call", Path: relativePath, Line: line, Message: "pass context.Context as the first HTTPHandlerFrontend argument"}) + case name == "StartBackground" && len(call.Args) == 0: + appendDiagnostic(actions, dedup, Diagnostic{Code: "context_call", Path: relativePath, Line: line, Message: "pass context.Context to StartBackground"}) + case name == "WithLogger" && len(call.Args) == 3: + appendDiagnostic(actions, dedup, Diagnostic{Code: "errs_with_logger", Path: relativePath, Line: line, Message: "pass context.Context before the slog logger to errs.Status.WithLogger"}) + } +} + +func looksLikeLoggerExpression(expression ast.Expr) bool { + switch current := expression.(type) { + case *ast.Ident: + name := strings.ToLower(current.Name) + return strings.Contains(name, "logger") || name == "log" || strings.Contains(name, "entry") + case *ast.SelectorExpr: + name := strings.ToLower(current.Sel.Name) + return strings.Contains(name, "logger") || name == "log" || strings.Contains(name, "entry") + case *ast.CallExpr: + selector, ok := current.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + name := selector.Sel.Name + return name == "WithFields" || name == "WithField" || name == "WithError" || strings.Contains(strings.ToLower(name), "logger") + default: + return false + } +} + +func isAlias(expression ast.Expr, aliases map[string]struct{}) bool { + identifier, ok := expression.(*ast.Ident) + if !ok { + return false + } + _, ok = aliases[identifier.Name] + return ok +} + +func receiverTypeName(function *ast.FuncDecl) string { + if function.Recv == nil || len(function.Recv.List) == 0 { + return "" + } + typeExpression := function.Recv.List[0].Type + if pointer, ok := typeExpression.(*ast.StarExpr); ok { + typeExpression = pointer.X + } + if identifier, ok := typeExpression.(*ast.Ident); ok { + return identifier.Name + } + return "" +} + +func firstParameterIsContext(parameters *ast.FieldList) bool { + if parameters == nil || len(parameters.List) == 0 { + return false + } + selector, ok := parameters.List[0].Type.(*ast.SelectorExpr) + return ok && selector.Sel.Name == "Context" +} + +func appendDiagnostic(actions *[]Diagnostic, dedup map[string]struct{}, action Diagnostic) { + key := diagnosticKey(action) + if _, exists := dedup[key]; exists { + return + } + dedup[key] = struct{}{} + *actions = append(*actions, action) +} + +func diagnosticKey(diagnostic Diagnostic) string { + return fmt.Sprintf("%s:%d:%s", diagnostic.Path, diagnostic.Line, diagnostic.Code) +} + +func lineContaining(body []byte, needle string) int { + for index, line := range strings.Split(string(body), "\n") { + if strings.Contains(line, needle) { + return index + 1 + } + } + return 1 +} diff --git a/internal/projectmigrate/diagnostics_test.go b/internal/projectmigrate/diagnostics_test.go new file mode 100644 index 0000000..a446ebb --- /dev/null +++ b/internal/projectmigrate/diagnostics_test.go @@ -0,0 +1,119 @@ +package projectmigrate + +import ( + "strings" + "testing" +) + +func TestScanManualActions(t *testing.T) { + goMod := `module git.lmq.io/kaopuvm/spider + +go 1.25.0 + +require ( + github.com/grpc-kit/pkg v0.4.3 + github.com/sirupsen/logrus v1.9.3 +) +` + root := writeProject(t, validEnvironment, goMod) + writeFile(t, root+"/user.go", []byte(`package spider + +import "github.com/sirupsen/logrus" + +type IndependentCfg struct { logger *logrus.Entry } +type Option func(*IndependentCfg) + +func (c *IndependentCfg) Init(opts ...Option) error { return nil } + +func migrate(ctx, lc, base, server, handler, status, mux, assets any, logger *logrus.Entry) { + logger.Warnf("value: %v", 1) + logger.WithFields(logrus.Fields{"key": "value"}) + logrus.WithField("package", "call") + _ = logger.Data + lc.Init() + base.Deregister() + base.HTTPHandlerFrontend(mux, assets) + server.StartBackground() + handler.NewMicroservice(lc) + status.WithLogger(logger, "%v", nil) +} +`), 0o600) + writeFile(t, root+"/registry.go", []byte(`package spider + +import "github.com/grpc-kit/pkg/sd" + +func register(conn, ttl any) { + sd.Register(conn, "name", "address", "value", ttl) +} +`), 0o600) + writeFile(t, root+"/managed.go", []byte(`package spider +import "github.com/sirupsen/logrus" +var managed *logrus.Entry +`), 0o600) + + project, err := LoadProject(root) + if err != nil { + t.Fatal(err) + } + actions, err := ScanManualActions(project, map[string]struct{}{"managed.go": {}}) + if err != nil { + t.Fatal(err) + } + for _, code := range []string{"go_version", "pkg_version", "logrus_dependency", "logrus_import", "logrus_type", "logrus_custom", "printf_logger", "context_signature", "context_call", "errs_with_logger"} { + if !hasDiagnosticCode(actions, code) { + t.Errorf("actions do not contain %q: %#v", code, actions) + } + } + for _, messagePart := range []string{"WithField", "Entry.Data", "sd.Register"} { + if !hasDiagnosticMessage(actions, messagePart) { + t.Errorf("actions do not contain message %q: %#v", messagePart, actions) + } + } + for _, action := range actions { + if action.Path == "managed.go" { + t.Fatalf("managed change produced a manual action: %#v", action) + } + if action.Path != "go.mod" && action.Line == 0 { + t.Errorf("source action has no line: %#v", action) + } + } +} + +func hasDiagnosticMessage(diagnostics []Diagnostic, part string) bool { + for _, diagnostic := range diagnostics { + if strings.Contains(diagnostic.Message, part) { + return true + } + } + return false +} + +func TestWritePlanIncludesManualActionLocation(t *testing.T) { + plan := Plan{ + Status: StatusManagedUpToDate, + TargetCLIVersion: "0.4.0", + TargetPkgVersion: "v0.5.0", + ManualActions: []Diagnostic{ + {Code: "context_call", Path: "handler/register.go", Line: 38, Message: "pass context"}, + }, + } + var output strings.Builder + if err := WritePlan(&output, plan); err != nil { + t.Fatal(err) + } + if !strings.Contains(output.String(), "Manual action [context_call] handler/register.go:38: pass context") { + t.Fatalf("output = %q", output.String()) + } + if !strings.Contains(output.String(), "Project compatibility: not verified") { + t.Fatalf("output does not explain compatibility status: %q", output.String()) + } +} + +func hasDiagnosticCode(diagnostics []Diagnostic, code string) bool { + for _, diagnostic := range diagnostics { + if diagnostic.Code == code { + return true + } + } + return false +} diff --git a/internal/projectmigrate/git.go b/internal/projectmigrate/git.go new file mode 100644 index 0000000..e9c7141 --- /dev/null +++ b/internal/projectmigrate/git.go @@ -0,0 +1,67 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "bytes" + "context" + "fmt" + "os/exec" +) + +func inspectApplyBlockers(root, targetVersion string) []Diagnostic { + var blockers []Diagnostic + if _, err := ValidateTargetCLIVersion(targetVersion); err != nil { + blockers = append(blockers, Diagnostic{Code: "release_version", Message: err.Error()}) + } + if err := ensureGitWorktree(context.Background(), root); err != nil { + blockers = append(blockers, Diagnostic{Code: "git_worktree", Message: err.Error()}) + return blockers + } + dirty, err := gitOutput(context.Background(), root, "status", "--porcelain", "--untracked-files=normal", "--", ".") + if err != nil { + blockers = append(blockers, Diagnostic{Code: "git_status", Message: err.Error()}) + } else if len(bytes.TrimSpace(dirty)) != 0 { + blockers = append(blockers, Diagnostic{Code: "dirty_worktree", Message: "project worktree is not clean"}) + } + return blockers +} + +func ensureGitWorktree(ctx context.Context, root string) error { + if _, err := gitOutput(ctx, root, "rev-parse", "--show-toplevel"); err != nil { + return fmt.Errorf("project is not in a Git worktree: %w", err) + } + return nil +} + +func ensureGitTracked(ctx context.Context, root, relativePath string) error { + if _, err := gitOutput(ctx, root, "ls-files", "--error-unmatch", "--", relativePath); err != nil { + return fmt.Errorf("%s is not tracked by Git: %w", relativePath, err) + } + return nil +} + +func gitOutput(ctx context.Context, root string, args ...string) ([]byte, error) { + commandArgs := append([]string{"-C", root}, args...) + cmd := exec.CommandContext(ctx, "git", commandArgs...) + output, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("git %v: %s", args, bytes.TrimSpace(exitErr.Stderr)) + } + return nil, err + } + return output, nil +} diff --git a/internal/projectmigrate/marker.go b/internal/projectmigrate/marker.go new file mode 100644 index 0000000..218cb51 --- /dev/null +++ b/internal/projectmigrate/marker.go @@ -0,0 +1,75 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "fmt" + "strings" +) + +const ( + goMarkerPrefix = `// Code generated by "grpc-kit-cli/` + scriptMarkerPrefix = `# Code generated by "grpc-kit-cli/` + markerSuffix = `". DO NOT EDIT.` +) + +var supportedSourceVersions = map[string]string{ + "0.3.8-beta.1": "v0.3.8", + "0.3.8": "v0.3.8", + "0.3.9-beta.1": "v0.3.9-beta.1", +} + +// Marker is a strict first-line grpc-kit-cli ownership declaration. +type Marker struct { + Version string + SourceFamily string + CommentPrefix string +} + +// ParseMarkerLine parses an exact Go or script marker. Similar comments, +// placeholders and non-canonical versions do not grant write ownership. +func ParseMarkerLine(line string) (Marker, error) { + line = strings.TrimSuffix(line, "\r") + prefix := "" + switch { + case strings.HasPrefix(line, goMarkerPrefix): + prefix = "//" + line = strings.TrimPrefix(line, goMarkerPrefix) + case strings.HasPrefix(line, scriptMarkerPrefix): + prefix = "#" + line = strings.TrimPrefix(line, scriptMarkerPrefix) + default: + return Marker{}, fmt.Errorf("first line is not an exact grpc-kit-cli marker") + } + if !strings.HasSuffix(line, markerSuffix) { + return Marker{}, fmt.Errorf("marker is missing the exact suffix %q", markerSuffix) + } + rawVersion := strings.TrimSuffix(line, markerSuffix) + version, err := normalizeVersion(rawVersion) + if err != nil { + return Marker{}, fmt.Errorf("invalid marker version: %w", err) + } + + return Marker{ + Version: version, + SourceFamily: supportedSourceVersions[version], + CommentPrefix: prefix, + }, nil +} + +// Supported reports whether the marker maps to a frozen source family. +func (m Marker) Supported() bool { + return m.SourceFamily != "" +} diff --git a/internal/projectmigrate/marker_test.go b/internal/projectmigrate/marker_test.go new file mode 100644 index 0000000..f37297b --- /dev/null +++ b/internal/projectmigrate/marker_test.go @@ -0,0 +1,58 @@ +package projectmigrate + +import "testing" + +func TestParseMarkerLine(t *testing.T) { + tests := []struct { + name string + line string + version string + family string + comment string + wantErr bool + }{ + { + name: "Go stable source", + line: `// Code generated by "grpc-kit-cli/v0.3.8". DO NOT EDIT.`, + version: "0.3.8", + family: "v0.3.8", + comment: "//", + }, + { + name: "script prerelease source with CRLF", + line: "# Code generated by \"grpc-kit-cli/0.3.9-beta.1\". DO NOT EDIT.\r", + version: "0.3.9-beta.1", + family: "v0.3.9-beta.1", + comment: "#", + }, + { + name: "valid but unsupported source", + line: `// Code generated by "grpc-kit-cli/0.4.0". DO NOT EDIT.`, + version: "0.4.0", + comment: "//", + }, + {name: "placeholder", line: `// Code generated by "grpc-kit-cli/{{ .Global.ReleaseVersion }}". DO NOT EDIT.`, wantErr: true}, + {name: "missing period", line: `// Code generated by "grpc-kit-cli/0.3.8". DO NOT EDIT`, wantErr: true}, + {name: "leading space", line: ` // Code generated by "grpc-kit-cli/0.3.8". DO NOT EDIT.`, wantErr: true}, + {name: "build metadata", line: `// Code generated by "grpc-kit-cli/0.3.8+local". DO NOT EDIT.`, wantErr: true}, + {name: "trailing newline", line: "// Code generated by \"grpc-kit-cli/0.3.8\". DO NOT EDIT.\n", wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + marker, err := ParseMarkerLine(test.line) + if (err != nil) != test.wantErr { + t.Fatalf("ParseMarkerLine() error = %v, wantErr %v", err, test.wantErr) + } + if test.wantErr { + return + } + if marker.Version != test.version || marker.SourceFamily != test.family || marker.CommentPrefix != test.comment { + t.Fatalf("ParseMarkerLine() = %#v, want version=%q family=%q comment=%q", marker, test.version, test.family, test.comment) + } + if marker.Supported() != (test.family != "") { + t.Fatalf("Marker.Supported() = %v, family = %q", marker.Supported(), test.family) + } + }) + } +} diff --git a/internal/projectmigrate/output.go b/internal/projectmigrate/output.go new file mode 100644 index 0000000..9fdd0d6 --- /dev/null +++ b/internal/projectmigrate/output.go @@ -0,0 +1,84 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "fmt" + "io" + + "github.com/pmezard/go-difflib/difflib" +) + +func WritePlan(output io.Writer, plan Plan) error { + if _, err := fmt.Fprintf(output, "Status: %s\nTarget CLI: %s\nTarget pkg: %s\n", plan.Status, plan.TargetCLIVersion, plan.TargetPkgVersion); err != nil { + return err + } + if err := writeDiagnostics(output, "Warning", plan.Warnings); err != nil { + return err + } + if err := writeDiagnostics(output, "Apply blocked", plan.ApplyBlockers); err != nil { + return err + } + if err := writeDiagnostics(output, "Manual action", plan.ManualActions); err != nil { + return err + } + if len(plan.ManualActions) != 0 { + if _, err := fmt.Fprintln(output, "Project compatibility: not verified; complete the manual actions before building."); err != nil { + return err + } + } + if err := writeDiagnostics(output, "Conflict", plan.Conflicts); err != nil { + return err + } + if len(plan.Changes) == 0 { + _, err := fmt.Fprintln(output, "Managed changes: none") + return err + } + if _, err := fmt.Fprintf(output, "Managed changes: %d\n", len(plan.Changes)); err != nil { + return err + } + for _, change := range plan.Changes { + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(string(change.Before)), + B: difflib.SplitLines(string(change.After)), + FromFile: "a/" + change.Path, + ToFile: "b/" + change.Path, + Context: 3, + }) + if err != nil { + return err + } + if _, err := io.WriteString(output, diff); err != nil { + return err + } + } + return nil +} + +func writeDiagnostics(output io.Writer, label string, diagnostics []Diagnostic) error { + for _, diagnostic := range diagnostics { + location := diagnostic.Path + if location == "" { + location = "project" + } + if diagnostic.Line > 0 { + location = fmt.Sprintf("%s:%d", location, diagnostic.Line) + } + if _, err := fmt.Fprintf(output, "%s [%s] %s: %s\n", label, diagnostic.Code, location, diagnostic.Message); err != nil { + return err + } + } + return nil +} diff --git a/internal/projectmigrate/plan.go b/internal/projectmigrate/plan.go new file mode 100644 index 0000000..65db939 --- /dev/null +++ b/internal/projectmigrate/plan.go @@ -0,0 +1,285 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "bufio" + "bytes" + "crypto/sha256" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" +) + +const maxManagedFileSize = 4 << 20 + +type Status string + +const ( + StatusManagedUpToDate Status = "managed_up_to_date" + StatusReady Status = "ready" + StatusApplied Status = "applied" + StatusConflict Status = "conflict" + StatusUnsupported Status = "unsupported" +) + +type Diagnostic struct { + Code string + Path string + Line int + Message string +} + +type Change struct { + Path string + Before []byte + After []byte + Mode fs.FileMode + BeforeSHA256 [sha256.Size]byte +} + +type Plan struct { + Project Project + FromCLIVersions []string + TargetCLIVersion string + TargetPkgVersion string + Status Status + Changes []Change + Warnings []Diagnostic + ApplyBlockers []Diagnostic + ManualActions []Diagnostic + Conflicts []Diagnostic +} + +func (p Plan) Blocked() bool { + return p.Status == StatusConflict || p.Status == StatusUnsupported || len(p.Conflicts) != 0 +} + +// BuildPlan computes the complete write set without modifying the project. +func BuildPlan(projectPath, targetCLIVersion string) (Plan, error) { + targetVersion, err := normalizeTargetCLIVersion(targetCLIVersion) + if err != nil { + return Plan{}, fmt.Errorf("target CLI version: %w", err) + } + project, err := LoadProject(projectPath) + if err != nil { + return Plan{}, fmt.Errorf("invalid project: %w", err) + } + plan := Plan{ + Project: project, + TargetCLIVersion: targetVersion, + TargetPkgVersion: currentTargetPkgVersion, + Status: StatusManagedUpToDate, + } + if project.SourceFamily == "" { + if project.SourceCLIVersion != targetVersion { + plan.Status = StatusUnsupported + plan.Conflicts = append(plan.Conflicts, Diagnostic{ + Code: "unsupported_source", + Path: "scripts/env", + Message: fmt.Sprintf("source CLI version %s has no frozen compatibility assets", project.SourceCLIVersion), + }) + plan.ManualActions, _ = ScanManualActions(project, nil) + return plan, nil + } + project.SourceFamily = "v0.3.8" + plan.Project.SourceFamily = project.SourceFamily + } + assetPaths, err := CompatibilityAssetPaths(project.SourceFamily) + if err != nil { + return Plan{}, err + } + assetSet := make(map[string]struct{}, len(assetPaths)) + for _, assetPath := range assetPaths { + assetSet[assetPath] = struct{}{} + } + + versions := make(map[string]struct{}) + err = filepath.WalkDir(project.Root, func(currentPath string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relativePath, err := filepath.Rel(project.Root, currentPath) + if err != nil { + return err + } + relativePath = filepath.ToSlash(relativePath) + _, isAsset := assetSet[relativePath] + if entry.IsDir() { + if entry.Name() == ".git" { + return filepath.SkipDir + } + if isAsset || relativePath == legacyPublicEmbedPath { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "unsafe_file", Path: relativePath, Message: "managed path is a directory"}) + } + return nil + } + if entry.Type()&os.ModeSymlink != 0 { + if isAsset || relativePath == legacyPublicEmbedPath { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "unsafe_file", Path: relativePath, Message: "managed path is a symlink"}) + } + return nil + } + if !entry.Type().IsRegular() { + if isAsset { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "unsafe_file", Path: relativePath, Message: "managed path is not a regular file"}) + } + return nil + } + + line, err := readFirstLine(currentPath) + if err != nil { + if isAsset || relativePath == legacyPublicEmbedPath { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "unreadable_marker", Path: relativePath, Message: err.Error()}) + } + return nil + } + if relativePath == legacyPublicEmbedPath && strings.Contains(line, `grpc-kit-cli/{{ .Global.ReleaseVersion }}`) { + body, err := readRegularFile(currentPath, maxManagedFileSize) + if err != nil { + return err + } + if IsKnownLegacyMarkerFile(relativePath, body) { + plan.Warnings = append(plan.Warnings, Diagnostic{Code: "known_legacy_marker", Path: relativePath, Message: "frozen legacy placeholder output is skipped"}) + } else { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "invalid_marker", Path: relativePath, Message: "legacy placeholder marker does not match the frozen fixture"}) + } + return nil + } + + marker, markerErr := ParseMarkerLine(line) + if markerErr != nil { + if looksLikeManagedMarker(line) { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "invalid_marker", Path: relativePath, Message: markerErr.Error()}) + } + return nil + } + versions[marker.Version] = struct{}{} + if !isAsset { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "unmapped_managed_file", Path: relativePath, Message: "managed file has no compatibility asset"}) + return nil + } + if marker.Version != targetVersion && !marker.Supported() { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "unsupported_version", Path: relativePath, Message: fmt.Sprintf("marker version %s is not supported", marker.Version)}) + return nil + } + + body, err := readRegularFile(currentPath, maxManagedFileSize) + if err != nil { + return fmt.Errorf("read managed file %s: %w", relativePath, err) + } + renderProject := project + if marker.SourceFamily != "" { + renderProject.SourceFamily = marker.SourceFamily + } + after, err := RenderCompatibilityAsset(renderProject, relativePath, targetVersion) + if err != nil { + plan.Conflicts = append(plan.Conflicts, Diagnostic{Code: "render_failed", Path: relativePath, Message: err.Error()}) + return nil + } + if bytes.Equal(body, after) { + return nil + } + info, err := entry.Info() + if err != nil { + return err + } + plan.Changes = append(plan.Changes, Change{ + Path: relativePath, + Before: body, + After: after, + Mode: info.Mode(), + BeforeSHA256: sha256.Sum256(body), + }) + return nil + }) + if err != nil { + return Plan{}, fmt.Errorf("scan project: %w", err) + } + + for version := range versions { + plan.FromCLIVersions = append(plan.FromCLIVersions, version) + } + sort.Strings(plan.FromCLIVersions) + if len(plan.FromCLIVersions) > 1 { + plan.Warnings = append(plan.Warnings, Diagnostic{Code: "mixed_versions", Message: "managed files contain multiple supported CLI versions: " + strings.Join(plan.FromCLIVersions, ", ")}) + } + sort.Slice(plan.Changes, func(i, j int) bool { return plan.Changes[i].Path < plan.Changes[j].Path }) + sortDiagnostics(plan.Warnings) + sortDiagnostics(plan.Conflicts) + + if len(plan.Conflicts) != 0 { + plan.Status = StatusConflict + for _, conflict := range plan.Conflicts { + if conflict.Code == "unsupported_source" || conflict.Code == "unsupported_version" { + plan.Status = StatusUnsupported + break + } + } + } else if len(plan.Changes) != 0 { + plan.Status = StatusReady + } + plan.ApplyBlockers = inspectApplyBlockers(project.Root, targetVersion) + changedPaths := make(map[string]struct{}, len(plan.Changes)) + for _, change := range plan.Changes { + changedPaths[change.Path] = struct{}{} + } + manualActions, diagnosticsErr := ScanManualActions(project, changedPaths) + if diagnosticsErr != nil { + plan.Warnings = append(plan.Warnings, Diagnostic{Code: "diagnostics_failed", Message: diagnosticsErr.Error()}) + } else { + plan.ManualActions = manualActions + } + sortDiagnostics(plan.ManualActions) + return plan, nil +} + +func looksLikeManagedMarker(line string) bool { + return strings.HasPrefix(line, `// Code generated by "grpc-kit-cli/`) || + strings.HasPrefix(line, `# Code generated by "grpc-kit-cli/`) +} + +func readFirstLine(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + reader := bufio.NewReader(io.LimitReader(file, 4097)) + line, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + return "", err + } + if len(line) > 4096 { + return "", fmt.Errorf("first line exceeds 4096 bytes") + } + return strings.TrimSuffix(line, "\n"), nil +} + +func sortDiagnostics(diagnostics []Diagnostic) { + sort.SliceStable(diagnostics, func(i, j int) bool { + if diagnostics[i].Path == diagnostics[j].Path { + if diagnostics[i].Line != diagnostics[j].Line { + return diagnostics[i].Line < diagnostics[j].Line + } + return diagnostics[i].Code < diagnostics[j].Code + } + return diagnostics[i].Path < diagnostics[j].Path + }) +} diff --git a/internal/projectmigrate/plan_test.go b/internal/projectmigrate/plan_test.go new file mode 100644 index 0000000..6fe0cd5 --- /dev/null +++ b/internal/projectmigrate/plan_test.go @@ -0,0 +1,156 @@ +package projectmigrate + +import ( + "bytes" + "os" + "path/filepath" + "slices" + "strings" + "testing" +) + +func TestBuildPlanForSpiderShape(t *testing.T) { + root := writePlanProject(t, false) + writeManagedStub(t, root, "cmd/server/main.go", "0.3.9-beta.1", "main") + writeManagedStub(t, root, "handler/microservice.go", "0.3.9-beta.1", "handler") + writeManagedStub(t, root, "handler/rpc_internal.go", "0.3.8-beta.1", "handler") + writeManagedStub(t, root, "handler/shutdown.go", "0.3.9-beta.1", "handler") + writeFile(t, filepath.Join(root, "handler", "register.go"), []byte("package handler\n// user managed\n"), 0o640) + writeFile(t, filepath.Join(root, legacyPublicEmbedPath), []byte(legacyPublicEmbedFixture), 0o640) + + plan, err := BuildPlan(root, "0.4.0") + if err != nil { + t.Fatalf("BuildPlan() error = %v", err) + } + if plan.Status != StatusReady || plan.Blocked() { + t.Fatalf("BuildPlan() status = %s, conflicts = %#v", plan.Status, plan.Conflicts) + } + wantChanges := []string{ + "cmd/server/main.go", + "handler/microservice.go", + "handler/rpc_internal.go", + "handler/shutdown.go", + "scripts/env", + } + if got := changePaths(plan.Changes); !slices.Equal(got, wantChanges) { + t.Fatalf("change paths = %v, want %v", got, wantChanges) + } + if !hasDiagnostic(plan.Warnings, "known_legacy_marker", legacyPublicEmbedPath) || !hasDiagnostic(plan.Warnings, "mixed_versions", "") { + t.Fatalf("warnings = %#v", plan.Warnings) + } + if slices.Contains(changePaths(plan.Changes), "handler/register.go") { + t.Fatal("user-managed register.go entered Changes") + } + registerBody, err := os.ReadFile(filepath.Join(root, "handler", "register.go")) + if err != nil { + t.Fatal(err) + } + if string(registerBody) != "package handler\n// user managed\n" { + t.Fatal("preview modified register.go") + } +} + +func TestBuildPlanRejectsUnmappedManagedFile(t *testing.T) { + root := writePlanProject(t, false) + writeManagedStub(t, root, "old/generated.go", "0.3.8", "old") + + plan, err := BuildPlan(root, "0.4.0") + if err != nil { + t.Fatal(err) + } + if plan.Status != StatusConflict || !hasDiagnostic(plan.Conflicts, "unmapped_managed_file", "old/generated.go") { + t.Fatalf("plan = %#v", plan) + } +} + +func TestBuildPlanRejectsModifiedLegacyMarkerFixture(t *testing.T) { + root := writePlanProject(t, false) + writeFile(t, filepath.Join(root, legacyPublicEmbedPath), []byte(legacyPublicEmbedFixture+"// changed\n"), 0o640) + + plan, err := BuildPlan(root, "0.4.0") + if err != nil { + t.Fatal(err) + } + if plan.Status != StatusConflict || !hasDiagnostic(plan.Conflicts, "invalid_marker", legacyPublicEmbedPath) { + t.Fatalf("plan = %#v", plan) + } +} + +func TestBuildPlanDoesNotCreateMissingAssets(t *testing.T) { + root := writePlanProject(t, false) + plan, err := BuildPlan(root, "0.4.0") + if err != nil { + t.Fatal(err) + } + if got := changePaths(plan.Changes); !slices.Equal(got, []string{"scripts/env"}) { + t.Fatalf("change paths = %v", got) + } + if _, err := os.Stat(filepath.Join(root, "handler", "microservice.go")); !os.IsNotExist(err) { + t.Fatalf("preview created a missing asset: %v", err) + } +} + +func TestWritePlanIncludesUnifiedDiff(t *testing.T) { + root := writePlanProject(t, false) + plan, err := BuildPlan(root, "0.4.0") + if err != nil { + t.Fatal(err) + } + var output bytes.Buffer + if err := WritePlan(&output, plan); err != nil { + t.Fatal(err) + } + for _, value := range []string{"Status: ready", "--- a/scripts/env", "+++ b/scripts/env", "CLI_VERSION=0.4.0"} { + if !strings.Contains(output.String(), value) { + t.Errorf("output does not contain %q:\n%s", value, output.String()) + } + } +} + +func writePlanProject(t *testing.T, allManaged bool) string { + t.Helper() + root := writeProject(t, validEnvironment, "module git.lmq.io/kaopuvm/spider\n\ngo 1.25.0\n") + if allManaged { + writeManagedStub(t, root, "cmd/server/main.go", "0.3.9-beta.1", "main") + writeManagedStub(t, root, "handler/microservice.go", "0.3.9-beta.1", "handler") + writeManagedStub(t, root, "handler/register.go", "0.3.9-beta.1", "handler") + writeManagedStub(t, root, "handler/rpc_internal.go", "0.3.8-beta.1", "handler") + writeManagedStub(t, root, "handler/shutdown.go", "0.3.9-beta.1", "handler") + } + return root +} + +func writeManagedStub(t *testing.T, root, relativePath, version, packageName string) { + t.Helper() + body := []byte(`// Code generated by "grpc-kit-cli/` + version + `". DO NOT EDIT. + +package ` + packageName + "\n") + writeFile(t, filepath.Join(root, filepath.FromSlash(relativePath)), body, 0o640) +} + +func writeFile(t *testing.T, path string, body []byte, mode os.FileMode) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, body, mode); err != nil { + t.Fatal(err) + } +} + +func changePaths(changes []Change) []string { + paths := make([]string, 0, len(changes)) + for _, change := range changes { + paths = append(paths, change.Path) + } + return paths +} + +func hasDiagnostic(diagnostics []Diagnostic, code, path string) bool { + for _, diagnostic := range diagnostics { + if diagnostic.Code == code && diagnostic.Path == path { + return true + } + } + return false +} diff --git a/internal/projectmigrate/project.go b/internal/projectmigrate/project.go new file mode 100644 index 0000000..a4e0a00 --- /dev/null +++ b/internal/projectmigrate/project.go @@ -0,0 +1,223 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "golang.org/x/mod/modfile" + "golang.org/x/mod/module" +) + +const ( + maxEnvironmentSize = 64 << 10 + maxGoModSize = 1 << 20 +) + +var ( + projectNamePattern = regexp.MustCompile(`^[a-z0-9]{4,}$`) + apiVersionPattern = regexp.MustCompile(`^v[1-9][0-9]*(?:(?:alpha|beta)[1-9][0-9]*)?$`) +) + +var derivedEnvironmentLines = map[string]string{ + "APPNAME": `${PRODUCT_CODE}-${SHORT_NAME}-${API_VERSION}`, + "SERVICE_CODE": `${SHORT_NAME}.${API_VERSION}.${PRODUCT_CODE}`, +} + +// Project contains the identity required to render managed migration assets. +type Project struct { + Root string + ModulePath string + SourceCLIVersion string + SourceFamily string + ProductCode string + ShortName string + APIVersion string +} + +type environment struct { + Marker Marker + CLIVersion string + ProductCode string + ShortName string + APIVersion string +} + +// LoadProject identifies a project without executing its scripts or changing +// any files. +func LoadProject(path string) (Project, error) { + root, err := filepath.Abs(path) + if err != nil { + return Project{}, fmt.Errorf("resolve project path: %w", err) + } + rootInfo, err := os.Lstat(root) + if err != nil { + return Project{}, fmt.Errorf("inspect project root: %w", err) + } + if rootInfo.Mode()&os.ModeSymlink != 0 || !rootInfo.IsDir() { + return Project{}, fmt.Errorf("project root must be a real directory, not a symlink") + } + + environmentBody, err := readRegularFile(filepath.Join(root, "scripts", "env"), maxEnvironmentSize) + if err != nil { + return Project{}, fmt.Errorf("read scripts/env: %w", err) + } + env, err := parseEnvironment(environmentBody) + if err != nil { + return Project{}, fmt.Errorf("parse scripts/env: %w", err) + } + + goModBody, err := readRegularFile(filepath.Join(root, "go.mod"), maxGoModSize) + if err != nil { + return Project{}, fmt.Errorf("read go.mod: %w", err) + } + parsedGoMod, err := modfile.Parse("go.mod", goModBody, nil) + if err != nil { + return Project{}, fmt.Errorf("parse go.mod: %w", err) + } + if parsedGoMod.Module == nil || parsedGoMod.Module.Mod.Path == "" { + return Project{}, fmt.Errorf("parse go.mod: module path is missing") + } + modulePath := parsedGoMod.Module.Mod.Path + if err := module.CheckPath(modulePath); err != nil { + return Project{}, fmt.Errorf("parse go.mod: invalid module path %q: %w", modulePath, err) + } + + return Project{ + Root: root, + ModulePath: modulePath, + SourceCLIVersion: env.CLIVersion, + SourceFamily: env.Marker.SourceFamily, + ProductCode: env.ProductCode, + ShortName: env.ShortName, + APIVersion: env.APIVersion, + }, nil +} + +func parseEnvironment(body []byte) (environment, error) { + scanner := bufio.NewScanner(bytes.NewReader(body)) + if !scanner.Scan() { + return environment{}, fmt.Errorf("file is empty") + } + marker, err := ParseMarkerLine(scanner.Text()) + if err != nil { + return environment{}, err + } + + values := make(map[string]string, 4) + lineNumber := 1 + for scanner.Scan() { + lineNumber++ + line := strings.TrimSuffix(scanner.Text(), "\r") + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, value, found := strings.Cut(line, "=") + if !found || key == "" { + return environment{}, fmt.Errorf("line %d is not a literal KEY=value assignment", lineNumber) + } + if expected, ok := derivedEnvironmentLines[key]; ok { + if value != expected { + return environment{}, fmt.Errorf("line %d has unexpected derived value for %s", lineNumber, key) + } + continue + } + switch key { + case "CLI_VERSION", "PRODUCT_CODE", "SHORT_NAME", "API_VERSION": + default: + return environment{}, fmt.Errorf("line %d assigns unsupported key %q", lineNumber, key) + } + if _, exists := values[key]; exists { + return environment{}, fmt.Errorf("line %d duplicates %s", lineNumber, key) + } + if value == "" || strings.ContainsAny(value, " \t$`;&|(){}") { + return environment{}, fmt.Errorf("line %d has non-literal value for %s", lineNumber, key) + } + values[key] = value + } + if err := scanner.Err(); err != nil { + return environment{}, fmt.Errorf("scan file: %w", err) + } + + for _, key := range []string{"CLI_VERSION", "PRODUCT_CODE", "SHORT_NAME", "API_VERSION"} { + if values[key] == "" { + return environment{}, fmt.Errorf("required key %s is missing", key) + } + } + cliVersion, err := normalizeVersion(values["CLI_VERSION"]) + if err != nil { + return environment{}, fmt.Errorf("invalid CLI_VERSION: %w", err) + } + if cliVersion != marker.Version { + return environment{}, fmt.Errorf("marker version %q does not match CLI_VERSION %q", marker.Version, cliVersion) + } + if !projectNamePattern.MatchString(values["PRODUCT_CODE"]) { + return environment{}, fmt.Errorf("PRODUCT_CODE %q does not match %s", values["PRODUCT_CODE"], projectNamePattern) + } + if !projectNamePattern.MatchString(values["SHORT_NAME"]) { + return environment{}, fmt.Errorf("SHORT_NAME %q does not match %s", values["SHORT_NAME"], projectNamePattern) + } + if !apiVersionPattern.MatchString(values["API_VERSION"]) { + return environment{}, fmt.Errorf("API_VERSION %q does not match %s", values["API_VERSION"], apiVersionPattern) + } + + return environment{ + Marker: marker, + CLIVersion: cliVersion, + ProductCode: values["PRODUCT_CODE"], + ShortName: values["SHORT_NAME"], + APIVersion: values["API_VERSION"], + }, nil +} + +func readRegularFile(path string, limit int64) ([]byte, error) { + pathInfo, err := os.Lstat(path) + if err != nil { + return nil, err + } + if pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.Mode().IsRegular() { + return nil, fmt.Errorf("%s must be a regular file, not a symlink", path) + } + if pathInfo.Size() > limit { + return nil, fmt.Errorf("%s exceeds the %d-byte limit", path, limit) + } + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + openedInfo, err := file.Stat() + if err != nil { + return nil, err + } + if !openedInfo.Mode().IsRegular() || !os.SameFile(pathInfo, openedInfo) { + return nil, fmt.Errorf("%s changed while it was being opened", path) + } + body, err := io.ReadAll(io.LimitReader(file, limit+1)) + if err != nil { + return nil, err + } + if int64(len(body)) > limit { + return nil, fmt.Errorf("%s exceeds the %d-byte limit", path, limit) + } + return body, nil +} diff --git a/internal/projectmigrate/project_test.go b/internal/projectmigrate/project_test.go new file mode 100644 index 0000000..84f7b02 --- /dev/null +++ b/internal/projectmigrate/project_test.go @@ -0,0 +1,114 @@ +package projectmigrate + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" +) + +const validEnvironment = `# Code generated by "grpc-kit-cli/0.3.9-beta.1". DO NOT EDIT. +# +# identity +CLI_VERSION=0.3.9-beta.1 +PRODUCT_CODE=oneops +SHORT_NAME=spider +API_VERSION=v1 +APPNAME=${PRODUCT_CODE}-${SHORT_NAME}-${API_VERSION} +SERVICE_CODE=${SHORT_NAME}.${API_VERSION}.${PRODUCT_CODE} +` + +func TestLoadProject(t *testing.T) { + root := writeProject(t, validEnvironment, "module git.lmq.io/kaopuvm/spider\n\ngo 1.25.0\n") + + project, err := LoadProject(root) + if err != nil { + t.Fatalf("LoadProject() error = %v", err) + } + if project.Root != root { + t.Fatalf("Root = %q, want %q", project.Root, root) + } + if project.ModulePath != "git.lmq.io/kaopuvm/spider" || + project.SourceCLIVersion != "0.3.9-beta.1" || + project.SourceFamily != "v0.3.9-beta.1" || + project.ProductCode != "oneops" || + project.ShortName != "spider" || + project.APIVersion != "v1" { + t.Fatalf("LoadProject() = %#v", project) + } +} + +func TestLoadProjectRejectsInvalidEnvironment(t *testing.T) { + tests := []struct { + name string + replace string + with string + want string + }{ + {name: "marker mismatch", replace: "CLI_VERSION=0.3.9-beta.1", with: "CLI_VERSION=0.3.8", want: "does not match"}, + {name: "duplicate", replace: "PRODUCT_CODE=oneops", with: "PRODUCT_CODE=oneops\nPRODUCT_CODE=oneops", want: "duplicates PRODUCT_CODE"}, + {name: "shell expansion", replace: "SHORT_NAME=spider", with: "SHORT_NAME=$(hostname)", want: "non-literal"}, + {name: "unsupported assignment", replace: "API_VERSION=v1", with: "API_VERSION=v1\nEXTRA=value", want: "unsupported key"}, + {name: "invalid product", replace: "PRODUCT_CODE=oneops", with: "PRODUCT_CODE=OneOps", want: "PRODUCT_CODE"}, + {name: "invalid API version", replace: "API_VERSION=v1", with: "API_VERSION=latest", want: "API_VERSION"}, + {name: "changed derived value", replace: "APPNAME=${PRODUCT_CODE}-${SHORT_NAME}-${API_VERSION}", with: "APPNAME=oneops-spider-v1", want: "unexpected derived value"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + environment := strings.Replace(validEnvironment, test.replace, test.with, 1) + root := writeProject(t, environment, "module git.lmq.io/kaopuvm/spider\n") + _, err := LoadProject(root) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("LoadProject() error = %v, want containing %q", err, test.want) + } + }) + } +} + +func TestLoadProjectRejectsInvalidGoMod(t *testing.T) { + root := writeProject(t, validEnvironment, "go 1.25.0\n") + _, err := LoadProject(root) + if err == nil || !strings.Contains(err.Error(), "module path is missing") { + t.Fatalf("LoadProject() error = %v, want missing module path", err) + } +} + +func TestLoadProjectRejectsSymlinkedIdentityFile(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("symlink behavior requires elevated privileges on some Windows hosts") + } + root := writeProject(t, validEnvironment, "module git.lmq.io/kaopuvm/spider\n") + target := filepath.Join(root, "actual-env") + if err := os.WriteFile(target, []byte(validEnvironment), 0o600); err != nil { + t.Fatal(err) + } + environmentPath := filepath.Join(root, "scripts", "env") + if err := os.Remove(environmentPath); err != nil { + t.Fatal(err) + } + if err := os.Symlink(target, environmentPath); err != nil { + t.Fatal(err) + } + + _, err := LoadProject(root) + if err == nil || !strings.Contains(err.Error(), "not a symlink") { + t.Fatalf("LoadProject() error = %v, want symlink rejection", err) + } +} + +func writeProject(t *testing.T, environment, goMod string) string { + t.Helper() + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "scripts"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "scripts", "env"), []byte(environment), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte(goMod), 0o600); err != nil { + t.Fatal(err) + } + return root +} diff --git a/internal/projectmigrate/source_fixture.go b/internal/projectmigrate/source_fixture.go new file mode 100644 index 0000000..979e0d1 --- /dev/null +++ b/internal/projectmigrate/source_fixture.go @@ -0,0 +1,94 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "crypto/sha256" + "fmt" +) + +const ( + cliV038FixtureCommit = "1270ce7cccf60ae3d9358d24cb48e84097b36c06" + spiderFixtureCommit = "a5af60793e996f8072d9c9444287c4059784b1bd" + legacyPublicEmbedPath = "public/embed.go" + legacyPublicEmbedSHA256 = "43e669ebe9a037964f803ebe000e62f6c25e1876e2b3e2d1ca12f4d84444f040" +) + +// SourceFixture records the immutable evidence used to select compatibility +// assets. SHA-256 values are over the exact Git blob contents at Commit. +type SourceFixture struct { + Name string + Commit string + ProjectCLIVersion string + Files map[string]string +} + +var spiderSourceFixture = SourceFixture{ + Name: "spider", + Commit: spiderFixtureCommit, + ProjectCLIVersion: "0.3.9-beta.1", + Files: map[string]string{ + "scripts/env": "ef67ba7a27e22429a52df0a4af61fac5ee983d4e0b5931c147372355104e6487", + "cmd/server/main.go": "8f166ff22d8e517fbd07ff65b024a01ed338f5e05c5d0ca053d27c88b2ebf77f", + "handler/microservice.go": "42f27c20a3648187dbb328c87c9afe8d227ca72c7db73092ff482a83e0ac6125", + "handler/rpc_internal.go": "7f81c06a15ad1cc6eeda776d0379c1a78e25cde1525345e08e3b5508604dfac4", + "handler/shutdown.go": "378dfa53f5658540c1c3e9514aa46d2a75437ced2f27ab2658dc5b964f5038ce", + legacyPublicEmbedPath: legacyPublicEmbedSHA256, + }, +} + +var cliV038SourceFixture = SourceFixture{ + Name: "cli-v0.3.8", + Commit: cliV038FixtureCommit, + ProjectCLIVersion: "0.3.8", + Files: map[string]string{ + "scripts/env": "7b607e4fad4c595cb55971ddf7dec6637f4ed9519153c7b648b97ffe527ca3f8", + "cmd/server/main.go": "3a6cf1f1562bc53ab71d83f61033ba4ea7ffc654ab35c0680320e4ea8db625ae", + "handler/microservice.go": "77b157cb720cb5920ca761d208fd4e3b9eeecfa5cecf9fe8143b89f20459c453", + "handler/register.go": "94ed7f786958baab11abdb39c55d4225c1229b9e6e849e0f611e5b5d2c237b33", + "handler/rpc_internal.go": "4c11ed29dcd38ca5906b185459b47b6742ef3b4df0218b65acc7a923f577d509", + "handler/shutdown.go": "c10dd197feaba118b5340f9d1c50c0f049477a1c6f447b8c9d3b4e1d84220182", + }, +} + +// CLIV038SourceFixture returns a defensive copy of the official v0.3.8 +// template evidence. +func CLIV038SourceFixture() SourceFixture { + return cloneSourceFixture(cliV038SourceFixture) +} + +// SpiderSourceFixture returns a defensive copy of the frozen source evidence. +func SpiderSourceFixture() SourceFixture { + return cloneSourceFixture(spiderSourceFixture) +} + +func cloneSourceFixture(source SourceFixture) SourceFixture { + fixture := source + fixture.Files = make(map[string]string, len(source.Files)) + for path, digest := range source.Files { + fixture.Files[path] = digest + } + return fixture +} + +// IsKnownLegacyMarkerFile recognizes only the exact frozen public/embed.go +// output affected by the historical non-.tmpl generator bug. +func IsKnownLegacyMarkerFile(relativePath string, body []byte) bool { + if relativePath != legacyPublicEmbedPath || firstLine(body) != `// Code generated by "grpc-kit-cli/{{ .Global.ReleaseVersion }}". DO NOT EDIT.` { + return false + } + digest := sha256.Sum256(body) + return fmt.Sprintf("%x", digest) == legacyPublicEmbedSHA256 +} diff --git a/internal/projectmigrate/source_fixture_test.go b/internal/projectmigrate/source_fixture_test.go new file mode 100644 index 0000000..24250a7 --- /dev/null +++ b/internal/projectmigrate/source_fixture_test.go @@ -0,0 +1,51 @@ +package projectmigrate + +import "testing" + +const legacyPublicEmbedFixture = `// Code generated by "grpc-kit-cli/{{ .Global.ReleaseVersion }}". DO NOT EDIT. + +package public + +import ( + "embed" +) + +//go:embed admin/* +//go:embed openapi/* +//go:embed webroot/* +var Assets embed.FS +` + +func TestSpiderSourceFixture(t *testing.T) { + fixture := SpiderSourceFixture() + if fixture.Commit != spiderFixtureCommit || fixture.ProjectCLIVersion != "0.3.9-beta.1" || len(fixture.Files) != 6 { + t.Fatalf("SpiderSourceFixture() = %#v", fixture) + } + fixture.Files[legacyPublicEmbedPath] = "changed" + if SpiderSourceFixture().Files[legacyPublicEmbedPath] != legacyPublicEmbedSHA256 { + t.Fatal("SpiderSourceFixture returned mutable package state") + } +} + +func TestCLIV038SourceFixture(t *testing.T) { + fixture := CLIV038SourceFixture() + if fixture.Commit != cliV038FixtureCommit || fixture.ProjectCLIVersion != "0.3.8" || len(fixture.Files) != 6 { + t.Fatalf("CLIV038SourceFixture() = %#v", fixture) + } + fixture.Files["scripts/env"] = "changed" + if CLIV038SourceFixture().Files["scripts/env"] == "changed" { + t.Fatal("CLIV038SourceFixture returned mutable package state") + } +} + +func TestIsKnownLegacyMarkerFile(t *testing.T) { + if !IsKnownLegacyMarkerFile(legacyPublicEmbedPath, []byte(legacyPublicEmbedFixture)) { + t.Fatal("exact frozen fixture was not recognized") + } + if IsKnownLegacyMarkerFile("other/embed.go", []byte(legacyPublicEmbedFixture)) { + t.Fatal("fixture was recognized at another path") + } + if IsKnownLegacyMarkerFile(legacyPublicEmbedPath, []byte(legacyPublicEmbedFixture+"\n")) { + t.Fatal("modified fixture was recognized") + } +} diff --git a/internal/projectmigrate/target.go b/internal/projectmigrate/target.go new file mode 100644 index 0000000..8df55c4 --- /dev/null +++ b/internal/projectmigrate/target.go @@ -0,0 +1,22 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +// The current migration target is deliberately independent from the CLI +// release version written to managed-file markers. +const ( + currentTargetPkgVersion = "v0.5.0" + currentTargetGoVersion = "1.25.13" +) diff --git a/internal/projectmigrate/version.go b/internal/projectmigrate/version.go new file mode 100644 index 0000000..819bab5 --- /dev/null +++ b/internal/projectmigrate/version.go @@ -0,0 +1,68 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package projectmigrate + +import ( + "fmt" + "strings" + + "golang.org/x/mod/semver" +) + +// ValidateTargetCLIVersion accepts only a stable, release-build semantic +// version and returns the marker representation without a leading v. +func ValidateTargetCLIVersion(raw string) (string, error) { + version, err := normalizeTargetCLIVersion(raw) + if err != nil { + return "", err + } + canonical := "v" + version + if semver.Prerelease(canonical) != "" { + return "", fmt.Errorf("CLI release version %q is a prerelease", raw) + } + + return version, nil +} + +// normalizeTargetCLIVersion accepts a non-zero canonical version for preview. +// Apply adds the stronger stable-release requirement above. +func normalizeTargetCLIVersion(raw string) (string, error) { + version, err := normalizeVersion(raw) + if err != nil { + return "", err + } + if semver.Compare("v"+version, "v0.0.0") <= 0 { + return "", fmt.Errorf("CLI release version %q is a development default", raw) + } + return version, nil +} + +func normalizeVersion(raw string) (string, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "", fmt.Errorf("CLI release version was not injected") + } + if trimmed != raw { + return "", fmt.Errorf("CLI release version %q contains surrounding whitespace", raw) + } + canonical := trimmed + if !strings.HasPrefix(canonical, "v") { + canonical = "v" + canonical + } + if !semver.IsValid(canonical) || semver.Canonical(canonical) != canonical { + return "", fmt.Errorf("CLI release version %q is not canonical semantic version", raw) + } + return strings.TrimPrefix(canonical, "v"), nil +} diff --git a/internal/projectmigrate/version_test.go b/internal/projectmigrate/version_test.go new file mode 100644 index 0000000..acba215 --- /dev/null +++ b/internal/projectmigrate/version_test.go @@ -0,0 +1,32 @@ +package projectmigrate + +import "testing" + +func TestValidateTargetCLIVersion(t *testing.T) { + tests := []struct { + name string + input string + want string + wantErr bool + }{ + {name: "without v prefix", input: "0.4.0", want: "0.4.0"}, + {name: "with v prefix", input: "v0.4.0", want: "0.4.0"}, + {name: "empty", input: "", wantErr: true}, + {name: "development default", input: "0.0.0", wantErr: true}, + {name: "prerelease", input: "0.3.9-beta.1", wantErr: true}, + {name: "non canonical", input: "0.4", wantErr: true}, + {name: "build metadata", input: "0.4.0+local", wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ValidateTargetCLIVersion(test.input) + if (err != nil) != test.wantErr { + t.Fatalf("ValidateTargetCLIVersion(%q) error = %v, wantErr %v", test.input, err, test.wantErr) + } + if got != test.want { + t.Fatalf("ValidateTargetCLIVersion(%q) = %q, want %q", test.input, got, test.want) + } + }) + } +} From 17fe7648683ee330cbb286a7ef1f57f3b59f710d Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 16:57:00 +0800 Subject: [PATCH 46/57] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20maintain-pro?= =?UTF-8?q?ject-migration=20=E7=BB=B4=E6=8A=A4=E6=8A=80=E8=83=BD=E4=B8=8E?= =?UTF-8?q?=20SOP=20=E5=B9=B6=E6=B7=BB=E5=8A=A0=20AGENTS=20=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../maintain-project-migration/SKILL.md | 26 ++++++ .../references/sop.md | 79 +++++++++++++++++++ AGENTS.md | 7 ++ 3 files changed, 112 insertions(+) create mode 100644 .agents/skills/maintain-project-migration/SKILL.md create mode 100644 .agents/skills/maintain-project-migration/references/sop.md create mode 100644 AGENTS.md diff --git a/.agents/skills/maintain-project-migration/SKILL.md b/.agents/skills/maintain-project-migration/SKILL.md new file mode 100644 index 0000000..b6d467f --- /dev/null +++ b/.agents/skills/maintain-project-migration/SKILL.md @@ -0,0 +1,26 @@ +--- +name: maintain-project-migration +description: 维护 grpc-kit CLI 服务模板、project migrate 兼容资产和正式 pkg 版本矩阵。适用于模板变化、pkg 版本升级、增加来源 fixture、调整 ManualAction 或发布迁移能力;不用于代替业务项目完成人工源码迁移。 +--- + +# 维护项目迁移能力 + +在 `grpc-kit/cli` 仓库根目录工作。开始修改前读取: + +- `../adm/docs/roadmap/cli/01-cli-project-upgrade-feasibility.md` +- `CHANGELOG/CHANGELOG-0.5.md` 及本次目标版本对应的 CHANGELOG +- `internal/projectmigrate/target.go`,以及同目录的资产、来源 fixture、Plan、诊断和兼容测试 +- [维护 SOP](references/sop.md) + +先分类变更,再决定是否触碰旧项目迁移资产。最新 `template/service`、某个历史来源模板和某个 pkg 迁移目标是三个不同对象;不能因为新建项目模板变化就把新功能注入旧项目。 + +始终保持以下约束: + +- `TargetCLIVersion` 只表示执行迁移的正式 CLI 版本;pkg 目标版本独立记录。 +- 只有首行完整 grpc-kit-cli `DO NOT EDIT` marker 的已有普通文件可进入 Change Plan。 +- 不自动修改用户代码、`go.mod`、`go.sum`,不创建、删除、重命名或 chmod 项目文件。 +- 兼容资产只引用来源 fixture 已有的项目内符号;新功能走新建模板或独立 feature 流程。 +- 正式 pkg 测试不使用本地 `replace`,也不使用不可复现的 `@latest`。 +- 不擅自修改 `VERSION`、创建 tag、发布产物或对脏的真实服务执行 apply。 + +完成维护后报告:变更分类、支持的来源与目标版本、资产写集合、最低/候选 pkg 验证结果、未执行的完整项目检查及其原因。 diff --git a/.agents/skills/maintain-project-migration/references/sop.md b/.agents/skills/maintain-project-migration/references/sop.md new file mode 100644 index 0000000..47ce6a2 --- /dev/null +++ b/.agents/skills/maintain-project-migration/references/sop.md @@ -0,0 +1,79 @@ +# project migrate 维护 SOP + +## 1. 先判断变化属于哪一类 + +| 变化 | 默认处理 | +|---|---| +| 仅影响以后 `new` 创建的项目 | 更新 `template/service` 与模板测试;不要修改历史兼容资产 | +| pkg 同一目标族的补丁版本,且对既有托管代码向后兼容 | 保留最低版本基线,增加候选版本兼容测试;不因“版本更新”重渲染资产 | +| pkg 新签名要求托管代码变化,或提高最低 Go 版本 | 建立新的明确迁移目标和资产根;同步 Plan、诊断、测试和文档 | +| 增加可迁移的历史 CLI 来源 | 从 tag 或确认 commit 冻结来源摘要,证明其可复用某一资产族后再登记 | +| 修复生成器缺陷 | 修复最新模板;历史文件只有在路径和全文证据充分时增加窄例外,不放宽 marker | +| 业务文件或扩展点变化 | 产生 ManualAction;除非文件仍满足托管所有权规则,否则不得自动写入 | + +`v0.x` 版本不能只凭版本号假设兼容。先检查正式 tag、目标模块 `go.mod`、CHANGELOG 和实际 API 签名,再确定属于“扩展测试矩阵”还是“新迁移目标”。 + +## 2. 建立可审计的版本事实 + +1. 确认 pkg 目标是已发布 tag,而不是本地分支、伪版本或 CHANGELOG 标题。 +2. 记录 pkg 最低 Go 版本及受影响 API;核对 context、logger、注册、启动和关闭签名。 +3. 区分三个版本:来源 CLI、执行迁移的目标 CLI、目标 pkg。禁止用其中一个推导另外两个。 +4. `internal/projectmigrate/target.go` 是当前迁移目标 pkg/Go 版本的代码内来源;修改目标时仍需用 `rg` 找出模板、资产目录、Makefile 默认值、README 和 CHANGELOG 中的所有引用,避免只改常量或测试字符串。 +5. 如果正式 CLI 版本尚未决定,可以完成 preview 和 fixture 验收,但必须继续拒绝 apply;不得先写一个猜测版本。 + +## 3. 维护模板与兼容资产 + +1. 更新 `template/service` 时,先判断变化是否只服务新项目。MCP、Flow、E2E 等新增能力不得自动进入旧来源资产。 +2. 需要更新旧项目托管代码时,在 `internal/projectmigrate/assets///` 维护独立资产。资产必须: + - 保留完整首行 marker; + - 只使用 `scripts/env` 和 `go.mod` 可恢复的参数; + - 不引用来源 fixture 不存在的本地符号; + - 渲染后无模板表达式残留,并可通过 Go 语法检查。 +3. 新增来源版本时,从发布 tag 或确认 commit 冻结路径和 SHA-256。真实脏工作区只能用于只读调查,不能作为 fixture 来源。 +4. 新增 pkg 迁移目标时保留旧目标资产和回归测试。不要原地把 `v0.5.0` 资产目录解释成 `v0.6.0`。 +5. 用户管理文件中的破坏点进入 `ManualAction`。诊断应提供稳定 code、文件、行号和建议,并排除本次 Change Plan 已整体替换的路径。 + +## 4. 正式 pkg 兼容矩阵 + +当前最低基线使用: + +```shell +make test-compatibility PKG_COMPAT_VERSION=v0.5.0 +``` + +当候选补丁版本发布后,同时执行最低版本和候选版本,例如: + +```shell +make test-compatibility PKG_COMPAT_VERSION=v0.5.0 +make test-compatibility PKG_COMPAT_VERSION=v0.5.8 +``` + +最低版本证明迁移资产没有误用较新 API;候选版本证明向前兼容。命令必须使用正式模块且无本地 `replace`。不要使用 `@latest`,也不要删除最低版本门禁来换取候选版本通过。 + +如果候选版本失败: + +- 属于 pkg 非预期回归:先报告并修复/发布 pkg,不改 CLI 资产掩盖问题; +- 属于明确的新 pkg 契约:建立新迁移目标,更新资产和 ManualAction; +- 只影响新建模板:更新 `template/service`,保持历史迁移资产不变。 + +## 5. 验收顺序 + +1. 运行 `gofmt` 和 `make test`。 +2. 对最低 pkg 和本次候选 pkg 分别运行 `make test-compatibility PKG_COMPAT_VERSION=`。 +3. 对每个支持来源验证 preview:状态、warning、ManualAction 和 Changes 路径必须稳定,preview 前后项目逐字节不变。 +4. 在从真实服务 clean commit 导出的临时 Git fixture 上执行 apply;断言 Git diff 路径严格等于 Plan,且没有 create/delete/mode change。 +5. 再次 preview 必须为 `managed_up_to_date`。 +6. 如果本次声明“完整项目兼容”,还必须在临时 fixture 中完成人工待办并执行该项目的生成、测试和构建。托管资产编译通过不能替代这一步。 +7. 运行 CLI 与文档仓库的 `git diff --check`,更新 README、目标 CHANGELOG 和路线文档中的真实验收结果。 + +## 6. 停止条件 + +出现以下任一情况时,不得宣称可以发布或执行真实项目 apply: + +- pkg 目标没有正式 tag,或正式模块无法获取; +- 来源版本没有发布产物/确认 commit 和冻结摘要; +- 兼容资产需要来源项目不存在的符号或必须创建文件; +- 正式 CLI 版本未决定、为空、`v0.0.0` 或 prerelease; +- 真实项目 Git 不 clean; +- Plan 有 unsupported/conflict,或 compatibility test 失败; +- 只完成托管文件验证,却声称业务项目已经完成编译兼容迁移。 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..65a6954 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# AGENTS.md + +## Repository skills + +- When changing `template/service`, the target grpc-kit/pkg version, `internal/projectmigrate` compatibility assets or diagnostics, or the `test-compatibility` gate, read `.agents/skills/maintain-project-migration/SKILL.md` completely and follow its referenced SOP. +- The migration skill governs CLI maintainer work. Do not copy it into generated service projects; service-level business skills have a different ownership and release lifecycle. + From 112942a62b057e9792c314f77afaf3ed1138f9cf Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 16:57:52 +0800 Subject: [PATCH 47/57] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Makefile=20t?= =?UTF-8?q?est=20=E4=B8=8E=20test-compatibility=20=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20CLI=20=E6=B5=8B=E8=AF=95=E5=8F=8A=E5=86=BB?= =?UTF-8?q?=E7=BB=93=E8=BF=81=E7=A7=BB=E8=B5=84=E4=BA=A7=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Makefile b/Makefile index 5c2883f..2eeec59 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ GOPATH := $(shell ${GO} env GOPATH) GOOS ?= $(shell ${GO} env GOOS) GOARCH ?= $(shell ${GO} env GOARCH) GOBUILD := ${GO} build +PKG_COMPAT_VERSION ?= v0.5.0 # 自动化版本号 GIT_COMMIT := $(shell git rev-parse HEAD) @@ -46,6 +47,17 @@ docker-build: ## Build docker image with the application. @echo ">> docker build" @docker buildx build --platform linux/amd64,linux/arm64 ./ -t ccr.ccs.tencentyun.com/grpc-kit/cli:${RELEASE_VERSION} --push +##@ Test + +.PHONY: test +test: ## Run the complete CLI package test set. + @${GO} test -mod=readonly ./cmd ./internal/projectmigrate ./template -count=1 + @${GO} vet ./cmd ./internal/projectmigrate ./template + +.PHONY: test-compatibility +test-compatibility: ## Compile frozen migration assets against PKG_COMPAT_VERSION. + @GRPC_KIT_RUN_COMPATIBILITY_TEST=1 GRPC_KIT_COMPAT_PKG_VERSION=${PKG_COMPAT_VERSION} ${GO} test ./internal/projectmigrate -run '^TestCompatibilityAssetsCompileAgainstReleasedPkg$$' -count=1 -v + ##@ Build Dependencies .PHONY: protoc From e9f6f925e0f4fd82e0138a9aa1e5db87728ea67e Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 16:58:06 +0800 Subject: [PATCH 48/57] =?UTF-8?q?refactor:=20version=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=B7=A5=E5=8E=82=E5=87=BD=E6=95=B0=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=B9=B6=E5=A4=84=E7=90=86=20main=20=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E9=94=99=E8=AF=AF=E9=80=80=E5=87=BA=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/version.go | 17 +++++++---------- main.go | 10 ++++++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cmd/version.go b/cmd/version.go index e44c328..a161cfa 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -22,16 +22,13 @@ import ( "github.com/spf13/cobra" ) -// versionCmd represents the version command -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number of grpc-kit-cli", - Long: `All software has versions. This is grpc-kit-cli's.`, - RunE: runFuncVersion, -} - -func init() { - rootCmd.AddCommand(versionCmd) +func newVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version number of grpc-kit-cli", + Long: `All software has versions. This is grpc-kit-cli's.`, + RunE: runFuncVersion, + } } func runFuncVersion(cmd *cobra.Command, args []string) error { diff --git a/main.go b/main.go index 1b99225..c9e6c4c 100644 --- a/main.go +++ b/main.go @@ -14,8 +14,14 @@ package main -import "github.com/grpc-kit/cli/cmd" +import ( + "os" + + "github.com/grpc-kit/cli/cmd" +) func main() { - cmd.Execute() + if err := cmd.Execute(); err != nil { + os.Exit(1) + } } From fc0b95a52fb069bc95e66722f671edcb22ba4a15 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 16:58:22 +0800 Subject: [PATCH 49/57] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=20project=20mi?= =?UTF-8?q?grate=20=E5=91=BD=E4=BB=A4=E7=9A=84=20README=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E8=AF=B4=E6=98=8E=E4=B8=8E=20CHANGELOG=20Added=20?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.5.md | 5 +++++ README.md | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.5.md b/CHANGELOG/CHANGELOG-0.5.md index acba32b..cbeb5da 100644 --- a/CHANGELOG/CHANGELOG-0.5.md +++ b/CHANGELOG/CHANGELOG-0.5.md @@ -11,6 +11,11 @@ ## [Unreleased] +### Added + +- 新增 `grpc-kit-cli project migrate [path]`,默认只读预览旧项目的托管文件变更和人工迁移清单;`--apply` 仅在正式稳定版 CLI、clean Git 工作树和输入摘要复核通过时更新已有托管文件。 +- 首期迁移目标固定为正式 `github.com/grpc-kit/pkg v0.5.0`。命令严格区分 CLI marker 版本和 pkg 版本,不修改 `go.mod`、业务源码及其他用户管理文件,也不提供可能被误解为 CLI 自更新的根级 `upgrade`/`update` alias。 + ### Changed #### grpc-kit/cli 模块 diff --git a/README.md b/README.md index 593167c..5da8d54 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,22 @@ 快速生成微服务模版,为同一产品提供统一的治理方式,提高多人协作效率 +## 迁移已有项目 + +`project migrate` 用于把旧项目中由 grpc-kit-cli 管理的文件迁移到 `pkg v0.5.0` 兼容版本。默认只预览计划和 diff,不写文件: + +```shell +grpc-kit-cli project migrate ./service +``` + +确认项目位于 clean Git 工作树、diff 符合预期,并完成输出中的人工迁移项后,可使用正式稳定版 CLI 应用托管文件变更: + +```shell +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` 语义。 + ## 生成代码并运行(本机方式) ### 创建应用模版 From d50edf4f01470744be69856c503b7f78da8aa80d Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 16:58:48 +0800 Subject: [PATCH 50/57] =?UTF-8?q?chore:=20=E5=B0=86=20go-difflib=20?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E4=B8=BA=E7=9B=B4=E6=8E=A5=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=B9=B6=E6=96=B0=E5=A2=9E=20golang.org/x/mod=20=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 3 ++- go.sum | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6a63b7a..8b19634 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,10 @@ require ( entgo.io/ent v0.14.5 github.com/grpc-kit/pkg v0.2.4 github.com/mitchellh/go-homedir v1.1.0 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 + golang.org/x/mod v0.28.0 ) require ( @@ -19,7 +21,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect diff --git a/go.sum b/go.sum index 0770174..5e6f90c 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= From 96a5bdd87077b20db880a1686d5a27bb8e240eda Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 17:44:47 +0800 Subject: [PATCH 51/57] =?UTF-8?q?feat:=20=E5=BC=BA=E5=8C=96=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=94=9F=E6=88=90=E4=B8=8E=E7=89=88=E6=9C=AC=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 22 ++-- cmd/new.go | 30 +++-- cmd/new_test.go | 56 +++++++++ cmd/project_migrate.go | 6 +- cmd/project_migrate_test.go | 32 ++--- cmd/root_test.go | 28 ++++- cmd/version.go | 33 +++-- cmd/version_test.go | 50 ++++++++ go.mod | 11 +- go.sum | 47 +------- internal/buildinfo/buildinfo.go | 80 +++++++++++++ internal/buildinfo/buildinfo_test.go | 35 ++++++ template/compatibility_test.go | 97 +++++++++++++++ template/service.go | 113 ++++++++++++++++-- template/service/go.mod.tmpl | 2 +- ...rnal_test.go => rpc_internal_test.go.tmpl} | 0 .../modeler/ent/{entc.go => entc.go.tmpl} | 0 .../ent/{generate.go => generate.go.tmpl} | 0 .../ent/schema/{demo.go => demo.go.tmpl} | 0 ...g_test.go => independent_cfg_test.go.tmpl} | 0 .../e2e/client/{client.go => client.go.tmpl} | 0 .../e2e/fixture/{main.go => main.go.tmpl} | 0 template/service_test.go | 95 ++++++++++++++- template/template.go | 1 + 24 files changed, 622 insertions(+), 116 deletions(-) create mode 100644 cmd/new_test.go create mode 100644 cmd/version_test.go create mode 100644 internal/buildinfo/buildinfo.go create mode 100644 internal/buildinfo/buildinfo_test.go create mode 100644 template/compatibility_test.go rename template/service/handler/{rpc_internal_test.go => rpc_internal_test.go.tmpl} (100%) rename template/service/modeler/ent/{entc.go => entc.go.tmpl} (100%) rename template/service/modeler/ent/{generate.go => generate.go.tmpl} (100%) rename template/service/modeler/ent/schema/{demo.go => demo.go.tmpl} (100%) rename template/service/modeler/{independent_cfg_test.go => independent_cfg_test.go.tmpl} (100%) rename template/service/test/e2e/client/{client.go => client.go.tmpl} (100%) rename template/service/test/e2e/fixture/{main.go => main.go.tmpl} (100%) diff --git a/Makefile b/Makefile index 2eeec59..3f56b44 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ GORUN := ${GO} run GOPATH := $(shell ${GO} env GOPATH) GOOS ?= $(shell ${GO} env GOOS) GOARCH ?= $(shell ${GO} env GOARCH) -GOBUILD := ${GO} build +GOBUILD := ${GO} build -mod=readonly PKG_COMPAT_VERSION ?= v0.5.0 # 自动化版本号 @@ -13,12 +13,12 @@ GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) BUILD_DATE := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') COMMIT_DATE := $(shell git --no-pager log -1 --format='%ct') RELEASE_VERSION ?= $(shell cat VERSION) -BUILD_LD_FLAGS := "-X 'github.com/grpc-kit/pkg/vars.Appname=grpc-kit-cli' \ - -X 'github.com/grpc-kit/pkg/vars.GitCommit=${GIT_COMMIT}' \ - -X 'github.com/grpc-kit/pkg/vars.GitBranch=${GIT_BRANCH}' \ - -X 'github.com/grpc-kit/pkg/vars.BuildDate=${BUILD_DATE}' \ - -X 'github.com/grpc-kit/pkg/vars.CommitUnixTime=${COMMIT_DATE}' \ - -X 'github.com/grpc-kit/pkg/vars.ReleaseVersion=${RELEASE_VERSION}'" +BUILD_LD_FLAGS := "-X 'github.com/grpc-kit/cli/internal/buildinfo.AppName=grpc-kit-cli' \ + -X 'github.com/grpc-kit/cli/internal/buildinfo.GitCommit=${GIT_COMMIT}' \ + -X 'github.com/grpc-kit/cli/internal/buildinfo.GitBranch=${GIT_BRANCH}' \ + -X 'github.com/grpc-kit/cli/internal/buildinfo.BuildDate=${BUILD_DATE}' \ + -X 'github.com/grpc-kit/cli/internal/buildinfo.CommitUnixTime=${COMMIT_DATE}' \ + -X 'github.com/grpc-kit/cli/internal/buildinfo.ReleaseVersion=${RELEASE_VERSION}'" # 自定义变量 BUILD_GOOS ?= $(shell ${GO} env GOOS) @@ -51,13 +51,17 @@ docker-build: ## Build docker image with the application. .PHONY: test test: ## Run the complete CLI package test set. - @${GO} test -mod=readonly ./cmd ./internal/projectmigrate ./template -count=1 - @${GO} vet ./cmd ./internal/projectmigrate ./template + @${GO} test -mod=readonly ./... -count=1 + @${GO} vet ./... .PHONY: test-compatibility test-compatibility: ## Compile frozen migration assets against PKG_COMPAT_VERSION. @GRPC_KIT_RUN_COMPATIBILITY_TEST=1 GRPC_KIT_COMPAT_PKG_VERSION=${PKG_COMPAT_VERSION} ${GO} test ./internal/projectmigrate -run '^TestCompatibilityAssetsCompileAgainstReleasedPkg$$' -count=1 -v +.PHONY: test-template-compatibility +test-template-compatibility: ## Generate the latest service template and compile it against the released pkg. + @GRPC_KIT_RUN_TEMPLATE_COMPATIBILITY_TEST=1 ${GO} test ./template -run '^TestGeneratedServiceCompilesAgainstReleasedPkg$$' -count=1 -v -timeout 20m + ##@ Build Dependencies .PHONY: protoc diff --git a/cmd/new.go b/cmd/new.go index c979377..1d76a2a 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -19,12 +19,17 @@ import ( "regexp" "strings" + "github.com/grpc-kit/cli/internal/buildinfo" "github.com/grpc-kit/cli/template" - "github.com/grpc-kit/pkg/vars" "github.com/spf13/cobra" ) +type newOptions struct { + output string +} + func newNewCommand() *cobra.Command { + options := newOptions{} cmd := &cobra.Command{ Use: "new", Short: "Create a new code templates for your product", @@ -33,7 +38,10 @@ it is newly created. For example: ./grpc-kit-cli new -t service -o default -p opsaid -s test1 `, - RunE: runFuncNew, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runFuncNew(cmd, options) + }, SilenceUsage: true, SilenceErrors: false, } @@ -43,11 +51,13 @@ it is newly created. For example: "api-version", "v1", "api version, like: v1alpha1, v1beta1, v1") cmd.Flags().StringVarP(&cfgType.Template.Service.Organization, "organization", "o", "grpc-kit", "the company or department where the product is located") + cmd.Flags().StringVarP(&options.output, + "output", "d", "", "output directory (default: short-name)") return cmd } -func runFuncNew(cmd *cobra.Command, args []string) error { +func runFuncNew(cmd *cobra.Command, options newOptions) error { re := regexp.MustCompile(`^([a-z0-9]){4,}$`) // 必须存在的参数校验 @@ -58,7 +68,7 @@ func runFuncNew(cmd *cobra.Command, args []string) error { return fmt.Errorf("must set -s or --short-name") } if cfgType.Global.ReleaseVersion == "" { - cfgType.Global.ReleaseVersion = vars.ReleaseVersion + cfgType.Global.ReleaseVersion = buildinfo.ReleaseVersion if cfgType.Global.ReleaseVersion == "" { cfgType.Global.ReleaseVersion = "v0.0.0" } @@ -102,16 +112,20 @@ func runFuncNew(cmd *cobra.Command, args []string) error { cfgType.Global.ShortName, cfgType.Template.Service.APIVersion, cfgType.Global.ProductCode) } - fmt.Println( - fmt.Sprintf("Generate code templates type: %v, use git repos: %v", - cfgType.Global.Type, cfgType.Global.Repository)) + if _, err := fmt.Fprintf(cmd.OutOrStdout(), "Generate code templates type: %v, use git repos: %v\n", + cfgType.Global.Type, cfgType.Global.Repository); err != nil { + return err + } t, err := template.New(cfgType) if err != nil { return err } - if err := t.Generate(); err != nil { + if options.output == "" { + options.output = cfgType.Global.ShortName + } + if err := t.GenerateTo(options.output); err != nil { return err } diff --git a/cmd/new_test.go b/cmd/new_test.go new file mode 100644 index 0000000..97a1b50 --- /dev/null +++ b/cmd/new_test.go @@ -0,0 +1,56 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/grpc-kit/cli/config" + "github.com/grpc-kit/cli/template" +) + +func TestNewCommandGeneratesToRequestedOutput(t *testing.T) { + previous := cfgType + cfgType = config.Config{ + Global: config.GlobalConfig{ + Type: template.TypeService, + ProductCode: "demo", + ShortName: "echo", + GitDomain: "example.com", + }, + } + t.Cleanup(func() { cfgType = previous }) + + outputPath := filepath.Join(t.TempDir(), "custom-output") + cmd := newNewCommand() + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetArgs([]string{"--output", outputPath}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + if _, err := os.Stat(filepath.Join(outputPath, "go.mod")); err != nil { + t.Fatalf("generated go.mod: %v", err) + } + if !strings.Contains(output.String(), "Generate code templates type: service") { + t.Fatalf("output = %q", output.String()) + } +} + +func TestNewCommandRejectsExtraArguments(t *testing.T) { + cmd := newNewCommand() + cmd.SetArgs([]string{"unexpected"}) + if err := cmd.Execute(); err == nil { + t.Fatal("Execute() error = nil, want argument validation error") + } +} diff --git a/cmd/project_migrate.go b/cmd/project_migrate.go index ae64968..1c81800 100644 --- a/cmd/project_migrate.go +++ b/cmd/project_migrate.go @@ -17,8 +17,8 @@ package cmd import ( "fmt" + "github.com/grpc-kit/cli/internal/buildinfo" "github.com/grpc-kit/cli/internal/projectmigrate" - "github.com/grpc-kit/pkg/vars" "github.com/spf13/cobra" ) @@ -40,7 +40,7 @@ func newProjectMigrateCommand() *cobra.Command { } if options.apply { - if _, err := projectmigrate.ValidateTargetCLIVersion(vars.ReleaseVersion); err != nil { + if _, err := projectmigrate.ValidateTargetCLIVersion(buildinfo.ReleaseVersion); err != nil { return fmt.Errorf("refuse --apply: %w", err) } } @@ -53,7 +53,7 @@ func newProjectMigrateCommand() *cobra.Command { } func runFuncProjectMigrate(cmd *cobra.Command, projectPath string, options projectMigrateOptions) error { - plan, err := projectmigrate.BuildPlan(projectPath, vars.ReleaseVersion) + plan, err := projectmigrate.BuildPlan(projectPath, buildinfo.ReleaseVersion) if err != nil { return err } diff --git a/cmd/project_migrate_test.go b/cmd/project_migrate_test.go index 63125b0..13f9113 100644 --- a/cmd/project_migrate_test.go +++ b/cmd/project_migrate_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/grpc-kit/pkg/vars" + "github.com/grpc-kit/cli/internal/buildinfo" ) func TestProjectMigrateRejectsExtraArguments(t *testing.T) { @@ -24,9 +24,9 @@ func TestProjectMigrateRejectsExtraArguments(t *testing.T) { } func TestProjectMigratePreviewAllowsPrereleaseVersion(t *testing.T) { - previous := vars.ReleaseVersion - vars.ReleaseVersion = "0.3.9-beta.1" - t.Cleanup(func() { vars.ReleaseVersion = previous }) + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) cmd := newRootCommand(func() error { return nil }) var output bytes.Buffer @@ -43,9 +43,9 @@ func TestProjectMigratePreviewAllowsPrereleaseVersion(t *testing.T) { } func TestProjectMigrateRejectsInvalidProject(t *testing.T) { - previous := vars.ReleaseVersion - vars.ReleaseVersion = "0.3.9-beta.1" - t.Cleanup(func() { vars.ReleaseVersion = previous }) + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) cmd := newRootCommand(func() error { return nil }) cmd.SetOut(io.Discard) @@ -59,9 +59,9 @@ func TestProjectMigrateRejectsInvalidProject(t *testing.T) { } func TestProjectMigrateRejectsUnsupportedSource(t *testing.T) { - previous := vars.ReleaseVersion - vars.ReleaseVersion = "0.3.9-beta.1" - t.Cleanup(func() { vars.ReleaseVersion = previous }) + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) root := writeCommandTestProject(t) environmentPath := filepath.Join(root, "scripts", "env") @@ -86,9 +86,9 @@ func TestProjectMigrateRejectsUnsupportedSource(t *testing.T) { } func TestProjectMigrateApplyRejectsPrereleaseBuild(t *testing.T) { - previous := vars.ReleaseVersion - vars.ReleaseVersion = "0.3.9-beta.1" - t.Cleanup(func() { vars.ReleaseVersion = previous }) + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) cmd := newRootCommand(func() error { return nil }) cmd.SetOut(io.Discard) @@ -102,9 +102,9 @@ func TestProjectMigrateApplyRejectsPrereleaseBuild(t *testing.T) { } func TestProjectMigrateApply(t *testing.T) { - previous := vars.ReleaseVersion - vars.ReleaseVersion = "0.4.0" - t.Cleanup(func() { vars.ReleaseVersion = previous }) + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "0.4.0" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) root := writeCommandTestProject(t) for _, args := range [][]string{ diff --git a/cmd/root_test.go b/cmd/root_test.go index 61c9520..c8809a2 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -1,13 +1,33 @@ package cmd import ( + "bytes" "errors" "io" "testing" - "github.com/grpc-kit/pkg/vars" + "github.com/grpc-kit/cli/internal/buildinfo" ) +func TestVersionCommandSkipsUserConfig(t *testing.T) { + loaded := false + cmd := newRootCommand(func() error { + loaded = true + return errors.New("user config must not be loaded") + }) + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetErr(io.Discard) + cmd.SetArgs([]string{"version", "--short"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute() error = %v", err) + } + if loaded { + t.Fatal("version command loaded user generation config") + } +} + func TestRootCommandReturnsCommandErrors(t *testing.T) { cmd := newRootCommand(func() error { return nil }) cmd.SetOut(io.Discard) @@ -20,9 +40,9 @@ func TestRootCommandReturnsCommandErrors(t *testing.T) { } func TestProjectCommandsSkipUserConfig(t *testing.T) { - previous := vars.ReleaseVersion - vars.ReleaseVersion = "0.3.9-beta.1" - t.Cleanup(func() { vars.ReleaseVersion = previous }) + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "0.3.9-beta.1" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) configErr := errors.New("user config must not be loaded") loaded := false diff --git a/cmd/version.go b/cmd/version.go index a161cfa..2812a40 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -18,25 +18,38 @@ import ( "encoding/json" "fmt" - "github.com/grpc-kit/pkg/vars" + "github.com/grpc-kit/cli/internal/buildinfo" "github.com/spf13/cobra" ) func newVersionCommand() *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Print the version number of grpc-kit-cli", - Long: `All software has versions. This is grpc-kit-cli's.`, - RunE: runFuncVersion, + short := false + cmd := &cobra.Command{ + Use: "version", + Short: "Print the version number of grpc-kit-cli", + Long: `All software has versions. This is grpc-kit-cli's.`, + Args: cobra.NoArgs, + Annotations: map[string]string{skipUserConfigAnnotation: "true"}, + RunE: func(cmd *cobra.Command, args []string) error { + return runFuncVersion(cmd, short) + }, } + cmd.Flags().BoolVar(&short, "short", false, "print only the release version") + return cmd } -func runFuncVersion(cmd *cobra.Command, args []string) error { - rawBody, err := json.MarshalIndent(vars.GetVersion(), "", " ") +func runFuncVersion(cmd *cobra.Command, short bool) error { + info := buildinfo.Get() + if short { + _, err := fmt.Fprintln(cmd.OutOrStdout(), info.ReleaseVersion) + return err + } + + rawBody, err := json.MarshalIndent(info, "", " ") if err != nil { return err } - fmt.Println(string(rawBody)) - return nil + _, err = fmt.Fprintln(cmd.OutOrStdout(), string(rawBody)) + return err } diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 0000000..46a0ae6 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,50 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package cmd + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/grpc-kit/cli/internal/buildinfo" +) + +func TestVersionShortUsesCommandOutput(t *testing.T) { + previous := buildinfo.ReleaseVersion + buildinfo.ReleaseVersion = "v0.5.0" + t.Cleanup(func() { buildinfo.ReleaseVersion = previous }) + + cmd := newVersionCommand() + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetArgs([]string{"--short"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + if got, want := output.String(), "v0.5.0\n"; got != want { + t.Fatalf("output = %q, want %q", got, want) + } +} + +func TestVersionJSONUsesStableFields(t *testing.T) { + cmd := newVersionCommand() + var output bytes.Buffer + cmd.SetOut(&output) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + var info buildinfo.Info + if err := json.Unmarshal(output.Bytes(), &info); err != nil { + t.Fatalf("Unmarshal output: %v\n%s", err, output.String()) + } + if info.ReleaseVersion == "" || info.GoVersion == "" || info.Platform == "" { + t.Fatalf("incomplete version output: %+v", info) + } +} diff --git a/go.mod b/go.mod index 8b19634..4c14ce0 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,6 @@ go 1.24.6 toolchain go1.24.7 require ( - entgo.io/ent v0.14.5 - github.com/grpc-kit/pkg v0.2.4 github.com/mitchellh/go-homedir v1.1.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/spf13/cobra v1.10.1 @@ -17,8 +15,7 @@ require ( require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect @@ -28,11 +25,7 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.46.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect - google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 5e6f90c..620294e 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,21 @@ -entgo.io/ent v0.14.5 h1:Rj2WOYJtCkWyFo6a+5wB3EfBRP0rnx1fMk6gGA0UUe4= -entgo.io/ent v0.14.5/go.mod h1:zTzLmWtPvGpmSwtkaayM2cm5m819NdM7z7tYPq3vN0U= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= -github.com/grpc-kit/pkg v0.2.4 h1:tHNB1iVDX6e0T+8mKDshpV+BW4r1fmcDGLEqALOZdII= -github.com/grpc-kit/pkg v0.2.4/go.mod h1:qkK9TaJyPXMFGZ1DrEuO1DibGayu3YOTVTNTMYyWWeU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -57,38 +44,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff h1:8Zg5TdmcbU8A7CXGjGXF1Slqu/nIFCRaR3S5gT2plIA= -google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:dbWfpVPvW/RqafStmRWBUpMN14puDezDMHxNYiRfQu0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go new file mode 100644 index 0000000..1cacc40 --- /dev/null +++ b/internal/buildinfo/buildinfo.go @@ -0,0 +1,80 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package buildinfo exposes metadata injected into the CLI binary at build time. +package buildinfo + +import ( + "fmt" + "runtime" + "strconv" +) + +var ( + AppName string + BuildDate string + GitCommit string + GitBranch string + CLIVersion string + CommitUnixTime string + ReleaseVersion string +) + +// Info is the stable JSON representation printed by grpc-kit-cli version. +type Info struct { + AppName string `json:"appname"` + BuildDate string `json:"build_date"` + GitCommit string `json:"git_commit"` + GitBranch string `json:"git_branch"` + GoVersion string `json:"go_version"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` + CLIVersion string `json:"cli_version"` + CommitUnixTime int64 `json:"commit_unix_time"` + ReleaseVersion string `json:"release_version"` +} + +// Get returns build metadata with deterministic development-build defaults. +func Get() Info { + commitUnixTime, err := strconv.ParseInt(CommitUnixTime, 10, 64) + if err != nil { + commitUnixTime = 0 + } + + info := Info{ + AppName: AppName, + BuildDate: BuildDate, + GitCommit: GitCommit, + GitBranch: GitBranch, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + CLIVersion: CLIVersion, + CommitUnixTime: commitUnixTime, + ReleaseVersion: ReleaseVersion, + } + if info.GitCommit == "" { + info.GitCommit = "1234567890123456789012345678901234567890" + } + if info.BuildDate == "" { + info.BuildDate = "1970-01-01T00:00:00Z" + } + if info.CLIVersion == "" { + info.CLIVersion = "0.0.0" + } + if info.ReleaseVersion == "" { + info.ReleaseVersion = "0.0.0" + } + return info +} diff --git a/internal/buildinfo/buildinfo_test.go b/internal/buildinfo/buildinfo_test.go new file mode 100644 index 0000000..0db4bea --- /dev/null +++ b/internal/buildinfo/buildinfo_test.go @@ -0,0 +1,35 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package buildinfo + +import "testing" + +func TestGetDefaults(t *testing.T) { + previousCommit := GitCommit + previousDate := BuildDate + previousCLI := CLIVersion + previousRelease := ReleaseVersion + previousUnixTime := CommitUnixTime + GitCommit, BuildDate, CLIVersion, ReleaseVersion, CommitUnixTime = "", "", "", "", "invalid" + t.Cleanup(func() { + GitCommit = previousCommit + BuildDate = previousDate + CLIVersion = previousCLI + ReleaseVersion = previousRelease + CommitUnixTime = previousUnixTime + }) + + info := Get() + if info.ReleaseVersion != "0.0.0" || info.CLIVersion != "0.0.0" { + t.Fatalf("Get() versions = %q/%q, want development defaults", info.ReleaseVersion, info.CLIVersion) + } + if info.CommitUnixTime != 0 { + t.Fatalf("Get().CommitUnixTime = %d, want 0", info.CommitUnixTime) + } +} diff --git a/template/compatibility_test.go b/template/compatibility_test.go new file mode 100644 index 0000000..23962ca --- /dev/null +++ b/template/compatibility_test.go @@ -0,0 +1,97 @@ +// Copyright © 2026 The gRPC Kit Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package template + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/grpc-kit/cli/config" + "golang.org/x/mod/modfile" +) + +const runTemplateCompatibilityTestEnv = "GRPC_KIT_RUN_TEMPLATE_COMPATIBILITY_TEST" + +// TestGeneratedServiceCompilesAgainstReleasedPkg is an opt-in release gate. +// It intentionally uses the public module proxy and rejects local replacements. +func TestGeneratedServiceCompilesAgainstReleasedPkg(t *testing.T) { + if os.Getenv(runTemplateCompatibilityTestEnv) != "1" { + t.Skip("set " + runTemplateCompatibilityTestEnv + "=1 to run the generated-service release gate") + } + + root := filepath.Join(t.TempDir(), "echo") + generator, err := New(config.Config{ + Global: config.GlobalConfig{ + Type: TypeService, + APIEndpoint: "api", + ProductCode: "demo", + ShortName: "echo", + ReleaseVersion: "v0.5.0", + Repository: "example.com/grpc-kit/echo", + Organization: "grpc-kit", + Appname: "demo-echo-v1", + ServiceCode: "echo.v1.demo", + ProtoPackage: "grpc_kit.api.demo.echo.v1", + ServiceTitle: "DemoEcho", + }, + Template: config.TemplateConfig{Service: config.TemplateService{APIVersion: "v1"}}, + }) + if err != nil { + t.Fatal(err) + } + if err = generator.GenerateTo(root); err != nil { + t.Fatalf("generate service: %v", err) + } + + moduleBody, err := os.ReadFile(filepath.Join(root, "go.mod")) + if err != nil { + t.Fatal(err) + } + module, err := modfile.Parse("go.mod", moduleBody, nil) + if err != nil { + t.Fatalf("parse generated go.mod: %v", err) + } + if len(module.Replace) != 0 { + t.Fatalf("generated go.mod contains local replacements: %+v", module.Replace) + } + var pkgVersion string + for _, requirement := range module.Require { + if requirement.Mod.Path == "github.com/grpc-kit/pkg" { + pkgVersion = requirement.Mod.Version + } + } + if pkgVersion != "v0.5.0" { + t.Fatalf("generated pkg version = %q, want v0.5.0", pkgVersion) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + gopathOutput, err := exec.CommandContext(ctx, "go", "env", "GOPATH").Output() + if err != nil { + t.Fatalf("go env GOPATH: %v", err) + } + environment := append(os.Environ(), "GOPATH="+strings.TrimSpace(string(gopathOutput)), "GOWORK=off") + for _, command := range [][]string{ + {"make", "generate"}, + {"go", "test", "./...", "-count=1"}, + {"go", "build", "./..."}, + } { + process := exec.CommandContext(ctx, command[0], command[1:]...) + process.Dir = root + process.Env = environment + if output, runErr := process.CombinedOutput(); runErr != nil { + t.Fatalf("%s: %v\n%s", strings.Join(command, " "), runErr, output) + } + } +} diff --git a/template/service.go b/template/service.go index cfda99f..0bbfb2e 100644 --- a/template/service.go +++ b/template/service.go @@ -15,20 +15,27 @@ package template import ( + "bytes" "fmt" "io/fs" "os" - "path" + "path/filepath" "strings" + texttemplate "text/template" "github.com/grpc-kit/cli/config" - "github.com/grpc-kit/pkg/file" ) type templateService struct { config config.Config } +type generatedFile struct { + name string + body []byte + mode fs.FileMode +} + // New 实例化 func newService(c config.Config) (*templateService, error) { return &templateService{config: c}, nil @@ -36,7 +43,61 @@ func newService(c config.Config) (*templateService, error) { // Generate 生产代码模版 func (t *templateService) Generate() error { - err := fs.WalkDir(Assets, ".", func(filePath string, d fs.DirEntry, err error) error { + return t.GenerateTo(t.config.Global.ShortName) +} + +// GenerateTo renders the complete service into a sibling temporary directory +// and publishes it with one rename. An existing output path is never modified. +func (t *templateService) GenerateTo(output string) error { + files, err := t.render() + if err != nil { + return err + } + + if strings.TrimSpace(output) == "" { + return fmt.Errorf("output directory must not be empty") + } + target, err := filepath.Abs(filepath.Clean(output)) + if err != nil { + return fmt.Errorf("resolve output directory %q: %w", output, err) + } + if _, err = os.Lstat(target); err == nil { + return fmt.Errorf("output path already exists: %s", output) + } else if !os.IsNotExist(err) { + return fmt.Errorf("inspect output path %q: %w", output, err) + } + + parent := filepath.Dir(target) + if err = os.MkdirAll(parent, 0o755); err != nil { + return fmt.Errorf("create output parent %q: %w", parent, err) + } + stage, err := os.MkdirTemp(parent, ".grpc-kit-cli-*") + if err != nil { + return fmt.Errorf("create staging directory: %w", err) + } + defer os.RemoveAll(stage) + + for _, file := range files { + destination := filepath.Join(stage, filepath.FromSlash(file.name)) + if err = os.MkdirAll(filepath.Dir(destination), 0o755); err != nil { + return fmt.Errorf("create directory for %s: %w", file.name, err) + } + if err = os.WriteFile(destination, file.body, file.mode); err != nil { + return fmt.Errorf("write %s: %w", file.name, err) + } + } + if err = os.Chmod(stage, 0o755); err != nil { + return fmt.Errorf("set output directory permissions: %w", err) + } + if err = os.Rename(stage, target); err != nil { + return fmt.Errorf("publish output directory %q: %w", output, err) + } + return nil +} + +func (t *templateService) render() ([]generatedFile, error) { + files := make([]generatedFile, 0) + err := fs.WalkDir(Assets, "service", func(filePath string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -54,18 +115,17 @@ func (t *templateService) Generate() error { fileBody := string(content) if strings.HasSuffix(filePath, ".tmpl") { - fileBody, err = file.ParseExecute(fileBody, t.config) + fileBody, err = renderTemplate(fileBody, t.config) if err != nil { return err } } - var filePerm os.FileMode - filePerm = 0666 + filePerm := fs.FileMode(0o666) // 如果是脚本文件,则设置可执行权限 if strings.HasSuffix(filePath, ".sh") || strings.HasSuffix(filePath, ".sh.tmpl") { - filePerm = 0755 + filePerm = 0o755 } fileName := strings.TrimPrefix(filePath, "service/") @@ -76,14 +136,41 @@ func (t *templateService) Generate() error { t.config.Global.ProductCode, t.config.Global.ShortName, t.config.Template.Service.APIVersion, - path.Base(fileName)) + filepath.Base(fileName)) } - return file.WriteString( - path.Join(t.config.Global.ShortName, fileName), - strings.TrimLeft(fileBody, "\n"), - filePerm) + cleanName := filepath.ToSlash(filepath.Clean(filepath.FromSlash(fileName))) + if cleanName == "." || cleanName == ".." || strings.HasPrefix(cleanName, "../") || filepath.IsAbs(fileName) { + return fmt.Errorf("template asset resolves outside output directory: %s", filePath) + } + files = append(files, generatedFile{ + name: cleanName, + body: []byte(strings.TrimLeft(fileBody, "\n")), + mode: filePerm, + }) + return nil }) + if err != nil { + return nil, err + } + return files, nil +} - return err +func renderTemplate(body string, data any) (string, error) { + functions := texttemplate.FuncMap{ + "title": strings.Title, + "toUpper": strings.ToUpper, + "toLower": strings.ToLower, + "toTitle": strings.ToTitle, + "trimSpace": strings.TrimSpace, + } + tmpl, err := texttemplate.New("").Funcs(functions).Option("missingkey=error").Parse(body) + if err != nil { + return "", err + } + var output bytes.Buffer + if err = tmpl.Execute(&output, data); err != nil { + return "", err + } + return output.String(), nil } diff --git a/template/service/go.mod.tmpl b/template/service/go.mod.tmpl index 2e00731..b1acdbd 100644 --- a/template/service/go.mod.tmpl +++ b/template/service/go.mod.tmpl @@ -12,7 +12,7 @@ require ( go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.83.1 google.golang.org/protobuf v1.36.11 k8s.io/client-go v0.34.1 ) diff --git a/template/service/handler/rpc_internal_test.go b/template/service/handler/rpc_internal_test.go.tmpl similarity index 100% rename from template/service/handler/rpc_internal_test.go rename to template/service/handler/rpc_internal_test.go.tmpl diff --git a/template/service/modeler/ent/entc.go b/template/service/modeler/ent/entc.go.tmpl similarity index 100% rename from template/service/modeler/ent/entc.go rename to template/service/modeler/ent/entc.go.tmpl diff --git a/template/service/modeler/ent/generate.go b/template/service/modeler/ent/generate.go.tmpl similarity index 100% rename from template/service/modeler/ent/generate.go rename to template/service/modeler/ent/generate.go.tmpl diff --git a/template/service/modeler/ent/schema/demo.go b/template/service/modeler/ent/schema/demo.go.tmpl similarity index 100% rename from template/service/modeler/ent/schema/demo.go rename to template/service/modeler/ent/schema/demo.go.tmpl diff --git a/template/service/modeler/independent_cfg_test.go b/template/service/modeler/independent_cfg_test.go.tmpl similarity index 100% rename from template/service/modeler/independent_cfg_test.go rename to template/service/modeler/independent_cfg_test.go.tmpl diff --git a/template/service/test/e2e/client/client.go b/template/service/test/e2e/client/client.go.tmpl similarity index 100% rename from template/service/test/e2e/client/client.go rename to template/service/test/e2e/client/client.go.tmpl diff --git a/template/service/test/e2e/fixture/main.go b/template/service/test/e2e/fixture/main.go.tmpl similarity index 100% rename from template/service/test/e2e/fixture/main.go rename to template/service/test/e2e/fixture/main.go.tmpl diff --git a/template/service_test.go b/template/service_test.go index 7e8886d..6361fed 100644 --- a/template/service_test.go +++ b/template/service_test.go @@ -97,7 +97,7 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { assertContains("modeler/mcp/registrar.go", "func (r *Registrar) Register", "server is nil") assertContains("modeler/mcp/registrar_test.go", "session.CallTool", "session.ReadResource", "session.GetPrompt") assertContains("config/app-dev-local.yaml", "aiconnector:", "mcp_server:") - assertContains("go.mod", "go 1.25.13", "github.com/grpc-kit/pkg v0.5.0", "github.com/modelcontextprotocol/go-sdk v1.7.0") + assertContains("go.mod", "go 1.25.13", "github.com/grpc-kit/pkg v0.5.0", "github.com/modelcontextprotocol/go-sdk v1.7.0", "google.golang.org/grpc v1.83.1") assertNotContains("go.mod", "github.com/sirupsen/logrus") assertContains("Makefile", ">> synchronize Go module dependencies", "@${GO} mod tidy") assertContains("AGENTS.md", "## Shared Skills", "scripts/skills/skills/generate-release-changelog/SKILL.md") @@ -228,6 +228,99 @@ func TestServiceTemplateRendersExtensions(t *testing.T) { } } +func TestGenerateToRefusesExistingOutputWithoutModification(t *testing.T) { + output := filepath.Join(t.TempDir(), "existing") + if err := os.Mkdir(output, 0o755); err != nil { + t.Fatal(err) + } + marker := filepath.Join(output, "keep.txt") + if err := os.WriteFile(marker, []byte("keep"), 0o600); err != nil { + t.Fatal(err) + } + + generator, err := New(config.Config{ + Global: config.GlobalConfig{ + Type: TypeService, + ProductCode: "demo", + ShortName: "echo", + ReleaseVersion: "v0.5.0", + Repository: "example.com/acme/echo", + }, + Template: config.TemplateConfig{Service: config.TemplateService{APIVersion: "v1"}}, + }) + if err != nil { + t.Fatal(err) + } + if err = generator.GenerateTo(output); err == nil || !strings.Contains(err.Error(), "already exists") { + t.Fatalf("GenerateTo() error = %v, want existing-output refusal", err) + } + body, err := os.ReadFile(marker) + if err != nil { + t.Fatal(err) + } + if string(body) != "keep" { + t.Fatalf("existing marker = %q, want unchanged", body) + } +} + +func TestGenerateToPublishesRequestedDirectory(t *testing.T) { + output := filepath.Join(t.TempDir(), "nested", "custom-name") + generator, err := New(config.Config{ + Global: config.GlobalConfig{ + Type: TypeService, + ProductCode: "demo", + ShortName: "echo", + ReleaseVersion: "v0.5.0", + Repository: "example.com/acme/echo", + Organization: "acme", + Appname: "demo-echo-v1", + ServiceCode: "echo.v1.demo", + ProtoPackage: "acme.api.demo.echo.v1", + ServiceTitle: "DemoEcho", + }, + Template: config.TemplateConfig{Service: config.TemplateService{APIVersion: "v1"}}, + }) + if err != nil { + t.Fatal(err) + } + if err = generator.GenerateTo(output); err != nil { + t.Fatalf("GenerateTo: %v", err) + } + if _, err = os.Stat(filepath.Join(output, "go.mod")); err != nil { + t.Fatalf("generated go.mod: %v", err) + } +} + +func TestServiceTemplateDoesNotExposeStaticGoPackages(t *testing.T) { + root := filepath.Join("service") + err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if !entry.IsDir() && filepath.Ext(path) == ".go" { + t.Errorf("template Go asset %s must use the .go.tmpl suffix", path) + } + return nil + }) + if err != nil { + t.Fatal(err) + } +} + +func TestToolInstallerMatchesGeneratedServiceAsset(t *testing.T) { + rootInstaller, err := os.ReadFile(filepath.Join("..", "scripts", "binaries.sh")) + if err != nil { + t.Fatal(err) + } + serviceInstaller, err := Assets.ReadFile("service/scripts/binaries.sh") + if err != nil { + t.Fatal(err) + } + if string(rootInstaller) != string(serviceInstaller) { + t.Fatal("root and generated-service tool installers have drifted") + } +} + func validateGeneratedSkill(t *testing.T, skillDir string) { t.Helper() diff --git a/template/template.go b/template/template.go index fd6ef94..aa6ec88 100644 --- a/template/template.go +++ b/template/template.go @@ -18,6 +18,7 @@ const ( // Template 代码模版的接口定义 type Template interface { Generate() error + GenerateTo(output string) error } // New xx From eaf3ba7a17f45c95099dd25907d82b5539a21e24 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 17:44:56 +0800 Subject: [PATCH 52/57] =?UTF-8?q?feat:=20=E6=B1=87=E6=80=BB=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E8=BF=81=E7=A7=BB=E4=BA=BA=E5=B7=A5=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/projectmigrate/diagnostics_test.go | 3 +++ internal/projectmigrate/output.go | 28 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/internal/projectmigrate/diagnostics_test.go b/internal/projectmigrate/diagnostics_test.go index a446ebb..3e7a1ca 100644 --- a/internal/projectmigrate/diagnostics_test.go +++ b/internal/projectmigrate/diagnostics_test.go @@ -104,6 +104,9 @@ func TestWritePlanIncludesManualActionLocation(t *testing.T) { if !strings.Contains(output.String(), "Manual action [context_call] handler/register.go:38: pass context") { t.Fatalf("output = %q", output.String()) } + if !strings.Contains(output.String(), "Manual actions: 1\n [context_call]: 1") { + t.Fatalf("output does not include grouped manual-action counts: %q", output.String()) + } if !strings.Contains(output.String(), "Project compatibility: not verified") { t.Fatalf("output does not explain compatibility status: %q", output.String()) } diff --git a/internal/projectmigrate/output.go b/internal/projectmigrate/output.go index 9fdd0d6..fd48955 100644 --- a/internal/projectmigrate/output.go +++ b/internal/projectmigrate/output.go @@ -17,6 +17,7 @@ package projectmigrate import ( "fmt" "io" + "sort" "github.com/pmezard/go-difflib/difflib" ) @@ -31,6 +32,9 @@ func WritePlan(output io.Writer, plan Plan) error { if err := writeDiagnostics(output, "Apply blocked", plan.ApplyBlockers); err != nil { return err } + if err := writeDiagnosticSummary(output, "Manual actions", plan.ManualActions); err != nil { + return err + } if err := writeDiagnostics(output, "Manual action", plan.ManualActions); err != nil { return err } @@ -67,6 +71,30 @@ func WritePlan(output io.Writer, plan Plan) error { return nil } +func writeDiagnosticSummary(output io.Writer, label string, diagnostics []Diagnostic) error { + if len(diagnostics) == 0 { + return nil + } + if _, err := fmt.Fprintf(output, "%s: %d\n", label, len(diagnostics)); err != nil { + return err + } + counts := make(map[string]int) + for _, diagnostic := range diagnostics { + counts[diagnostic.Code]++ + } + codes := make([]string, 0, len(counts)) + for code := range counts { + codes = append(codes, code) + } + sort.Strings(codes) + for _, code := range codes { + if _, err := fmt.Fprintf(output, " [%s]: %d\n", code, counts[code]); err != nil { + return err + } + } + return nil +} + func writeDiagnostics(output io.Writer, label string, diagnostics []Diagnostic) error { for _, diagnostic := range diagnostics { location := diagnostic.Path From d454cd668dca31ce6ed8208cf7f50016b1bfdf5d Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 17:45:06 +0800 Subject: [PATCH 53/57] =?UTF-8?q?build:=20=E5=9B=BA=E5=8C=96=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E9=AA=8C=E8=AF=81=E4=B8=8E=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=B7=A5=E5=85=B7=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/codeql-analysis.yml | 8 +- .github/workflows/test.yml | 60 +++++++++++ Dockerfile | 40 +++---- scripts/binaries.sh | 126 +++++++++++++--------- template/service/scripts/binaries.sh | 125 ++++++++++++--------- template/service/scripts/generate.sh.tmpl | 2 +- 6 files changed, 233 insertions(+), 128 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 710011d..95bfc68 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v6 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b5e50dd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + - run: make test + - run: make build + + migration-compatibility: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v7 + with: + go-version: 1.25.13 + cache-dependency-path: go.sum + - run: make test-compatibility PKG_COMPAT_VERSION=v0.5.0 + + template-compatibility: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v7 + with: + go-version: 1.25.13 + cache-dependency-path: go.sum + - name: Install generation tools + run: | + 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 + - name: Install protobuf include sources + run: | + mkdir -p "$(go env GOPATH)/src/github.com/grpc-kit" + mkdir -p "$(go env GOPATH)/src/github.com/googleapis" + mkdir -p "$(go env GOPATH)/src/github.com/grpc-ecosystem" + git clone --branch v0.3.1 --depth 1 https://github.com/grpc-kit/api.git "$(go env GOPATH)/src/github.com/grpc-kit/api" + git init "$(go env GOPATH)/src/github.com/googleapis/googleapis" + 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" + - run: make test-template-compatibility diff --git a/Dockerfile b/Dockerfile index 6d048ec..eca41b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,20 @@ -#ARG TARGETPLATFORM +ARG TARGETPLATFORM +ARG GO_IMAGE=golang:1.25.13-bookworm@sha256:e401dae1bf814e29204a8cb7915682e1780951e609ca0dd8865ee1937f510c48 -#FROM --platform=$TARGETPLATFORM golang:1.20-bullseye as builder -FROM golang:1.20-bullseye as builder +FROM --platform=$TARGETPLATFORM ${GO_IMAGE} AS builder + +ARG HTTPS_PROXY # 更换镜像地址 -RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list +RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \ + && sed -i 's#security.debian.org#mirrors.ustc.edu.cn/debian-security#g' /etc/apt/sources.list.d/debian.sources # 设置时区与依赖的基础工具包 RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install -y tzdata \ + && apt-get install -y --no-install-recommends tzdata unzip docker.io \ && ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && dpkg-reconfigure -f noninteractive tzdata \ - && apt-get install -y unzip docker.io + && rm -rf /var/lib/apt/lists/* # 设置环境变量 ENV GO111MODULE=on @@ -25,25 +27,23 @@ COPY . . # 编译 grpc-kit-cli # 下载 protoc 相关二进制 # 安装其他依赖的 proto 定义文件 -RUN go mod tidy \ - && make build \ +RUN go mod download \ + && make build \ && make protoc \ && make protoc-gen-go \ && make protoc-gen-go-grpc \ && make protoc-gen-grpc-gateway \ && make protoc-gen-openapiv2 \ - && git clone -b v0.3.0 --depth 1 https://github.com/grpc-kit/api.git $GOPATH/src/github.com/grpc-kit/api \ - && git clone --depth 1 https://github.com/googleapis/googleapis.git $GOPATH/src/github.com/googleapis/googleapis \ - && git clone -b v2.18.1 --depth 1 https://github.com/grpc-ecosystem/grpc-gateway.git $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway - -# 用于容器内构建镜像 -FROM --platform=$TARGETPLATFORM gcr.io/kaniko-project/executor:v1.9.1 as kaniko + && git clone -b v0.3.1 --depth 1 https://github.com/grpc-kit/api.git $GOPATH/src/github.com/grpc-kit/api \ + && mkdir -p $GOPATH/src/github.com/googleapis \ + && git init $GOPATH/src/github.com/googleapis/googleapis \ + && 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 # 用于 go 应用的编译 -FROM --platform=$TARGETPLATFORM golang:1.20-bullseye - -# 拷贝 kaniko 内容 -COPY --from=kaniko /kaniko /kaniko +FROM --platform=$TARGETPLATFORM ${GO_IMAGE} # 拷贝上阶段编译后的文件 COPY --from=builder /usr/local/src/build/grpc-kit-cli-* /go/bin/grpc-kit-cli @@ -52,7 +52,7 @@ COPY --from=builder /go/bin/protoc-gen-go /go/bin/protoc-gen-go COPY --from=builder /go/bin/protoc-gen-go-grpc /go/bin/protoc-gen-go-grpc COPY --from=builder /go/bin/protoc-gen-grpc-gateway /go/bin/protoc-gen-grpc-gateway COPY --from=builder /go/bin/protoc-gen-openapiv2 /go/bin/protoc-gen-openapiv2 -COPY --from=builder /usr/local/include/google /usr/local/include/google +COPY --from=builder /go/include/google /go/include/google COPY --from=builder /go/src/ /go/src/ COPY --from=builder /usr/bin/docker /usr/bin/docker diff --git a/scripts/binaries.sh b/scripts/binaries.sh index 82ae389..2e58f17 100755 --- a/scripts/binaries.sh +++ b/scripts/binaries.sh @@ -1,71 +1,93 @@ -#!/bin/bash +#!/usr/bin/env bash -source scripts/env +set -euo pipefail -if test -z $1; then - echo "Usage:" - echo "\t ./scripts/binaries.sh protoc-gen-go" - exit 0; -fi +readonly PROTOC_VERSION="21.12" +readonly PROTOC_GEN_GO_VERSION="v1.36.11" +readonly PROTOC_GEN_GO_GRPC_VERSION="v1.6.2" +readonly GRPC_GATEWAY_VERSION="v2.29.0" -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-aarch_64.zip -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-aarch_64.zip -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-x86_64.zip -function protoc() { - GOHOSTOS=$(go env GOHOSTOS) - GOARCH=$(go env GOARCH) - GOPATH=$(go env GOPATH) - - TARGET_OS=$GOHOSTOS - TARGET_ARCH=$GOARCH - - if test "$GOHOSTOS" == "darwin"; then - TARGET_OS="osx" +verify_sha256() { + local checksum=$1 archive=$2 + if command -v sha256sum >/dev/null 2>&1; then + printf '%s %s\n' "${checksum}" "${archive}" | sha256sum -c - + return fi - - if test "$GOARCH" == "arm64"; then - TARGET_ARCH="aarch_64" - elif test "$GOARCH" == "amd64"; then - TARGET_ARCH="x86_64" + if command -v shasum >/dev/null 2>&1; then + printf '%s %s\n' "${checksum}" "${archive}" | shasum -a 256 --check + return fi + echo "sha256sum or shasum is required to verify protoc" >&2 + return 1 +} + +install_protoc() { + local host_os host_arch target_os target_arch archive checksum download_dir go_path + host_os=$(go env GOHOSTOS) + host_arch=$(go env GOHOSTARCH) + target_os=${host_os} + target_arch=${host_arch} - echo "https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip" + case "${host_os}" in + linux) ;; + darwin) target_os="osx" ;; + *) echo "unsupported protoc operating system: ${host_os}" >&2; return 1 ;; + esac + case "${host_arch}" in + amd64) target_arch="x86_64" ;; + arm64) target_arch="aarch_64" ;; + *) echo "unsupported protoc architecture: ${host_arch}" >&2; return 1 ;; + esac - cd /tmp - curl -L -O "https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip" + case "${target_os}/${target_arch}" in + linux/x86_64) checksum="3a4c1e5f2516c639d3079b1586e703fc7bcfa2136d58bda24d1d54f949c315e8" ;; + linux/aarch_64) checksum="2dd17f75d66a682640b136e31848da9fb2eefe68d55303baf8b32617374f6711" ;; + osx/x86_64) checksum="9448ff40278504a7ae5139bb70c962acc78c32d8fc54b4890a55c14c68b9d10a" ;; + osx/aarch_64) checksum="96839af0caed64352442fc8236f4bdf7c1cd6efcfaa98fa5db37307a73fc7c70" ;; + esac - unzip protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip - mv bin/protoc "$GOPATH/bin/" - mv include/google /usr/local/include/ + archive="protoc-${PROTOC_VERSION}-${target_os}-${target_arch}.zip" + download_dir=$(mktemp -d "${TMPDIR:-/tmp}/grpc-kit-protoc.XXXXXX") + trap 'rm -rf "${download_dir}"' RETURN + curl --fail --location --silent --show-error --retry 3 \ + "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${archive}" \ + --output "${download_dir}/${archive}" + verify_sha256 "${checksum}" "${download_dir}/${archive}" + unzip -q "${download_dir}/${archive}" -d "${download_dir}/protoc" - rm -f protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip - rm -f readme.txt - rmdir bin/ - rmdir include/ + go_path=$(go env GOPATH) + mkdir -p "${go_path}/bin" "${go_path}/include" + install -m 0755 "${download_dir}/protoc/bin/protoc" "${go_path}/bin/protoc" + rm -rf "${go_path}/include/google" + cp -R "${download_dir}/protoc/include/google" "${go_path}/include/" + rm -rf "${download_dir}" + trap - RETURN } -function protoc-gen-go-grpc() { - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 +install_protoc_gen_go() { + go install "google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VERSION}" } -function protoc-gen-go() { - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 +install_protoc_gen_go_grpc() { + go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc@${PROTOC_GEN_GO_GRPC_VERSION}" } -function protoc-gen-grpc-gateway() { - go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.15.2 +install_protoc_gen_grpc_gateway() { + go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@${GRPC_GATEWAY_VERSION}" } -function protoc-gen-openapiv2() { - go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.15.2 +install_protoc_gen_openapiv2() { + go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@${GRPC_GATEWAY_VERSION}" } -FILE=$(which $1) -if test -f "$FILE"; then - echo "the binary already exists at: "$FILE"" -else - $1 - echo "download complete, this will place binaries in your \$GOBIN, make sure that your \$GOBIN is in your \$PATH." -fi -#$1 +case "${1:-}" in + protoc) install_protoc ;; + protoc-gen-go) install_protoc_gen_go ;; + protoc-gen-go-grpc) install_protoc_gen_go_grpc ;; + protoc-gen-grpc-gateway) install_protoc_gen_grpc_gateway ;; + protoc-gen-openapiv2) install_protoc_gen_openapiv2 ;; + *) + echo "usage: $0 {protoc|protoc-gen-go|protoc-gen-go-grpc|protoc-gen-grpc-gateway|protoc-gen-openapiv2}" >&2 + exit 2 + ;; +esac diff --git a/template/service/scripts/binaries.sh b/template/service/scripts/binaries.sh index fd54109..2e58f17 100755 --- a/template/service/scripts/binaries.sh +++ b/template/service/scripts/binaries.sh @@ -1,70 +1,93 @@ -#!/bin/bash +#!/usr/bin/env bash -source scripts/env +set -euo pipefail -if test -z $1; then - echo "Usage:" - echo "\t ./scripts/binaries.sh protoc-gen-go" - exit 0; -fi +readonly PROTOC_VERSION="21.12" +readonly PROTOC_GEN_GO_VERSION="v1.36.11" +readonly PROTOC_GEN_GO_GRPC_VERSION="v1.6.2" +readonly GRPC_GATEWAY_VERSION="v2.29.0" -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-aarch_64.zip -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-aarch_64.zip -# https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-x86_64.zip -function protoc() { - GOHOSTOS=$(go env GOHOSTOS) - GOARCH=$(go env GOARCH) - GOPATH=$(go env GOPATH) - - TARGET_OS=$GOHOSTOS - TARGET_ARCH=$GOARCH - - if test "$GOHOSTOS" == "darwin"; then - TARGET_OS="osx" +verify_sha256() { + local checksum=$1 archive=$2 + if command -v sha256sum >/dev/null 2>&1; then + printf '%s %s\n' "${checksum}" "${archive}" | sha256sum -c - + return fi - - if test "$GOARCH" == "arm64"; then - TARGET_ARCH="aarch_64" - elif test "$GOARCH" == "amd64"; then - TARGET_ARCH="x86_64" + if command -v shasum >/dev/null 2>&1; then + printf '%s %s\n' "${checksum}" "${archive}" | shasum -a 256 --check + return fi + echo "sha256sum or shasum is required to verify protoc" >&2 + return 1 +} + +install_protoc() { + local host_os host_arch target_os target_arch archive checksum download_dir go_path + host_os=$(go env GOHOSTOS) + host_arch=$(go env GOHOSTARCH) + target_os=${host_os} + target_arch=${host_arch} - echo "https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip" + case "${host_os}" in + linux) ;; + darwin) target_os="osx" ;; + *) echo "unsupported protoc operating system: ${host_os}" >&2; return 1 ;; + esac + case "${host_arch}" in + amd64) target_arch="x86_64" ;; + arm64) target_arch="aarch_64" ;; + *) echo "unsupported protoc architecture: ${host_arch}" >&2; return 1 ;; + esac - cd /tmp - curl -L -O "https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip" + case "${target_os}/${target_arch}" in + linux/x86_64) checksum="3a4c1e5f2516c639d3079b1586e703fc7bcfa2136d58bda24d1d54f949c315e8" ;; + linux/aarch_64) checksum="2dd17f75d66a682640b136e31848da9fb2eefe68d55303baf8b32617374f6711" ;; + osx/x86_64) checksum="9448ff40278504a7ae5139bb70c962acc78c32d8fc54b4890a55c14c68b9d10a" ;; + osx/aarch_64) checksum="96839af0caed64352442fc8236f4bdf7c1cd6efcfaa98fa5db37307a73fc7c70" ;; + esac - unzip protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip - mv bin/protoc "$GOPATH/bin/" - mv include/google /usr/local/include/ + archive="protoc-${PROTOC_VERSION}-${target_os}-${target_arch}.zip" + download_dir=$(mktemp -d "${TMPDIR:-/tmp}/grpc-kit-protoc.XXXXXX") + trap 'rm -rf "${download_dir}"' RETURN + curl --fail --location --silent --show-error --retry 3 \ + "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/${archive}" \ + --output "${download_dir}/${archive}" + verify_sha256 "${checksum}" "${download_dir}/${archive}" + unzip -q "${download_dir}/${archive}" -d "${download_dir}/protoc" - rm -f protoc-21.12-"$TARGET_OS"-"$TARGET_ARCH".zip - rm -f readme.txt - rmdir bin/ - rmdir include/ + go_path=$(go env GOPATH) + mkdir -p "${go_path}/bin" "${go_path}/include" + install -m 0755 "${download_dir}/protoc/bin/protoc" "${go_path}/bin/protoc" + rm -rf "${go_path}/include/google" + cp -R "${download_dir}/protoc/include/google" "${go_path}/include/" + rm -rf "${download_dir}" + trap - RETURN } -function protoc-gen-go-grpc() { - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 +install_protoc_gen_go() { + go install "google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VERSION}" } -function protoc-gen-go() { - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28 +install_protoc_gen_go_grpc() { + go install "google.golang.org/grpc/cmd/protoc-gen-go-grpc@${PROTOC_GEN_GO_GRPC_VERSION}" } -function protoc-gen-grpc-gateway() { - go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.15.2 +install_protoc_gen_grpc_gateway() { + go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@${GRPC_GATEWAY_VERSION}" } -function protoc-gen-openapiv2() { - go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.15.2 +install_protoc_gen_openapiv2() { + go install "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@${GRPC_GATEWAY_VERSION}" } -FILE=$(which $1) -if test -f "$FILE"; then - echo "the binary already exists at: "$FILE"" -else - $1 - echo "download complete, this will place binaries in your \$GOBIN, make sure that your \$GOBIN is in your \$PATH." -fi +case "${1:-}" in + protoc) install_protoc ;; + protoc-gen-go) install_protoc_gen_go ;; + protoc-gen-go-grpc) install_protoc_gen_go_grpc ;; + protoc-gen-grpc-gateway) install_protoc_gen_grpc_gateway ;; + protoc-gen-openapiv2) install_protoc_gen_openapiv2 ;; + *) + echo "usage: $0 {protoc|protoc-gen-go|protoc-gen-go-grpc|protoc-gen-grpc-gateway|protoc-gen-openapiv2}" >&2 + exit 2 + ;; +esac diff --git a/template/service/scripts/generate.sh.tmpl b/template/service/scripts/generate.sh.tmpl index 6599304..db8d2de 100755 --- a/template/service/scripts/generate.sh.tmpl +++ b/template/service/scripts/generate.sh.tmpl @@ -6,7 +6,7 @@ function gen_protoc() { # 生成 *.pb.go 文件 protoc \ -I ./ \ - -I /usr/local/include/ \ + -I "${GOPATH}"/include/ \ -I "${GOPATH}"/src \ -I "${GOPATH}"/src/github.com/grpc-ecosystem/grpc-gateway/ \ -I "${GOPATH}"/src/github.com/googleapis/googleapis/ \ From 3be3ca749a149d95ac9f1512994c22bbd6ea56db Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 17:45:14 +0800 Subject: [PATCH 54/57] =?UTF-8?q?docs:=20=E8=A1=A5=E5=85=85=20v0.5=20?= =?UTF-8?q?=E5=8F=91=E5=B8=83=E4=B8=8E=E5=85=BC=E5=AE=B9=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG/CHANGELOG-0.5.md | 12 ++++++++++++ README.md | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/CHANGELOG/CHANGELOG-0.5.md b/CHANGELOG/CHANGELOG-0.5.md index cbeb5da..aae100a 100644 --- a/CHANGELOG/CHANGELOG-0.5.md +++ b/CHANGELOG/CHANGELOG-0.5.md @@ -15,11 +15,18 @@ - 新增 `grpc-kit-cli project migrate [path]`,默认只读预览旧项目的托管文件变更和人工迁移清单;`--apply` 仅在正式稳定版 CLI、clean Git 工作树和输入摘要复核通过时更新已有托管文件。 - 首期迁移目标固定为正式 `github.com/grpc-kit/pkg v0.5.0`。命令严格区分 CLI marker 版本和 pkg 版本,不修改 `go.mod`、业务源码及其他用户管理文件,也不提供可能被误解为 CLI 自更新的根级 `upgrade`/`update` alias。 +- `grpc-kit-cli new` 新增 `--output` / `-d`,可显式指定项目输出目录;`grpc-kit-cli version` 新增 `--short`,便于发布脚本只读取版本号。 +- 新增普通 CLI、冻结迁移资产和最新服务模板三层 CI 门禁;最新模板门禁会完整执行代码生成、单测和构建,且不使用本地 module `replace`。 ### Changed #### grpc-kit/cli 模块 +- `new` 改为先完整渲染到同级临时目录,再以 rename 一次性发布;目标路径已存在时拒绝覆盖,避免失败后留下半生成项目。 +- CLI 构建信息迁入自身的 `internal/buildinfo`,根模块不再为版本输出和文件写入依赖 `github.com/grpc-kit/pkg`;模板静态 Go 资产统一使用 `.go.tmpl`,标准 `go test ./...` 与 `go vet ./...` 现已覆盖全部 CLI package。 +- 生成工具与服务模板依赖对齐:固定 `protoc-gen-go v1.36.11`、`protoc-gen-go-grpc v1.6.2`、grpc-gateway/openapiv2 `v2.29.0`,并为 `protoc v21.12` 的 Linux/macOS、amd64/arm64 发布包校验 SHA-256。 +- CLI 构建镜像对齐 Go `1.25.13`,移除硬编码代理与构建阶段的 `go mod tidy`,protobuf include 源与 grpc-gateway 均固定到已验证版本/提交。 + - **Breaking**:服务模板日志从 logrus 切换为 Go 标准库 `log/slog`。 1. `Microservice`、`IndependentCfg`、flow client 与 MCP registrar 的 logger 类型改为 `*slog.Logger`;`GetLogger`、`WithLogger`、`WithWorkflow`、`flow.NewClient` 等 API 名称保持不变 @@ -45,3 +52,8 @@ 2. 为 `NewMicroservice`、`Init`、`HTTPHandlerFrontend`、`sd.Register`、`Deregister`、`StartBackground` 和 `WithLogger` 调用补充已有 ctx 3. 删除对临时 `*Context` 方法及 logrus 兼容入口的调用 4. 运行代码生成、单元测试与构建,确认自定义 handler、注册流程和关闭流程均已适配新签名 + +### Fixed + +- 迁移计划输出增加按诊断代码聚合的 `Manual actions` 计数,保留逐文件、逐行明细。 +- 修复模板目录中的静态 `.go` 资产被根模块误识别为 CLI package,导致标准全量测试无法运行的问题。 diff --git a/README.md b/README.md index 5da8d54..939e616 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,13 @@ mkdir -p $GOPATH/src/github.com/opsaid cd $GOPATH/src/github.com/opsaid grpc-kit-cli new -t service -p opsaid -s test1 + +# 可选:指定输出目录;目标路径已存在时命令会拒绝覆盖 +grpc-kit-cli new -t service -p opsaid -s test1 --output ./services/test1 ``` +`new` 会先在同级临时目录完成全部渲染,再一次性发布目标目录。渲染或写入失败不会留下半生成项目。 + ### 下载依赖的环境 ```shell @@ -148,3 +153,24 @@ OK ```shell # curl -u user1:grpc-kit-cli http://127.0.0.1:8080/api/demo ``` + +## 发布与兼容验证 + +```shell +# CLI 全量单测与 vet +make test + +# 冻结迁移资产对最低支持 pkg 的兼容验证 +make test-compatibility PKG_COMPAT_VERSION=v0.5.0 + +# 最新 new 模板的生成、代码生成、单测与构建验证 +make test-template-compatibility +``` + +`test-compatibility` 验证历史迁移资产,不会因以后发布 `pkg v0.5.8` 而失效。发布同一兼容系列的新补丁时,应保留 `v0.5.0` 最低基线,并额外对候选版本执行一次;只有引入新的破坏性目标时才新增迁移资产族。 + +脚本需要稳定消费 CLI 版本号时可使用: + +```shell +grpc-kit-cli version --short +``` From 8ff70c60c84edc6a1433b133a7b79b9680d447ae Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 21:22:34 +0800 Subject: [PATCH 55/57] =?UTF-8?q?docs:=20=E6=8E=A5=E5=85=A5=20migrate-logr?= =?UTF-8?q?us-to-slog=20=E5=85=B1=E4=BA=AB=E6=8A=80=E8=83=BD=E6=8C=87?= =?UTF-8?q?=E5=BC=95=E5=B9=B6=E6=98=8E=E7=A1=AE=20project=20migrate=20?= =?UTF-8?q?=E5=86=99=E5=85=A5=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/maintain-project-migration/SKILL.md | 1 + .agents/skills/maintain-project-migration/references/sop.md | 1 + CHANGELOG/CHANGELOG-0.5.md | 1 + README.md | 4 +++- template/service/AGENTS.md.tmpl | 1 + 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.agents/skills/maintain-project-migration/SKILL.md b/.agents/skills/maintain-project-migration/SKILL.md index b6d467f..9556c61 100644 --- a/.agents/skills/maintain-project-migration/SKILL.md +++ b/.agents/skills/maintain-project-migration/SKILL.md @@ -19,6 +19,7 @@ description: 维护 grpc-kit CLI 服务模板、project migrate 兼容资产和 - `TargetCLIVersion` 只表示执行迁移的正式 CLI 版本;pkg 目标版本独立记录。 - 只有首行完整 grpc-kit-cli `DO NOT EDIT` marker 的已有普通文件可进入 Change Plan。 - 不自动修改用户代码、`go.mod`、`go.sum`,不创建、删除、重命名或 chmod 项目文件。 +- 用户可以另行使用共享技能 `migrate-logrus-to-slog` 完成业务源码迁移;该授权属于 AI 编辑工作流,不得反向扩大 `project migrate` 的 Change Plan。 - 兼容资产只引用来源 fixture 已有的项目内符号;新功能走新建模板或独立 feature 流程。 - 正式 pkg 测试不使用本地 `replace`,也不使用不可复现的 `@latest`。 - 不擅自修改 `VERSION`、创建 tag、发布产物或对脏的真实服务执行 apply。 diff --git a/.agents/skills/maintain-project-migration/references/sop.md b/.agents/skills/maintain-project-migration/references/sop.md index 47ce6a2..0b973b7 100644 --- a/.agents/skills/maintain-project-migration/references/sop.md +++ b/.agents/skills/maintain-project-migration/references/sop.md @@ -32,6 +32,7 @@ 3. 新增来源版本时,从发布 tag 或确认 commit 冻结路径和 SHA-256。真实脏工作区只能用于只读调查,不能作为 fixture 来源。 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。 ## 4. 正式 pkg 兼容矩阵 diff --git a/CHANGELOG/CHANGELOG-0.5.md b/CHANGELOG/CHANGELOG-0.5.md index aae100a..730075c 100644 --- a/CHANGELOG/CHANGELOG-0.5.md +++ b/CHANGELOG/CHANGELOG-0.5.md @@ -17,6 +17,7 @@ - 首期迁移目标固定为正式 `github.com/grpc-kit/pkg v0.5.0`。命令严格区分 CLI marker 版本和 pkg 版本,不修改 `go.mod`、业务源码及其他用户管理文件,也不提供可能被误解为 CLI 自更新的根级 `upgrade`/`update` alias。 - `grpc-kit-cli new` 新增 `--output` / `-d`,可显式指定项目输出目录;`grpc-kit-cli version` 新增 `--short`,便于发布脚本只读取版本号。 - 新增普通 CLI、冻结迁移资产和最新服务模板三层 CI 门禁;最新模板门禁会完整执行代码生成、单测和构建,且不使用本地 module `replace`。 +- 新服务的 Agent 指引增加共享技能 `migrate-logrus-to-slog` 入口,用于由 AI 完成旧服务用户源码的语义迁移;`project migrate` 的托管文件写入边界保持不变。 ### Changed diff --git a/README.md b/README.md index 939e616..b16828c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ 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.` 标记、且存在内嵌兼容资产的已有文件;不会修改 `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 审阅、生成、测试和构建。 ## 生成代码并运行(本机方式) diff --git a/template/service/AGENTS.md.tmpl b/template/service/AGENTS.md.tmpl index 997aee3..b5de195 100644 --- a/template/service/AGENTS.md.tmpl +++ b/template/service/AGENTS.md.tmpl @@ -13,6 +13,7 @@ This file provides guidance to AI coding agents (Claude Code, Cursor, Copilot, e - Run `make skills` to install or update grpc-kit shared skills under `scripts/skills/skills`. - When a task matches a shared skill, read its complete `SKILL.md` before acting and follow any referenced resources it requires. - For release notes, version summaries, or CHANGELOG updates, use `scripts/skills/skills/generate-release-changelog/SKILL.md`. +- For migrating an older generated service from logrus to `log/slog`, use `scripts/skills/skills/migrate-logrus-to-slog/SKILL.md`; let `project migrate` handle CLI-managed files first when available. - If the shared skill directory is unavailable, report that it has not been initialized before falling back to repository conventions. ## Service Skills From b040ccaf0727401ca5a03ca5deb76812b568ee92 Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 22:08:53 +0800 Subject: [PATCH 56/57] =?UTF-8?q?docs:=20=E5=AE=9A=E7=A8=BF=20CHANGELOG-0.?= =?UTF-8?q?5=20=E4=B8=BA=200.5.0=20=E7=89=88=E6=9C=AC=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20README=20=E9=95=9C=E5=83=8F=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- CHANGELOG/CHANGELOG-0.5.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG/CHANGELOG-0.5.md b/CHANGELOG/CHANGELOG-0.5.md index 730075c..e1cace9 100644 --- a/CHANGELOG/CHANGELOG-0.5.md +++ b/CHANGELOG/CHANGELOG-0.5.md @@ -9,7 +9,7 @@ | Fixed | 功能的修复 | | Security | 有关安全问题的修复 | -## [Unreleased] +## [0.5.0] - 2026-09-16 ### Added diff --git a/README.md b/README.md index b16828c..64b44ae 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ docker run \ --rm \ -v $(pwd):/usr/local/src \ -w /usr/local/src \ - ccr.ccs.tencentyun.com/grpc-kit/cli:0.3.5 \ + ccr.ccs.tencentyun.com/grpc-kit/cli:0.5.0 \ grpc-kit-cli new -t service -p opsaid -s test1 ``` @@ -80,7 +80,7 @@ docker run -i -t --rm \ -v $(pwd):/usr/local/src \ -w /usr/local/src \ --network host \ - ccr.ccs.tencentyun.com/grpc-kit/cli:0.3.5 \ + ccr.ccs.tencentyun.com/grpc-kit/cli:0.5.0 \ make run ``` From ad111a03d8b081b8fd80f423bbae4e034b42ef0b Mon Sep 17 00:00:00 2001 From: mingqing Date: Wed, 16 Sep 2026 22:08:53 +0800 Subject: [PATCH 57/57] release: cut the 0.5.0 Co-Authored-By: Claude Code --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 03fca44..8f0916f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.9-beta.1 +0.5.0