Compare commits
14 Commits
v0.27.0-pr
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 19ee9746e9 | |||
| 2c45dc1e89 | |||
| 021d53304a | |||
| f9ca6631ca | |||
| 9665fb5974 | |||
| d65af03162 | |||
| f0c62152d7 | |||
| 01a03149f9 | |||
| 0c5b571b31 | |||
| f2c5957c15 | |||
| 0d2c6040e2 | |||
| 1052f156bd | |||
| 0b6c0043b6 | |||
| f8e0dcc5a6 |
16
ADVANCED.md
16
ADVANCED.md
@@ -57,6 +57,8 @@ files, err := oss.New(client, bucket, 32<<20)
|
|||||||
|
|
||||||
业务需要临时链接时显式依赖额外接口 `storage.Presigner`。S3/OSS 实现 `PresignGet`;Local 用 `storage.NewSigner(files, downloadBaseURL, secret)` 提供 HTTP 下载 Handler。链接 TTL 为 1 秒到 1 小时,私钥/临时凭据可能让云链接提前失效。签发链接前必须完成业务授权;对象 key 及 bucket 本身不是租户权限。签名链接不要写日志。受限直传和云端分片现已提供可选接口,见 [上传与续传说明](storage/README.md)。生命周期、ACL、CDN 自定义域名和版本管理仍由平台/部署管理。
|
业务需要临时链接时显式依赖额外接口 `storage.Presigner`。S3/OSS 实现 `PresignGet`;Local 用 `storage.NewSigner(files, downloadBaseURL, secret)` 提供 HTTP 下载 Handler。链接 TTL 为 1 秒到 1 小时,私钥/临时凭据可能让云链接提前失效。签发链接前必须完成业务授权;对象 key 及 bucket 本身不是租户权限。签名链接不要写日志。受限直传和云端分片现已提供可选接口,见 [上传与续传说明](storage/README.md)。生命周期、ACL、CDN 自定义域名和版本管理仍由平台/部署管理。
|
||||||
|
|
||||||
|
需要 inline 预览、显示文件名或指定响应 MIME 时,使用可选 `storage.ReadPresigner.PresignGetWithOptions`,返回 URL 和签名内的实际到期时间。Local、S3/R2、OSS 使用相同选项;Local 支持 HEAD 与范围读取,云端由原生存储服务处理。旧下载接口不变,见 [签名读取说明](storage/README.md#签名预览与下载)和[完整示例](examples/signedRead/README.md)。
|
||||||
|
|
||||||
R2 实现 S3 的子集,配置入口使用标准账户 endpoint;法域专属 endpoint 可显式配置。兼容性依据:[R2 S3 API](https://developers.cloudflare.com/r2/api/s3/api/)、[R2 签名链接](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)。OSS 使用其[官方 Go SDK v2](https://www.alibabacloud.com/help/en/oss/developer-reference/manual-for-go-sdk-v2/),没有假定全部 S3 行为与 OSS 等价。
|
R2 实现 S3 的子集,配置入口使用标准账户 endpoint;法域专属 endpoint 可显式配置。兼容性依据:[R2 S3 API](https://developers.cloudflare.com/r2/api/s3/api/)、[R2 签名链接](https://developers.cloudflare.com/r2/api/s3/presigned-urls/)。OSS 使用其[官方 Go SDK v2](https://www.alibabacloud.com/help/en/oss/developer-reference/manual-for-go-sdk-v2/),没有假定全部 S3 行为与 OSS 等价。
|
||||||
|
|
||||||
验证区分:Local 契约、OSS SDK HTTP fixture、S3 SDK fixture 和 R2 签名参数属于本地测试;Linux runner 再跑真实 MinIO 上传/读取/删除与签名拒绝。**MinIO 通过不能代替 OSS/R2 实网验证。**迁移存储平台还需要复制已有对象和调整部署权限,不只是切配置。
|
验证区分:Local 契约、OSS SDK HTTP fixture、S3 SDK fixture 和 R2 签名参数属于本地测试;Linux runner 再跑真实 MinIO 上传/读取/删除与签名拒绝。**MinIO 通过不能代替 OSS/R2 实网验证。**迁移存储平台还需要复制已有对象和调整部署权限,不只是切配置。
|
||||||
@@ -121,6 +123,20 @@ log:
|
|||||||
|
|
||||||
`telemetry.New` 接受 OTel `sdktrace.SpanExporter` 和 `sdkmetric.Reader`;可注入官方 OTLP exporter、PeriodicReader 或测试用 exporter,基础包不强制选择 collector 协议。`app.Use(runtime.Middleware())` 采集入站,`telemetry.Transport` 采集出站并传播 W3C trace context;Redis 使用 `client.AddHook(telemetry.RedisHook{Runtime: runtime})`,SQL 使用 `telemetry.SQL{DB: db, Runtime: runtime}`。业务操作/事务可用 `runtime.Do(ctx, "fixedOperationName", fn)` 包裹。
|
`telemetry.New` 接受 OTel `sdktrace.SpanExporter` 和 `sdkmetric.Reader`;可注入官方 OTLP exporter、PeriodicReader 或测试用 exporter,基础包不强制选择 collector 协议。`app.Use(runtime.Middleware())` 采集入站,`telemetry.Transport` 采集出站并传播 W3C trace context;Redis 使用 `client.AddHook(telemetry.RedisHook{Runtime: runtime})`,SQL 使用 `telemetry.SQL{DB: db, Runtime: runtime}`。业务操作/事务可用 `runtime.Do(ctx, "fixedOperationName", fn)` 包裹。
|
||||||
|
|
||||||
|
上游网关如果使用自定义 Header,可配置通用兼容传播器,不在业务 Handler 里解析 Header:
|
||||||
|
|
||||||
|
```go
|
||||||
|
propagator, err := telemetry.NewHeaderPropagator(telemetry.HeaderPropagationConfig{
|
||||||
|
TraceIDHeader: "X-Trace-Id", SpanIDHeader: "X-Span-Id", RequestIDHeader: "X-Request-Id",
|
||||||
|
})
|
||||||
|
if err != nil { return err }
|
||||||
|
runtime, err := telemetry.New(telemetry.Config{
|
||||||
|
Service: "billing", SampleRatio: 1, Propagator: propagator,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`HeaderPropagator` 同时支持 W3C `traceparent`;两者同时存在时优先 W3C。有效的兼容 Header 会成为远程父 Span,当前服务仍生成自己的 Span ID,`logx.TraceID` / `RequestID`、访问日志和审计会沿用上下文,出站 `telemetry.Transport` 会同时传播 W3C 和配置的兼容 Header。自定义 Header 不携带采样标志,适配器默认把有效成对 ID 当作已采样;需要保留上游采样决定时使用 W3C。只有受信网关入口才应启用该适配器,Trace/Span ID 不是认证、租户或幂等凭据。完整离线示例见 [telemetryHeaders](examples/telemetryHeaders/README.md)。
|
||||||
|
|
||||||
创建者负责在停止请求/Worker 后调用 `runtime.Shutdown(ctx)` 刷新并关闭 exporter/reader。指标按注册路由模板和固定方法分类;自动采集不记录 URL query、SQL 正文、Redis key、token 或业务载荷。业务自定义 span 名称同样应固定,避免用用户 ID 或对象 key。
|
创建者负责在停止请求/Worker 后调用 `runtime.Shutdown(ctx)` 刷新并关闭 exporter/reader。指标按注册路由模板和固定方法分类;自动采集不记录 URL query、SQL 正文、Redis key、token 或业务载荷。业务自定义 span 名称同样应固定,避免用用户 ID 或对象 key。
|
||||||
|
|
||||||
## 分布式限流与幂等
|
## 分布式限流与幂等
|
||||||
|
|||||||
4
API.md
4
API.md
@@ -109,7 +109,7 @@ service user-api {
|
|||||||
- 来源标签中的 `optional` / `omitempty`、标量 `default`、标量 `options=a|b`、数值 `range=[1:100]`(支持开区间和单边界)。
|
- 来源标签中的 `optional` / `omitempty`、标量 `default`、标量 `options=a|b`、数值 `range=[1:100]`(支持开区间和单边界)。
|
||||||
- `binding` 的 `required`、`min`、`max`、`len`、`email`。长度按 Unicode 字符或切片元素数;范围边界必须有限且位于 ±2^53 内。
|
- `binding` 的 `required`、`min`、`max`、`len`、`email`。长度按 Unicode 字符或切片元素数;范围边界必须有限且位于 ±2^53 内。
|
||||||
|
|
||||||
暂不支持匿名嵌入、别名、递归类型、未声明的外部 Go 类型、未通过 scalar 显式接入的自定义编解码、`[]byte`、泛型、`json:",string"`、任意 validator 标签、go-zero 的 JWT/签名/SSE 声明,以及客户端 SDK 生成。认证验证通过声明关联的命名中间件实现。未列出的语法会明确报错。
|
暂不支持匿名嵌入、别名、递归请求类型、未声明的外部 Go 类型、未通过 scalar 显式接入的自定义编解码、`[]byte`、泛型、`json:",string"`、任意 validator 标签、go-zero 的 JWT/签名/SSE 声明,以及客户端 SDK 生成。认证验证通过声明关联的命名中间件实现。未列出的语法会明确报错。
|
||||||
|
|
||||||
业务错误、具名鉴权、多行文档及下述开发命令从 CLI `v0.7.0-preview.1` 提供。公共响应模板从 `v0.8.0-preview` 提供,CLI 与业务项目中的框架依赖需一起升级。
|
业务错误、具名鉴权、多行文档及下述开发命令从 CLI `v0.7.0-preview.1` 提供。公共响应模板从 `v0.8.0-preview` 提供,CLI 与业务项目中的框架依赖需一起升级。
|
||||||
|
|
||||||
@@ -331,3 +331,5 @@ python3 scripts/verifyManagedProject.py
|
|||||||
Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 `performance/managedProjectLocal.json`;完整记录见 [第一版验收](performance/managedProjectAcceptance.md)。
|
Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 `performance/managedProjectLocal.json`;完整记录见 [第一版验收](performance/managedProjectAcceptance.md)。
|
||||||
|
|
||||||
内置健康与 OpenAPI 路由可通过配置禁用或修改路径,`.api` 不再固定保留 `/health/live`、`/health/ready`、`/openapi.json`。启用的内置路径与业务 GET 路由重叠时,生成的 NewApp 在监听前返回配置错误;禁用后可以由 `.api` 自己维护这些路径。见 [内置接口配置](PROJECT.md#配置内置接口)。
|
内置健康与 OpenAPI 路由可通过配置禁用或修改路径,`.api` 不再固定保留 `/health/live`、`/health/ready`、`/openapi.json`。启用的内置路径与业务 GET 路由重叠时,生成的 NewApp 在监听前返回配置错误;禁用后可以由 `.api` 自己维护这些路径。见 [内置接口配置](PROJECT.md#配置内置接口)。
|
||||||
|
|
||||||
|
响应 DTO 自引用与相互引用从 v0.28.2 起支持,保留强类型并生成 OpenAPI `$ref`;见[递归响应说明](DEVELOPMENT.md#递归响应-dto)。直接值循环与字段组合循环仍然非法。
|
||||||
|
|||||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,22 @@
|
|||||||
|
# v0.30.0-preview
|
||||||
|
|
||||||
|
- 新增通用 `telemetry.HeaderPropagator` 与 `HeaderPropagationConfig`,配置化接入 Trace ID、Span ID、Request ID Header;不绑定任何网关名称。
|
||||||
|
- 入站优先使用标准 W3C `traceparent`;缺少有效 W3C 父链路时,合法的兼容 Header 对成为远程父 Span。日志、审计、访问日志沿用 request/trace 上下文。
|
||||||
|
- 出站 `telemetry.Transport` 同时传播 W3C 和配置的兼容 Header;旧配置不变,默认仍只使用 W3C。
|
||||||
|
- 补充自定义 Header、W3C 优先级、非法 ID、父子 Span 和通用离线示例回归;不引入业务类型、网关名称或第三方依赖。
|
||||||
|
|
||||||
|
# v0.29.0-preview
|
||||||
|
|
||||||
|
- 新增可选 `storage.ReadPresigner` / `ReadOptions` / `ReadLink`,签名读取可选择 inline/attachment、显示文件名、MIME 类型,返回签名内的实际到期时间。原 Store/Presigner 和旧下载链接兼容,TTL 上限仍为一小时。
|
||||||
|
- Local、S3/R2、OSS 均实现;全部选项参与本地 HMAC 或云端原生 SDK 签名,标准编码中文文件名并拒绝非法响应头。
|
||||||
|
- 本地读取支持 GET/HEAD、单段与多段 Range、206/416、条件请求,按文件内容真实长度处理,流式读取不暴露本地元数据。顺序读取的第三方 Store 保持完整 200 回退。
|
||||||
|
- 补充通用 PDF 运行示例、篡改与到期回归、OSS V1/V4 签名校验、真实隔离 MinIO 验证和远程安装检查;不引入业务模型或第三方依赖。
|
||||||
|
|
||||||
|
# v0.28.2-preview
|
||||||
|
|
||||||
|
- 修复响应 DTO 的自引用/相互引用生成,支持 slice、pointer、map;OpenAPI 使用命名组件与循环引用,保留约束、描述、示例及 scalar 输出 schema。
|
||||||
|
- 拒绝 Go 非法值循环与递归请求;保留格式化、生成、契约变更和只读检查能力。新增通用树示例与真实生成消费者回归。
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v0.27.0-preview(2026-09-29)
|
## v0.27.0-preview(2026-09-29)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 通用业务开发指南(v0.27.0-preview)
|
# 通用业务开发指南(v0.30.0-preview)
|
||||||
|
|
||||||
从 [README](README.md) 创建服务后,业务主要维护 `.api`、配置、`svc.Init`、Logic、Repository 和项目中间件。本页说明通用接入能力;字段、错误码、权限、表结构和事务边界都由项目定义。
|
从 [README](README.md) 创建服务后,业务主要维护 `.api`、配置、`svc.Init`、Logic、Repository 和项目中间件。本页说明通用接入能力;字段、错误码、权限、表结构和事务边界都由项目定义。
|
||||||
|
|
||||||
@@ -35,6 +35,30 @@ delete /item
|
|||||||
|
|
||||||
基础类型数组还支持 int/int8/int16/int32、uint/uint16/uint32/uint64、float32/float64。nil 根数组都输出 `[]`;`[]any` 在 OpenAPI 中是 `type: array, items: {}`,不限制元素形状,但元素必须可被标准 JSON 编码器序列化。`[]uint8`(Go 的 byte slice)会编码为 base64,故明确拒绝;数字数组可用 `[]int32`,二进制用下载接口。当前不支持根嵌套数组或根自定义 scalar 数组。
|
基础类型数组还支持 int/int8/int16/int32、uint/uint16/uint32/uint64、float32/float64。nil 根数组都输出 `[]`;`[]any` 在 OpenAPI 中是 `type: array, items: {}`,不限制元素形状,但元素必须可被标准 JSON 编码器序列化。`[]uint8`(Go 的 byte slice)会编码为 base64,故明确拒绝;数字数组可用 `[]int32`,二进制用下载接口。当前不支持根嵌套数组或根自定义 scalar 数组。
|
||||||
|
|
||||||
|
## 递归响应 DTO
|
||||||
|
|
||||||
|
v0.28.2 起,响应 DTO 可引用自身或相互引用;名字与字段由项目定义:
|
||||||
|
|
||||||
|
```text
|
||||||
|
type Node {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Children []Node `json:"children"`
|
||||||
|
Next *Node `json:"next,omitempty"`
|
||||||
|
Lookup map[string]Node `json:"lookup,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
@handler tree
|
||||||
|
get /tree returns (Node)
|
||||||
|
```
|
||||||
|
|
||||||
|
Go 类型保留 `[]Node`、`*Node`、`map[string]Node`,不降级为 `any`。生成器将递归响应图中的命名 DTO 写入 `components.schemas.DTO_<类型名>`,字段使用 `$ref`;对象、数组、可空根对象、raw 及自定义响应模板都支持。可空引用使用 `anyOf` 的引用与 null 分支,描述和示例通过 `allOf` 保留,遵循 [OpenAPI 3.0.3 引用规则](https://spec.openapis.org/oas/v3.0.3.html#reference-object)。
|
||||||
|
|
||||||
|
字段命名、标签、约束、重复字段与示例继续校验,自定义 scalar 使用声明的输出类型。有限 JSON 示例按实际内容检查,仍受 32 层/10000 节点的编译限制。类型引用不会无限展开;再次生成和契约变更检查会遍历引用图并保留手写代码。
|
||||||
|
|
||||||
|
范围是 `.api` 的响应类型。递归请求 DTO(包括间接包含递归类型的请求)仍不支持,`contract.New/Describe` 与手写 `openapi.SchemaOf` 的限制不变;它们不会自动获得递归请求绑定能力。直接值递归如 `Child Node`、仅由值字段形成的相互递归,以及字段组合循环仍会报错。业务返回值必须是可序列化的有限数据,不能构造对象指针环。
|
||||||
|
|
||||||
|
升级 CLI 后,在项目自己的 `.api` 中将临时 `[]any` 恢复成 `[]项目类型`,执行 `go-omni api generate`、`go-omni api generate --check` 和 `go test ./...`。修改 `.api` 后应先生成再检查;不要手改生成的 DTO。不改变项目的菜单、分类、权限或数据构造规则。完整可运行步骤见 [递归响应示例](examples/recursiveResponse/README.md)。
|
||||||
|
|
||||||
## 契约驱动的请求策略
|
## 契约驱动的请求策略
|
||||||
|
|
||||||
入口文件可以声明默认策略,单个接口按需覆盖:
|
入口文件可以声明默认策略,单个接口按需覆盖:
|
||||||
@@ -199,6 +223,31 @@ go-omni workspace check --file omni.workspace.json
|
|||||||
|
|
||||||
各服务保持独立 module;可显式填写各服务的 `api`(相对服务目录)和 `module`。所有服务预检通过后再写;每个服务独立提交文件事务,不承诺跨项目的崩溃原子性。check 汇总格式与生成漂移;业务编译/测试仍在各 module 执行。
|
各服务保持独立 module;可显式填写各服务的 `api`(相对服务目录)和 `module`。所有服务预检通过后再写;每个服务独立提交文件事务,不承诺跨项目的崩溃原子性。check 汇总格式与生成漂移;业务编译/测试仍在各 module 执行。
|
||||||
|
|
||||||
|
### 混合 API 与通用工程
|
||||||
|
|
||||||
|
v0.29.0-preview 起,workspace 支持在同一份清单中管理 API、通用工程,也支持仅包含通用工程的工作区:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{ "dir": "apps/interface", "kind": "api" },
|
||||||
|
{ "dir": "apps/program", "kind": "project" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`kind` 可省略:根据 `.goOmni/apiManifest.json` 或 `.goOmni/projectManifest.json` 识别。没有清单时,沿用旧 API 解释,需显式提供 api 入口才能首次生成。两种清单同时存在、显式 kind 与已有清单冲突、未知 kind 均报错。project 类型不能填写 api;可选 module 用来核对工程身份,不会重写 go.mod。
|
||||||
|
|
||||||
|
| 命令 | API 工程 | 通用工程 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| workspace generate | API 生成器 | project 生成器 |
|
||||||
|
| workspace check | 契约格式检查 + API 生成漂移检查 | project 生成漂移检查 |
|
||||||
|
| workspace format | 格式化 API 契约 | 显示 SKIP,不修改 Go 文件 |
|
||||||
|
|
||||||
|
检查会继续处理独立工程并汇总错误,任一失败返回非零。生成/格式化先预检全部目标;预检失败时输出 FAIL 与其他工程的 SKIP,不写任何生成结果。清单语法、目录越界/重叠或共享目录提供方失败属于工作区级错误,会直接中止。已有文件保护与路径规则保持,事务仍以单个工程为单位。
|
||||||
|
|
||||||
|
workspace 不创建或修改 go.work、go.mod、go.sum,不替代 Go 编译和业务测试。它只调用生成器;显式 errorCatalogs 导出仍按下节约定执行 Go 提供方,配置类型不会额外启动服务。完整 [混合工程与 go.work 示例](examples/mixedWorkspace/README.md)。
|
||||||
|
|
||||||
### 同一份 Go 错误目录供多个服务生成
|
### 同一份 Go 错误目录供多个服务生成
|
||||||
|
|
||||||
在 workspace 清单显式声明提供目录的 Go 包和函数:
|
在 workspace 清单显式声明提供目录的 Go 包和函数:
|
||||||
|
|||||||
81
PROGRAM.md
Normal file
81
PROGRAM.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 通用 Go 项目模板
|
||||||
|
|
||||||
|
从 v0.28.0-preview 起,`project new/generate/check` 提供不绑定协议与业务的工程骨架。默认只有配置、日志、依赖装配、生命周期和普通 Go 运行入口;不生成 `.api`、HTTP Handler、Swagger、业务任务,也不监听端口、连接数据库或队列。不增加第三方依赖。
|
||||||
|
|
||||||
|
## 创建与运行
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.30.0-preview
|
||||||
|
go-omni project new demo --module example.com/demo
|
||||||
|
cd demo
|
||||||
|
go mod tidy
|
||||||
|
go run . -check-config
|
||||||
|
go run . # 默认等待 SIGINT / SIGTERM
|
||||||
|
go build -o program .
|
||||||
|
```
|
||||||
|
|
||||||
|
目标目录必须为空;未指定 module 时使用目录名。远程安装的 CLI 将自己的固定版本写入 go.mod,无本地替换。当前通用模板要求框架 v0.28.0-preview 或更新版本。源码开发可显式 `--framework-dir /path/to/go-omni`,不会从当前工作目录隐式选择本地框架。`--framework-version` 可指定兼容版本。框架版本、依赖更新由项目修改 go.mod,重新生成不会代为升级依赖。
|
||||||
|
|
||||||
|
常驻程序、一次性程序用同一份骨架:项目自己的 `Run` 决定何时完成。完整生成项目见 [examples/program](examples/program/README.md),不需要外部服务。
|
||||||
|
|
||||||
|
## 文件职责
|
||||||
|
|
||||||
|
| 路径 | 维护方与职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `main.go` | 生成器:信号、启动、运行入口监督、退出状态与日志收尾 |
|
||||||
|
| `internal/config/loadGen.go` | 生成器:统一配置加载 |
|
||||||
|
| `internal/svc/runtimeGen.go` | 生成器:依赖初始化及失败收尾 |
|
||||||
|
| `internal/config/config.go` | 项目:Config、Default、Validate;初始只含 log/lifecycle |
|
||||||
|
| `internal/config/loader.go` | 项目:按需注册额外文件格式 |
|
||||||
|
| `etc/config.yaml` | 项目:配置与显式环境引用 |
|
||||||
|
| `internal/svc/serviceContext.go` | 项目:依赖字段、Init、资源与后台工作登记 |
|
||||||
|
| `internal/run/run.go` | 项目:Run 的实际实现 |
|
||||||
|
| `go.mod`、README、.gitignore、其他 Go 文件 | 项目:生成器不覆盖 |
|
||||||
|
| `.goOmni/projectManifest.json` | 生成器:工程身份、模板版本、生成文件哈希;应提交 Git |
|
||||||
|
|
||||||
|
配置遵守 [CONFIG.md](CONFIG.md):默认值 → 文件显式 `${NAME}` / `${NAME:-fallback}` → 类型绑定 → 校验,没有隐式环境覆盖。JSON/YAML/XML 内置;`-config ""` 只用默认值。`-check-config` 不打开日志文件、不初始化依赖、不调用 Run。日志通过独立实例传给 svc,并注入 Context/WorkContext;不替换进程级默认日志实例。
|
||||||
|
|
||||||
|
## 运行与退出
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Run(ctx context.Context, deps *svc.ServiceContext) error {
|
||||||
|
// 一次性程序可完成处理后直接 return nil。
|
||||||
|
// 常驻程序收到 ctx.Done() 后停止自己的主流程。
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`nil` 表示正常完成,返回 error 表示运行失败。后台组件用 `RequestShutdown(err)` 报告致命故障,`RequestShutdown(nil)` 请求正常停止。框架不替项目重试、重新调度或重启任务。
|
||||||
|
|
||||||
|
依赖在 `svc.Init` 中装配:创建后立即用 `OnClose` 登记清理,后台组件同时登记同名 `OnStop` 和 `OnDrain`。所有登记应在 Init 返回前完成;Run 不得并发追加生命周期登记。组件何时接收工作由项目决定,停止后不得重新开始领取或启动未登记的工作。
|
||||||
|
|
||||||
|
- `Context()` 是提前停止通知,用来停止主流程或接收新工作。
|
||||||
|
- `WorkContext()` 在正常排空期间仍有效,用于已接受的工作。
|
||||||
|
- `OnStop` 停止接收新工作,不关闭处理中的工作仍依赖的资源。
|
||||||
|
- `OnDrain` 返回 nil,必须意味着组件处理、确认和 defer 收尾均已结束。仅返回 `ctx.Err()` 不能证明结束。
|
||||||
|
- 全部停止钩子先于全部排空钩子;同阶段并行。有先后依赖的组件在一个钩子内显式排序。
|
||||||
|
- 全部工作确认结束后,资源按 `OnClose` 登记的反序释放,最后关闭入口拥有的日志。
|
||||||
|
|
||||||
|
生成入口自动监督 Run 本身,以 `go-omni/project/run` 保留名称登记完成屏障。收到信号、Run 返回或报告故障后开始收尾,同时等待 Run 及其他已登记工作。Run 不应等待资源关闭:资源关闭发生在 Run 完成之后。
|
||||||
|
|
||||||
|
初始化失败同样收尾已创建的资源和已启动的工作。Init/Run 内 panic 被转为失败,安全收尾后进程以非零状态退出;任意自建 goroutine 的 panic 仍由项目负责处理。Run 不响应取消或其他工作无法确认结束时,`lifecycle.stopTimeout/drainTimeout/cancelTimeout` 限定等待窗口,返回 `ErrResourcesRetained`,跳过共享资源和日志关闭,以失败状态退出。取消后的工作不能被 Go 强制终止,保留资源不代表工作已经完成。SIGKILL 和进程崩溃不会执行退出钩子。
|
||||||
|
|
||||||
|
详见 [生命周期契约](lifecycle/README.md)。现成组件必须按其真实停止/完成语义适配,不能把某个 `Run/Close` 返回直接视为业务结束。
|
||||||
|
|
||||||
|
## 生成检查与兼容
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go-omni project generate --dry-run
|
||||||
|
go-omni project generate
|
||||||
|
go-omni project check # 等价于 project generate --check
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
生成器不执行项目 Go 代码、不下载依赖。check/预览只读,不创建锁文件;生成时使用独立项目锁、路径和哈希预检。生成文件被手工修改会报冲突,不提供强制覆盖。删除的生成文件可补回;缺失的项目所有文件必须显式恢复,避免把删除的业务文件当成空白骨架重建。
|
||||||
|
|
||||||
|
每个文件通过原子替换写入,普通写入失败回滚此前变更;整个目录不具备崩溃事务性。生成期间不要同时手工编辑。发现遗留 project.lock 时,先确认没有生成进程再手工移除。
|
||||||
|
|
||||||
|
check 检查模板漂移和必要文件,不检查业务正确性,也不能替代 Go 编译与测试。新的模板版本会重新生成受管文件;不自动合并项目所有文件或更新 go.mod。未知的新清单版本会要求使用兼容 CLI。
|
||||||
|
|
||||||
|
原有 `new` / `api new` 及 API 工程行为保留。通用工程使用自己的清单和命令,不能交给 `api generate` 或 `add` 转换;新增业务包直接写普通 Go。从 v0.29.0-preview 起,`workspace generate/check` 可以同时处理 API 与通用工程。清单可填写 `kind: api | project`,省略时按生成清单识别;format 对通用工程显示 SKIP。见 [完整混合示例](examples/mixedWorkspace/README.md)。各 module 仍独立,workspace 命令不维护 go.work 或模块依赖。
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# 生成项目与文件维护边界
|
# API 生成项目与文件维护边界
|
||||||
|
|
||||||
v0.27.0-preview 提供受管理的生成项目。安装同版远程 CLI,生成的项目直接引用该版本,无需本地 replace。根目录入口支持 `go run .` 和 `go build .`;框架本身仍然是可直接引用的库。安装和私有仓库访问见 [README](README.md#准备环境与安装)。
|
通用 Go 工程使用独立的 `project new/generate/check`,见 [PROGRAM.md](PROGRAM.md)。本文说明 API 契约工程。
|
||||||
|
|
||||||
|
v0.28.0-preview 提供受管理的生成项目。安装同版远程 CLI,生成的项目直接引用该版本,无需本地 replace。根目录入口支持 `go run .` 和 `go build .`;框架本身仍然是可直接引用的库。安装和私有仓库访问见 [README](README.md#准备环境与安装)。
|
||||||
|
|
||||||
## 创建和日常开发
|
## 创建和日常开发
|
||||||
|
|
||||||
@@ -215,7 +217,7 @@ get /download (DownloadReq)
|
|||||||
|
|
||||||
## 已有服务接入步骤
|
## 已有服务接入步骤
|
||||||
|
|
||||||
每个独立服务生成一个项目,module 名称、上层目录和共享业务包由项目选择。接入时固定 CLI 与框架版本为 v0.27.0-preview:
|
每个独立服务生成一个项目,module 名称、上层目录和共享业务包由项目选择。接入时固定 CLI 与框架版本为 v0.28.0-preview:
|
||||||
|
|
||||||
1. 先保留旧服务,整理实际路由及现有响应、鉴权和超时要求。在新目录执行 `go-omni new --api /path/to/service.api --module example.com/service ./service`;不对旧入口强制覆盖。
|
1. 先保留旧服务,整理实际路由及现有响应、鉴权和超时要求。在新目录执行 `go-omni new --api /path/to/service.api --module example.com/service ./service`;不对旧入口强制覆盖。
|
||||||
2. 在 `.api` 中选择 `@app(defaultResponse: ...)`,显式保留原项目的字段、成功码、错误码和 `dataMode`。需要独立回执的路由单独声明 raw/produces。普通路由、404/405 和提前返回的中间件一起验收。
|
2. 在 `.api` 中选择 `@app(defaultResponse: ...)`,显式保留原项目的字段、成功码、错误码和 `dataMode`。需要独立回执的路由单独声明 raw/produces。普通路由、404/405 和提前返回的中间件一起验收。
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -1,12 +1,22 @@
|
|||||||
# GoOmni
|
# GoOmni
|
||||||
|
|
||||||
GoOmni 是面向多个业务项目复用的 Go 后端框架:用 `.api` 定义接口、请求和返回类型,生成项目目录、HTTP 接入代码及 OpenAPI,业务代码负责 Logic 和 Repository。也可以在已有项目中直接使用 `omni.New()`,或单独引入日志、存储等模块。
|
GoOmni 是面向多个项目复用的 Go 基础框架。通用工程使用 `project new` 生成配置、日志、依赖装配、生命周期和普通 Go 运行入口;需要 HTTP 接口时可选择 `.api` 工程生成,也可以在已有项目中按需引入独立模块。运行方式、业务类型和外部依赖由项目决定。
|
||||||
|
|
||||||
|
**v0.29 改动**:通用签名读取新增 inline/attachment、中文显示文件名、可选 Content-Type 与实际到期时间;Local、S3/R2、OSS 使用同一接口,原下载接口兼容。本地支持 HEAD、Range、206/416,所有响应选项参与签名。见 [存储说明](storage/README.md#签名预览与下载)及[可运行 PDF 示例](examples/signedRead/README.md)。
|
||||||
|
|
||||||
|
**v0.30 改动**:telemetry 新增配置化 `HeaderPropagator`,兼容网关传入的 Trace/Span/Request Header,同时保留 W3C `traceparent`;入站父链路、日志关联和出站传播统一处理。框架不绑定任何网关名称。见[可运行示例](examples/telemetryHeaders/README.md)和[观测说明](ADVANCED.md#熔断与观测)。
|
||||||
|
|
||||||
|
**v0.28.2 改动**:API 响应 DTO 支持 `Children []Node`、`Next *Node`、递归 map 和相互引用,生成强类型 Go 字段及有限的 OpenAPI `$ref` 图。字段约束、示例校验、生成与漂移检查继续有效;请求递归和非法值递归仍会报错。见 [可运行示例](examples/recursiveResponse/README.md)与[接入说明](DEVELOPMENT.md#递归响应-dto)。
|
||||||
|
|
||||||
|
**v0.28.1 改动**:`workspace generate/check` 支持 API 与通用工程混合处理,可选 `kind: api | project`,省略时按生成清单识别。`format` 对通用工程显示跳过;检查只读,生成前统一预检。见 [混合工作区示例](examples/mixedWorkspace/README.md)。
|
||||||
|
|
||||||
|
**v0.28 改动**:新增独立的 `project new/generate/check`,默认无 API、监听端口、业务任务或外部服务配置;保护手写代码,并等待主运行入口和登记的后台工作结束后释放依赖。见 [通用工程指南](PROGRAM.md)和[可运行示例](examples/program/README.md)。
|
||||||
|
|
||||||
**v0.27 改动**:统一三种内置服务实现的默认/路由/硬请求体限额,配置冲突在启动前报错;停机以处理函数实际退出为资源清理前提,超时仍有请求时保留共享依赖。见 [HTTP 策略](transport/README.md#统一请求体限额)、[停机规则](lifecycle/README.md#http-请求与依赖的关闭屏障)和[可运行示例](examples/httpSafety/README.md)。
|
**v0.27 改动**:统一三种内置服务实现的默认/路由/硬请求体限额,配置冲突在启动前报错;停机以处理函数实际退出为资源清理前提,超时仍有请求时保留共享依赖。见 [HTTP 策略](transport/README.md#统一请求体限额)、[停机规则](lifecycle/README.md#http-请求与依赖的关闭屏障)和[可运行示例](examples/httpSafety/README.md)。
|
||||||
|
|
||||||
**v0.26 改动**:通用 [i18n](i18n/README.md),提供独立词典、语言匹配、回退、命名参数/条件/复数、可选 HTTP 与错误适配、检查/导出 CLI 及前端运行时。文案由项目维护;[完整示例](examples/i18n/README.md)可直接运行。CLI 与框架使用同版 v0.27.0-preview。同时修复 Go 1.22 下请求体限额错误的识别。
|
**v0.26 改动**:通用 [i18n](i18n/README.md),提供独立词典、语言匹配、回退、命名参数/条件/复数、可选 HTTP 与错误适配、检查/导出 CLI 及前端运行时。文案由项目维护;[完整示例](examples/i18n/README.md)可直接运行。CLI 与框架使用同版 v0.30.0-preview。同时修复 Go 1.22 下请求体限额错误的识别。
|
||||||
|
|
||||||
**从这份 README 开始接入。** 当前版本 **`v0.27.0-preview`**,新生成项目采用根目录入口和 `config / svc / logic / handler`。可以固定版本开展业务接入;文件维护边界和已有服务迁移步骤见 [项目指南](PROJECT.md),版本变化与验收见 [发布说明](RELEASE.md)。
|
**从这份 README 开始接入。** 当前版本 **`v0.30.0-preview`**,通用工程采用根目录入口和 `config / svc / run`;API 工程使用 `config / svc / logic / handler`。可以固定版本开展业务接入;文件维护边界和已有服务迁移步骤见 [项目指南](PROJECT.md),版本变化与验收见 [发布说明](RELEASE.md)。
|
||||||
|
|
||||||
**v0.25 改动**:`database.InTx` 返回可识别的事务阶段与结果,保留业务/驱动及回滚错误链;提交报错保守标记为结果不确定,不等同于已回滚。[使用规则](database/README.md#事务阶段与结果)、[可运行故障示例](examples/transactions/README.md)。
|
**v0.25 改动**:`database.InTx` 返回可识别的事务阶段与结果,保留业务/驱动及回滚错误链;提交报错保守标记为结果不确定,不等同于已回滚。[使用规则](database/README.md#事务阶段与结果)、[可运行故障示例](examples/transactions/README.md)。
|
||||||
|
|
||||||
@@ -38,6 +48,8 @@ Go **1.22+**。框架不依赖 Gin,默认使用标准库网络层,提供可
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 项目、接口与文档生成 | `go-omni new`;改 `.api` 后 `api generate`;`--check` 检查漂移 | [下方快速开始](#新项目生成并运行)、[CLI](cmd/go-omni/README.md)、[完整生成项目](examples/managedService/README.md) |
|
| 项目、接口与文档生成 | `go-omni new`;改 `.api` 后 `api generate`;`--check` 检查漂移 | [下方快速开始](#新项目生成并运行)、[CLI](cmd/go-omni/README.md)、[完整生成项目](examples/managedService/README.md) |
|
||||||
| 公共字段与跨服务共享契约 | `.api` 组合对象字段;`omni.imports.json` 声明共享根;workspace 批量生成 | [公共字段示例](GENERATION.md#公共字段组合)、[共享契约与 workspace](DEVELOPMENT.md) |
|
| 公共字段与跨服务共享契约 | `.api` 组合对象字段;`omni.imports.json` 声明共享根;workspace 批量生成 | [公共字段示例](GENERATION.md#公共字段组合)、[共享契约与 workspace](DEVELOPMENT.md) |
|
||||||
|
| 混合工程工作区 | `workspace generate/check` 按清单分发 API/project;format 仅处理 API | [可运行示例](examples/mixedWorkspace/README.md)、[工作区说明](DEVELOPMENT.md#混合-api-与通用工程) |
|
||||||
|
| 通用 Go 工程 | `go-omni project new/generate/check`;配置、日志、依赖、运行与退出 | [通用工程指南](PROGRAM.md)、[完整可运行示例](examples/program/README.md) |
|
||||||
| 中间件依赖工厂、可选 Repository | `omni.gen.json` 选择 factories / none;在 `New<Name>(deps)` 写一次实现 | [配置与完整工厂示例](GENERATION.md) |
|
| 中间件依赖工厂、可选 Repository | `omni.gen.json` 选择 factories / none;在 `New<Name>(deps)` 写一次实现 | [配置与完整工厂示例](GENERATION.md) |
|
||||||
| 路由、分组、请求上下文 | `omni.New()`、`app.GET` / `Group` / `Use`;业务使用 `context.Context` | [基本服务](examples/basic/main.go)、[已有项目接入](#已有项目按模块接入) |
|
| 路由、分组、请求上下文 | `omni.New()`、`app.GET` / `Group` / `Use`;业务使用 `context.Context` | [基本服务](examples/basic/main.go)、[已有项目接入](#已有项目按模块接入) |
|
||||||
| HTTP 网络与扩展适配 | 配置 transport 为 nethttp / native / http2;自定义 Router/Runner | [网络层配置与契约](transport/README.md)、[性能与限制](PERFORMANCE.md) |
|
| HTTP 网络与扩展适配 | 配置 transport 为 nethttp / native / http2;自定义 Router/Runner | [网络层配置与契约](transport/README.md)、[性能与限制](PERFORMANCE.md) |
|
||||||
@@ -60,12 +72,28 @@ Go **1.22+**。框架不依赖 Gin,默认使用标准库网络层,提供可
|
|||||||
| 请求追踪、访问日志、恢复、限流、幂等、超时 | `app.Use(...)` 按需组合 middleware;跨副本选择 Redis 实现 | [模块说明与限制](MODULES.md#请求追踪与基础指标)、[分布式实现](ADVANCED.md#分布式限流与幂等) |
|
| 请求追踪、访问日志、恢复、限流、幂等、超时 | `app.Use(...)` 按需组合 middleware;跨副本选择 Redis 实现 | [模块说明与限制](MODULES.md#请求追踪与基础指标)、[分布式实现](ADVANCED.md#分布式限流与幂等) |
|
||||||
| 本地后台任务与调度 | `task.New`、Submit、Every;有界队列、排空关闭 | [task 示例与生命周期](MODULES.md#后台执行task) |
|
| 本地后台任务与调度 | `task.New`、Submit、Every;有界队列、排空关闭 | [task 示例与生命周期](MODULES.md#后台执行task) |
|
||||||
| 持久队列 | 业务使用 Publisher / Consumer;Redis Streams 提供至少一次交付 | [队列初始化/消费示例](ADVANCED.md#可靠队列) |
|
| 持久队列 | 业务使用 Publisher / Consumer;Redis Streams 提供至少一次交付 | [队列初始化/消费示例](ADVANCED.md#可靠队列) |
|
||||||
| 文件存储、直传、云端分片 | Store 的 Put/Get/Delete;Local、S3/R2、OSS 按配置装配;扩展接口提供分片续传 | [存储接口与完整例子](storage/README.md)、[本地存储服务](examples/platform/README.md) |
|
| 文件存储、签名预览/下载、直传与云端分片 | Store 的 Put/Get/Delete;ReadPresigner 签名响应选项;Local、S3/R2、OSS 按配置装配 | [存储接口与完整例子](storage/README.md)、[本地存储服务](examples/platform/README.md) |
|
||||||
| 日志关联、指标与链路 | `metrics.New().Middleware()`;telemetry 注入 OTel exporter/reader | [基础指标](MODULES.md#请求追踪与基础指标)、[OTel 用法](ADVANCED.md#熔断与观测)、[完整接入](examples/platform/main.go) |
|
| 日志关联、指标与链路 | `metrics.New().Middleware()`;telemetry 注入 OTel exporter/reader;`HeaderPropagator` 兼容自定义网关 Header | [基础指标](MODULES.md#请求追踪与基础指标)、[OTel 用法](ADVANCED.md#熔断与观测)、[Header 示例](examples/telemetryHeaders/README.md) |
|
||||||
| 审计与分页 | `audit.New` 注入 Sink/白名单;`pagination.Parse` / `NewResult` | [解释与调用例子](ADVANCED.md#迁移审计与分页) |
|
| 审计与分页 | `audit.New` 注入 Sink/白名单;`pagination.Parse` / `NewResult` | [解释与调用例子](ADVANCED.md#迁移审计与分页) |
|
||||||
| 可选结构转换与深拷贝 | `copyx.Copy` / `Clone` / `New`;`Check[S,T]` 预检类型,Go converter 处理差异 | [下方示例](#可选结构转换与深拷贝)、[完整 copyx 指南](COPY.md)、[可运行代码](examples/copying) |
|
| 可选结构转换与深拷贝 | `copyx.Copy` / `Clone` / `New`;`Check[S,T]` 预检类型,Go converter 处理差异 | [下方示例](#可选结构转换与深拷贝)、[完整 copyx 指南](COPY.md)、[可运行代码](examples/copying) |
|
||||||
| 自定义适配器验收 | `transporttest.Run`、`storagetest.Run` / `RunUploads` 验证扩展契约 | [网络契约测试入口](transport/transporttest/router.go)、[存储契约测试入口](storage/storagetest/contract.go) |
|
| 自定义适配器验收 | `transporttest.Run`、`storagetest.Run` / `RunUploads` 验证扩展契约 | [网络契约测试入口](transport/transporttest/router.go)、[存储契约测试入口](storage/storagetest/contract.go) |
|
||||||
|
|
||||||
|
## 通用工程快速开始
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.30.0-preview
|
||||||
|
go-omni project new demo --module example.com/demo
|
||||||
|
cd demo
|
||||||
|
go mod tidy
|
||||||
|
go run . -check-config
|
||||||
|
go run .
|
||||||
|
# Ctrl+C / SIGTERM 退出;项目自己的 Run 也可以完成后直接返回。
|
||||||
|
go-omni project check
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
项目只修改配置、svc 依赖初始化和 Run;默认没有 HTTP 端口、数据库、队列或自动重试。`main.go`、加载和装配代码由生成器维护;手改受管文件会报冲突,手写文件保持。完整维护边界、异常退出和超时行为见 [PROGRAM.md](PROGRAM.md)。
|
||||||
|
|
||||||
## 浏览器跨域 CORS
|
## 浏览器跨域 CORS
|
||||||
|
|
||||||
同源部署保持默认关闭;需要跨域时,在生成项目的配置文件中加入:
|
同源部署保持默认关闭;需要跨域时,在生成项目的配置文件中加入:
|
||||||
@@ -158,7 +186,7 @@ module 地址是 `git.yuntuo.hk/k2safe/go-omni`。先确保本机 Go 和 Git 可
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
export GOPRIVATE="$(go env GOPRIVATE),git.yuntuo.hk"
|
export GOPRIVATE="$(go env GOPRIVATE),git.yuntuo.hk"
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.27.0-preview
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.30.0-preview
|
||||||
export PATH="$(go env GOPATH)/bin:$PATH"
|
export PATH="$(go env GOPATH)/bin:$PATH"
|
||||||
go-omni --help
|
go-omni --help
|
||||||
```
|
```
|
||||||
@@ -315,7 +343,7 @@ go-omni new --api catalog.api \
|
|||||||
在业务项目的 **go.mod 所在目录**执行:
|
在业务项目的 **go.mod 所在目录**执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get git.yuntuo.hk/k2safe/go-omni@v0.27.0-preview
|
go get git.yuntuo.hk/k2safe/go-omni@v0.30.0-preview
|
||||||
```
|
```
|
||||||
|
|
||||||
已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。
|
已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。
|
||||||
@@ -646,7 +674,7 @@ go test ./...
|
|||||||
|
|
||||||
`main.go`、`Gen.go`、`docs/openapi.json` 和 manifest 由生成器管理,手动修改会报告冲突。Logic、Repository、中间件实现、配置和依赖装配属于项目,重复生成保留手写内容。契约变化会提示受影响的 Logic 文件和 DTO 字段,明确的签名错误会给出原签名与期望签名;生成后 `--check` 仍能检出未修复的问题。将 `.api`、`omni.gen.json`、生成文件和 `.goOmni/apiManifest.json` 一起提交,CI 同时运行 `generate --check` 与 `go test ./...`。详见 [契约变更提示与检查边界](PROJECT.md#契约变更提示)。
|
`main.go`、`Gen.go`、`docs/openapi.json` 和 manifest 由生成器管理,手动修改会报告冲突。Logic、Repository、中间件实现、配置和依赖装配属于项目,重复生成保留手写内容。契约变化会提示受影响的 Logic 文件和 DTO 字段,明确的签名错误会给出原签名与期望签名;生成后 `--check` 仍能检出未修复的问题。将 `.api`、`omni.gen.json`、生成文件和 `.goOmni/apiManifest.json` 一起提交,CI 同时运行 `generate --check` 与 `go test ./...`。详见 [契约变更提示与检查边界](PROJECT.md#契约变更提示)。
|
||||||
|
|
||||||
CLI 与业务 module 固定使用 `v0.27.0-preview`。v0.21 及更早项目先按 [配置迁移](CONFIG.md#v022-迁移旧模式已移除) 整理旧标签、文件与 Load,再升级依赖与 CLI 并重新生成,框架维护的 main/runtimeGen 自动更新;v0.13 及更早项目按 [升级预览](DEVELOPMENT.md#v013-项目升级) 处理,不直接覆盖手写入口或业务文件。使用过旧 mapping 的项目必须先完成 [移除迁移](COPY.md#从旧-mapping-迁移),旧配置或旧生成文件会阻止生成。升级后执行生成检查与业务回归。版本变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。
|
CLI 与业务 module 固定使用 `v0.30.0-preview`。v0.21 及更早项目先按 [配置迁移](CONFIG.md#v022-迁移旧模式已移除) 整理旧标签、文件与 Load,再升级依赖与 CLI 并重新生成,框架维护的 main/runtimeGen 自动更新;v0.13 及更早项目按 [升级预览](DEVELOPMENT.md#v013-项目升级) 处理,不直接覆盖手写入口或业务文件。使用过旧 mapping 的项目必须先完成 [移除迁移](COPY.md#从旧-mapping-迁移),旧配置或旧生成文件会阻止生成。升级后执行生成检查与业务回归。版本变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。
|
||||||
|
|
||||||
## 按需深入
|
## 按需深入
|
||||||
|
|
||||||
|
|||||||
106
RELEASE.md
106
RELEASE.md
@@ -1,3 +1,107 @@
|
|||||||
|
# v0.30.0-preview
|
||||||
|
|
||||||
|
新增通用 `telemetry.HeaderPropagator`,让服务在不修改业务 Handler 的情况下接入使用自定义 Trace/Span/Request Header 的上游网关。配置 Header 名称后,入站建立远程父 Span,`logx.TraceID` / `RequestID`、访问日志和审计沿用上下文;出站 `telemetry.Transport` 同时注入 W3C 和兼容 Header。
|
||||||
|
|
||||||
|
W3C `traceparent` 始终优先于兼容 Header。兼容 Header 只接受合法的 32 位 Trace ID 和 16 位 Span ID,当前服务生成自己的 Span ID;Request ID 单独传播,不参与父 Span。旧项目不配置新传播器时,默认行为不变。兼容 Header 不携带采样标志,有效成对值按已采样处理;需要传递采样决定时使用 W3C。
|
||||||
|
|
||||||
|
```go
|
||||||
|
propagator, err := telemetry.NewHeaderPropagator(telemetry.HeaderPropagationConfig{
|
||||||
|
TraceIDHeader: "X-Trace-Id", SpanIDHeader: "X-Span-Id", RequestIDHeader: "X-Request-Id",
|
||||||
|
})
|
||||||
|
if err != nil { return err }
|
||||||
|
runtime, err := telemetry.New(telemetry.Config{
|
||||||
|
Service: "billing", SampleRatio: 1, Propagator: propagator,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
只在受信网关入口启用兼容传播;Trace/Span ID 不是认证、租户或幂等凭据。完整 [接入说明](ADVANCED.md#熔断与观测)、[离线示例](examples/telemetryHeaders/README.md)。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get git.yuntuo.hk/k2safe/go-omni@v0.30.0-preview
|
||||||
|
go test -race ./telemetry ./examples/telemetryHeaders
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# v0.29.0-preview
|
||||||
|
|
||||||
|
新增通用签名读取选项:预览或下载、显示文件名和 MIME 类型,由调用方选择。`PresignGetWithOptions` 返回 URL 与签名内的到期时间;原 `PresignGet` 默认下载、第三方原有接口和旧本地链接继续兼容。有效期仍是 1 秒到 1 小时,不增加撤销或业务分享机制。
|
||||||
|
|
||||||
|
Local、S3/R2、OSS 使用同一接口,所有选项参与签名。中文文件名采用标准编码,拒绝 CR/LF 等非法头内容。本地支持 HEAD、单段/多段 Range、206/416 与条件请求,按真实内容范围流式输出;过期与篡改在读取文件之前拒绝。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get git.yuntuo.hk/k2safe/go-omni@v0.29.0-preview
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.29.0-preview
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
[接口与接入边界](storage/README.md#签名预览与下载)、[完整示例](examples/signedRead/README.md)、[验收记录](reports/signedReadAcceptance.md)。生成模板最低运行时版本不变;使用新读取接口需要升级框架依赖。OSS 默认域名可能强制下载,应在 SDK 签名前配置所需域名;OSS/R2 云账号尚未实测,部署方须按实际桶、域名和浏览器验收。
|
||||||
|
|
||||||
|
57 个测试包的全量 race 与 Go 1.22 集成回归、vet/build、隔离 MinIO 验证通过。远程空缓存综合 107 项检查、11 组生成消费者、通用工程 30 项与混合工作区 16 项通过,均无本地 replace。标签固定在 `f0c62152d72b1b50672c29f5191ad88f8be3eee4`,见 [远程报告](reports/signedReadRemote.json)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# v0.28.2-preview
|
||||||
|
|
||||||
|
修复 `.api` 响应 DTO 自引用与相互引用的生成限制。`Children []Node`、`Next *Node`、`map[string]Node` 保留强类型,响应文档使用有限的命名组件与 `$ref`;可空引用、字段描述、示例、约束和 scalar 输出类型保持准确。框架不引入业务模型。
|
||||||
|
|
||||||
|
请求绑定继续拒绝递归类型,Go 非法直接值循环与字段组合循环继续报错。已有非递归类型保持原有生成行为;无需修改运行时配置或响应 JSON。升级 CLI,在项目 `.api` 中恢复强类型后重新生成,再执行检查和项目回归。生成模板最低运行时版本不变。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
|
||||||
|
go-omni api generate
|
||||||
|
go-omni api generate --check
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
[递归响应说明](DEVELOPMENT.md#递归响应-dto)、[可运行示例](examples/recursiveResponse/README.md)。56 个测试包全量 race 与 Go 1.22 集成回归、vet/build、独立 OpenAPI 校验均通过,见[验收记录](reports/recursiveResponseAcceptance.md)。
|
||||||
|
|
||||||
|
远程空缓存验收完成:响应与兼容 101 项检查、11 组生成消费者,通用工程 30 项、混合工作区 16 项检查全部通过,均无本地 replace。标签固定在 `0c5b571b31b68d25cdb3e8640970e2a53ddcbea6`,详见[远程报告](reports/recursiveResponseRemote.json)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# v0.28.1-preview
|
||||||
|
|
||||||
|
workspace generate/check 支持 API 与通用工程混合分发,也可处理纯通用工程工作区。services 新增可选 kind(api/project);省略时按生成清单识别,保留无清单时显式 api 入口的原有用法。类型与清单冲突、歧义和不合法选项会报错,项目身份、路径与文件归属继续由各生成器校验。
|
||||||
|
|
||||||
|
check 只读并汇总独立工程的失败;format 只处理 API 契约,对通用工程明确显示 SKIP。generate 预检所有工程后再写入,预检失败时不修改其他工程;不改变各模块依赖或 go.work,不增加业务逻辑和第三方依赖。模板与运行时接口未变,新 workspace 功能需要更新 CLI,旧生成工程可直接纳入。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-preview
|
||||||
|
go-omni workspace generate
|
||||||
|
go-omni workspace check
|
||||||
|
```
|
||||||
|
|
||||||
|
[混合工作区示例](examples/mixedWorkspace/README.md)、[清单规则](DEVELOPMENT.md#混合-api-与通用工程)、[验收记录](reports/workspaceAcceptance.md)。56 个测试包全量 race、5 个 CLI 包的 Go 1.22 回归、实际生成的多模块构建、vet/build 均通过。
|
||||||
|
|
||||||
|
远程空缓存验收完成:混合工作区 16 项、通用工程 30 项、原有生成器 98 项检查全部通过,均无本地 replace;见 [混合报告](reports/workspaceRemote.json)、[通用报告](reports/workspaceProgramRemote.json)、[兼容报告](reports/workspaceManagedRemote.json)。标签固定在 `0d2c6040e2694795f203304148dbaec00318c608`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# v0.28.0-preview
|
||||||
|
|
||||||
|
新增通用 Go 工程生成命令 `project new/generate/check`。默认只有配置、日志、svc 依赖装配、生命周期与项目维护的 Run 入口,不生成 API、HTTP Handler、Swagger、业务任务或网络监听,也不自动连接数据库/队列或重试任务。常驻和一次性程序由普通 Go 代码决定。
|
||||||
|
|
||||||
|
入口监督 Run 的真实完成并复用现有 Runtime:信号、正常结束、运行错误或初始化失败都会进入收尾;先停止接收工作、等待已登记工作及 Run 结束,再反序释放依赖并关闭日志。Run/Init panic 转为失败;超时仍有工作则传播 ErrResourcesRetained,保留共享资源及日志,以非零状态退出。组件登记在 svc.Init 完成,业务语义和调度方式由项目负责。
|
||||||
|
|
||||||
|
配置遵守现有显式文件引用约定;支持 -check-config,在初始化日志和依赖前校验。生成器只管理 main/loadGen/runtimeGen,手写文件保留,生成文件被修改时拒绝覆盖;check/预览只读,独立 manifest/锁和共享文件事务保障生成。新模板要求框架 v0.28.0-preview;已有 new/api/workspace 行为保留,无强制迁移和新增第三方依赖。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview
|
||||||
|
go-omni project new demo --module example.com/demo
|
||||||
|
cd demo
|
||||||
|
go mod tidy
|
||||||
|
go run . -check-config
|
||||||
|
go run .
|
||||||
|
go-omni project check
|
||||||
|
```
|
||||||
|
|
||||||
|
完整 [通用工程指南](PROGRAM.md)、[可运行示例](examples/program/README.md)、[回归记录](reports/programAcceptance.md)。56 个测试包全量 race 与 Go 1.22 回归、生成消费者、vet/build 和真实进程退出验证通过。
|
||||||
|
|
||||||
|
空缓存远程安装已完成:通用工程 30 项检查通过(26.45 秒),原有生成项目 98 项检查、10 组消费者通过(111.07 秒),均无本地 replace。见 [通用远程报告](reports/programRemote.json)、[原有能力远程报告](reports/programManagedRemote.json)。标签固定在 `0b6c0043b6c3b58648645aeee03bf80801e6aa3f`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v0.27.0-preview
|
# v0.27.0-preview
|
||||||
|
|
||||||
新增统一请求体策略与请求完成屏障,无业务字段、数据库依赖或新第三方包。`server.http.defaultMaxBodyBytes` 控制普通路由默认值(零为 8 MiB),`maxBodyBytes` 为不可由路由突破的服务硬上限(零仅保留 1 TiB 框架上界)。已有显式路由和生成契约限额保持,冲突会在启动前报错;旧 nethttp 手写大请求需显式设置路由或默认上限。multipart 单文件、中间件缓存及代理限额仍各自配置。
|
新增统一请求体策略与请求完成屏障,无业务字段、数据库依赖或新第三方包。`server.http.defaultMaxBodyBytes` 控制普通路由默认值(零为 8 MiB),`maxBodyBytes` 为不可由路由突破的服务硬上限(零仅保留 1 TiB 框架上界)。已有显式路由和生成契约限额保持,冲突会在启动前报错;旧 nethttp 手写大请求需显式设置路由或默认上限。multipart 单文件、中间件缓存及代理限额仍各自配置。
|
||||||
@@ -15,6 +119,8 @@ go test ./...
|
|||||||
|
|
||||||
54 个测试包全量 race、Go 1.22 兼容检查、生成项目集成测试、vet/build 及真实进程关闭检查通过,见 [验收记录](reports/httpSafetyAcceptance.md)。
|
54 个测试包全量 race、Go 1.22 兼容检查、生成项目集成测试、vet/build 及真实进程关闭检查通过,见 [验收记录](reports/httpSafetyAcceptance.md)。
|
||||||
|
|
||||||
|
空模块缓存安装远程固定版本后,10 组生成消费者和 98 项发布检查通过,耗时 107.57 秒,无本地 replace;覆盖上传限额、取消收尾、HTTP/2 断连、生成项目依赖与日志保留。标签固定在 `3b00bb55d189cbe480c7d15c8d14bcd09d3f2d52`,见 [远程验收记录](reports/httpSafetyRemote.json)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# v0.26.0-preview
|
# v0.26.0-preview
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# go-omni CLI
|
# go-omni CLI
|
||||||
|
|
||||||
|
v0.28.1 的 `workspace generate/check` 支持 API 与通用工程混合处理。每个 services 项可指定 `kind: api | project`,省略时按工程清单识别,旧 API 配置继续有效。format 只处理 API 契约,通用工程显示 SKIP;check 只读并汇总各工程失败。见 [可运行示例](../../examples/mixedWorkspace/README.md)。
|
||||||
|
|
||||||
|
v0.28 新增 `project new/generate/check`:生成无 API、无监听端口和无外部服务依赖的通用 Go 程序,配置/日志/生命周期自动装配,项目维护 svc 与 Run。`project generate --dry-run` 预览,`project check` 只读检查;详见 [通用工程指南](../../PROGRAM.md)。
|
||||||
|
|
||||||
v0.26 新增 `i18n check|export|types|extract`:检查独立语言资源、提取 Go 静态消息引用或已导出的错误目录,导出指定命名空间的前端包和 TypeScript 类型。输入只读,结果写 stdout;动态引用明确报告。完整命令见 [国际化工具链](../../i18n/README.md#cli-提取检查与导出)。`.api errorCatalog` 可声明 translation 引用,旧 runtime 在生成前拒绝,接入时使用当前同版 CLI 与框架。
|
v0.26 新增 `i18n check|export|types|extract`:检查独立语言资源、提取 Go 静态消息引用或已导出的错误目录,导出指定命名空间的前端包和 TypeScript 类型。输入只读,结果写 stdout;动态引用明确报告。完整命令见 [国际化工具链](../../i18n/README.md#cli-提取检查与导出)。`.api errorCatalog` 可声明 translation 引用,旧 runtime 在生成前拒绝,接入时使用当前同版 CLI 与框架。
|
||||||
|
|
||||||
v0.19 移除结构映射生成:`omni.gen.json` 只保留 Repository 和中间件选项。普通接口直接使用生成 DTO;需要类型转换时用普通 Go 或可选 [copyx](../../COPY.md)。旧 `mappings` / `mappingFiles` 必须按迁移步骤清理,CLI 不会静默删除仍在调用的旧生成文件。
|
v0.19 移除结构映射生成:`omni.gen.json` 只保留 Repository 和中间件选项。普通接口直接使用生成 DTO;需要类型转换时用普通 Go 或可选 [copyx](../../COPY.md)。旧 `mappings` / `mappingFiles` 必须按迁移步骤清理,CLI 不会静默删除仍在调用的旧生成文件。
|
||||||
@@ -10,13 +14,13 @@ v0.16 增加基础类型/动态 JSON 数组返回、OpenAPI 公共引用和 work
|
|||||||
|
|
||||||
v0.14 新增 `api format [--check]`、`api upgrade [--dry-run|--preview <空目录>|--apply]`,以及 `workspace format|generate|check --file omni.workspace.json`。详见 [通用开发指南](../../DEVELOPMENT.md)。
|
v0.14 新增 `api format [--check]`、`api upgrade [--dry-run|--preview <空目录>|--apply]`,以及 `workspace format|generate|check --file omni.workspace.json`。详见 [通用开发指南](../../DEVELOPMENT.md)。
|
||||||
|
|
||||||
新项目采用根目录 `main.go` 与 `internal/config、svc、logic、handler、types`。完整职责、启动扩展、流式接口和生成文件边界见 [项目指南](../../PROJECT.md)。
|
API 新项目采用根目录 `main.go` 与 `internal/config、svc、logic、handler、types`。完整职责、启动扩展、流式接口和生成文件边界见 [项目指南](../../PROJECT.md)。
|
||||||
|
|
||||||
## 创建项目
|
## 创建 API 项目
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 仓库读取权限与 PATH 配置见根目录 README
|
# 仓库读取权限与 PATH 配置见根目录 README
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.27.0-preview
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.30.0-preview
|
||||||
go-omni new --module example.com/hello ./hello
|
go-omni new --module example.com/hello ./hello
|
||||||
|
|
||||||
# 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在
|
# 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在
|
||||||
@@ -29,6 +33,8 @@ go-omni new --api service.api --module example.com/example ./example
|
|||||||
|
|
||||||
存储配置和客户端位于 config/svc,业务使用统一接口。local 存储签名端点由生成器接入;上传、下载等业务路由由 `.api` 定义。认证与存储路径由项目实现。
|
存储配置和客户端位于 config/svc,业务使用统一接口。local 存储签名端点由生成器接入;上传、下载等业务路由由 `.api` 定义。认证与存储路径由项目实现。
|
||||||
|
|
||||||
|
响应 DTO 支持自引用及相互引用,生成强类型 Go 字段与 OpenAPI `$ref`;见[递归响应示例](../../examples/recursiveResponse/README.md)。请求递归仍会报错。
|
||||||
|
|
||||||
## 日常生成
|
## 日常生成
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -51,14 +57,14 @@ main、Gen.go、OpenAPI 和 manifest 由生成器维护;业务文件按字节
|
|||||||
|
|
||||||
## 版本与迁移
|
## 版本与迁移
|
||||||
|
|
||||||
CLI 与框架依赖固定使用 v0.27.0-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换:
|
CLI 与框架依赖固定使用 v0.30.0-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 在 go-omni 源码仓库中执行
|
# 在 go-omni 源码仓库中执行
|
||||||
go run ./cmd/go-omni new --module example.com/hello --framework-dir . ../hello
|
go run ./cmd/go-omni new --module example.com/hello --framework-dir . ../hello
|
||||||
```
|
```
|
||||||
|
|
||||||
当前模板与依赖以 v0.27.0-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
|
当前模板与依赖以 v0.30.0-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
|
||||||
|
|
||||||
## 数据库迁移命令
|
## 数据库迁移命令
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
|
||||||
goparser "go/parser"
|
|
||||||
gotoken "go/token"
|
gotoken "go/token"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -69,56 +67,17 @@ func Compile(spec *Spec) (*Compiled, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resolved := map[string]reflect.Type{}
|
graph, err := newResponseGraph(spec, definitions, scalarTypes, scalarDefs)
|
||||||
resolving := map[string]bool{}
|
if err != nil {
|
||||||
var resolve func(string, int) (reflect.Type, error)
|
return nil, err
|
||||||
var expression func(ast.Expr, int) (reflect.Type, error)
|
}
|
||||||
|
for _, r := range spec.Routes {
|
||||||
expression = func(e ast.Expr, depth int) (reflect.Type, error) {
|
if graph.recursive[r.Request] {
|
||||||
if depth > 32 {
|
return nil, fmt.Errorf("%s: recursive request DTO %s is unsupported; recursive types are supported for responses only", r.Position, r.Request)
|
||||||
return nil, errors.New("type nesting exceeds 32")
|
|
||||||
}
|
|
||||||
switch e := e.(type) {
|
|
||||||
case *ast.Ident:
|
|
||||||
if t, ok := scalarTypes[e.Name]; ok {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
if t, ok := primitiveTypes[e.Name]; ok {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
return resolve(e.Name, depth+1)
|
|
||||||
case *ast.StarExpr:
|
|
||||||
if _, double := e.X.(*ast.StarExpr); double {
|
|
||||||
return nil, errors.New("multiple pointer levels unsupported")
|
|
||||||
}
|
|
||||||
t, err := expression(e.X, depth+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reflect.PointerTo(t), nil
|
|
||||||
case *ast.ArrayType:
|
|
||||||
if e.Len != nil {
|
|
||||||
return nil, errors.New("use slices, not fixed arrays")
|
|
||||||
}
|
|
||||||
t, err := expression(e.Elt, depth+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reflect.SliceOf(t), nil
|
|
||||||
case *ast.MapType:
|
|
||||||
key, ok := e.Key.(*ast.Ident)
|
|
||||||
if !ok || key.Name != "string" {
|
|
||||||
return nil, errors.New("map keys must be string")
|
|
||||||
}
|
|
||||||
t, err := expression(e.Value, depth+1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return reflect.MapOf(reflect.TypeOf(""), t), nil
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unsupported type expression")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
resolved := map[string]reflect.Type{}
|
||||||
|
var resolve func(string, int) (reflect.Type, error)
|
||||||
resolve = func(name string, depth int) (reflect.Type, error) {
|
resolve = func(name string, depth int) (reflect.Type, error) {
|
||||||
if depth > 32 {
|
if depth > 32 {
|
||||||
return nil, errors.New("type nesting exceeds 32")
|
return nil, errors.New("type nesting exceeds 32")
|
||||||
@@ -126,27 +85,13 @@ func Compile(spec *Spec) (*Compiled, error) {
|
|||||||
if t, ok := resolved[name]; ok {
|
if t, ok := resolved[name]; ok {
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
if resolving[name] {
|
|
||||||
return nil, fmt.Errorf("recursive type %s is unsupported", name)
|
|
||||||
}
|
|
||||||
typ, ok := definitions[name]
|
typ, ok := definitions[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unknown type %s", name)
|
return nil, fmt.Errorf("unknown type %s", name)
|
||||||
}
|
}
|
||||||
resolving[name] = true
|
|
||||||
defer delete(resolving, name)
|
|
||||||
var fields []reflect.StructField
|
var fields []reflect.StructField
|
||||||
seen := map[string]bool{}
|
for i, f := range typ.Fields {
|
||||||
for _, f := range typ.Fields {
|
t, err := graph.expression(graph.exprs[name][i], depth+1, resolve)
|
||||||
if !exportedName.MatchString(f.Name) || seen[f.Name] {
|
|
||||||
return nil, fmt.Errorf("%s: invalid or duplicate field %s", name, f.Name)
|
|
||||||
}
|
|
||||||
seen[f.Name] = true
|
|
||||||
expr, err := goparser.ParseExpr(f.Type)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t, err := expression(expr, depth+1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err)
|
return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err)
|
||||||
}
|
}
|
||||||
@@ -158,6 +103,12 @@ func Compile(spec *Spec) (*Compiled, error) {
|
|||||||
}
|
}
|
||||||
c := &Compiled{Spec: spec, Bindings: map[string]*contract.Binder{}}
|
c := &Compiled{Spec: spec, Bindings: map[string]*contract.Binder{}}
|
||||||
for _, t := range spec.Types {
|
for _, t := range spec.Types {
|
||||||
|
if graph.recursive[t.Name] {
|
||||||
|
if _, err := graph.component(t.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
rt, err := resolve(t.Name, 0)
|
rt, err := resolve(t.Name, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -274,7 +225,7 @@ func Compile(spec *Spec) (*Compiled, error) {
|
|||||||
}
|
}
|
||||||
var response *openapi.Schema
|
var response *openapi.Schema
|
||||||
if r.Response != "" {
|
if r.Response != "" {
|
||||||
s, err := responseDataSchema(r.Response, c.Bindings)
|
s, err := responseDataSchema(r.Response, c.Bindings, graph)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -316,6 +267,13 @@ func Compile(spec *Spec) (*Compiled, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
decorateDocument(doc, spec, c.Bindings)
|
decorateDocument(doc, spec, c.Bindings)
|
||||||
|
if len(graph.components) > 0 {
|
||||||
|
components := map[string]any{}
|
||||||
|
for name, schema := range graph.components {
|
||||||
|
components[name] = schema
|
||||||
|
}
|
||||||
|
doc["components"].(map[string]any)["schemas"] = components
|
||||||
|
}
|
||||||
compactDocument(doc)
|
compactDocument(doc)
|
||||||
c.Document, err = json.MarshalIndent(doc, "", " ")
|
c.Document, err = json.MarshalIndent(doc, "", " ")
|
||||||
return c, err
|
return c, err
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/internal/genfs"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/scaffold"
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/scaffold"
|
||||||
"golang.org/x/mod/modfile"
|
"golang.org/x/mod/modfile"
|
||||||
"golang.org/x/mod/module"
|
"golang.org/x/mod/module"
|
||||||
@@ -22,7 +23,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
|||||||
|
|
||||||
// Runtime-only releases need not raise the managed template compatibility floor.
|
// Runtime-only releases need not raise the managed template compatibility floor.
|
||||||
const minimumVersion = "v0.22.0-preview"
|
const minimumVersion = "v0.22.0-preview"
|
||||||
const currentVersion = "v0.27.0-preview"
|
const currentVersion = "v0.30.0-preview"
|
||||||
const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n"
|
const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n"
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -345,25 +346,7 @@ func moduleFile(o Options) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return []byte(text), nil
|
return []byte(text), nil
|
||||||
}
|
}
|
||||||
func canonical(path string) string {
|
func canonical(path string) string { return genfs.Canonical(path) }
|
||||||
path, _ = filepath.Abs(path)
|
|
||||||
candidate := path
|
|
||||||
var tail []string
|
|
||||||
for {
|
|
||||||
if resolved, err := filepath.EvalSymlinks(candidate); err == nil {
|
|
||||||
for i := len(tail) - 1; i >= 0; i-- {
|
|
||||||
resolved = filepath.Join(resolved, tail[i])
|
|
||||||
}
|
|
||||||
return resolved
|
|
||||||
}
|
|
||||||
parent := filepath.Dir(candidate)
|
|
||||||
if parent == candidate {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
tail = append(tail, filepath.Base(candidate))
|
|
||||||
candidate = parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map[string][]byte, error) {
|
func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map[string][]byte, error) {
|
||||||
generated, seeds := map[string][]byte{}, map[string][]byte{}
|
generated, seeds := map[string][]byte{}, map[string][]byte{}
|
||||||
|
|||||||
299
cmd/go-omni/apigen/recursiveResponse_test.go
Normal file
299
cmd/go-omni/apigen/recursiveResponse_test.go
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
package apigen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func compileRecursive(t *testing.T, source string) *Compiled {
|
||||||
|
t.Helper()
|
||||||
|
spec, err := parseText(t, source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c, err := Compile(spec)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
again, err := Compile(spec)
|
||||||
|
if err != nil || !bytes.Equal(c.Document, again.Document) {
|
||||||
|
t.Fatal("non-deterministic compilation", err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursiveDocument(t *testing.T, c *Compiled) map[string]any {
|
||||||
|
t.Helper()
|
||||||
|
var doc map[string]any
|
||||||
|
if err := json.Unmarshal(c.Document, &doc); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Inspect reference targets without recursively inlining the graph.
|
||||||
|
var visit func(any)
|
||||||
|
visit = func(value any) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
if ref, ok := v["$ref"].(string); ok {
|
||||||
|
if len(v) != 1 {
|
||||||
|
t.Fatalf("OpenAPI 3.0 ref has ignored siblings: %v", v)
|
||||||
|
}
|
||||||
|
var target any = doc
|
||||||
|
for _, part := range strings.Split(strings.TrimPrefix(ref, "#/"), "/") {
|
||||||
|
m, ok := target.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("bad reference", ref)
|
||||||
|
}
|
||||||
|
target = m[part]
|
||||||
|
}
|
||||||
|
if target == nil {
|
||||||
|
t.Fatal("dangling reference", ref)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for key, child := range v {
|
||||||
|
if key != "example" && key != "examples" {
|
||||||
|
visit(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, child := range v {
|
||||||
|
visit(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(doc)
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursiveResponseGraph(t *testing.T) {
|
||||||
|
source := readFile(t, filepath.Join(frameworkRoot(t), "examples/recursiveResponse/service.api"))
|
||||||
|
c := compileRecursive(t, source)
|
||||||
|
doc := recursiveDocument(t, c)
|
||||||
|
schemas := doc["components"].(map[string]any)["schemas"].(map[string]any)
|
||||||
|
node := schemas["DTO_Node"].(map[string]any)
|
||||||
|
props := node["properties"].(map[string]any)
|
||||||
|
children := props["children"].(map[string]any)
|
||||||
|
if children["type"] != "array" || children["maxItems"] != float64(100) || children["items"].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" {
|
||||||
|
t.Fatal(children)
|
||||||
|
}
|
||||||
|
if props["lookup"].(map[string]any)["additionalProperties"].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" {
|
||||||
|
t.Fatal(props)
|
||||||
|
}
|
||||||
|
next := props["next"].(map[string]any)["anyOf"].([]any)
|
||||||
|
if len(next) != 2 || next[0].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" || next[1].(map[string]any)["nullable"] != true {
|
||||||
|
t.Fatal(next)
|
||||||
|
}
|
||||||
|
for _, pair := range [][2]string{{"Branch", "Leaf"}, {"Leaf", "Branch"}} {
|
||||||
|
p := schemas["DTO_"+pair[0]].(map[string]any)["properties"].(map[string]any)[strings.ToLower(pair[1])].(map[string]any)
|
||||||
|
if p["anyOf"].([]any)[0].(map[string]any)["$ref"] != "#/components/schemas/DTO_"+pair[1] {
|
||||||
|
t.Fatal(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(c.Document) > 18000 {
|
||||||
|
t.Fatal("recursive graph expanded", len(c.Document))
|
||||||
|
}
|
||||||
|
first, _ := json.Marshal(doc)
|
||||||
|
compactDocument(doc)
|
||||||
|
second, _ := json.Marshal(doc)
|
||||||
|
if !bytes.Equal(first, second) {
|
||||||
|
t.Fatal("document compaction changed references")
|
||||||
|
}
|
||||||
|
// Change tracking traverses every named dependency once, including cycles.
|
||||||
|
snapshot := snapshotContract(c.Spec)
|
||||||
|
if len(snapshot.Routes) == 0 {
|
||||||
|
t.Fatal("empty contract snapshot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursiveResponseRejections(t *testing.T) {
|
||||||
|
tests := []struct{ name, types, route, want string }{
|
||||||
|
{"valueSelf", "type Node { Child Node `json:\"child\"` }", "get /tree returns (Node)", "recursive value"},
|
||||||
|
{"valueMutual", "type Node { Child Other `json:\"child\"` }\ntype Other { Child Node `json:\"child\"` }", "get /tree returns (Node)", "recursive value"},
|
||||||
|
{"request", "type Node { Children []Node `json:\"children\"` }", "post /tree (Node) returns (Node)", "recursive request"},
|
||||||
|
{"wrappedRequest", "type Node { Children []Node `json:\"children\"` }\ntype Req { Value Node `json:\"value\"` }", "post /tree (Req)", "recursive request"},
|
||||||
|
{"unknown", "type Node { Children []Node `json:\"children\"`\n Bad Missing `json:\"bad\"` }", "get /tree returns (Node)", "unknown type"},
|
||||||
|
{"duplicate", "type Node { Children []Node `json:\"children\"`\n Again string `json:\"children\"` }", "get /tree returns (Node)", "conflicting composed field"},
|
||||||
|
{"nonJSON", "type Node { Children []Node `json:\"children\"`\n Name string `form:\"name\"` }", "get /tree returns (Node)", "response fields must use json"},
|
||||||
|
{"nestedNonJSON", "type Node { Children []Node `json:\"children\"`\n Detail Detail `json:\"detail\"` }\ntype Detail { Name string `form:\"name\"` }", "get /tree returns (Node)", "response fields must use json"},
|
||||||
|
{"doublePointer", "type Node { Next **Node `json:\"next\"` }", "get /tree returns (Node)", "multiple pointer"},
|
||||||
|
{"fixedArray", "type Node { Children [1]Node `json:\"children\"` }", "get /tree returns (Node)", "fixed arrays"},
|
||||||
|
{"mapKey", "type Node { Children map[int]Node `json:\"children\"` }", "get /tree returns (Node)", "map keys"},
|
||||||
|
{"badRule", "type Node { Next *Node `json:\"next\" binding:\"min=1\"` }", "get /tree returns (Node)", "requires a number"},
|
||||||
|
{"badDefault", "type Node { Next *Node `json:\"next,default=x\"` }", "get /tree returns (Node)", "default requires"},
|
||||||
|
{"upload", "type Node { Children []Node `json:\"children\"`\n Upload File `file:\"upload\"` }", "get /tree returns (Node)", "response fields must use json"},
|
||||||
|
{"composition", "type Node { Node }", "get /tree returns (Node)", "cycle"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
spec, err := parseText(t, tc.types+"\nservice demo {\n@handler tree\n"+tc.route+"\n}")
|
||||||
|
if err == nil {
|
||||||
|
_, err = Compile(spec)
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||||
|
t.Fatalf("want %q, got %v", tc.want, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursiveResponseExamples(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
value string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{`[]`, true}, {`[{"name":"first","children":[{"name":"second","children":[]}]}]`, true},
|
||||||
|
{`[{"name":"","children":[]}]`, false}, {`[{"name":"first"}]`, false},
|
||||||
|
{`[{"name":"first","children":[],"unknown":true}]`, false},
|
||||||
|
{`[{"name":"first","children":[]},{"name":123,"children":[]}]`, false},
|
||||||
|
{`[{"name":"first","children":[{"name":"second","children":[{"name":"","children":[]}]}]}]`, false},
|
||||||
|
{`null`, false}, {`{} {}`, false},
|
||||||
|
} {
|
||||||
|
t.Run(tc.value, func(t *testing.T) {
|
||||||
|
source := "type Node {\n Name string `json:\"name\" binding:\"min=1\"`\n Children []Node `json:\"children\" description:\"Nested nodes\" example:" + fmt.Sprintf("%q", tc.value) + "`\n}\nservice demo {\n @handler tree\nget /tree returns (Node)\n}"
|
||||||
|
spec, err := parseText(t, source)
|
||||||
|
if err == nil {
|
||||||
|
_, err = Compile(spec)
|
||||||
|
}
|
||||||
|
if (err == nil) != tc.valid {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, typ := range []string{"*Node", "map[string]*Node", "[]*Node", "*[]Node"} {
|
||||||
|
source := "type Node {\n Name string `json:\"name\"`\n Next " + typ + " `json:\"next,omitempty\" description:\"Next node\"`\n}\nservice demo {\n@handler tree\nget /tree returns (*Node)\n}"
|
||||||
|
recursiveDocument(t, compileRecursive(t, source))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursiveResponseImportsAndMetadata(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
writeFile(t, filepath.Join(root, "service.api"), "import \"node.api\"\nimport \"detail.api\"\nservice demo {\n@handler tree\nget /tree returns (Node)\n}")
|
||||||
|
writeFile(t, filepath.Join(root, "node.api"), "type Node {\n Children []Node `json:\"children\"`\n Detail Detail `json:\"detail\" description:\"Node detail\" example:\"{\\\"count\\\":9007199254740993,\\\"parent\\\":null}\"`\n}")
|
||||||
|
writeFile(t, filepath.Join(root, "detail.api"), "type Detail {\n Count int64 `json:\"count\"`\n Parent *Node `json:\"parent\"`\n}")
|
||||||
|
spec, err := Parse(filepath.Join(root, "service.api"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c, err := Compile(spec)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
doc := recursiveDocument(t, c)
|
||||||
|
schemas := doc["components"].(map[string]any)["schemas"].(map[string]any)
|
||||||
|
detail := schemas["DTO_Node"].(map[string]any)["properties"].(map[string]any)["detail"].(map[string]any)
|
||||||
|
if detail["description"] != "Node detail" || detail["allOf"].([]any)[0].(map[string]any)["$ref"] != "#/components/schemas/DTO_Detail" {
|
||||||
|
t.Fatal(detail)
|
||||||
|
}
|
||||||
|
if !bytes.Contains(c.Document, []byte("9007199254740993")) {
|
||||||
|
t.Fatal("example integer lost precision")
|
||||||
|
}
|
||||||
|
// Definition order cannot change cycle classification or component schemas.
|
||||||
|
spec.Types[0], spec.Types[1] = spec.Types[1], spec.Types[0]
|
||||||
|
other, err := Compile(spec)
|
||||||
|
if err != nil || !bytes.Equal(c.Document, other.Document) {
|
||||||
|
t.Fatal("definition order changed schemas", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar := `scalar ID (
|
||||||
|
goImport: "example.com/scalars"
|
||||||
|
goType: ID
|
||||||
|
jsonInput: string | int64
|
||||||
|
jsonOutput: string
|
||||||
|
)
|
||||||
|
type Node {
|
||||||
|
Value ID ` + "`json:\"value\" example:\"123\"`" + `
|
||||||
|
Children []Node ` + "`json:\"children\"`" + `
|
||||||
|
}
|
||||||
|
service demo {
|
||||||
|
@handler tree
|
||||||
|
get /tree returns (Node)
|
||||||
|
}`
|
||||||
|
doc = recursiveDocument(t, compileRecursive(t, scalar))
|
||||||
|
value := doc["components"].(map[string]any)["schemas"].(map[string]any)["DTO_Node"].(map[string]any)["properties"].(map[string]any)["value"].(map[string]any)
|
||||||
|
if value["type"] != "string" || value["example"] != nil {
|
||||||
|
t.Fatal("scalar input leaked into response", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursiveResponseConsumer(t *testing.T) {
|
||||||
|
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") != "1" {
|
||||||
|
t.Skip("set GO_OMNI_SCAFFOLD_INTEGRATION=1")
|
||||||
|
}
|
||||||
|
root := frameworkRoot(t)
|
||||||
|
dir := filepath.Join(t.TempDir(), "recursive")
|
||||||
|
o := Options{Dir: dir, API: filepath.Join(root, "examples/recursiveResponse/service.api"), Module: "example.com/recursive", FrameworkDir: root}
|
||||||
|
if err := generateManagedTestProject(t, o); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
doc := recursiveDocument(t, &Compiled{Document: []byte(readFile(t, filepath.Join(dir, "docs/openapi.json")))})
|
||||||
|
schemas := doc["components"].(map[string]any)["schemas"].(map[string]any)
|
||||||
|
children := schemas["DTO_Node"].(map[string]any)["properties"].(map[string]any)["children"].(map[string]any)
|
||||||
|
if children["items"].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" {
|
||||||
|
t.Fatal("generated response lost its recursive schema", children)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"tree", "forest", "optional", "raw", "pair"} {
|
||||||
|
writeFile(t, filepath.Join(dir, "internal/logic/trees/"+name+".go"), "package trees\n")
|
||||||
|
}
|
||||||
|
writeFile(t, filepath.Join(dir, "internal/logic/trees/operations.go"), readFile(t, filepath.Join(root, "examples/recursiveResponse/operations.go.txt")))
|
||||||
|
writeFile(t, filepath.Join(dir, "consumer_test.go"), readFile(t, "testdata/recursive/consumer_test.go"))
|
||||||
|
o.API = filepath.Join(dir, "api/service.api")
|
||||||
|
before := snapshot(t, dir)
|
||||||
|
if err := generateManagedTestProject(t, o); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
o.Check = true
|
||||||
|
if err := generateManagedTestProject(t, o); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, dir)) {
|
||||||
|
t.Fatal("regeneration/check modified business code")
|
||||||
|
}
|
||||||
|
runConsumerGo(t, dir, "test", "-mod=mod", "-race", "-count=1", "./...")
|
||||||
|
// A recursive descendant change must still be visible to read-only checking.
|
||||||
|
writeFile(t, o.API, strings.Replace(readFile(t, o.API), "max=100", "max=90", 1))
|
||||||
|
before = snapshot(t, dir)
|
||||||
|
if err := generateManagedTestProject(t, o); err == nil {
|
||||||
|
t.Fatal("check missed recursive DTO drift")
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, dir)) {
|
||||||
|
t.Fatal("drift check wrote files")
|
||||||
|
}
|
||||||
|
o.Check = false
|
||||||
|
if err := generateManagedTestProject(t, o); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
o.Check = true
|
||||||
|
if err := generateManagedTestProject(t, o); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if os.Getenv("GO_OMNI_RELEASE_CLI") != "" {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", frameworkModule)
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||||
|
data, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err, string(data))
|
||||||
|
}
|
||||||
|
var dep struct {
|
||||||
|
Version, Sum string
|
||||||
|
Replace any
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &dep); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if dep.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || dep.Sum == "" || dep.Replace != nil {
|
||||||
|
t.Fatal("not a fixed remote consumer")
|
||||||
|
}
|
||||||
|
t.Logf("remote framework %s checksum %s; no local replace", dep.Version, dep.Sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
370
cmd/go-omni/apigen/responseGraph.go
Normal file
370
cmd/go-omni/apigen/responseGraph.go
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
package apigen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
goparser "go/parser"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/contract"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/openapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Named response types form a graph, not an infinitely expanded reflect.StructOf
|
||||||
|
// tree. Compile each object's local fields with the existing contract compiler,
|
||||||
|
// then link named fields through OpenAPI references. Request binders still use
|
||||||
|
// the complete, nonrecursive reflection tree.
|
||||||
|
type responseGraph struct {
|
||||||
|
types map[string]Type
|
||||||
|
exprs map[string][]ast.Expr
|
||||||
|
recursive map[string]bool // includes types that can reach a cycle
|
||||||
|
scalars map[string]reflect.Type
|
||||||
|
scalarDefs []contract.Scalar
|
||||||
|
components map[string]*openapi.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResponseGraph(spec *Spec, types map[string]Type, scalars map[string]reflect.Type, defs []contract.Scalar) (*responseGraph, error) {
|
||||||
|
g := &responseGraph{types: types, exprs: map[string][]ast.Expr{}, recursive: map[string]bool{}, scalars: scalars, scalarDefs: defs, components: map[string]*openapi.Schema{}}
|
||||||
|
dependencies, values := map[string][]string{}, map[string][]string{}
|
||||||
|
for _, typ := range spec.Types {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, f := range typ.Fields {
|
||||||
|
if !exportedName.MatchString(f.Name) || seen[f.Name] {
|
||||||
|
return nil, fmt.Errorf("%s: invalid or duplicate field %s", typ.Name, f.Name)
|
||||||
|
}
|
||||||
|
seen[f.Name] = true
|
||||||
|
e, err := goparser.ParseExpr(f.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Validate every expression, including otherwise unused declarations.
|
||||||
|
if _, err = g.expression(e, 0, func(name string, _ int) (reflect.Type, error) {
|
||||||
|
dependencies[typ.Name] = append(dependencies[typ.Name], name)
|
||||||
|
return reflect.TypeOf(struct{}{}), nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s.%s: %w", typ.Name, f.Name, err)
|
||||||
|
}
|
||||||
|
g.exprs[typ.Name] = append(g.exprs[typ.Name], e)
|
||||||
|
if id, ok := e.(*ast.Ident); ok && types[id.Name].Name != "" {
|
||||||
|
values[typ.Name] = append(values[typ.Name], id.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only pointer/slice/map indirection can break a Go struct layout cycle.
|
||||||
|
state := map[string]int{}
|
||||||
|
var valueCycle func(string) error
|
||||||
|
valueCycle = func(name string) error {
|
||||||
|
if state[name] == 1 {
|
||||||
|
return fmt.Errorf("%s: invalid recursive value type; use a pointer, slice or map", name)
|
||||||
|
}
|
||||||
|
if state[name] == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
state[name] = 1
|
||||||
|
for _, next := range values[name] {
|
||||||
|
if err := valueCycle(next); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state[name] = 2
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, typ := range spec.Types {
|
||||||
|
if err := valueCycle(typ.Name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = map[string]int{}
|
||||||
|
var cyclic func(string) bool
|
||||||
|
cyclic = func(name string) bool {
|
||||||
|
if state[name] == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if state[name] == 2 {
|
||||||
|
return g.recursive[name]
|
||||||
|
}
|
||||||
|
state[name] = 1
|
||||||
|
for _, next := range dependencies[name] {
|
||||||
|
if cyclic(next) {
|
||||||
|
g.recursive[name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state[name] = 2
|
||||||
|
return g.recursive[name]
|
||||||
|
}
|
||||||
|
for _, typ := range spec.Types {
|
||||||
|
cyclic(typ.Name)
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *responseGraph) expression(e ast.Expr, depth int, named func(string, int) (reflect.Type, error)) (reflect.Type, error) {
|
||||||
|
if depth > 32 {
|
||||||
|
return nil, fmt.Errorf("type nesting exceeds 32")
|
||||||
|
}
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
if t := g.scalars[e.Name]; t != nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
if t := primitiveTypes[e.Name]; t != nil {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
if g.types[e.Name].Name == "" {
|
||||||
|
return nil, fmt.Errorf("unknown type %s", e.Name)
|
||||||
|
}
|
||||||
|
return named(e.Name, depth+1)
|
||||||
|
case *ast.StarExpr:
|
||||||
|
if _, double := e.X.(*ast.StarExpr); double {
|
||||||
|
return nil, fmt.Errorf("multiple pointer levels unsupported")
|
||||||
|
}
|
||||||
|
t, err := g.expression(e.X, depth+1, named)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reflect.PointerTo(t), nil
|
||||||
|
case *ast.ArrayType:
|
||||||
|
if e.Len != nil {
|
||||||
|
return nil, fmt.Errorf("use slices, not fixed arrays")
|
||||||
|
}
|
||||||
|
t, err := g.expression(e.Elt, depth+1, named)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reflect.SliceOf(t), nil
|
||||||
|
case *ast.MapType:
|
||||||
|
key, ok := e.Key.(*ast.Ident)
|
||||||
|
if !ok || key.Name != "string" {
|
||||||
|
return nil, fmt.Errorf("map keys must be string")
|
||||||
|
}
|
||||||
|
t, err := g.expression(e.Value, depth+1, named)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reflect.MapOf(reflect.TypeOf(""), t), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type expression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *responseGraph) component(name string) (*openapi.Schema, error) {
|
||||||
|
key := "DTO_" + name
|
||||||
|
ref := &openapi.Schema{Ref: "#/components/schemas/" + key}
|
||||||
|
if _, ok := g.components[key]; ok {
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
// Register before following edges so both self and mutual references terminate.
|
||||||
|
g.components[key] = nil
|
||||||
|
typ := g.types[name]
|
||||||
|
var fields []reflect.StructField
|
||||||
|
for i, f := range typ.Fields {
|
||||||
|
t, err := g.expression(g.exprs[name][i], 0, func(string, int) (reflect.Type, error) { return reflect.TypeOf(struct{}{}), nil })
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag, err := withoutExample(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fields = append(fields, reflect.StructField{Name: f.Name, Type: t, Tag: tag})
|
||||||
|
}
|
||||||
|
b, err := contract.Describe(reflect.StructOf(fields), g.scalarDefs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", typ.Position, err)
|
||||||
|
}
|
||||||
|
s, err := b.ResponseSchema()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", typ.Position, err)
|
||||||
|
}
|
||||||
|
for i, f := range typ.Fields {
|
||||||
|
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
|
||||||
|
property := s.Properties[wire]
|
||||||
|
// Examples are checked against a finite projection of their actual values,
|
||||||
|
// with the same contract rules and without running application decoders.
|
||||||
|
if _, ok := reflect.StructTag(fieldTag(f)).Lookup("example"); ok {
|
||||||
|
example, err := g.example(f, g.exprs[name][i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err)
|
||||||
|
}
|
||||||
|
property.Example = example
|
||||||
|
}
|
||||||
|
property, err = g.link(g.exprs[name][i], property)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.Properties[wire] = property
|
||||||
|
}
|
||||||
|
g.components[key] = s
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *responseGraph) link(e ast.Expr, s *openapi.Schema) (*openapi.Schema, error) {
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
if g.types[e.Name].Name == "" {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
ref, err := g.component(e.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.Nullable {
|
||||||
|
ref = nullableReference(ref)
|
||||||
|
}
|
||||||
|
if s.Description != "" || s.Example != nil {
|
||||||
|
// OpenAPI 3.0 ignores siblings on Reference Objects.
|
||||||
|
ref = &openapi.Schema{AllOf: []*openapi.Schema{ref}, Description: s.Description, Example: s.Example}
|
||||||
|
}
|
||||||
|
return ref, nil
|
||||||
|
case *ast.StarExpr:
|
||||||
|
return g.link(e.X, s)
|
||||||
|
case *ast.ArrayType:
|
||||||
|
item, err := g.link(e.Elt, s.Items)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.Items = item
|
||||||
|
case *ast.MapType:
|
||||||
|
item, err := g.link(e.Value, s.AdditionalProperties)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.AdditionalProperties = item
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullableReference(s *openapi.Schema) *openapi.Schema {
|
||||||
|
return &openapi.Schema{AnyOf: []*openapi.Schema{s, {Type: "object", Nullable: true, Enum: []any{nil}}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withoutExample(f Field) (reflect.StructTag, error) {
|
||||||
|
tags, err := contract.ParseTags(fieldTag(f))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
delete(tags, "example")
|
||||||
|
keys := make([]string, 0, len(tags))
|
||||||
|
for key := range tags {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
var parts []string
|
||||||
|
for _, key := range keys {
|
||||||
|
parts = append(parts, key+":"+strconv.Quote(tags[key]))
|
||||||
|
}
|
||||||
|
return reflect.StructTag(strings.Join(parts, " ")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *responseGraph) example(f Field, e ast.Expr) (any, error) {
|
||||||
|
raw := reflect.StructTag(fieldTag(f)).Get("example")
|
||||||
|
var value any
|
||||||
|
// Plain string examples intentionally are not JSON literals.
|
||||||
|
if f.Type == "string" || f.Type == "*string" {
|
||||||
|
value = raw
|
||||||
|
} else {
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(raw))
|
||||||
|
decoder.UseNumber()
|
||||||
|
if err := decoder.Decode(&value); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid example JSON: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
budget := 10000
|
||||||
|
t, err := g.projectExample(e, []any{value}, 0, &budget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, err := contract.Describe(reflect.StructOf([]reflect.StructField{{Name: f.Name, Type: t, Tag: reflect.StructTag(fieldTag(f))}}), g.scalarDefs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s, err := b.ResponseSchema()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
|
||||||
|
return s.Properties[wire].Example, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only materialize named objects where the finite example has a non-null value.
|
||||||
|
// Empty slices/maps and null pointers stop expansion. Unioning collection values
|
||||||
|
// checks every item using one reflection type, including missing/unknown fields.
|
||||||
|
func (g *responseGraph) projectExample(e ast.Expr, values []any, depth int, budget *int) (reflect.Type, error) {
|
||||||
|
*budget--
|
||||||
|
if depth > 32 || *budget < 0 {
|
||||||
|
return nil, fmt.Errorf("example type expansion exceeds 32 levels or 10000 nodes")
|
||||||
|
}
|
||||||
|
var present []any
|
||||||
|
for _, v := range values {
|
||||||
|
if v != nil {
|
||||||
|
present = append(present, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch e := e.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
return g.expression(e, 0, func(name string, _ int) (reflect.Type, error) {
|
||||||
|
if len(present) == 0 {
|
||||||
|
return reflect.TypeOf(struct{}{}), nil
|
||||||
|
}
|
||||||
|
var fields []reflect.StructField
|
||||||
|
for i, f := range g.types[name].Fields {
|
||||||
|
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
|
||||||
|
var nested []any
|
||||||
|
for _, v := range present {
|
||||||
|
if obj, ok := v.(map[string]any); ok {
|
||||||
|
if child, found := obj[wire]; found {
|
||||||
|
nested = append(nested, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, err := g.projectExample(g.exprs[name][i], nested, depth+1, budget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag, err := withoutExample(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fields = append(fields, reflect.StructField{Name: f.Name, Type: t, Tag: tag})
|
||||||
|
}
|
||||||
|
return reflect.StructOf(fields), nil
|
||||||
|
})
|
||||||
|
case *ast.StarExpr:
|
||||||
|
t, err := g.projectExample(e.X, present, depth+1, budget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reflect.PointerTo(t), nil
|
||||||
|
case *ast.ArrayType:
|
||||||
|
var nested []any
|
||||||
|
for _, v := range present {
|
||||||
|
if list, ok := v.([]any); ok {
|
||||||
|
nested = append(nested, list...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, err := g.projectExample(e.Elt, nested, depth+1, budget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reflect.SliceOf(t), nil
|
||||||
|
case *ast.MapType:
|
||||||
|
var nested []any
|
||||||
|
for _, v := range present {
|
||||||
|
if obj, ok := v.(map[string]any); ok {
|
||||||
|
for _, v := range obj {
|
||||||
|
nested = append(nested, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, err := g.projectExample(e.Value, nested, depth+1, budget)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reflect.MapOf(reflect.TypeOf(""), t), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported example type")
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ func responseGoType(value string) string {
|
|||||||
}
|
}
|
||||||
return "*types." + name
|
return "*types." + name
|
||||||
}
|
}
|
||||||
func responseDataSchema(value string, bindings map[string]*contract.Binder) (*openapi.Schema, error) {
|
func responseDataSchema(value string, bindings map[string]*contract.Binder, graph *responseGraph) (*openapi.Schema, error) {
|
||||||
kind, name := responseShape(value)
|
kind, name := responseShape(value)
|
||||||
if kind == "null" {
|
if kind == "null" {
|
||||||
return &openapi.Schema{Type: "object", Nullable: true, Enum: []any{nil}}, nil
|
return &openapi.Schema{Type: "object", Nullable: true, Enum: []any{nil}}, nil
|
||||||
@@ -56,11 +56,15 @@ func responseDataSchema(value string, bindings map[string]*contract.Binder) (*op
|
|||||||
}
|
}
|
||||||
return s.Properties["value"], nil
|
return s.Properties["value"], nil
|
||||||
}
|
}
|
||||||
b, ok := bindings[name]
|
var s *openapi.Schema
|
||||||
if !ok {
|
var err error
|
||||||
|
if graph.recursive[name] {
|
||||||
|
s, err = graph.component(name)
|
||||||
|
} else if b := bindings[name]; b != nil {
|
||||||
|
s, err = b.ResponseSchema()
|
||||||
|
} else {
|
||||||
return nil, fmt.Errorf("unknown response DTO %q; use Name, []Name, []string/[]int64/[]bool/[]any, *Name or null", name)
|
return nil, fmt.Errorf("unknown response DTO %q; use Name, []Name, []string/[]int64/[]bool/[]any, *Name or null", name)
|
||||||
}
|
}
|
||||||
s, err := b.ResponseSchema()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -68,6 +72,9 @@ func responseDataSchema(value string, bindings map[string]*contract.Binder) (*op
|
|||||||
case "array":
|
case "array":
|
||||||
return &openapi.Schema{Type: "array", Items: s}, nil
|
return &openapi.Schema{Type: "array", Items: s}, nil
|
||||||
case "nullable":
|
case "nullable":
|
||||||
|
if s.Ref != "" {
|
||||||
|
return nullableReference(s), nil
|
||||||
|
}
|
||||||
s.Nullable = true
|
s.Nullable = true
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|||||||
77
cmd/go-omni/apigen/testdata/recursive/consumer_test.go
vendored
Normal file
77
cmd/go-omni/apigen/testdata/recursive/consumer_test.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"example.com/recursive/internal/handler"
|
||||||
|
"example.com/recursive/internal/svc"
|
||||||
|
"example.com/recursive/internal/types"
|
||||||
|
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecursiveHTTP(t *testing.T) {
|
||||||
|
app := omni.New()
|
||||||
|
defer app.Close()
|
||||||
|
cfg := logx.Default()
|
||||||
|
cfg.Writer = io.Discard
|
||||||
|
logger, err := logx.New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer logger.Close()
|
||||||
|
deps, err := svc.New(context.Background(), logger)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer deps.Close()
|
||||||
|
if err := handler.RegisterRoutes(app, deps); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, path := range []string{"/tree", "/forest", "/optional", "/raw", "/pair"} {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
app.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
|
||||||
|
if w.Code != 200 {
|
||||||
|
t.Fatal(path, w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
data := w.Body.Bytes()
|
||||||
|
if path != "/raw" {
|
||||||
|
var envelope struct {
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data = envelope.Data
|
||||||
|
}
|
||||||
|
switch path {
|
||||||
|
case "/optional":
|
||||||
|
if string(data) != "null" {
|
||||||
|
t.Fatal(string(data))
|
||||||
|
}
|
||||||
|
case "/pair":
|
||||||
|
var pair types.Branch
|
||||||
|
if err := json.Unmarshal(data, &pair); err != nil || pair.Leaf == nil || pair.Leaf.Branch != nil {
|
||||||
|
t.Fatal(err, string(data))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
var node types.Node
|
||||||
|
if path == "/forest" {
|
||||||
|
var forest []types.Node
|
||||||
|
if err := json.Unmarshal(data, &forest); err != nil || len(forest) != 1 {
|
||||||
|
t.Fatal(err, string(data))
|
||||||
|
}
|
||||||
|
node = forest[0]
|
||||||
|
} else if err := json.Unmarshal(data, &node); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(node.Children) != 1 || len(node.Children[0].Children) != 1 || node.Children[0].Children[0].Name != "leaf" || node.Lookup["entry"].Name != "lookup" {
|
||||||
|
t.Fatal(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,15 +3,18 @@ package apigen
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
|
||||||
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WorkspaceService struct {
|
type WorkspaceService struct {
|
||||||
Dir string `json:"dir"`
|
Dir string `json:"dir"`
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
API string `json:"api,omitempty"`
|
API string `json:"api,omitempty"`
|
||||||
Module string `json:"module,omitempty"`
|
Module string `json:"module,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -19,10 +22,14 @@ type WorkspaceConfig struct {
|
|||||||
Services []WorkspaceService `json:"services"`
|
Services []WorkspaceService `json:"services"`
|
||||||
ErrorCatalogs []WorkspaceCatalog `json:"errorCatalogs,omitempty"`
|
ErrorCatalogs []WorkspaceCatalog `json:"errorCatalogs,omitempty"`
|
||||||
}
|
}
|
||||||
|
type workspaceTarget struct {
|
||||||
|
dir, kind, module, entry string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
// Workspace operates independent Go modules listed in an explicit local file.
|
// Workspace dispatches to each independent project's generator. Directory/schema
|
||||||
// All projects are preflighted before generate/format writes. Each generator
|
// validation precedes any providers; all targets are preflighted before writes.
|
||||||
// transaction is atomic within its project; no cross-module transaction is promised.
|
// Transactions remain per-project, not crash-atomic across the workspace.
|
||||||
func Workspace(path, command string, w io.Writer) error {
|
func Workspace(path, command string, w io.Writer) error {
|
||||||
if command != "generate" && command != "check" && command != "format" {
|
if command != "generate" && command != "check" && command != "format" {
|
||||||
return errors.New("workspace command must be generate, check or format")
|
return errors.New("workspace command must be generate, check or format")
|
||||||
@@ -53,8 +60,8 @@ func Workspace(path, command string, w io.Writer) error {
|
|||||||
return errors.New("workspace must list 1..64 services")
|
return errors.New("workspace must list 1..64 services")
|
||||||
}
|
}
|
||||||
root := canonical(filepath.Dir(real))
|
root := canonical(filepath.Dir(real))
|
||||||
var options []Options
|
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
|
// Validate the entire workspace boundary before resolving or running any target.
|
||||||
for _, service := range cfg.Services {
|
for _, service := range cfg.Services {
|
||||||
if service.Dir == "" || service.Dir == "." {
|
if service.Dir == "" || service.Dir == "." {
|
||||||
return errors.New("workspace requires distinct relative service directories")
|
return errors.New("workspace requires distinct relative service directories")
|
||||||
@@ -62,27 +69,26 @@ func Workspace(path, command string, w io.Writer) error {
|
|||||||
if err = safePath(root, service.Dir+"/.goOmniWorkspaceCheck"); err != nil {
|
if err = safePath(root, service.Dir+"/.goOmniWorkspaceCheck"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dir := filepath.Join(root, filepath.FromSlash(service.Dir))
|
key := strings.ToLower(filepath.Join(root, filepath.FromSlash(service.Dir)))
|
||||||
key := strings.ToLower(dir)
|
|
||||||
for prior := range seen {
|
for prior := range seen {
|
||||||
if prior == key || strings.HasPrefix(key, prior+string(filepath.Separator)) || strings.HasPrefix(prior, key+string(filepath.Separator)) {
|
if prior == key || strings.HasPrefix(key, prior+string(filepath.Separator)) || strings.HasPrefix(prior, key+string(filepath.Separator)) {
|
||||||
return errors.New("workspace service directories overlap")
|
return errors.New("workspace service directories overlap")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
entry := service.API
|
}
|
||||||
if entry == "" {
|
targets := make([]workspaceTarget, 0, len(cfg.Services))
|
||||||
entry, err = ProjectEntry(dir)
|
for _, service := range cfg.Services {
|
||||||
} else {
|
t := workspaceTarget{dir: filepath.Join(root, filepath.FromSlash(service.Dir)), module: service.Module}
|
||||||
if err = safePath(dir, entry); err != nil {
|
t.kind, t.err = workspaceKind(t.dir, service.Kind, service.API)
|
||||||
return err
|
if t.err == nil && t.kind == "api" {
|
||||||
|
if service.API == "" {
|
||||||
|
t.entry, t.err = ProjectEntry(t.dir)
|
||||||
|
} else if t.err = safePath(t.dir, service.API); t.err == nil {
|
||||||
|
t.entry = filepath.Join(t.dir, filepath.FromSlash(service.API))
|
||||||
}
|
}
|
||||||
entry = filepath.Join(dir, entry)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
targets = append(targets, t)
|
||||||
return err
|
|
||||||
}
|
|
||||||
options = append(options, Options{API: entry, Dir: dir, Module: service.Module, DryRun: true})
|
|
||||||
}
|
}
|
||||||
var catalogs map[string][]byte
|
var catalogs map[string][]byte
|
||||||
if command != "format" {
|
if command != "format" {
|
||||||
@@ -90,26 +96,41 @@ func Workspace(path, command string, w io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := range options {
|
|
||||||
options[i].documentCatalogs = catalogs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var failures error
|
var failures error
|
||||||
for _, o := range options {
|
report := func(t workspaceTarget, e error) {
|
||||||
if command == "format" {
|
if e != nil {
|
||||||
spec, e := Parse(o.API)
|
failures = errors.Join(failures, fmt.Errorf("%s: %w", t.dir, e))
|
||||||
if e == nil {
|
fmt.Fprintf(w, "FAIL %s: %v\n", t.dir, e)
|
||||||
_, e = Compile(spec)
|
} else if command == "format" && t.kind == "project" {
|
||||||
}
|
fmt.Fprintf(w, "SKIP %s (project has no API contract to format)\n", t.dir)
|
||||||
err = e
|
|
||||||
} else {
|
} else {
|
||||||
_, err = Generate(o)
|
fmt.Fprintf(w, "OK %s\n", t.dir)
|
||||||
}
|
}
|
||||||
if err != nil {
|
}
|
||||||
failures = errors.Join(failures, fmt.Errorf("%s: %w", o.Dir, err))
|
if command == "check" {
|
||||||
|
// Each generator checks its own manifest and managed outputs. An invalid
|
||||||
|
// project must not suppress diagnostics for another independent project.
|
||||||
|
for _, t := range targets {
|
||||||
|
report(t, runWorkspaceTarget(t, command, false, catalogs))
|
||||||
|
}
|
||||||
|
return failures
|
||||||
|
}
|
||||||
|
for i := range targets {
|
||||||
|
t := &targets[i]
|
||||||
|
t.err = runWorkspaceTarget(*t, command, true, catalogs)
|
||||||
|
}
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.err != nil {
|
||||||
|
report(t, t.err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if failures != nil {
|
if failures != nil {
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.err == nil {
|
||||||
|
fmt.Fprintf(w, "SKIP %s (workspace preflight failed; no changes applied)\n", t.dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
return failures
|
return failures
|
||||||
}
|
}
|
||||||
if command == "generate" {
|
if command == "generate" {
|
||||||
@@ -119,26 +140,79 @@ func Workspace(path, command string, w io.Writer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, o := range options {
|
for _, t := range targets {
|
||||||
switch command {
|
report(t, runWorkspaceTarget(t, command, false, catalogs))
|
||||||
case "format":
|
|
||||||
_, err = FormatProject(o.API, false)
|
|
||||||
case "generate":
|
|
||||||
o.DryRun = false
|
|
||||||
_, err = Generate(o)
|
|
||||||
case "check":
|
|
||||||
_, fmtErr := FormatProject(o.API, true)
|
|
||||||
o.DryRun = false
|
|
||||||
o.Check = true
|
|
||||||
_, genErr := Generate(o)
|
|
||||||
err = errors.Join(fmtErr, genErr)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
failures = errors.Join(failures, fmt.Errorf("%s: %w", o.Dir, err))
|
|
||||||
fmt.Fprintf(w, "FAIL %s\n", o.Dir)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "OK %s\n", o.Dir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return failures
|
return failures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An explicit API entry remains supported before the first API generation.
|
||||||
|
// Without manifests, omitted kind preserves the previous API interpretation.
|
||||||
|
// Manifests of both kinds or a conflicting declaration never silently choose one.
|
||||||
|
func workspaceKind(dir, kind, entry string) (string, error) {
|
||||||
|
if kind != "" && kind != "api" && kind != "project" {
|
||||||
|
return "", fmt.Errorf("unknown workspace kind %q; expected api or project", kind)
|
||||||
|
}
|
||||||
|
detected := ""
|
||||||
|
for _, candidate := range []struct{ kind, path string }{{"api", manifestPath}, {"project", ".goOmni/projectManifest.json"}} {
|
||||||
|
if err := safePath(dir, candidate.path); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(filepath.Join(dir, candidate.path)); err == nil {
|
||||||
|
if detected != "" {
|
||||||
|
return "", errors.New("ambiguous project: both API and project manifests exist")
|
||||||
|
}
|
||||||
|
detected = candidate.kind
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if kind != "" && detected != "" && kind != detected {
|
||||||
|
return "", fmt.Errorf("workspace kind %q conflicts with %s manifest", kind, detected)
|
||||||
|
}
|
||||||
|
if kind == "" {
|
||||||
|
kind = detected
|
||||||
|
}
|
||||||
|
if kind == "" {
|
||||||
|
kind = "api"
|
||||||
|
}
|
||||||
|
if kind == "project" && entry != "" {
|
||||||
|
return "", errors.New("workspace project kind does not accept an api entry")
|
||||||
|
}
|
||||||
|
return kind, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWorkspaceTarget(t workspaceTarget, command string, preview bool, catalogs map[string][]byte) error {
|
||||||
|
if t.err != nil {
|
||||||
|
return t.err
|
||||||
|
}
|
||||||
|
if t.kind == "project" {
|
||||||
|
if command == "format" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := projectgen.Generate(projectgen.Options{Dir: t.dir, Module: t.module, DryRun: preview, Check: command == "check"})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o := Options{API: t.entry, Dir: t.dir, Module: t.module, DryRun: preview, documentCatalogs: catalogs}
|
||||||
|
switch command {
|
||||||
|
case "format":
|
||||||
|
if preview {
|
||||||
|
spec, err := Parse(t.entry)
|
||||||
|
if err == nil {
|
||||||
|
_, err = Compile(spec)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := FormatProject(t.entry, false)
|
||||||
|
return err
|
||||||
|
case "generate":
|
||||||
|
_, err := Generate(o)
|
||||||
|
return err
|
||||||
|
case "check":
|
||||||
|
_, formatErr := FormatProject(t.entry, true)
|
||||||
|
o.Check = true
|
||||||
|
_, generateErr := Generate(o)
|
||||||
|
return errors.Join(formatErr, generateErr)
|
||||||
|
}
|
||||||
|
return errors.New("unsupported workspace command")
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -30,8 +31,11 @@ func Catalog() errors.Catalog {return errors.Catalog{DefaultStatus:400,Definitio
|
|||||||
}
|
}
|
||||||
writeFile(t, filepath.Join(dir, "api/omni.imports.json"), `{"roots":{"common":{"path":"../../../contracts"}},"errors":{"source":"@common/errors.json","include":["INVALID"],"routes":{"GET /api/greet/:name":["MISSING"]}}}`)
|
writeFile(t, filepath.Join(dir, "api/omni.imports.json"), `{"roots":{"common":{"path":"../../../contracts"}},"errors":{"source":"@common/errors.json","include":["INVALID"],"routes":{"GET /api/greet/:name":["MISSING"]}}}`)
|
||||||
}
|
}
|
||||||
|
if _, err := projectgen.New(projectgen.Options{Dir: filepath.Join(root, "services/program"), Module: "example.com/program", FrameworkDir: frameworkRoot(t)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
workspace := filepath.Join(root, "omni.workspace.json")
|
workspace := filepath.Join(root, "omni.workspace.json")
|
||||||
writeFile(t, workspace, `{"errorCatalogs":[{"dir":".","package":"example.com/catalogs/internal/apierrors","function":"Catalog","output":"contracts/errors.json"}],"services":[{"dir":"services/api"},{"dir":"services/id"}]}`)
|
writeFile(t, workspace, `{"errorCatalogs":[{"dir":".","package":"example.com/catalogs/internal/apierrors","function":"Catalog","output":"contracts/errors.json"}],"services":[{"dir":"services/api"},{"dir":"services/id"},{"dir":"services/program"}]}`)
|
||||||
if err := Workspace(workspace, "generate", io.Discard); err != nil {
|
if err := Workspace(workspace, "generate", io.Discard); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
226
cmd/go-omni/apigen/workspaceMixed_test.go
Normal file
226
cmd/go-omni/apigen/workspaceMixed_test.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package apigen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mixedWorkspace(t *testing.T) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
root := t.TempDir()
|
||||||
|
if _, err := New(Options{Dir: filepath.Join(root, "apps/interface"), Module: "example.com/interface", FrameworkDir: frameworkRoot(t)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := projectgen.New(projectgen.Options{Dir: filepath.Join(root, "apps/program"), Module: "example.com/program", FrameworkDir: frameworkRoot(t)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
config := filepath.Join(root, "omni.workspace.json")
|
||||||
|
writeFile(t, config, `{"services":[{"dir":"apps/interface"},{"dir":"apps/program"}]}`)
|
||||||
|
if err := Workspace(config, "format", io.Discard); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Workspace(config, "generate", io.Discard); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return root, config
|
||||||
|
}
|
||||||
|
func TestWorkspaceMixedKindsAndReadOnlyCheck(t *testing.T) {
|
||||||
|
for _, explicit := range []bool{false, true} {
|
||||||
|
t.Run(map[bool]string{false: "inferred", true: "explicit"}[explicit], func(t *testing.T) {
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
if explicit {
|
||||||
|
writeFile(t, config, `{"services":[{"dir":"apps/interface","kind":"api","module":"example.com/interface"},{"dir":"apps/program","kind":"project","module":"example.com/program"}]}`)
|
||||||
|
}
|
||||||
|
// Workspaces do not manage the Go toolchain's workspace or dependency files.
|
||||||
|
writeFile(t, filepath.Join(root, "go.work"), "go 1.22.0\nuse (\n ./apps/interface\n ./apps/program\n)\n")
|
||||||
|
writeFile(t, filepath.Join(root, "go.work.sum"), "workspace sums marker\n")
|
||||||
|
for _, dir := range []string{"apps/interface", "apps/program"} {
|
||||||
|
writeFile(t, filepath.Join(root, dir, "go.sum"), "module sums marker\n")
|
||||||
|
}
|
||||||
|
runFile := filepath.Join(root, "apps/program/internal/run/run.go")
|
||||||
|
writeFile(t, runFile, readFile(t, runFile)+"\n// project-owned work\n")
|
||||||
|
before := snapshot(t, root)
|
||||||
|
for _, op := range []string{"check", "generate", "check"} {
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := Workspace(config, op, &output); err != nil {
|
||||||
|
t.Fatal(op, err)
|
||||||
|
}
|
||||||
|
if strings.Count(output.String(), "OK ") != 2 {
|
||||||
|
t.Fatal(output.String())
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal(op, "changed clean workspace or project-owned files")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Formatting only touches API contracts, even when project Go has drift.
|
||||||
|
main := filepath.Join(root, "apps/program/main.go")
|
||||||
|
writeFile(t, main, readFile(t, main)+"\n// drift intentionally not formatted\n")
|
||||||
|
before = snapshot(t, root)
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := Workspace(config, "format", &output); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output.String(), "SKIP ") || !strings.Contains(output.String(), "apps/program") {
|
||||||
|
t.Fatal(output.String())
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("format changed a plain Go project")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestWorkspaceMixedFailureAggregation(t *testing.T) {
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
for _, dir := range []string{"apps/interface", "apps/program"} {
|
||||||
|
path := filepath.Join(root, dir, "main.go")
|
||||||
|
writeFile(t, path, readFile(t, path)+"\n// manual drift\n")
|
||||||
|
}
|
||||||
|
before := snapshot(t, root)
|
||||||
|
var output bytes.Buffer
|
||||||
|
err := Workspace(config, "check", &output)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "apps/interface") || !strings.Contains(err.Error(), "apps/program") || strings.Count(output.String(), "FAIL ") != 2 {
|
||||||
|
t.Fatal(err, output.String())
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("failed check wrote files")
|
||||||
|
}
|
||||||
|
// Identity failures also keep unrelated project diagnostics.
|
||||||
|
writeFile(t, config, `{"services":[{"dir":"apps/interface","kind":"project"},{"dir":"apps/program"}]}`)
|
||||||
|
output.Reset()
|
||||||
|
before = snapshot(t, root)
|
||||||
|
err = Workspace(config, "check", &output)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "conflicts") || strings.Count(output.String(), "FAIL ") != 2 {
|
||||||
|
t.Fatal(err, output.String())
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("identity failure changed workspace")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestWorkspaceMixedPreflightBeforeAnyWrites(t *testing.T) {
|
||||||
|
for _, broken := range []string{"apps/interface", "apps/program"} {
|
||||||
|
t.Run(broken, func(t *testing.T) {
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
stale := "apps/interface"
|
||||||
|
if stale == broken {
|
||||||
|
stale = "apps/program"
|
||||||
|
}
|
||||||
|
// The first valid generator could repair its missing output, but must not
|
||||||
|
// write if another project's ownership check will fail.
|
||||||
|
if err := os.Remove(filepath.Join(root, stale, "main.go")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
path := filepath.Join(root, broken, "main.go")
|
||||||
|
writeFile(t, path, readFile(t, path)+"\n// manual change\n")
|
||||||
|
before := snapshot(t, root)
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := Workspace(config, "generate", &output); err == nil {
|
||||||
|
t.Fatal("bad preflight accepted")
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("partial regeneration before failed preflight")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output.String(), "SKIP ") || !strings.Contains(output.String(), "FAIL ") {
|
||||||
|
t.Fatal(output.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
for _, dir := range []string{"apps/interface", "apps/program"} {
|
||||||
|
if err := os.Remove(filepath.Join(root, dir, "main.go")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := Workspace(config, "generate", io.Discard); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Workspace(config, "check", io.Discard); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestWorkspaceKindGuardsAndLegacyEntry(t *testing.T) {
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
for _, tc := range []struct{ services, needle string }{
|
||||||
|
{`[{"dir":"apps/program","kind":"worker"}]`, "unknown workspace kind"},
|
||||||
|
{`[{"dir":"apps/program","kind":"api"}]`, "conflicts"},
|
||||||
|
{`[{"dir":"apps/interface","kind":"project"}]`, "conflicts"},
|
||||||
|
{`[{"dir":"apps/program","api":"api/service.api"}]`, "does not accept"},
|
||||||
|
{`[{"dir":"apps/program","kind":"project","api":"api/service.api"}]`, "does not accept"},
|
||||||
|
{`[{"dir":"apps/program","module":"example.com/other"}]`, "module differs"},
|
||||||
|
{`[{"dir":"../outside","kind":"project"}]`, "unsafe"},
|
||||||
|
{`[{"dir":"apps/program"},{"dir":"apps/program/nested","kind":"project"}]`, "overlap"},
|
||||||
|
{`[{"dir":"apps/program","kind":"project","kind":"api"}]`, "duplicate"},
|
||||||
|
} {
|
||||||
|
writeFile(t, config, `{"services":`+tc.services+`}`)
|
||||||
|
before := snapshot(t, root)
|
||||||
|
err := Workspace(config, "check", io.Discard)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tc.needle) {
|
||||||
|
t.Fatal(tc.services, err)
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("invalid kind wrote files")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeFile(t, config, `{"services":[{"dir":"apps/program"}]}`)
|
||||||
|
other := filepath.Join(root, "apps/program/.goOmni/apiManifest.json")
|
||||||
|
writeFile(t, other, "{}")
|
||||||
|
if err := Workspace(config, "check", io.Discard); err == nil || !strings.Contains(err.Error(), "both API and project") {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(other); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// A malformed manifest must be routed to its owner, never fall back to API.
|
||||||
|
writeFile(t, filepath.Join(root, "apps/program/.goOmni/projectManifest.json"), "invalid")
|
||||||
|
if err := Workspace(config, "check", io.Discard); err == nil || !strings.Contains(err.Error(), "invalid project manifest") {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Explicit API entry outside a managed project preserves the original path.
|
||||||
|
legacy := filepath.Join(root, "legacy")
|
||||||
|
writeFile(t, filepath.Join(legacy, "service.api"), "service legacy {\n@handler ping\nget /ping\n}\n")
|
||||||
|
writeFile(t, config, `{"services":[{"dir":"legacy","api":"service.api","module":"example.com/legacy"}]}`)
|
||||||
|
if err := Workspace(config, "generate", io.Discard); err != nil {
|
||||||
|
t.Fatal("legacy API entry", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestWorkspaceManifestSymlinkRejected(t *testing.T) {
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
path := filepath.Join(root, "apps/program/.goOmni/projectManifest.json")
|
||||||
|
target := filepath.Join(t.TempDir(), "manifest.json")
|
||||||
|
writeFile(t, target, readFile(t, path))
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(target, path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before := snapshot(t, root)
|
||||||
|
for _, op := range []string{"check", "format", "generate"} {
|
||||||
|
if err := Workspace(config, op, io.Discard); err == nil || !strings.Contains(err.Error(), "symlink") {
|
||||||
|
t.Fatal(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("followed symlink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestWorkspaceProjectOnly(t *testing.T) {
|
||||||
|
root, config := mixedWorkspace(t)
|
||||||
|
raw, _ := json.Marshal(WorkspaceConfig{Services: []WorkspaceService{{Dir: "apps/program"}}})
|
||||||
|
writeFile(t, config, string(raw))
|
||||||
|
before := snapshot(t, root)
|
||||||
|
for _, op := range []string{"format", "generate", "check"} {
|
||||||
|
if err := Workspace(config, op, io.Discard); err != nil {
|
||||||
|
t.Fatal(op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sameFiles(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("plain workspace changed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/internal/genfs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -244,44 +245,13 @@ func apply(root, module, entry string, generated, seeds map[string][]byte, check
|
|||||||
}
|
}
|
||||||
return result, checkErr
|
return result, checkErr
|
||||||
}
|
}
|
||||||
// A rollback restores bytes/modes if an ordinary filesystem write fails.
|
transaction := make([]genfs.Change, 0, len(changes))
|
||||||
// File replacement is atomic; the whole directory is not crash-atomic.
|
|
||||||
var applied []change
|
|
||||||
for _, c := range changes {
|
for _, c := range changes {
|
||||||
if err = safePath(root, c.name); err == nil {
|
transaction = append(transaction, genfs.Change{Name: c.name, Before: c.before, After: c.after, Existed: c.existed, Remove: c.remove, Mode: c.mode})
|
||||||
current, e := os.ReadFile(filepath.Join(root, filepath.FromSlash(c.name)))
|
|
||||||
if c.existed {
|
|
||||||
if e != nil || !bytes.Equal(current, c.before) {
|
|
||||||
err = fmt.Errorf("file changed during generation: %s", c.name)
|
|
||||||
}
|
|
||||||
} else if !errors.Is(e, os.ErrNotExist) {
|
|
||||||
err = fmt.Errorf("file appeared during generation: %s", c.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
if c.remove {
|
|
||||||
err = os.Remove(filepath.Join(root, filepath.FromSlash(c.name)))
|
|
||||||
} else {
|
|
||||||
err = atomicWrite(root, c.name, c.after, c.mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
for i := len(applied) - 1; i >= 0; i-- {
|
|
||||||
previous := applied[i]
|
|
||||||
var rollback error
|
|
||||||
if previous.existed {
|
|
||||||
rollback = atomicWrite(root, previous.name, previous.before, previous.mode)
|
|
||||||
} else {
|
|
||||||
rollback = os.Remove(filepath.Join(root, filepath.FromSlash(previous.name)))
|
|
||||||
}
|
|
||||||
err = errors.Join(err, rollback)
|
|
||||||
}
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
applied = append(applied, c)
|
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, genfs.Apply(root, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
func digest(data []byte) string { sum := sha256.Sum256(data); return hex.EncodeToString(sum[:]) }
|
func digest(data []byte) string { sum := sha256.Sum256(data); return hex.EncodeToString(sum[:]) }
|
||||||
func ownedPath(name string) bool {
|
func ownedPath(name string) bool {
|
||||||
if name == "internal/config/loadGen.go" {
|
if name == "internal/config/loadGen.go" {
|
||||||
@@ -298,52 +268,7 @@ func ownedPath(name string) bool {
|
|||||||
}
|
}
|
||||||
return strings.HasPrefix(name, "internal/handler/") && strings.HasSuffix(name, "/handlersGen.go")
|
return strings.HasPrefix(name, "internal/handler/") && strings.HasSuffix(name, "/handlersGen.go")
|
||||||
}
|
}
|
||||||
func safePath(root, name string) error {
|
func safePath(root, name string) error { return genfs.SafePath(root, name) }
|
||||||
if filepath.IsAbs(name) || strings.Contains(name, "\\") || filepath.ToSlash(filepath.Clean(name)) != name || name == "." || strings.HasPrefix(name, "../") {
|
|
||||||
return fmt.Errorf("api: unsafe output path %q", name)
|
|
||||||
}
|
|
||||||
path := root
|
|
||||||
parts := strings.Split(name, "/")
|
|
||||||
for i, part := range parts {
|
|
||||||
path = filepath.Join(path, part)
|
|
||||||
info, err := os.Lstat(path)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
return fmt.Errorf("api: refusing symlink %s", path)
|
|
||||||
}
|
|
||||||
if i < len(parts)-1 && !info.IsDir() {
|
|
||||||
return fmt.Errorf("api: expected directory %s", path)
|
|
||||||
}
|
|
||||||
if i == len(parts)-1 && !info.Mode().IsRegular() {
|
|
||||||
return fmt.Errorf("api: expected regular file %s", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func atomicWrite(root, name string, data []byte, mode os.FileMode) error {
|
func atomicWrite(root, name string, data []byte, mode os.FileMode) error {
|
||||||
target := filepath.Join(root, filepath.FromSlash(name))
|
return genfs.AtomicWrite(root, name, data, mode)
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f, err := os.CreateTemp(filepath.Dir(target), ".apiGen-*")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
if err = f.Chmod(mode); err == nil {
|
|
||||||
_, err = f.Write(data)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
err = f.Sync()
|
|
||||||
}
|
|
||||||
err = errors.Join(err, f.Close())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Rename(f.Name(), target)
|
|
||||||
}
|
}
|
||||||
|
|||||||
133
cmd/go-omni/internal/genfs/files.go
Normal file
133
cmd/go-omni/internal/genfs/files.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// Package genfs shares guarded filesystem operations between project generators.
|
||||||
|
package genfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SafePath(root, name string) error {
|
||||||
|
if filepath.IsAbs(name) || strings.Contains(name, "\\") || filepath.ToSlash(filepath.Clean(name)) != name || name == "." || strings.HasPrefix(name, "../") {
|
||||||
|
return fmt.Errorf("generation: unsafe output path %q", name)
|
||||||
|
}
|
||||||
|
path := root
|
||||||
|
parts := strings.Split(name, "/")
|
||||||
|
for i, part := range parts {
|
||||||
|
path = filepath.Join(path, part)
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return fmt.Errorf("generation: refusing symlink %s", path)
|
||||||
|
}
|
||||||
|
if i < len(parts)-1 && !info.IsDir() {
|
||||||
|
return fmt.Errorf("generation: expected directory %s", path)
|
||||||
|
}
|
||||||
|
if i == len(parts)-1 && !info.Mode().IsRegular() {
|
||||||
|
return fmt.Errorf("generation: expected regular file %s", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func AtomicWrite(root, name string, data []byte, mode os.FileMode) error {
|
||||||
|
target := filepath.Join(root, filepath.FromSlash(name))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp(filepath.Dir(target), ".omniGen-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
if err = f.Chmod(mode); err == nil {
|
||||||
|
_, err = f.Write(data)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = f.Sync()
|
||||||
|
}
|
||||||
|
err = errors.Join(err, f.Close())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(f.Name(), target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change records the preflight snapshot used for conflict detection and rollback.
|
||||||
|
type Change struct {
|
||||||
|
Name string
|
||||||
|
Before, After []byte
|
||||||
|
Existed, Remove bool
|
||||||
|
Mode os.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply replaces individual files atomically and rolls back ordinary write failures.
|
||||||
|
// Callers serialize generators with a project lock. This is not crash-atomic across files.
|
||||||
|
func Apply(root string, changes []Change) error {
|
||||||
|
var applied []Change
|
||||||
|
for _, c := range changes {
|
||||||
|
err := SafePath(root, c.Name)
|
||||||
|
if err == nil {
|
||||||
|
current, e := os.ReadFile(filepath.Join(root, filepath.FromSlash(c.Name)))
|
||||||
|
if c.Existed {
|
||||||
|
if e != nil || !bytes.Equal(current, c.Before) {
|
||||||
|
err = fmt.Errorf("file changed during generation: %s", c.Name)
|
||||||
|
}
|
||||||
|
} else if !errors.Is(e, os.ErrNotExist) {
|
||||||
|
err = fmt.Errorf("file appeared during generation: %s", c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if c.Remove {
|
||||||
|
err = os.Remove(filepath.Join(root, filepath.FromSlash(c.Name)))
|
||||||
|
} else {
|
||||||
|
err = AtomicWrite(root, c.Name, c.After, c.Mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
for i := len(applied) - 1; i >= 0; i-- {
|
||||||
|
previous := applied[i]
|
||||||
|
if e := SafePath(root, previous.Name); e != nil {
|
||||||
|
err = errors.Join(err, e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if previous.Existed {
|
||||||
|
err = errors.Join(err, AtomicWrite(root, previous.Name, previous.Before, previous.Mode))
|
||||||
|
} else {
|
||||||
|
err = errors.Join(err, os.Remove(filepath.Join(root, filepath.FromSlash(previous.Name))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
applied = append(applied, c)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical resolves existing parent symlinks even for an output not yet created.
|
||||||
|
func Canonical(path string) string {
|
||||||
|
path, _ = filepath.Abs(path)
|
||||||
|
candidate := path
|
||||||
|
var tail []string
|
||||||
|
for {
|
||||||
|
if resolved, err := filepath.EvalSymlinks(candidate); err == nil {
|
||||||
|
for i := len(tail) - 1; i >= 0; i-- {
|
||||||
|
resolved = filepath.Join(resolved, tail[i])
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(candidate)
|
||||||
|
if parent == candidate {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
tail = append(tail, filepath.Base(candidate))
|
||||||
|
candidate = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
47
cmd/go-omni/internal/genfs/files_test.go
Normal file
47
cmd/go-omni/internal/genfs/files_test.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package genfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConflictRollsBackEarlierWrites(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "one"), []byte("before"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(root, "two"), []byte("concurrent"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err := Apply(root, []Change{{Name: "one", Before: []byte("before"), After: []byte("after"), Existed: true, Mode: 0600}, {Name: "new", After: []byte("created"), Mode: 0644}, {Name: "two", Before: []byte("stale"), After: []byte("after"), Existed: true, Mode: 0644}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("conflict accepted")
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(filepath.Join(root, "one"))
|
||||||
|
info, _ := os.Stat(filepath.Join(root, "one"))
|
||||||
|
if string(data) != "before" || info.Mode().Perm() != 0600 {
|
||||||
|
t.Fatal("rollback did not restore bytes/mode")
|
||||||
|
}
|
||||||
|
if _, err = os.Stat(filepath.Join(root, "new")); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("new output survived rollback")
|
||||||
|
}
|
||||||
|
data, _ = os.ReadFile(filepath.Join(root, "two"))
|
||||||
|
if string(data) != "concurrent" {
|
||||||
|
t.Fatal("concurrent edit overwritten")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestUnsafePaths(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
for _, name := range []string{"../escape", "a/../../escape", "/outside", "a\\b", ".", "a/../b"} {
|
||||||
|
if SafePath(root, name) == nil {
|
||||||
|
t.Fatal(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.Symlink(t.TempDir(), filepath.Join(root, "link")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if SafePath(root, "link/file") == nil {
|
||||||
|
t.Fatal("followed symlink")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
|
case "project":
|
||||||
|
if err := runProject(os.Args[2:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "go-omni project:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
case "i18n":
|
case "i18n":
|
||||||
if err := runI18n(os.Args[2:]); err != nil {
|
if err := runI18n(os.Args[2:]); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "go-omni i18n:", err)
|
fmt.Fprintln(os.Stderr, "go-omni i18n:", err)
|
||||||
@@ -175,6 +180,7 @@ func usage() {
|
|||||||
fmt.Fprintln(os.Stderr, "GoOmni project tool")
|
fmt.Fprintln(os.Stderr, "GoOmni project tool")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
fmt.Fprintln(os.Stderr, "Usage:")
|
fmt.Fprintln(os.Stderr, "Usage:")
|
||||||
|
fmt.Fprintln(os.Stderr, " go-omni project <new|generate|check> Generate a transport-independent Go program")
|
||||||
fmt.Fprintln(os.Stderr, " go-omni i18n <check|export|types|extract> Validate and export independent language resources")
|
fmt.Fprintln(os.Stderr, " go-omni i18n <check|export|types|extract> Validate and export independent language resources")
|
||||||
fmt.Fprintln(os.Stderr, " go-omni migrate [--dir migrations] [--apply] [--dsn-env GO_OMNI_DB_DSN]")
|
fmt.Fprintln(os.Stderr, " go-omni migrate [--dir migrations] [--apply] [--dsn-env GO_OMNI_DB_DSN]")
|
||||||
fmt.Fprintln(os.Stderr, " go-omni new [--module <module>] <directory>")
|
fmt.Fprintln(os.Stderr, " go-omni new [--module <module>] <directory>")
|
||||||
@@ -217,6 +223,9 @@ func runAdd(args []string) error {
|
|||||||
return fmt.Errorf("unexpected argument %q", arg)
|
return fmt.Errorf("unexpected argument %q", arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, err := os.Lstat(filepath.Join(directory, ".goOmni", "projectManifest.json")); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return errors.New("plain Go projects add packages as ordinary Go code; use project generate for infrastructure")
|
||||||
|
}
|
||||||
if _, err := os.Stat(filepath.Join(directory, ".goOmni", "apiManifest.json")); err == nil {
|
if _, err := os.Stat(filepath.Join(directory, ".goOmni", "apiManifest.json")); err == nil {
|
||||||
return errors.New("managed API projects add modules through .api imports and api generate")
|
return errors.New("managed API projects add modules through .api imports and api generate")
|
||||||
}
|
}
|
||||||
|
|||||||
102
cmd/go-omni/project.go
Normal file
102
cmd/go-omni/project.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runProject(args []string) error {
|
||||||
|
if len(args) == 0 || args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
|
||||||
|
projectUsage()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
command := args[0]
|
||||||
|
if command != "new" && command != "generate" && command != "check" {
|
||||||
|
return fmt.Errorf("unknown project command %q", command)
|
||||||
|
}
|
||||||
|
o := projectgen.Options{Check: command == "check"}
|
||||||
|
for i := 1; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
if arg == "--help" || arg == "-h" {
|
||||||
|
projectUsage()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if arg == "--check" || arg == "--dry-run" {
|
||||||
|
if command != "generate" {
|
||||||
|
return fmt.Errorf("%s belongs to project generate", arg)
|
||||||
|
}
|
||||||
|
if arg == "--check" {
|
||||||
|
o.Check = true
|
||||||
|
} else {
|
||||||
|
o.DryRun = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(arg, "-") {
|
||||||
|
if command != "new" || o.Dir != "" {
|
||||||
|
return fmt.Errorf("unexpected argument %q", arg)
|
||||||
|
}
|
||||||
|
o.Dir = arg
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, value, inline := strings.Cut(arg, "=")
|
||||||
|
switch name {
|
||||||
|
case "--dir", "--module", "--framework-dir", "--framework-version":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown project option %q", name)
|
||||||
|
}
|
||||||
|
if command != "new" && name != "--dir" {
|
||||||
|
return fmt.Errorf("%s belongs to project new", name)
|
||||||
|
}
|
||||||
|
if !inline {
|
||||||
|
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "-") {
|
||||||
|
return fmt.Errorf("%s requires a value", name)
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
value = args[i]
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return fmt.Errorf("%s requires a value", name)
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "--dir":
|
||||||
|
if o.Dir != "" {
|
||||||
|
return errors.New("project directory specified more than once")
|
||||||
|
}
|
||||||
|
o.Dir = value
|
||||||
|
case "--module":
|
||||||
|
o.Module = value
|
||||||
|
case "--framework-dir":
|
||||||
|
o.FrameworkDir = value
|
||||||
|
case "--framework-version":
|
||||||
|
o.FrameworkVersion = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var out projectgen.Output
|
||||||
|
var err error
|
||||||
|
if command == "new" {
|
||||||
|
out, err = projectgen.New(o)
|
||||||
|
} else {
|
||||||
|
out, err = projectgen.Generate(o)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if o.DryRun {
|
||||||
|
for _, change := range out.Changes {
|
||||||
|
fmt.Printf("%-7s %s\n", change.Action, change.Path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("project %s: %d file changes\n", command, len(out.Changes))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func projectUsage() {
|
||||||
|
fmt.Println("go-omni project new <directory> [--module <module>] [--framework-version <version>|--framework-dir <directory>]")
|
||||||
|
fmt.Println("go-omni project generate [--dir <project>] [--check|--dry-run]")
|
||||||
|
fmt.Println("go-omni project check [--dir <project>]")
|
||||||
|
fmt.Println("Creates a plain Go program with config, logging, dependency lifecycle and an editable Run entry.")
|
||||||
|
}
|
||||||
27
cmd/go-omni/project_test.go
Normal file
27
cmd/go-omni/project_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProjectCLI(t *testing.T) {
|
||||||
|
root := filepath.Join(t.TempDir(), "program")
|
||||||
|
for _, args := range [][]string{{"new", root, "--module", "example.com/program"}, {"generate", "--dir", root, "--dry-run"}, {"generate", "--dir", root}, {"check", "--dir", root}, {"generate", "--dir", root, "--check"}} {
|
||||||
|
if err := runProject(args); err != nil {
|
||||||
|
t.Fatal(args, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := runAdd([]string{"item", "--dir", root}); err == nil {
|
||||||
|
t.Fatal("add adopted plain project")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(root, "internal/item")); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("business module generated")
|
||||||
|
}
|
||||||
|
for _, args := range [][]string{{"new"}, {"new", "a", "b"}, {"new", "--api", "x"}, {"check", "--dry-run"}, {"generate", "--check", "--dry-run"}, {"generate", "--module", "x"}, {"new", "--module"}, {"new", "--dir="}, {"new", "--dir", "x", "y"}, {"unknown"}} {
|
||||||
|
if err := runProject(args); err == nil {
|
||||||
|
t.Fatal("accepted", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
190
cmd/go-omni/projectgen/consumer_test.go
Normal file
190
cmd/go-omni/projectgen/consumer_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package projectgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGeneratedProgramConsumer(t *testing.T) {
|
||||||
|
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") != "1" {
|
||||||
|
t.Skip("set GO_OMNI_SCAFFOLD_INTEGRATION=1")
|
||||||
|
}
|
||||||
|
root := filepath.Join(t.TempDir(), "program")
|
||||||
|
cli := os.Getenv("GO_OMNI_RELEASE_CLI")
|
||||||
|
if cli != "" {
|
||||||
|
runCommand(t, "", cli, "project", "new", root, "--module", "example.com/plain")
|
||||||
|
} else {
|
||||||
|
framework, err := filepath.Abs("../../..")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = New(Options{Dir: root, Module: "example.com/plain", FrameworkDir: framework}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only project-owned files are customized. All entry/loading/lifecycle code
|
||||||
|
// remains exactly what the local or downloaded CLI generated.
|
||||||
|
svc := get(t, root, "internal/svc/serviceContext.go")
|
||||||
|
svc = strings.Replace(svc, "return nil", "return Initialize(ctx, c)", 1)
|
||||||
|
svc += "\nvar Initialize = func(context.Context, *ServiceContext) error { return nil }\n"
|
||||||
|
put(t, root, "internal/svc/serviceContext.go", svc)
|
||||||
|
put(t, root, "internal/run/run.go", `package run
|
||||||
|
import("context";"errors";"os";"example.com/plain/internal/svc")
|
||||||
|
var Execute=func(ctx context.Context,deps *svc.ServiceContext)error{
|
||||||
|
switch os.Getenv("GO_OMNI_PROGRAM_TEST_MODE") {case "error":return errors.New("process failure marker");case "panic":panic("process panic marker");case "unresponsive":select{}}
|
||||||
|
<-ctx.Done();return nil
|
||||||
|
}
|
||||||
|
func Run(ctx context.Context,deps *svc.ServiceContext)error{return Execute(ctx,deps)}
|
||||||
|
`)
|
||||||
|
put(t, root, "consumer_test.go", get(t, "testdata", "consumer_test.go"))
|
||||||
|
runCommand(t, root, "go", "mod", "tidy")
|
||||||
|
runCommand(t, root, "go", "test", "-race", "-count=1", "./...")
|
||||||
|
runCommand(t, root, "go", "vet", "./...")
|
||||||
|
binary := filepath.Join(root, "program")
|
||||||
|
runCommand(t, root, "go", "build", "-race", "-o", binary, ".")
|
||||||
|
before := snapshot(t, root)
|
||||||
|
for _, mode := range []string{"generate", "check", "dry-run"} {
|
||||||
|
if cli != "" {
|
||||||
|
args := []string{"project", mode, "--dir", root}
|
||||||
|
if mode == "dry-run" {
|
||||||
|
args = []string{"project", "generate", "--dir", root, "--dry-run"}
|
||||||
|
}
|
||||||
|
runCommand(t, root, cli, args...)
|
||||||
|
} else {
|
||||||
|
if _, err := Generate(Options{Dir: root, Check: mode == "check", DryRun: mode == "dry-run"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
after := snapshot(t, root)
|
||||||
|
for name, data := range before {
|
||||||
|
if after[name] != data {
|
||||||
|
t.Fatal("consumer changed during regeneration", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Validation must not open even the configured log file.
|
||||||
|
logfile := filepath.Join(root, "check-must-not-open.log")
|
||||||
|
cfg := filepath.Join(root, "check.json")
|
||||||
|
raw, _ := json.Marshal(map[string]any{"log": map[string]any{"file": logfile}})
|
||||||
|
put(t, root, "check.json", string(raw))
|
||||||
|
runCommand(t, root, binary, "-config", cfg, "-check-config")
|
||||||
|
if _, err := os.Stat(logfile); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("check-config opened logger", err)
|
||||||
|
}
|
||||||
|
put(t, root, "check.json", `{"log":{"level":"invalid"}}`)
|
||||||
|
cmd := exec.Command(binary, "-config", cfg, "-check-config")
|
||||||
|
cmd.Dir = root
|
||||||
|
if data, err := cmd.CombinedOutput(); err == nil || !strings.Contains(string(data), "log.level") {
|
||||||
|
t.Fatal("invalid config accepted", string(data), err)
|
||||||
|
}
|
||||||
|
for _, sig := range []os.Signal{os.Interrupt, syscall.SIGTERM} {
|
||||||
|
testProcess(t, root, binary, sig, "")
|
||||||
|
}
|
||||||
|
for _, mode := range []string{"error", "panic", "unresponsive"} {
|
||||||
|
var sig os.Signal
|
||||||
|
if mode == "unresponsive" {
|
||||||
|
sig = syscall.SIGTERM
|
||||||
|
}
|
||||||
|
testProcess(t, root, binary, sig, mode)
|
||||||
|
}
|
||||||
|
if cli != "" {
|
||||||
|
data := runCommand(t, root, "go", "list", "-m", "-json", frameworkModule)
|
||||||
|
var dep struct {
|
||||||
|
Version, Sum string
|
||||||
|
Replace any
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(data), &dep); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if dep.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || dep.Replace != nil || dep.Sum == "" {
|
||||||
|
t.Fatal("not a fixed remote consumer", dep)
|
||||||
|
}
|
||||||
|
t.Logf("remote framework %s checksum %s; no local replace", dep.Version, dep.Sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func runCommand(t *testing.T, dir, name string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, name, args...)
|
||||||
|
if dir != "" {
|
||||||
|
cmd.Dir = dir
|
||||||
|
}
|
||||||
|
cmd.Env = append(os.Environ(), "GOWORK=off")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s %v: %v\n%s", name, args, err, output)
|
||||||
|
}
|
||||||
|
t.Logf("%s %v\n%s", name, args, output)
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
func testProcess(t *testing.T, root, binary string, sig os.Signal, mode string) {
|
||||||
|
t.Helper()
|
||||||
|
logfile, err := os.CreateTemp(root, "signal-*.log")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer logfile.Close()
|
||||||
|
cfg := filepath.Join(root, "process.json")
|
||||||
|
put(t, root, "process.json", `{"lifecycle":{"stopTimeout":"100ms","drainTimeout":"30ms","cancelTimeout":"100ms"}}`)
|
||||||
|
cmd := exec.Command(binary, "-config", cfg)
|
||||||
|
cmd.Env = append(os.Environ(), "GO_OMNI_PROGRAM_TEST_MODE="+mode)
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Stdout = logfile
|
||||||
|
cmd.Stderr = logfile
|
||||||
|
if err = cmd.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- cmd.Wait() }()
|
||||||
|
defer func() { _ = cmd.Process.Kill() }()
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for {
|
||||||
|
data, _ := os.ReadFile(logfile.Name())
|
||||||
|
if strings.Contains(string(data), "program.starting") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
t.Fatal("program did not start", string(data))
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if path, err := exec.LookPath("lsof"); err == nil {
|
||||||
|
output, _ := exec.Command(path, "-nP", "-a", "-p", strconv.Itoa(cmd.Process.Pid), "-i").CombinedOutput()
|
||||||
|
if strings.Contains(string(output), "TCP") || strings.Contains(string(output), "UDP") {
|
||||||
|
t.Fatal("starter opened network socket", string(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sig != nil {
|
||||||
|
if err = cmd.Process.Signal(sig); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case err = <-done:
|
||||||
|
data, _ := os.ReadFile(logfile.Name())
|
||||||
|
if mode == "" && err != nil {
|
||||||
|
t.Fatal("signal exit failed", err, string(data))
|
||||||
|
}
|
||||||
|
if mode != "" {
|
||||||
|
exit, ok := err.(*exec.ExitError)
|
||||||
|
if !ok || exit.ExitCode() != 1 {
|
||||||
|
t.Fatal("abnormal process exit status", err, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mode == "unresponsive" && !strings.Contains(string(data), "shared resources retained") {
|
||||||
|
t.Fatal("timeout did not retain resources", string(data))
|
||||||
|
}
|
||||||
|
t.Logf("process mode=%q signal=%v passed", mode, sig)
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Fatal("signal exit timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
359
cmd/go-omni/projectgen/generate.go
Normal file
359
cmd/go-omni/projectgen/generate.go
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
// Package projectgen generates transport-independent Go programs.
|
||||||
|
package projectgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go/format"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/internal/genfs"
|
||||||
|
"golang.org/x/mod/modfile"
|
||||||
|
"golang.org/x/mod/module"
|
||||||
|
"golang.org/x/mod/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
const currentVersion = "v0.30.0-preview"
|
||||||
|
const minimumVersion = "v0.28.0-preview"
|
||||||
|
const manifestPath = ".goOmni/projectManifest.json"
|
||||||
|
const lockPath = ".goOmni/project.lock"
|
||||||
|
const header = "// Code generated by go-omni project. DO NOT EDIT.\n"
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var templates embed.FS
|
||||||
|
|
||||||
|
var managed = map[string]bool{"main.go": true, "internal/config/loadGen.go": true, "internal/svc/runtimeGen.go": true}
|
||||||
|
var seeds = []string{"go.mod", ".gitignore", "README.md", "etc/config.yaml", "internal/config/config.go", "internal/config/loader.go", "internal/svc/serviceContext.go", "internal/run/run.go"}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Dir, Module, FrameworkDir, FrameworkVersion string
|
||||||
|
Check, DryRun bool
|
||||||
|
}
|
||||||
|
type FileChange struct{ Path, Action string }
|
||||||
|
type Output struct{ Changes []FileChange }
|
||||||
|
type manifest struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
TemplateVersion string `json:"templateVersion"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Generated map[string]string `json:"generated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New only accepts an empty directory. FrameworkDir is an explicit development opt-in.
|
||||||
|
func New(o Options) (Output, error) {
|
||||||
|
if o.Dir == "" || o.Check || o.DryRun {
|
||||||
|
return Output{}, errors.New("project new requires a directory and does not accept --check/--dry-run")
|
||||||
|
}
|
||||||
|
if entries, err := os.ReadDir(o.Dir); err == nil && len(entries) > 0 {
|
||||||
|
return Output{}, errors.New("project new requires an empty directory")
|
||||||
|
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return Output{}, err
|
||||||
|
}
|
||||||
|
if o.Module == "" {
|
||||||
|
o.Module = filepath.Base(genfs.Canonical(o.Dir))
|
||||||
|
}
|
||||||
|
if err := module.CheckImportPath(o.Module); err != nil {
|
||||||
|
return Output{}, err
|
||||||
|
}
|
||||||
|
if _, err := moduleFile(o, genfs.Canonical(o.Dir), true); err != nil {
|
||||||
|
return Output{}, err
|
||||||
|
}
|
||||||
|
return generate(o, true)
|
||||||
|
}
|
||||||
|
func Generate(o Options) (Output, error) { return generate(o, false) }
|
||||||
|
|
||||||
|
func generate(o Options, create bool) (out Output, err error) {
|
||||||
|
if o.Check && o.DryRun {
|
||||||
|
return out, errors.New("--check and --dry-run cannot be combined")
|
||||||
|
}
|
||||||
|
if o.Dir == "" {
|
||||||
|
o.Dir = "."
|
||||||
|
}
|
||||||
|
root, err := filepath.Abs(o.Dir)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
if info, e := os.Lstat(root); e == nil {
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return out, errors.New("project directory must be a real directory")
|
||||||
|
}
|
||||||
|
} else if !create || !errors.Is(e, os.ErrNotExist) {
|
||||||
|
return out, e
|
||||||
|
}
|
||||||
|
root = genfs.Canonical(root)
|
||||||
|
for _, name := range append(append([]string{}, seeds...), manifestPath, lockPath) {
|
||||||
|
if err = genfs.SafePath(root, name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name := range managed {
|
||||||
|
if err = genfs.SafePath(root, name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, other := range []string{".goOmni/apiManifest.json", "api/service.api"} {
|
||||||
|
if _, e := os.Lstat(filepath.Join(root, other)); !errors.Is(e, os.ErrNotExist) {
|
||||||
|
return out, errors.New("project generator requires its own project; existing API layout is not adopted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !o.Check && !o.DryRun {
|
||||||
|
if err = os.MkdirAll(filepath.Join(root, ".goOmni"), 0755); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
lock, e := os.OpenFile(filepath.Join(root, lockPath), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||||
|
if e != nil {
|
||||||
|
return out, fmt.Errorf("acquire project.lock (check for a running generator before removing a stale lock): %w", e)
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintf(lock, "pid=%d\n", os.Getpid())
|
||||||
|
err = errors.Join(err, lock.Close())
|
||||||
|
defer os.Remove(filepath.Join(root, lockPath))
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var old manifest
|
||||||
|
oldData, readErr := os.ReadFile(filepath.Join(root, manifestPath))
|
||||||
|
if create {
|
||||||
|
if !errors.Is(readErr, os.ErrNotExist) {
|
||||||
|
return out, errors.New("project manifest appeared during creation")
|
||||||
|
}
|
||||||
|
// Recheck after taking the lock; never adopt files created concurrently.
|
||||||
|
entries, e := os.ReadDir(root)
|
||||||
|
if e != nil {
|
||||||
|
return out, e
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name() != ".goOmni" {
|
||||||
|
return out, errors.New("project directory changed during creation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries, e = os.ReadDir(filepath.Join(root, ".goOmni"))
|
||||||
|
if e != nil {
|
||||||
|
return out, e
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name() != "project.lock" {
|
||||||
|
return out, errors.New("project metadata appeared during creation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if readErr != nil {
|
||||||
|
return out, fmt.Errorf("read project manifest (create with project new first): %w", readErr)
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(oldData))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
if err = decoder.Decode(&old); err != nil {
|
||||||
|
return out, fmt.Errorf("invalid project manifest: %w", err)
|
||||||
|
}
|
||||||
|
var extra any
|
||||||
|
if decoder.Decode(&extra) != io.EOF {
|
||||||
|
return out, errors.New("invalid trailing project manifest data")
|
||||||
|
}
|
||||||
|
if old.Version != 1 || old.Kind != "project" || old.Module == "" || len(old.Generated) != len(managed) || !semver.IsValid(old.TemplateVersion) || semver.Compare(old.TemplateVersion, minimumVersion) > 0 {
|
||||||
|
return out, errors.New("unsupported project manifest; use a compatible CLI")
|
||||||
|
}
|
||||||
|
if o.Module != "" && o.Module != old.Module {
|
||||||
|
return out, errors.New("module differs from project manifest")
|
||||||
|
}
|
||||||
|
o.Module = old.Module
|
||||||
|
for name := range managed {
|
||||||
|
if len(old.Generated[name]) != 64 {
|
||||||
|
return out, fmt.Errorf("invalid manifest ownership for %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o.Module == "" {
|
||||||
|
o.Module = filepath.Base(root)
|
||||||
|
}
|
||||||
|
if err = module.CheckImportPath(o.Module); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
moduleData, err := moduleFile(o, root, create)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
rendered, err := render(o.Module, moduleData)
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
next := manifest{Version: 1, Kind: "project", TemplateVersion: minimumVersion, Module: o.Module, Generated: map[string]string{}}
|
||||||
|
var changes []genfs.Change
|
||||||
|
names := make([]string, 0, len(rendered))
|
||||||
|
for name := range rendered {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, name := range names {
|
||||||
|
target := filepath.Join(root, filepath.FromSlash(name))
|
||||||
|
before, e := os.ReadFile(target)
|
||||||
|
exists := e == nil
|
||||||
|
if e != nil && !errors.Is(e, os.ErrNotExist) {
|
||||||
|
return out, e
|
||||||
|
}
|
||||||
|
generated := managed[name]
|
||||||
|
if !generated && !create {
|
||||||
|
if !exists {
|
||||||
|
return out, fmt.Errorf("project-owned file %s is missing; restore it explicitly", name)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if generated {
|
||||||
|
next.Generated[name] = digest(rendered[name])
|
||||||
|
if exists && (create || digest(before) != old.Generated[name]) {
|
||||||
|
return out, fmt.Errorf("generated file %s was edited; move custom code to project-owned files", name)
|
||||||
|
}
|
||||||
|
} else if exists {
|
||||||
|
return out, fmt.Errorf("refusing to adopt %s", name)
|
||||||
|
}
|
||||||
|
if exists && bytes.Equal(before, rendered[name]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mode := os.FileMode(0644)
|
||||||
|
if exists {
|
||||||
|
info, e := os.Stat(target)
|
||||||
|
if e != nil {
|
||||||
|
return out, e
|
||||||
|
}
|
||||||
|
mode = info.Mode().Perm()
|
||||||
|
}
|
||||||
|
changes = append(changes, genfs.Change{Name: name, Before: before, After: rendered[name], Existed: exists, Mode: mode})
|
||||||
|
}
|
||||||
|
data, err := json.MarshalIndent(next, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
data = append(data, '\n')
|
||||||
|
if !bytes.Equal(oldData, data) {
|
||||||
|
changes = append(changes, genfs.Change{Name: manifestPath, Before: oldData, After: data, Existed: oldData != nil, Mode: 0644})
|
||||||
|
}
|
||||||
|
for _, c := range changes {
|
||||||
|
action := "create"
|
||||||
|
if c.Existed {
|
||||||
|
action = "update"
|
||||||
|
}
|
||||||
|
out.Changes = append(out.Changes, FileChange{c.Name, action})
|
||||||
|
}
|
||||||
|
sort.Slice(out.Changes, func(i, j int) bool { return out.Changes[i].Path < out.Changes[j].Path })
|
||||||
|
if o.DryRun {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
if o.Check {
|
||||||
|
if len(changes) > 0 {
|
||||||
|
return out, fmt.Errorf("project generation drift: %v", out.Changes)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
return out, genfs.Apply(root, changes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleFile(o Options, root string, create bool) ([]byte, error) {
|
||||||
|
if !create {
|
||||||
|
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mf, err := modfile.Parse("go.mod", data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mf.Module == nil || mf.Module.Mod.Path != o.Module {
|
||||||
|
return nil, errors.New("go.mod module differs from project manifest")
|
||||||
|
}
|
||||||
|
if o.FrameworkDir != "" || o.FrameworkVersion != "" {
|
||||||
|
return nil, errors.New("framework options belong to project new; update go.mod explicitly")
|
||||||
|
}
|
||||||
|
for _, r := range mf.Require {
|
||||||
|
if r.Mod.Path == frameworkModule && semver.Compare(r.Mod.Version, minimumVersion) >= 0 {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("go.mod must require go-omni %s or newer", minimumVersion)
|
||||||
|
}
|
||||||
|
version := o.FrameworkVersion
|
||||||
|
if version == "" {
|
||||||
|
version = currentVersion
|
||||||
|
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Path == frameworkModule && info.Main.Sum != "" && semver.IsValid(info.Main.Version) {
|
||||||
|
version = info.Main.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !semver.IsValid(version) || semver.Major(version) != "v0" && semver.Major(version) != "v1" || semver.Compare(version, minimumVersion) < 0 {
|
||||||
|
return nil, fmt.Errorf("framework version must be %s or newer", minimumVersion)
|
||||||
|
}
|
||||||
|
mf := new(modfile.File)
|
||||||
|
if err := mf.AddModuleStmt(o.Module); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := mf.AddGoStmt("1.22.0"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := mf.AddRequire(frameworkModule, version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if o.FrameworkDir != "" {
|
||||||
|
local, err := filepath.Abs(o.FrameworkDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(filepath.Join(local, "go.mod"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
source, err := modfile.Parse("go.mod", data, nil)
|
||||||
|
if err != nil || source.Module == nil || source.Module.Mod.Path != frameworkModule {
|
||||||
|
return nil, errors.New("--framework-dir must contain go-omni")
|
||||||
|
}
|
||||||
|
local = genfs.Canonical(local)
|
||||||
|
rel, err := filepath.Rel(root, local)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rel = filepath.ToSlash(rel)
|
||||||
|
if !strings.HasPrefix(rel, ".") {
|
||||||
|
rel = "./" + rel
|
||||||
|
}
|
||||||
|
if err = mf.AddReplace(frameworkModule, "", rel, ""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mf.Format()
|
||||||
|
}
|
||||||
|
func render(moduleName string, moduleData []byte) (map[string][]byte, error) {
|
||||||
|
files := map[string][]byte{"go.mod": moduleData}
|
||||||
|
paths := append([]string{}, seeds[1:]...)
|
||||||
|
for name := range managed {
|
||||||
|
paths = append(paths, name)
|
||||||
|
}
|
||||||
|
for _, name := range paths {
|
||||||
|
raw, err := templates.ReadFile("templates/" + strings.ReplaceAll(name, "/", "_") + ".tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s := strings.ReplaceAll(string(raw), "\"PROJECT_MODULE\"", strconv.Quote(moduleName))
|
||||||
|
s = strings.ReplaceAll(s, "PROJECT_MODULE/", moduleName+"/")
|
||||||
|
if managed[name] {
|
||||||
|
s = header + s
|
||||||
|
}
|
||||||
|
data := []byte(s)
|
||||||
|
if strings.HasSuffix(name, ".go") {
|
||||||
|
data, err = format.Source(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("format %s: %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files[name] = data
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
func digest(data []byte) string { sum := sha256.Sum256(data); return hex.EncodeToString(sum[:]) }
|
||||||
215
cmd/go-omni/projectgen/generate_test.go
Normal file
215
cmd/go-omni/projectgen/generate_test.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package projectgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func put(t *testing.T, root, name, content string) {
|
||||||
|
t.Helper()
|
||||||
|
p := filepath.Join(root, name)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func get(t *testing.T, root, name string) string {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(filepath.Join(root, name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
func snapshot(t *testing.T, root string) map[string]string {
|
||||||
|
t.Helper()
|
||||||
|
files := map[string]string{}
|
||||||
|
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
data, e := os.ReadFile(path)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
rel, _ := filepath.Rel(root, path)
|
||||||
|
files[rel] = string(data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
func fresh(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
root := filepath.Join(t.TempDir(), "program")
|
||||||
|
if _, err := New(Options{Dir: root, Module: "example.com/program"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOwnershipAndDrift(t *testing.T) {
|
||||||
|
root := fresh(t)
|
||||||
|
files := snapshot(t, root)
|
||||||
|
for name, source := range files {
|
||||||
|
if strings.HasSuffix(name, ".api") || strings.Contains(name, "handler") || strings.Contains(name, "openapi") {
|
||||||
|
t.Fatal("unexpected API output", name)
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(name, ".go") {
|
||||||
|
for _, forbidden := range []string{"net/http", "httpx", "middleware", "ListenAndServe", "database/", "queue/", "task.New"} {
|
||||||
|
if strings.Contains(source, forbidden) {
|
||||||
|
t.Fatal(name, forbidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(files["go.mod"], "replace") || !strings.Contains(files["go.mod"], currentVersion) {
|
||||||
|
t.Fatal("default dependency must be remote and fixed")
|
||||||
|
}
|
||||||
|
for _, name := range seeds {
|
||||||
|
if name != "go.mod" {
|
||||||
|
put(t, root, name, get(t, root, name)+"\n// project edit\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
before := snapshot(t, root)
|
||||||
|
for _, o := range []Options{{Dir: root}, {Dir: root, Check: true}, {Dir: root, DryRun: true}} {
|
||||||
|
out, err := Generate(o)
|
||||||
|
if err != nil || len(out.Changes) != 0 {
|
||||||
|
t.Fatal(out, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("project files changed")
|
||||||
|
}
|
||||||
|
original := get(t, root, "main.go")
|
||||||
|
put(t, root, "main.go", original+"\n// manual edit\n")
|
||||||
|
for _, o := range []Options{{Dir: root}, {Dir: root, Check: true}, {Dir: root, DryRun: true}} {
|
||||||
|
if _, err := Generate(o); err == nil || !strings.Contains(err.Error(), "was edited") {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put(t, root, "main.go", original)
|
||||||
|
if err := os.Remove(filepath.Join(root, "main.go")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before = snapshot(t, root)
|
||||||
|
if _, err := Generate(Options{Dir: root, Check: true}); err == nil {
|
||||||
|
t.Fatal("missing output accepted")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("check wrote files")
|
||||||
|
}
|
||||||
|
out, err := Generate(Options{Dir: root, DryRun: true})
|
||||||
|
if err != nil || len(out.Changes) != 1 {
|
||||||
|
t.Fatal(out, err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("dry-run wrote files")
|
||||||
|
}
|
||||||
|
if _, err = Generate(Options{Dir: root}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if get(t, root, "main.go") != original {
|
||||||
|
t.Fatal("missing output not restored")
|
||||||
|
}
|
||||||
|
if err = os.Remove(filepath.Join(root, "internal/run/run.go")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = Generate(Options{Dir: root}); err == nil || !strings.Contains(err.Error(), "project-owned") {
|
||||||
|
t.Fatal("silently recreated handwritten file", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordedTemplateUpgrade(t *testing.T) {
|
||||||
|
root := fresh(t)
|
||||||
|
var m manifest
|
||||||
|
if err := json.Unmarshal([]byte(get(t, root, manifestPath)), &m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
original := get(t, root, "main.go")
|
||||||
|
older := original + "\n// previous generator baseline\n"
|
||||||
|
put(t, root, "main.go", older)
|
||||||
|
m.Generated["main.go"] = digest([]byte(older))
|
||||||
|
data, _ := json.Marshal(m)
|
||||||
|
put(t, root, manifestPath, string(data))
|
||||||
|
before := snapshot(t, root)
|
||||||
|
if _, err := Generate(Options{Dir: root, Check: true}); err == nil {
|
||||||
|
t.Fatal("stale baseline accepted")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("check mutated baseline")
|
||||||
|
}
|
||||||
|
if _, err := Generate(Options{Dir: root}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if get(t, root, "main.go") != original {
|
||||||
|
t.Fatal("template was not updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestProjectGuards(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, needle string
|
||||||
|
mutate func(*testing.T, string)
|
||||||
|
}{
|
||||||
|
{"module", "module differs", func(t *testing.T, r string) {
|
||||||
|
put(t, r, "go.mod", strings.Replace(get(t, r, "go.mod"), "example.com/program", "example.com/other", 1))
|
||||||
|
}},
|
||||||
|
{"oldFramework", "must require", func(t *testing.T, r string) {
|
||||||
|
put(t, r, "go.mod", strings.ReplaceAll(get(t, r, "go.mod"), currentVersion, "v0.22.0-preview"))
|
||||||
|
}},
|
||||||
|
{"manifest", "unsupported", func(t *testing.T, r string) { put(t, r, manifestPath, `{"version":1,"kind":"api"}`) }},
|
||||||
|
{"lock", "project.lock", func(t *testing.T, r string) { put(t, r, lockPath, "occupied") }},
|
||||||
|
{"otherGenerator", "existing API", func(t *testing.T, r string) { put(t, r, ".goOmni/apiManifest.json", "{}") }},
|
||||||
|
{"symlink", "symlink", func(t *testing.T, r string) {
|
||||||
|
if e := os.Remove(filepath.Join(r, "main.go")); e != nil {
|
||||||
|
t.Fatal(e)
|
||||||
|
}
|
||||||
|
target := filepath.Join(t.TempDir(), "outside.go")
|
||||||
|
put(t, filepath.Dir(target), "outside.go", "outside")
|
||||||
|
if e := os.Symlink(target, filepath.Join(r, "main.go")); e != nil {
|
||||||
|
t.Fatal(e)
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
root := fresh(t)
|
||||||
|
tc.mutate(t, root)
|
||||||
|
before := snapshot(t, root)
|
||||||
|
if _, err := Generate(Options{Dir: root}); err == nil || !strings.Contains(err.Error(), tc.needle) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("failed generation changed files")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
root := fresh(t)
|
||||||
|
before := snapshot(t, root)
|
||||||
|
if _, err := New(Options{Dir: root}); err == nil {
|
||||||
|
t.Fatal("new overwrote project")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, snapshot(t, root)) {
|
||||||
|
t.Fatal("new mutated project")
|
||||||
|
}
|
||||||
|
if _, err := Generate(Options{Dir: t.TempDir(), Check: true}); err == nil {
|
||||||
|
t.Fatal("adopted unmanaged directory")
|
||||||
|
}
|
||||||
|
for _, version := range []string{"v0.27.0-preview", "latest", "v2.0.0", "v0.28.0-preview\nreplace bad"} {
|
||||||
|
if _, err := New(Options{Dir: filepath.Join(t.TempDir(), "app"), FrameworkVersion: version}); err == nil {
|
||||||
|
t.Fatal("invalid version", version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := New(Options{Dir: filepath.Join(t.TempDir(), "app"), Module: "example.com/a\nreplace bad"}); err == nil {
|
||||||
|
t.Fatal("invalid module")
|
||||||
|
}
|
||||||
|
}
|
||||||
6
cmd/go-omni/projectgen/templates/.gitignore.tmpl
Normal file
6
cmd/go-omni/projectgen/templates/.gitignore.tmpl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/program
|
||||||
|
/var/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
/.goOmni/project.lock
|
||||||
55
cmd/go-omni/projectgen/templates/README.md.tmpl
Normal file
55
cmd/go-omni/projectgen/templates/README.md.tmpl
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Go program
|
||||||
|
|
||||||
|
This project has no HTTP listener, routes, tasks or external dependencies configured.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go mod tidy
|
||||||
|
go run . -check-config
|
||||||
|
go run .
|
||||||
|
# SIGINT/SIGTERM stops the program.
|
||||||
|
go build -o program .
|
||||||
|
go-omni project generate --dry-run
|
||||||
|
go-omni project generate
|
||||||
|
go-omni project check
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the same fixed CLI and framework version. Local --framework-dir is only for
|
||||||
|
explicit source development; normal creation pins the installed CLI version.
|
||||||
|
|
||||||
|
Edit internal/config/config.go (Config/Default/Validate), etc/config.yaml and
|
||||||
|
internal/config/loader.go for optional decoders. JSON/YAML/XML resolve only
|
||||||
|
explicit ${NAME} / ${NAME:-fallback}; fixed values are never implicitly overridden.
|
||||||
|
-check-config validates without opening the logger or dependencies.
|
||||||
|
|
||||||
|
Add dependencies in internal/svc/serviceContext.go, registering OnClose immediately.
|
||||||
|
Register all OnStop/OnDrain hooks in Init, before starting work. Paired hooks use the
|
||||||
|
same name. OnStop stops acquisition; OnDrain returns nil only after all processing,
|
||||||
|
acknowledgements and defers finish. Runtime executes stop, then drain, then resource
|
||||||
|
cleanup in reverse registration order. Callbacks within a phase run concurrently;
|
||||||
|
combine components with ordering requirements in a single hook. Accepted work uses
|
||||||
|
WorkContext; Context is the early stop notification. No automatic retry or restart
|
||||||
|
policy is installed. Adapt external components according to their actual semantics.
|
||||||
|
|
||||||
|
Write program behavior in internal/run/run.go. Return nil when a one-shot run finishes;
|
||||||
|
return an error for fatal failure. The default Run waits for shutdown. Components may
|
||||||
|
call RequestShutdown(err) to report fatal failure. Signal or Run completion starts
|
||||||
|
shutdown; the entry waits for Run itself and registered background work. Do not make
|
||||||
|
Run wait for resource cleanup: cleanup follows Run completion. Do not register hooks
|
||||||
|
concurrently from Run. The drain name go-omni/project/run is reserved.
|
||||||
|
|
||||||
|
Run/Init panics are converted to startup/run failures and registered resources are
|
||||||
|
cleaned safely. Panics in arbitrary project goroutines must be handled by the project.
|
||||||
|
Unresponsive work is bounded by lifecycle timeouts: unfinished work causes
|
||||||
|
ErrResourcesRetained, skips shared resource/logger cleanup and exits unsuccessfully.
|
||||||
|
Go cannot forcibly stop a goroutine. SIGKILL/process crashes cannot run cleanup.
|
||||||
|
|
||||||
|
main.go, internal/config/loadGen.go and internal/svc/runtimeGen.go are generator-owned.
|
||||||
|
Manual edits cause a conflict. Other starter files are project-owned and preserved;
|
||||||
|
missing project-owned files require explicit restoration. Check is read-only and
|
||||||
|
reports missing or stale generated files. It does not compile or execute project code.
|
||||||
|
Commit .goOmni/projectManifest.json with the project. Generation uses a project lock,
|
||||||
|
preflight checks and per-file atomic replacement with rollback for ordinary write errors;
|
||||||
|
it is not crash-atomic across files. Avoid editing files during generation.
|
||||||
|
|
||||||
|
Full guide: https://git.yuntuo.hk/k2safe/go-omni/src/branch/main/PROGRAM.md
|
||||||
9
cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl
Normal file
9
cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Project-owned. Environment references are explicit: ${NAME} or ${NAME:-fallback}.
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
format: json
|
||||||
|
output: stderr
|
||||||
|
lifecycle:
|
||||||
|
stopTimeout: 5s
|
||||||
|
drainTimeout: 10s
|
||||||
|
cancelTimeout: 5s
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config, Default and Validate are project-owned. Add project settings here.
|
||||||
|
type Config struct {
|
||||||
|
Log logx.Config `json:"log"`
|
||||||
|
Lifecycle lifecycle.ShutdownConfig `json:"lifecycle"`
|
||||||
|
}
|
||||||
|
func Default() Config {
|
||||||
|
log:=logx.Default();log.Service="PROJECT_MODULE"
|
||||||
|
return Config{Log:log,Lifecycle:lifecycle.DefaultShutdownConfig()}
|
||||||
|
}
|
||||||
|
func(c Config)Validate()error {
|
||||||
|
if err:=c.Log.Validate();err!=nil{return omniConfig.AtField("log",err)}
|
||||||
|
if err:=c.Lifecycle.Validate();err!=nil{return omniConfig.AtField("lifecycle",err)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||||
|
func Load(path string)(Config,error){
|
||||||
|
loader,err:=NewLoader();if err!=nil{return Config{},err}
|
||||||
|
return omniConfig.LoadProject(path,Default(),loader)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package config
|
||||||
|
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||||
|
// NewLoader is project-owned. Register additional decoders here if needed.
|
||||||
|
func NewLoader()(*omniConfig.Loader,error){return omniConfig.NewLoader()}
|
||||||
13
cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl
Normal file
13
cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package run
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"PROJECT_MODULE/internal/svc"
|
||||||
|
)
|
||||||
|
// Run is project-owned. Return nil for normal completion, or an error to fail.
|
||||||
|
// The starter waits for shutdown; a one-shot program can simply return when done.
|
||||||
|
// Register background stop/drain hooks in svc.Init, not concurrently here.
|
||||||
|
// Accepted background work uses deps.WorkContext(), not the early-stop ctx.
|
||||||
|
func Run(ctx context.Context,deps *svc.ServiceContext)error {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package svc
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
"PROJECT_MODULE/internal/config"
|
||||||
|
)
|
||||||
|
func New(ctx context.Context,logger *logx.Logger,cfg config.Config)(result *ServiceContext,err error){
|
||||||
|
if ctx==nil{ctx=context.Background()}
|
||||||
|
if err=ctx.Err();err!=nil{return nil,errors.Join(err,lifecycle.Cause(ctx))}
|
||||||
|
if logger==nil{return nil,errors.New("service context requires a logger")}
|
||||||
|
if err=cfg.Validate();err!=nil{return nil,err}
|
||||||
|
runtime,err:=lifecycle.NewRuntimeWithConfig(logx.WithLogger(ctx,logger),cfg.Lifecycle);if err!=nil{return nil,err}
|
||||||
|
c:=&ServiceContext{Runtime:runtime,Config:cfg,Logger:logger}
|
||||||
|
defer func(){
|
||||||
|
if p:=recover();p!=nil{err=fmt.Errorf("dependency initialization panic: %v",p)}
|
||||||
|
if err!=nil{result=nil;err=c.Finish(err)}
|
||||||
|
}()
|
||||||
|
if err=c.Init(c.Context());err!=nil{return nil,err}
|
||||||
|
return c,nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package svc
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
"PROJECT_MODULE/internal/config"
|
||||||
|
)
|
||||||
|
// ServiceContext is project-owned. Add dependencies without global variables.
|
||||||
|
// Runtime owns registered resources; Logger is borrowed from the entry point.
|
||||||
|
type ServiceContext struct {
|
||||||
|
*lifecycle.Runtime
|
||||||
|
Config config.Config
|
||||||
|
Logger *logx.Logger
|
||||||
|
}
|
||||||
|
// Init runs before Run. Register resources immediately with OnClose.
|
||||||
|
// Register paired OnStop/OnDrain hooks before exposing background work.
|
||||||
|
// Use WorkContext for accepted work; Context is the early stop notification.
|
||||||
|
// OnDrain returns nil only after processing and final cleanup have completed.
|
||||||
|
// Complete all registrations here, before Run can race with shutdown.
|
||||||
|
// The drain name "go-omni/project/run" is reserved for the generated entry.
|
||||||
|
func(c *ServiceContext)Init(ctx context.Context)error{return nil}
|
||||||
60
cmd/go-omni/projectgen/templates/main.go.tmpl
Normal file
60
cmd/go-omni/projectgen/templates/main.go.tmpl
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
"PROJECT_MODULE/internal/config"
|
||||||
|
program "PROJECT_MODULE/internal/run"
|
||||||
|
"PROJECT_MODULE/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
path:=flag.String("config","etc/config.yaml","configuration file; empty uses Default()")
|
||||||
|
check:=flag.Bool("check-config",false,"validate configuration without initializing dependencies")
|
||||||
|
flag.Parse()
|
||||||
|
if *check {if _,err:=config.Load(*path);err!=nil{log.Fatal(err)};return}
|
||||||
|
ctx,stop:=signal.NotifyContext(context.Background(),os.Interrupt,syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
if err:=run(ctx,*path);err!=nil{log.Fatal(err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context,path string)(runErr error) {
|
||||||
|
cfg,err:=config.Load(path);if err!=nil{return err}
|
||||||
|
logger,err:=logx.New(cfg.Log);if err!=nil{return err}
|
||||||
|
var deps *svc.ServiceContext
|
||||||
|
defer func(){
|
||||||
|
if p:=recover();p!=nil{runErr=fmt.Errorf("program initialization panic: %v",p)}
|
||||||
|
if deps!=nil{runErr=deps.Finish(runErr)}
|
||||||
|
// Unjoined work may still use its dependencies and logger. The process exits
|
||||||
|
// unsuccessfully; resource retention never means the work completed.
|
||||||
|
if !errors.Is(runErr,lifecycle.ErrResourcesRetained){runErr=errors.Join(runErr,logger.Close())}
|
||||||
|
}()
|
||||||
|
deps,err=svc.New(ctx,logger,cfg);if err!=nil{return err}
|
||||||
|
if deps.Context().Err()!=nil{return nil}
|
||||||
|
done:=make(chan struct{})
|
||||||
|
// This reserved drain proves Run has actually returned, including its defers.
|
||||||
|
// Runtime bounds the wait even when Run ignores cancellation.
|
||||||
|
deps.OnDrain("go-omni/project/run",func(context.Context)error{<-done;return nil})
|
||||||
|
logger.InfoContext(deps.Context(),"program.starting")
|
||||||
|
go func(){
|
||||||
|
var result error
|
||||||
|
returned:=false
|
||||||
|
defer func(){
|
||||||
|
if p:=recover();p!=nil{result=fmt.Errorf("program run panic: %v",p)} else if !returned{result=errors.New("program run exited without returning")}
|
||||||
|
deps.RequestShutdown(result)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
result=program.Run(deps.Context(),deps)
|
||||||
|
returned=true
|
||||||
|
}()
|
||||||
|
<-deps.Context().Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
283
cmd/go-omni/projectgen/testdata/consumer_test.go
vendored
Normal file
283
cmd/go-omni/projectgen/testdata/consumer_test.go
vendored
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"example.com/plain/internal/config"
|
||||||
|
program "example.com/plain/internal/run"
|
||||||
|
"example.com/plain/internal/svc"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
svc.Initialize = func(context.Context, *svc.ServiceContext) error { return nil }
|
||||||
|
program.Execute = func(context.Context, *svc.ServiceContext) error { return nil }
|
||||||
|
t.Cleanup(func() {
|
||||||
|
svc.Initialize = func(context.Context, *svc.ServiceContext) error { return nil }
|
||||||
|
program.Execute = func(context.Context, *svc.ServiceContext) error { return nil }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func shortConfig(t *testing.T, logfile string) string {
|
||||||
|
t.Helper()
|
||||||
|
p := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
text := "log:\n output: discard\nlifecycle:\n stopTimeout: 100ms\n drainTimeout: 30ms\n cancelTimeout: 100ms\n"
|
||||||
|
if logfile != "" {
|
||||||
|
text = "log:\n file: " + logfile + "\nlifecycle:\n stopTimeout: 100ms\n drainTimeout: 30ms\n cancelTimeout: 100ms\n"
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(p, []byte(text), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFiniteRunAndCleanupOrder(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
var events []string
|
||||||
|
svc.Initialize = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
events = append(events, "init")
|
||||||
|
c.OnClose(func() error { events = append(events, "first"); return nil })
|
||||||
|
c.OnClose(func() error { events = append(events, "second"); return nil })
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
program.Execute = func(context.Context, *svc.ServiceContext) error {
|
||||||
|
defer func() { events = append(events, "run defer") }()
|
||||||
|
events = append(events, "run")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := run(context.Background(), ""); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(events, []string{"init", "run", "run defer", "second", "first"}) {
|
||||||
|
t.Fatal(events)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestInitializationAndRunFailures(t *testing.T) {
|
||||||
|
for _, stage := range []string{"init error", "init panic", "run error", "run panic", "run goexit", "component failure"} {
|
||||||
|
t.Run(stage, func(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
boom := errors.New("failure marker")
|
||||||
|
var closed atomic.Int32
|
||||||
|
var started atomic.Bool
|
||||||
|
svc.Initialize = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
c.OnClose(func() error { closed.Add(1); return nil })
|
||||||
|
if stage == "init error" {
|
||||||
|
return boom
|
||||||
|
}
|
||||||
|
if stage == "init panic" {
|
||||||
|
panic(boom)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
program.Execute = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
started.Store(true)
|
||||||
|
switch stage {
|
||||||
|
case "run error":
|
||||||
|
return boom
|
||||||
|
case "run panic":
|
||||||
|
panic(boom)
|
||||||
|
case "run goexit":
|
||||||
|
runtime.Goexit()
|
||||||
|
case "component failure":
|
||||||
|
c.RequestShutdown(boom)
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := run(context.Background(), "")
|
||||||
|
if err == nil || closed.Load() != 1 {
|
||||||
|
t.Fatal(stage, err, closed.Load())
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(stage, "init") && started.Load() {
|
||||||
|
t.Fatal("run started after failed initialization")
|
||||||
|
}
|
||||||
|
if strings.Contains(stage, "error") || stage == "component failure" {
|
||||||
|
if !errors.Is(err, boom) {
|
||||||
|
t.Fatal("failure cause lost", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestGracefulStopDrainsAcknowledgementBeforeClose(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
ready := make(chan struct{})
|
||||||
|
stop := make(chan struct{})
|
||||||
|
done := make(chan struct{})
|
||||||
|
var closed, acked atomic.Bool
|
||||||
|
var canceledEarly atomic.Bool
|
||||||
|
svc.Initialize = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
c.OnClose(func() error {
|
||||||
|
if !acked.Load() {
|
||||||
|
return errors.New("closed before acknowledgement")
|
||||||
|
}
|
||||||
|
closed.Store(true)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
c.OnStop("component", func(context.Context) error { close(stop); return nil })
|
||||||
|
c.OnDrain("component", func(context.Context) error { <-done; return nil })
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
<-stop
|
||||||
|
if c.WorkContext().Err() != nil {
|
||||||
|
canceledEarly.Store(true)
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
acked.Store(true)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
program.Execute = func(ctx context.Context, c *svc.ServiceContext) error { close(ready); <-ctx.Done(); return nil }
|
||||||
|
result := make(chan error, 1)
|
||||||
|
go func() { result <- run(ctx, "") }()
|
||||||
|
<-ready
|
||||||
|
cancel()
|
||||||
|
if err := <-result; err != nil || !closed.Load() || canceledEarly.Load() {
|
||||||
|
t.Fatal(err, closed.Load(), canceledEarly.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestInitializationFailureDrainsStartedWork(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
done := make(chan struct{})
|
||||||
|
stop := make(chan struct{})
|
||||||
|
var closed atomic.Bool
|
||||||
|
svc.Initialize = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
c.OnClose(func() error {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
closed.Store(true)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("resource closed while starting work still active")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
c.OnStop("partial", func(context.Context) error { close(stop); return nil })
|
||||||
|
c.OnDrain("partial", func(context.Context) error { <-done; return nil })
|
||||||
|
go func() { defer close(done); <-stop; time.Sleep(10 * time.Millisecond) }()
|
||||||
|
return errors.New("later dependency failed")
|
||||||
|
}
|
||||||
|
if err := run(context.Background(), ""); err == nil || !closed.Load() {
|
||||||
|
t.Fatal(err, closed.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestCancellationGraceJoinsWork(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
done := make(chan struct{})
|
||||||
|
var closed atomic.Bool
|
||||||
|
svc.Initialize = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
c.OnClose(func() error { closed.Store(true); return nil })
|
||||||
|
c.OnStop("cooperative", func(context.Context) error { return nil })
|
||||||
|
c.OnDrain("cooperative", func(context.Context) error { <-done; return nil })
|
||||||
|
go func() { <-c.WorkContext().Done(); close(done) }()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := run(context.Background(), shortConfig(t, ""))
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) || errors.Is(err, lifecycle.ErrResourcesRetained) || !closed.Load() {
|
||||||
|
t.Fatal(err, closed.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestUnjoinedWorkRetainsDependenciesAndLogger(t *testing.T) {
|
||||||
|
for _, which := range []string{"run", "background", "init failure"} {
|
||||||
|
t.Run(which, func(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
release := make(chan struct{})
|
||||||
|
joined := make(chan struct{})
|
||||||
|
ready := make(chan struct{})
|
||||||
|
var closed atomic.Bool
|
||||||
|
var held *svc.ServiceContext
|
||||||
|
logfile := filepath.Join(t.TempDir(), "retained.log")
|
||||||
|
svc.Initialize = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
held = c
|
||||||
|
c.OnClose(func() error { closed.Store(true); return nil })
|
||||||
|
if which != "run" {
|
||||||
|
c.OnStop("stuck", func(context.Context) error { return nil })
|
||||||
|
c.OnDrain("stuck", func(context.Context) error { <-joined; return nil })
|
||||||
|
go func() { defer close(joined); <-release; c.Logger.Info("after shutdown deadline") }()
|
||||||
|
}
|
||||||
|
if which == "init failure" {
|
||||||
|
return errors.New("initialization failed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
program.Execute = func(ctx context.Context, c *svc.ServiceContext) error {
|
||||||
|
close(ready)
|
||||||
|
if which == "run" {
|
||||||
|
defer close(joined)
|
||||||
|
<-release
|
||||||
|
c.Logger.Info("after shutdown deadline")
|
||||||
|
} else {
|
||||||
|
<-ctx.Done()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make(chan error, 1)
|
||||||
|
go func() { result <- run(ctx, shortConfig(t, logfile)) }()
|
||||||
|
if which != "init failure" {
|
||||||
|
<-ready
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case err = <-result:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("shutdown hung")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, lifecycle.ErrResourcesRetained) || closed.Load() {
|
||||||
|
t.Fatal(err, closed.Load())
|
||||||
|
}
|
||||||
|
close(release)
|
||||||
|
<-joined
|
||||||
|
// In the run case the entry's completion notification follows the user defer.
|
||||||
|
// Close is idempotent and must retain resources even after late completion.
|
||||||
|
if err = held.Close(); !errors.Is(err, lifecycle.ErrResourcesRetained) || closed.Load() {
|
||||||
|
t.Fatal("retention was lost", err)
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(logfile)
|
||||||
|
if !strings.Contains(string(data), "after shutdown deadline") {
|
||||||
|
t.Fatal("logger closed while work still running", string(data))
|
||||||
|
}
|
||||||
|
_ = held.Logger.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestExplicitConfigurationAndPreflight(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
t.Setenv("PLAIN_LEVEL", "debug")
|
||||||
|
t.Setenv("LOG_LEVEL", "error")
|
||||||
|
p := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
if err := os.WriteFile(p, []byte("log:\n level: ${PLAIN_LEVEL}\n"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err := config.Load(p)
|
||||||
|
if err != nil || cfg.Log.Level != "debug" {
|
||||||
|
t.Fatal(cfg.Log.Level, err)
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(p, []byte("log:\n level: info\n"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err = config.Load(p)
|
||||||
|
if err != nil || cfg.Log.Level != "info" {
|
||||||
|
t.Fatal("implicit environment override", err)
|
||||||
|
}
|
||||||
|
var initCalled atomic.Bool
|
||||||
|
svc.Initialize = func(context.Context, *svc.ServiceContext) error { initCalled.Store(true); return nil }
|
||||||
|
if err = os.WriteFile(p, []byte("lifecycle:\n drainTimeout: -1s\n"), 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = run(context.Background(), p); err == nil || initCalled.Load() {
|
||||||
|
t.Fatal("invalid configuration initialized dependencies", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
|||||||
// Source builds can carry an unpublished VCS version (including +dirty).
|
// Source builds can carry an unpublished VCS version (including +dirty).
|
||||||
// Keep this fallback in sync with the next release; installed CLIs use their
|
// Keep this fallback in sync with the next release; installed CLIs use their
|
||||||
// downloaded module version, which also has a module checksum.
|
// downloaded module version, which also has a module checksum.
|
||||||
const defaultFrameworkVersion = "v0.27.0-preview"
|
const defaultFrameworkVersion = "v0.30.0-preview"
|
||||||
|
|
||||||
// This target is not a claim that the release already exists. Source users
|
// This target is not a claim that the release already exists. Source users
|
||||||
// select a local framework checkout until the module templates are published.
|
// select a local framework checkout until the module templates are published.
|
||||||
|
|||||||
162
cmd/go-omni/workspace_test.go
Normal file
162
cmd/go-omni/workspace_test.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The same consumer is exercised with a source-built CLI before release and a
|
||||||
|
// downloaded CLI after release. Project modules are always independently built.
|
||||||
|
func TestMixedWorkspaceConsumer(t *testing.T) {
|
||||||
|
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") != "1" {
|
||||||
|
t.Skip("set GO_OMNI_SCAFFOLD_INTEGRATION=1")
|
||||||
|
}
|
||||||
|
root := t.TempDir()
|
||||||
|
cli := os.Getenv("GO_OMNI_RELEASE_CLI")
|
||||||
|
local := cli == ""
|
||||||
|
framework, err := filepath.Abs("../..")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if local {
|
||||||
|
cli = filepath.Join(t.TempDir(), "go-omni")
|
||||||
|
workspaceCommand(t, "", nil, false, "go", "build", "-o", cli, ".")
|
||||||
|
}
|
||||||
|
modulePath := "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
for _, kind := range []string{"api", "project"} {
|
||||||
|
dir := filepath.Join(root, kind)
|
||||||
|
args := []string{kind, "new", dir, "--module", "example.com/" + kind}
|
||||||
|
if local {
|
||||||
|
args = append(args, "--framework-dir", framework)
|
||||||
|
}
|
||||||
|
workspaceCommand(t, root, nil, false, cli, args...)
|
||||||
|
workspaceCommand(t, dir, nil, false, "go", "mod", "tidy")
|
||||||
|
data := workspaceCommand(t, dir, nil, false, "go", "list", "-m", "-json", modulePath)
|
||||||
|
if !local {
|
||||||
|
var dep struct {
|
||||||
|
Version, Sum string
|
||||||
|
Replace any
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(data), &dep); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if dep.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || dep.Replace != nil || dep.Sum == "" {
|
||||||
|
t.Fatal("consumer is not a fixed remote dependency", dep)
|
||||||
|
}
|
||||||
|
t.Logf("%s remote framework %s checksum %s; no local replace", kind, dep.Version, dep.Sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write := func(name, data string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(filepath.Join(root, name), []byte(data), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write("omni.workspace.json", `{"services":[{"dir":"api","kind":"api"},{"dir":"project","kind":"project"}]}`)
|
||||||
|
for _, op := range []string{"format", "generate", "check"} {
|
||||||
|
output := workspaceCommand(t, root, nil, false, cli, "workspace", op)
|
||||||
|
if op == "format" && !strings.Contains(output, "SKIP ") {
|
||||||
|
t.Fatal("missing project format skip", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Run the actual Go multi-module toolchain independently of generation.
|
||||||
|
workspaceCommand(t, root, nil, false, "go", "work", "init", "./api", "./project")
|
||||||
|
work, err := filepath.EvalSymlinks(filepath.Join(root, "go.work"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
workspaceCommand(t, root, []string{"GOWORK=" + work}, false, "go", "test", "-race", "./api/...", "./project/...")
|
||||||
|
workspaceCommand(t, root, []string{"GOWORK=" + work}, false, "go", "build", "./api/...", "./project/...")
|
||||||
|
before := workspaceSnapshot(t, root)
|
||||||
|
workspaceCommand(t, root, nil, false, cli, "workspace", "check")
|
||||||
|
if !reflect.DeepEqual(before, workspaceSnapshot(t, root)) {
|
||||||
|
t.Fatal("check modified Go workspace/module files")
|
||||||
|
}
|
||||||
|
// Omitted kinds discover both released manifests as well.
|
||||||
|
write("omni.workspace.json", `{"services":[{"dir":"api"},{"dir":"project"}]}`)
|
||||||
|
workspaceCommand(t, root, nil, false, cli, "workspace", "check")
|
||||||
|
originals := map[string]string{}
|
||||||
|
for _, kind := range []string{"api", "project"} {
|
||||||
|
name := kind + "/main.go"
|
||||||
|
data, err := os.ReadFile(filepath.Join(root, name))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
originals[name] = string(data)
|
||||||
|
write(name, string(data)+"\n// intentional drift\n")
|
||||||
|
}
|
||||||
|
before = workspaceSnapshot(t, root)
|
||||||
|
output := workspaceCommand(t, root, nil, true, cli, "workspace", "check")
|
||||||
|
if strings.Count(output, "FAIL ") != 2 {
|
||||||
|
t.Fatal("did not aggregate both projects", output)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(before, workspaceSnapshot(t, root)) {
|
||||||
|
t.Fatal("failed check wrote files")
|
||||||
|
}
|
||||||
|
// The healthy project's missing output must stay missing when another fails.
|
||||||
|
write("api/main.go", originals["api/main.go"])
|
||||||
|
if err := os.Remove(filepath.Join(root, "api/main.go")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before = workspaceSnapshot(t, root)
|
||||||
|
workspaceCommand(t, root, nil, true, cli, "workspace", "generate")
|
||||||
|
if !reflect.DeepEqual(before, workspaceSnapshot(t, root)) {
|
||||||
|
t.Fatal("partial workspace generation")
|
||||||
|
}
|
||||||
|
write("project/main.go", originals["project/main.go"])
|
||||||
|
workspaceCommand(t, root, nil, false, cli, "workspace", "generate")
|
||||||
|
workspaceCommand(t, root, nil, false, cli, "workspace", "check")
|
||||||
|
}
|
||||||
|
func workspaceCommand(t *testing.T, dir string, extra []string, wantFailure bool, name string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, name, args...)
|
||||||
|
if dir != "" {
|
||||||
|
cmd.Dir = dir
|
||||||
|
}
|
||||||
|
cmd.Env = append(append(os.Environ(), "GOWORK=off"), extra...)
|
||||||
|
data, err := cmd.CombinedOutput()
|
||||||
|
if wantFailure {
|
||||||
|
exit, ok := err.(*exec.ExitError)
|
||||||
|
if !ok || exit.ExitCode() != 1 {
|
||||||
|
t.Fatalf("expected exit 1 from %s %v: %v\n%s", name, args, err, data)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatalf("%s %v: %v\n%s", name, args, err, data)
|
||||||
|
}
|
||||||
|
t.Logf("%s %v\n%s", name, args, data)
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
func workspaceSnapshot(t *testing.T, root string) map[string]string {
|
||||||
|
t.Helper()
|
||||||
|
files := map[string]string{}
|
||||||
|
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
data, e := os.ReadFile(path)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
rel, e := filepath.Rel(root, path)
|
||||||
|
if e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
files[rel] = string(data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
@@ -28,3 +28,5 @@ overrides it. Application transport read timeouts still control slow clients.
|
|||||||
See [API.md](../API.md) for source tags, validation rules, null/presence behavior,
|
See [API.md](../API.md) for source tags, validation rules, null/presence behavior,
|
||||||
strict decoding and supported type limits. This API leaves the existing
|
strict decoding and supported type limits. This API leaves the existing
|
||||||
`Context.BindJSON/BindQuery/BindURI` behavior unchanged.
|
`Context.BindJSON/BindQuery/BindURI` behavior unchanged.
|
||||||
|
|
||||||
|
Recursive response DTOs in `.api` are compiled by the CLI as named OpenAPI references; see [the response guide](../DEVELOPMENT.md#递归响应-dto). This does not change the nonrecursive request type requirement of `New`, `ForType`, or `Describe`.
|
||||||
|
|||||||
39
examples/mixedWorkspace/README.md
Normal file
39
examples/mixedWorkspace/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# API 与通用工程混合工作区
|
||||||
|
|
||||||
|
本例只定义生成工程的类型与目录,不包含业务模型、外部服务或特定调度方式。使用 v0.30.0-preview 或更新版本 CLI。
|
||||||
|
|
||||||
|
下面从一个空目录创建两个独立 Go module;API 工程来自 API 模板,通用工程默认不监听端口。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.30.0-preview
|
||||||
|
mkdir mixed-demo
|
||||||
|
cd mixed-demo
|
||||||
|
go-omni api new apps/interface --module example.com/interface
|
||||||
|
go-omni project new apps/program --module example.com/program
|
||||||
|
```
|
||||||
|
|
||||||
|
将本目录的 `omni.workspace.json` 复制到 `mixed-demo` 根目录,然后执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go-omni workspace format # 格式化 API 契约;通用工程显示 SKIP
|
||||||
|
go-omni workspace generate
|
||||||
|
go-omni workspace check
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以省略两个 `kind`:CLI 根据各工程的生成清单识别。显式声明与清单冲突会报错;不会根据目录名称猜测类型。
|
||||||
|
|
||||||
|
Go 多模块编译由 Go 工具链独立管理:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
(cd apps/interface && go mod tidy)
|
||||||
|
(cd apps/program && go mod tidy)
|
||||||
|
go work init ./apps/interface ./apps/program
|
||||||
|
go test ./apps/interface/... ./apps/program/...
|
||||||
|
go build ./apps/interface/... ./apps/program/...
|
||||||
|
```
|
||||||
|
|
||||||
|
workspace check 不修改 `go.work`、`go.mod`、`go.sum` 或项目源文件;不启动应用。若显式配置了共享错误目录提供方,仍会按既有约定构建并调用该 Go 导出函数,见 [目录导出规则](../../DEVELOPMENT.md#同一份-go-错误目录供多个服务生成)。
|
||||||
|
|
||||||
|
手工修改任一生成的 `main.go` 后,workspace check 会报告对应工程失败并返回非零退出码;两个工程都存在问题时,会汇总报告。workspace generate 先预检所有工程,再进行各自的文件事务;一个预检失败时,其他工程也不写入。不承诺多个模块之间的崩溃事务性。
|
||||||
|
|
||||||
|
本例命令与 `go.work` 编译流程由 `cmd/go-omni/TestMixedWorkspaceConsumer` 在本地和远程发布验收中执行。
|
||||||
6
examples/mixedWorkspace/omni.workspace.json
Normal file
6
examples/mixedWorkspace/omni.workspace.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{ "dir": "apps/interface", "kind": "api" },
|
||||||
|
{ "dir": "apps/program", "kind": "project" }
|
||||||
|
]
|
||||||
|
}
|
||||||
6
examples/program/.gitignore
vendored
Normal file
6
examples/program/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/program
|
||||||
|
/var/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
/.goOmni/project.lock
|
||||||
11
examples/program/.goOmni/projectManifest.json
Normal file
11
examples/program/.goOmni/projectManifest.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"kind": "project",
|
||||||
|
"templateVersion": "v0.28.0-preview",
|
||||||
|
"module": "example.com/program",
|
||||||
|
"generated": {
|
||||||
|
"internal/config/loadGen.go": "8a1a2746f46596a7893832ca411bd7e2d23978661943d322ee732d2c095cbe47",
|
||||||
|
"internal/svc/runtimeGen.go": "0eb681169112302add633cc7355fbd19844b01fa0b38b94625ec18127e5fb937",
|
||||||
|
"main.go": "36168d3e3fe822f74cede8eac3fffe5502174899e242c41526f6ad5cab7af4fd"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
examples/program/README.md
Normal file
18
examples/program/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 通用程序示例
|
||||||
|
|
||||||
|
由 `go-omni project new` 生成,入口、加载和装配文件未经修改。项目仅在配置添加 `once`,在 `internal/run/run.go` 决定完成即退出或等待信号;无端口、数据库、队列和业务任务。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd examples/program
|
||||||
|
go mod tidy
|
||||||
|
go run . -check-config
|
||||||
|
go run . # once: true,完成即退出
|
||||||
|
# 编辑 etc/config.yaml 为 once: false 后运行,用 Ctrl+C / SIGTERM 停止。
|
||||||
|
go build -o program .
|
||||||
|
go-omni project check
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
使用同版远程 v0.28.0-preview CLI 与框架,无本地 replace。源码发布前开发可临时 `go mod edit -replace git.yuntuo.hk/k2safe/go-omni=../..`,验证后移除此本地替换。
|
||||||
|
|
||||||
|
配置、svc、Run 归项目维护;main/Gen.go 和清单由生成器维护。背景组件在 Init 登记停止与排空,已接受工作使用 WorkContext;未结束时不会提前释放共享依赖。详细规则见 [PROGRAM.md](../../PROGRAM.md)。
|
||||||
12
examples/program/etc/config.yaml
Normal file
12
examples/program/etc/config.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Project-owned. Environment references are explicit: ${NAME} or ${NAME:-fallback}.
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
format: json
|
||||||
|
output: stderr
|
||||||
|
lifecycle:
|
||||||
|
stopTimeout: 5s
|
||||||
|
drainTimeout: 10s
|
||||||
|
cancelTimeout: 5s
|
||||||
|
|
||||||
|
# true: finite run; false: wait for SIGINT/SIGTERM.
|
||||||
|
once: true
|
||||||
5
examples/program/go.mod
Normal file
5
examples/program/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module example.com/program
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require git.yuntuo.hk/k2safe/go-omni v0.28.0-preview
|
||||||
29
examples/program/internal/config/config.go
Normal file
29
examples/program/internal/config/config.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config, Default and Validate are project-owned. Add project settings here.
|
||||||
|
type Config struct {
|
||||||
|
Once bool `json:"once"`
|
||||||
|
Log logx.Config `json:"log"`
|
||||||
|
Lifecycle lifecycle.ShutdownConfig `json:"lifecycle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Default() Config {
|
||||||
|
log := logx.Default()
|
||||||
|
log.Service = "example.com/program"
|
||||||
|
return Config{Once: true, Log: log, Lifecycle: lifecycle.DefaultShutdownConfig()}
|
||||||
|
}
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
if err := c.Log.Validate(); err != nil {
|
||||||
|
return omniConfig.AtField("log", err)
|
||||||
|
}
|
||||||
|
if err := c.Lifecycle.Validate(); err != nil {
|
||||||
|
return omniConfig.AtField("lifecycle", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
12
examples/program/internal/config/loadGen.go
Normal file
12
examples/program/internal/config/loadGen.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Code generated by go-omni project. DO NOT EDIT.
|
||||||
|
package config
|
||||||
|
|
||||||
|
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||||
|
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
loader, err := NewLoader()
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
return omniConfig.LoadProject(path, Default(), loader)
|
||||||
|
}
|
||||||
6
examples/program/internal/config/loader.go
Normal file
6
examples/program/internal/config/loader.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import omniConfig "git.yuntuo.hk/k2safe/go-omni/config"
|
||||||
|
|
||||||
|
// NewLoader is project-owned. Register additional decoders here if needed.
|
||||||
|
func NewLoader() (*omniConfig.Loader, error) { return omniConfig.NewLoader() }
|
||||||
19
examples/program/internal/run/run.go
Normal file
19
examples/program/internal/run/run.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"example.com/program/internal/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run is project-owned. Return nil for normal completion, or an error to fail.
|
||||||
|
// The starter waits for shutdown; a one-shot program can simply return when done.
|
||||||
|
// Register background stop/drain hooks in svc.Init, not concurrently here.
|
||||||
|
// Accepted background work uses deps.WorkContext(), not the early-stop ctx.
|
||||||
|
func Run(ctx context.Context, deps *svc.ServiceContext) error {
|
||||||
|
if deps.Config.Once {
|
||||||
|
deps.Logger.InfoContext(ctx, "program.completed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
examples/program/internal/svc/runtimeGen.go
Normal file
44
examples/program/internal/svc/runtimeGen.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Code generated by go-omni project. DO NOT EDIT.
|
||||||
|
package svc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"example.com/program/internal/config"
|
||||||
|
"fmt"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New(ctx context.Context, logger *logx.Logger, cfg config.Config) (result *ServiceContext, err error) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if err = ctx.Err(); err != nil {
|
||||||
|
return nil, errors.Join(err, lifecycle.Cause(ctx))
|
||||||
|
}
|
||||||
|
if logger == nil {
|
||||||
|
return nil, errors.New("service context requires a logger")
|
||||||
|
}
|
||||||
|
if err = cfg.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
runtime, err := lifecycle.NewRuntimeWithConfig(logx.WithLogger(ctx, logger), cfg.Lifecycle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &ServiceContext{Runtime: runtime, Config: cfg, Logger: logger}
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
err = fmt.Errorf("dependency initialization panic: %v", p)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
result = nil
|
||||||
|
err = c.Finish(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err = c.Init(c.Context()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
24
examples/program/internal/svc/serviceContext.go
Normal file
24
examples/program/internal/svc/serviceContext.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package svc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"example.com/program/internal/config"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceContext is project-owned. Add dependencies without global variables.
|
||||||
|
// Runtime owns registered resources; Logger is borrowed from the entry point.
|
||||||
|
type ServiceContext struct {
|
||||||
|
*lifecycle.Runtime
|
||||||
|
Config config.Config
|
||||||
|
Logger *logx.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs before Run. Register resources immediately with OnClose.
|
||||||
|
// Register paired OnStop/OnDrain hooks before exposing background work.
|
||||||
|
// Use WorkContext for accepted work; Context is the early stop notification.
|
||||||
|
// OnDrain returns nil only after processing and final cleanup have completed.
|
||||||
|
// Complete all registrations here, before Run can race with shutdown.
|
||||||
|
// The drain name "go-omni/project/run" is reserved for the generated entry.
|
||||||
|
func (c *ServiceContext) Init(ctx context.Context) error { return nil }
|
||||||
89
examples/program/main.go
Normal file
89
examples/program/main.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Code generated by go-omni project. DO NOT EDIT.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"example.com/program/internal/config"
|
||||||
|
program "example.com/program/internal/run"
|
||||||
|
"example.com/program/internal/svc"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
path := flag.String("config", "etc/config.yaml", "configuration file; empty uses Default()")
|
||||||
|
check := flag.Bool("check-config", false, "validate configuration without initializing dependencies")
|
||||||
|
flag.Parse()
|
||||||
|
if *check {
|
||||||
|
if _, err := config.Load(*path); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
if err := run(ctx, *path); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, path string) (runErr error) {
|
||||||
|
cfg, err := config.Load(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger, err := logx.New(cfg.Log)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var deps *svc.ServiceContext
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
runErr = fmt.Errorf("program initialization panic: %v", p)
|
||||||
|
}
|
||||||
|
if deps != nil {
|
||||||
|
runErr = deps.Finish(runErr)
|
||||||
|
}
|
||||||
|
// Unjoined work may still use its dependencies and logger. The process exits
|
||||||
|
// unsuccessfully; resource retention never means the work completed.
|
||||||
|
if !errors.Is(runErr, lifecycle.ErrResourcesRetained) {
|
||||||
|
runErr = errors.Join(runErr, logger.Close())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
deps, err = svc.New(ctx, logger, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if deps.Context().Err() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
|
// This reserved drain proves Run has actually returned, including its defers.
|
||||||
|
// Runtime bounds the wait even when Run ignores cancellation.
|
||||||
|
deps.OnDrain("go-omni/project/run", func(context.Context) error { <-done; return nil })
|
||||||
|
logger.InfoContext(deps.Context(), "program.starting")
|
||||||
|
go func() {
|
||||||
|
var result error
|
||||||
|
returned := false
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
result = fmt.Errorf("program run panic: %v", p)
|
||||||
|
} else if !returned {
|
||||||
|
result = errors.New("program run exited without returning")
|
||||||
|
}
|
||||||
|
deps.RequestShutdown(result)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
result = program.Run(deps.Context(), deps)
|
||||||
|
returned = true
|
||||||
|
}()
|
||||||
|
<-deps.Context().Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
32
examples/recursiveResponse/README.md
Normal file
32
examples/recursiveResponse/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 递归响应类型
|
||||||
|
|
||||||
|
通用节点、树集合和相互引用对象的响应示例。没有数据库、权限或业务服务依赖。
|
||||||
|
|
||||||
|
在 go-omni 仓库根目录执行(新建独立临时目录):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.30.0-preview
|
||||||
|
example_dir="$(mktemp -d)/recursive"
|
||||||
|
go-omni api generate --api examples/recursiveResponse/service.api --dir "$example_dir" --module example.com/recursive
|
||||||
|
# 以下文件仅是本次新生成示例的占位实现,替换为示例自身维护的逻辑。
|
||||||
|
for name in tree forest optional raw pair; do
|
||||||
|
printf 'package trees\n' > "$example_dir/internal/logic/trees/$name.go"
|
||||||
|
done
|
||||||
|
cp examples/recursiveResponse/operations.go.txt "$example_dir/internal/logic/trees/operations.go"
|
||||||
|
cd "$example_dir"
|
||||||
|
go mod tidy
|
||||||
|
go-omni api generate --check
|
||||||
|
go test ./...
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
按启动日志显示的监听地址访问 `/tree`、`/forest`、`/optional`、`/raw`、`/pair`。前三层节点通过 `[]types.Node` 编译和返回;`/optional` 返回 null,`/raw` 直接返回节点,`/pair` 返回相互引用类型的有限实例。`docs/openapi.json` 中的 `DTO_Node` 通过 `$ref` 引用自身,`DTO_Branch`、`DTO_Leaf` 互相引用。
|
||||||
|
|
||||||
|
嵌套集合遵循普通 Go JSON 规则,示例将叶节点的 Children 初始化为空切片,使其输出 `[]`;框架不会递归改写项目返回数据。业务数据不能包含实际指针环。
|
||||||
|
|
||||||
|
框架开发时可在首次生成命令后加 `--framework-dir .` 使用本地代码。回归同时覆盖本地与已发布远程 CLI、真实 HTTP 响应、生成幂等与只读漂移检查:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 回到 go-omni 仓库根目录执行
|
||||||
|
GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./cmd/go-omni/apigen -run Recursive
|
||||||
|
```
|
||||||
22
examples/recursiveResponse/operations.go.txt
Normal file
22
examples/recursiveResponse/operations.go.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package trees
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"example.com/recursive/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sample() types.Node {
|
||||||
|
return types.Node{Name: "root", Children: []types.Node{
|
||||||
|
{Name: "child", Children: []types.Node{
|
||||||
|
{Name: "leaf", Children: []types.Node{}},
|
||||||
|
}},
|
||||||
|
}, Lookup: map[string]types.Node{"entry": {Name: "lookup", Children: []types.Node{}}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Logic) Tree(context.Context) (*types.Node, error) { node := sample(); return &node, nil }
|
||||||
|
func (s *Logic) Forest(context.Context) ([]types.Node, error) { return []types.Node{sample()}, nil }
|
||||||
|
func (s *Logic) Optional(context.Context) (*types.Node, error) { return nil, nil }
|
||||||
|
func (s *Logic) Raw(context.Context) (*types.Node, error) { node := sample(); return &node, nil }
|
||||||
|
func (s *Logic) Pair(context.Context) (*types.Branch, error) {
|
||||||
|
return &types.Branch{Leaf: &types.Leaf{}}, nil
|
||||||
|
}
|
||||||
33
examples/recursiveResponse/service.api
Normal file
33
examples/recursiveResponse/service.api
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
type Node {
|
||||||
|
Name string `json:"name" binding:"min=1"`
|
||||||
|
Children []Node `json:"children" binding:"max=100"`
|
||||||
|
Next *Node `json:"next,omitempty"`
|
||||||
|
Lookup map[string]Node `json:"lookup,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Branch {
|
||||||
|
Leaf *Leaf `json:"leaf"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Leaf {
|
||||||
|
Branch *Branch `json:"branch"`
|
||||||
|
}
|
||||||
|
|
||||||
|
@server (group: trees)
|
||||||
|
service recursive-example {
|
||||||
|
@handler tree
|
||||||
|
get /tree returns (Node)
|
||||||
|
|
||||||
|
@handler forest
|
||||||
|
get /forest returns ([]Node)
|
||||||
|
|
||||||
|
@handler optional
|
||||||
|
get /optional returns (*Node)
|
||||||
|
|
||||||
|
@handler raw
|
||||||
|
@response raw
|
||||||
|
get /raw returns (Node)
|
||||||
|
|
||||||
|
@handler pair
|
||||||
|
get /pair returns (Branch)
|
||||||
|
}
|
||||||
15
examples/signedRead/README.md
Normal file
15
examples/signedRead/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 通用签名读取
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run ./examples/signedRead
|
||||||
|
# 或执行真实 HTTP 检查并退出
|
||||||
|
go run ./examples/signedRead -check
|
||||||
|
```
|
||||||
|
|
||||||
|
示例自动创建临时私有存储、一页 PDF 和随机签名密钥,监听随机 loopback 端口。打开终端显示的首页,可预览或下载带中文文件名的 PDF;两条链接从签发起 30 分钟有效。首页显示签名内的实际到期时间,不会通过刷新延长链接;重启会生成新的文件和密钥。Ctrl+C 清理临时目录。
|
||||||
|
|
||||||
|
示例使用 `storage.ReadPresigner` 对应的 `PresignGetWithOptions` 能力,本地 `Signer` 与 S3/R2、OSS 适配器采用同一组 `ReadOptions`。省略 Disposition 默认 attachment,inline 请求浏览器内联展示;实际展示仍由浏览器设置和文件格式支持决定。原 `PresignGet` 接口与既有下载链接保持兼容。
|
||||||
|
|
||||||
|
本地支持 GET、HEAD、Range、206/416,响应范围只包含文件内容。云端由原生存储地址响应,GET 签名不通用于 HEAD;OSS 默认域名可能强制下载,应在 SDK 签名前配置自己的域名。详见 [存储接入说明](../../storage/README.md#签名预览与下载)。
|
||||||
|
|
||||||
|
这只是回环地址上的固定测试文件演示。实际项目先检查权限,再签发指定对象的链接;配置公开签名入口,不能从请求 Host 或转发头猜测。文件路径、权限、审计和页面状态由项目维护。不要记录 URL 查询参数,不公开存储目录。链接到期仅阻止后续读取,不收回已获得的内容;重新签发不会撤销旧链接。
|
||||||
183
examples/signedRead/main.go
Normal file
183
examples/signedRead/main.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
// signedRead is an offline, loopback-only preview/download demonstration.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/storage/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
check := flag.Bool("check", false, "run HTTP checks and exit")
|
||||||
|
flag.Parse()
|
||||||
|
if err := run(*check); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(check bool) error {
|
||||||
|
root, err := os.MkdirTemp("", "go-omni-signed-read-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(root)
|
||||||
|
files, err := local.New(root, 1<<20)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := samplePDF()
|
||||||
|
if _, err = files.Put(context.Background(), "sample.pdf", bytes.NewReader(content), int64(len(content)), "application/pdf"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
baseURL := "http://" + listener.Addr().String()
|
||||||
|
secret := make([]byte, 32)
|
||||||
|
if _, err = rand.Read(secret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signer, err := storage.NewSigner(files, baseURL+"/file", secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Production applications authorize the caller before this operation. The
|
||||||
|
// sample intentionally exposes only a generated fixture on loopback.
|
||||||
|
preview, err := signer.PresignGetWithOptions(context.Background(), "sample.pdf", 30*time.Minute, storage.ReadOptions{
|
||||||
|
Disposition: storage.DispositionInline, FileName: "示例报告.pdf", ContentType: "application/pdf",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
download, err := signer.PresignGetWithOptions(context.Background(), "sample.pdf", 30*time.Minute, storage.ReadOptions{FileName: "示例报告.pdf"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
page := template.Must(template.New("page").Parse(`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>Signed read example</title>
|
||||||
|
<body><h1>文件预览与下载</h1><p><a href="{{.Preview.URL}}">打开示例 PDF</a> · <a href="{{.Download.URL}}">下载示例 PDF</a></p>
|
||||||
|
<p>链接到期时间:{{.Preview.ExpiresAt}}</p><p>只读演示:无数据库、业务身份或公开磁盘目录。</p></body></html>`))
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/file", signer)
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" || r.Method != http.MethodGet {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "private, no-store")
|
||||||
|
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||||
|
_ = page.Execute(w, struct{ Preview, Download storage.ReadLink }{preview, download})
|
||||||
|
})
|
||||||
|
server := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 30 * time.Second}
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- server.Serve(listener) }()
|
||||||
|
defer server.Close()
|
||||||
|
if check {
|
||||||
|
if err := checkHTTP(preview, download, content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("signed read: PDF preview, Chinese filename, download, HEAD and Range passed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Never print signed URLs or enable request logging containing query strings.
|
||||||
|
fmt.Println("Open " + baseURL + " (Ctrl+C to stop)")
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer stop()
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return server.Shutdown(shutdown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHTTP(preview, download storage.ReadLink, content []byte) error {
|
||||||
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
link storage.ReadLink
|
||||||
|
method, span, disposition string
|
||||||
|
status int
|
||||||
|
body []byte
|
||||||
|
}{
|
||||||
|
{preview, "GET", "", "inline", 200, content},
|
||||||
|
{preview, "HEAD", "", "inline", 200, nil},
|
||||||
|
{preview, "GET", "bytes=0-7", "inline", 206, content[:8]},
|
||||||
|
{download, "GET", "", "attachment", 200, content},
|
||||||
|
} {
|
||||||
|
req, err := http.NewRequest(tc.method, tc.link.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid example link")
|
||||||
|
}
|
||||||
|
if tc.span != "" {
|
||||||
|
req.Header.Set("Range", tc.span)
|
||||||
|
}
|
||||||
|
response, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("example request failed")
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
kind, params, headerErr := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
|
||||||
|
if err != nil || headerErr != nil || response.StatusCode != tc.status || !bytes.Equal(body, tc.body) || kind != tc.disposition || params["filename"] != "示例报告.pdf" || response.Header.Get("Content-Type") != "application/pdf" {
|
||||||
|
return errors.New("example response did not match signed options")
|
||||||
|
}
|
||||||
|
size := len(tc.body)
|
||||||
|
if tc.method == "HEAD" {
|
||||||
|
size = len(content)
|
||||||
|
}
|
||||||
|
if response.Header.Get("Content-Length") != strconv.Itoa(size) || response.Header.Get("Cache-Control") != "private, no-store" {
|
||||||
|
return errors.New("example size or cache policy mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A complete, single-page PDF with a real cross-reference table. No generator
|
||||||
|
// dependency or business document is needed for this example.
|
||||||
|
func samplePDF() []byte {
|
||||||
|
stream := "BT /F1 24 Tf 72 720 Td (go-omni signed read) Tj ET\n"
|
||||||
|
objects := []string{
|
||||||
|
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||||
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||||
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>",
|
||||||
|
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||||||
|
fmt.Sprintf("<< /Length %d >>\nstream\n%sendstream", len(stream), stream),
|
||||||
|
}
|
||||||
|
var pdf bytes.Buffer
|
||||||
|
pdf.WriteString("%PDF-1.4\n")
|
||||||
|
offsets := []int{0}
|
||||||
|
for i, object := range objects {
|
||||||
|
offsets = append(offsets, pdf.Len())
|
||||||
|
fmt.Fprintf(&pdf, "%d 0 obj\n%s\nendobj\n", i+1, object)
|
||||||
|
}
|
||||||
|
xref := pdf.Len()
|
||||||
|
fmt.Fprintf(&pdf, "xref\n0 %d\n0000000000 65535 f \n", len(offsets))
|
||||||
|
for _, offset := range offsets[1:] {
|
||||||
|
fmt.Fprintf(&pdf, "%010d 00000 n \n", offset)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&pdf, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n", len(offsets), xref)
|
||||||
|
return pdf.Bytes()
|
||||||
|
}
|
||||||
9
examples/signedRead/main_test.go
Normal file
9
examples/signedRead/main_test.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSignedReadHTTP(t *testing.T) {
|
||||||
|
if err := run(true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
examples/telemetryHeaders/README.md
Normal file
11
examples/telemetryHeaders/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 配置化网关 Trace Header
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go run ./examples/telemetryHeaders
|
||||||
|
```
|
||||||
|
|
||||||
|
这个离线示例使用标准库 `httptest`,不连接网关、数据库或外部 Collector。它把 `X-Trace-Id`、`X-Span-Id` 和 `X-Request-Id` 配置给通用 `telemetry.HeaderPropagator`,再通过 `app.Use(runtime.Middleware())` 进入服务。业务 Handler 只读取 `logx.TraceID` / `logx.RequestID`,不解析 Header。
|
||||||
|
|
||||||
|
`HeaderPropagator` 同时支持标准 W3C `traceparent`;两种格式同时存在时优先 W3C。出站请求使用 `telemetry.Transport` 时会注入 W3C 和配置的兼容 Header。旧项目不配置它时,go-omni 仍只使用默认 W3C 传播。
|
||||||
|
|
||||||
|
示例中的 ID 是固定测试值。生产环境只有在网关或受信代理已经校验这些 Header 时才启用兼容传播;Trace ID/Span ID 不是认证凭据,也不能用于授权、幂等或租户隔离。自定义 Header 不携带采样标志,适配器会把有效的成对 ID 视为已采样;需要保留上游采样决定时应使用 `traceparent`。
|
||||||
62
examples/telemetryHeaders/main.go
Normal file
62
examples/telemetryHeaders/main.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// telemetryHeaders demonstrates a provider-neutral compatibility boundary for
|
||||||
|
// gateways that carry trace IDs in configurable HTTP headers.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
propagator, err := telemetry.NewHeaderPropagator(telemetry.HeaderPropagationConfig{
|
||||||
|
TraceIDHeader: "X-Trace-Id", SpanIDHeader: "X-Span-Id", RequestIDHeader: "X-Request-Id",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
runtime, err := telemetry.New(telemetry.Config{Service: "header-example", SampleRatio: 1, Propagator: propagator})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer runtime.Close()
|
||||||
|
app := omni.New(omni.WithLogger(false), omni.WithRequestIDHeader("X-Request-Id"))
|
||||||
|
defer app.Close()
|
||||||
|
app.Use(runtime.Middleware())
|
||||||
|
app.GET("/", func(c *omni.Context) error {
|
||||||
|
fmt.Printf("traceId=%s requestId=%s\n", logx.TraceID(c.Context()), logx.RequestID(c.Context()))
|
||||||
|
return c.OK(map[string]string{"traceId": logx.TraceID(c.Context()), "requestId": logx.RequestID(c.Context())})
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(app)
|
||||||
|
defer server.Close()
|
||||||
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Trace-Id", "11111111111111111111111111111111")
|
||||||
|
req.Header.Set("X-Span-Id", "2222222222222222")
|
||||||
|
req.Header.Set("X-Request-Id", "example-request")
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("status=%d response=%s", resp.StatusCode, body)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
9
examples/telemetryHeaders/main_test.go
Normal file
9
examples/telemetryHeaders/main_test.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTelemetryHeadersExample(t *testing.T) {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,3 +96,7 @@ err := app.RunContext(control.Context())
|
|||||||
实际使用时 App 和依赖通常先初始化,再由并发监控回调请求退出。资源在 HTTP 排空完成后由调用方关闭,并读取 `control.Err()` 合并清理期间报告的首个故障。通知本身不会关闭资源、执行 os.Exit 或发送操作系统信号。
|
实际使用时 App 和依赖通常先初始化,再由并发监控回调请求退出。资源在 HTTP 排空完成后由调用方关闭,并读取 `control.Err()` 合并清理期间报告的首个故障。通知本身不会关闭资源、执行 os.Exit 或发送操作系统信号。
|
||||||
|
|
||||||
`RequestShutdown(nil)` 表示正常停止,非空 error 表示故障。Controller 保留首个非空故障,重复调用和并发调用安全。`Cause(ctx)` 将符合 errors.Is(err, context.Canceled) 的取消原因视为正常停止(包含新版 Go 的信号原因),保留其他取消原因和 DeadlineExceeded。显式 RequestShutdown(err) 上报的非空错误始终由 Controller.Err 保留,包括包装 context.Canceled 的错误。App.RunContext、ServeContext、RunWith 均保留这些异常原因,与网络适配器无关。
|
`RequestShutdown(nil)` 表示正常停止,非空 error 表示故障。Controller 保留首个非空故障,重复调用和并发调用安全。`Cause(ctx)` 将符合 errors.Is(err, context.Canceled) 的取消原因视为正常停止(包含新版 Go 的信号原因),保留其他取消原因和 DeadlineExceeded。显式 RequestShutdown(err) 上报的非空错误始终由 Controller.Err 保留,包括包装 context.Canceled 的错误。App.RunContext、ServeContext、RunWith 均保留这些异常原因,与网络适配器无关。
|
||||||
|
|
||||||
|
## 通用工程入口
|
||||||
|
|
||||||
|
`go-omni project new` 可生成不监听端口的普通 Go 程序,直接复用 Runtime。入口监督项目 Run 的完成,并与 Init 登记的后台工作一起排空;Run/Init 异常和资源保留信号会传递到退出状态。完整运行契约与配置见 [通用工程指南](../PROGRAM.md)。
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Schema struct {
|
type Schema struct {
|
||||||
|
Ref string `json:"$ref,omitempty"`
|
||||||
|
AllOf []*Schema `json:"allOf,omitempty"`
|
||||||
MaxBytes *int64 `json:"x-omni-max-bytes,omitempty"`
|
MaxBytes *int64 `json:"x-omni-max-bytes,omitempty"`
|
||||||
Example any `json:"example,omitempty"`
|
Example any `json:"example,omitempty"`
|
||||||
EnumDescriptions []string `json:"x-enum-descriptions,omitempty"`
|
EnumDescriptions []string `json:"x-enum-descriptions,omitempty"`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package openapi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ func TestSchemaAdditionalPropertiesAndExactNumbers(t *testing.T) {
|
|||||||
`{"type":"object","additionalProperties":true}`,
|
`{"type":"object","additionalProperties":true}`,
|
||||||
`{"type":"object","additionalProperties":{"type":"string"}}`,
|
`{"type":"object","additionalProperties":{"type":"string"}}`,
|
||||||
`{"type":"integer","default":18446744073709551615,"enum":[18446744073709551615]}`,
|
`{"type":"integer","default":18446744073709551615,"enum":[18446744073709551615]}`,
|
||||||
|
`{"$ref":"#/components/schemas/Node"}`,
|
||||||
|
`{"description":"Node","allOf":[{"anyOf":[{"$ref":"#/components/schemas/Node"},{"type":"object","nullable":true,"enum":[null]}]}]}`,
|
||||||
} {
|
} {
|
||||||
var s Schema
|
var s Schema
|
||||||
if err := json.Unmarshal([]byte(text), &s); err != nil {
|
if err := json.Unmarshal([]byte(text), &s); err != nil {
|
||||||
@@ -30,6 +33,9 @@ func TestSchemaAdditionalPropertiesAndExactNumbers(t *testing.T) {
|
|||||||
if err = json.Unmarshal(b, &roundTrip); err != nil {
|
if err = json.Unmarshal(b, &roundTrip); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if !reflect.DeepEqual(s, roundTrip) {
|
||||||
|
t.Fatalf("schema round trip changed: %s", b)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
api, _ := New("Exact numbers", "1")
|
api, _ := New("Exact numbers", "1")
|
||||||
s := &Schema{Type: "integer", Enum: []any{json.Number("18446744073709551615")}}
|
s := &Schema{Type: "integer", Enum: []any{json.Number("18446744073709551615")}}
|
||||||
|
|||||||
25
reports/headerPropagationAcceptance.md
Normal file
25
reports/headerPropagationAcceptance.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Header 传播适配验收
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
本次为 go-omni 增加通用 `telemetry.HeaderPropagator`。调用方配置 Trace ID、Span ID 和可选 Request ID Header 后,框架负责入站父链路、日志关联、访问日志和出站传播;框架不识别任何网关名称,也不解析业务 Header。
|
||||||
|
|
||||||
|
标准 W3C `traceparent` 与兼容 Header 同时出现时优先 W3C。兼容 Header 必须是合法的 32 位 Trace ID 和 16 位 Span ID;旧 Header 没有采样位,合法成对值按已采样处理。Request ID 只用于关联,不参与 Span 父子关系。兼容传播只应放在已校验 Header 的受信入口。
|
||||||
|
|
||||||
|
## 验收结果
|
||||||
|
|
||||||
|
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./...`:通过,包含生成器、生命周期、存储、i18n、telemetry 和离线示例。
|
||||||
|
- `go test -race ./telemetry ./examples/telemetryHeaders`:通过。
|
||||||
|
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 GOTOOLCHAIN=go1.22.12 go test -count=1 ./...`:通过。
|
||||||
|
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
|
||||||
|
- `go run ./examples/telemetryHeaders`:通过,示例打印固定 Trace ID、Request ID 和 JSON 响应。
|
||||||
|
- 发布后空模块缓存验收:`verifyManagedRelease.py` 通过 110 项,`verifyProgramRelease.py` 通过 30 项,`verifyWorkspaceRelease.py` 通过 16 项;三份结果分别见 `headerPropagationRemote.json`、`headerPropagationProgramRemote.json` 和 `headerPropagationWorkspaceRemote.json`。
|
||||||
|
|
||||||
|
完整竞态套件在高并发生成器生命周期用例中出现了已有的时序超时(`TestGeneratedMainLifecycle/drainBeforeClose`、`TestGeneratedBackgroundPhases/nethttp`);单独重跑受影响的 `TestManagedGeneratedProject` 通过,功能定向竞态测试通过,未发现本次 Header 传播相关竞态。
|
||||||
|
|
||||||
|
## 代码边界
|
||||||
|
|
||||||
|
- 默认 `telemetry.Config` 仍使用 W3C 传播,未配置新适配器的项目行为不变。
|
||||||
|
- `telemetry.Transport` 在兼容模式下同时注入 W3C 与配置 Header,调用方请求对象不会被修改。
|
||||||
|
- 无效 ID、重复 Header 名称以及试图覆盖 `traceparent` / `tracestate` 的配置会被拒绝或忽略。
|
||||||
|
- 不增加认证、租户、幂等、业务重试或网关依赖。
|
||||||
74
reports/headerPropagationProgramRemote.json
Normal file
74
reports/headerPropagationProgramRemote.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.30.0-preview",
|
||||||
|
"frameworkSum": "h1:sUtS24GAmm1QQYYqdV2XWZ3STR2V/KPqtxRcXGokH48=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 21.72,
|
||||||
|
"tests": [
|
||||||
|
"TestConflictRollsBackEarlierWrites",
|
||||||
|
"TestUnsafePaths",
|
||||||
|
"TestGeneratedProgramConsumer",
|
||||||
|
"TestOwnershipAndDrift",
|
||||||
|
"TestRecordedTemplateUpgrade",
|
||||||
|
"TestProjectGuards/module",
|
||||||
|
"TestProjectGuards/oldFramework",
|
||||||
|
"TestProjectGuards/manifest",
|
||||||
|
"TestProjectGuards/lock",
|
||||||
|
"TestProjectGuards/otherGenerator",
|
||||||
|
"TestProjectGuards/symlink",
|
||||||
|
"TestProjectGuards"
|
||||||
|
],
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"installedVersionDefault",
|
||||||
|
"generatedConsumerBuildRaceVet",
|
||||||
|
"defaultNoAPIFiles",
|
||||||
|
"defaultNoNetworkListener",
|
||||||
|
"finiteProgram",
|
||||||
|
"SIGINT",
|
||||||
|
"SIGTERM",
|
||||||
|
"initializationFailureAndPanic",
|
||||||
|
"runErrorPanicGoexit",
|
||||||
|
"fatalComponentFailure",
|
||||||
|
"nonzeroProcessFailureAndTimeoutExits",
|
||||||
|
"partialStartupDraining",
|
||||||
|
"acknowledgementBeforeResourceClose",
|
||||||
|
"workContextSurvivesGracefulStop",
|
||||||
|
"cancellationGrace",
|
||||||
|
"unresponsiveRunAndBackgroundWork",
|
||||||
|
"retainedResourcesAndLogger",
|
||||||
|
"explicitConfigurationOnly",
|
||||||
|
"checkConfigWithoutDependencies",
|
||||||
|
"handwrittenFilesPreserved",
|
||||||
|
"generatedFileConflict",
|
||||||
|
"missingGeneratedFileRestored",
|
||||||
|
"missingHandwrittenFileNotRecreated",
|
||||||
|
"readOnlyCheckAndDryRun",
|
||||||
|
"templateDriftUpgrade",
|
||||||
|
"manifestIdentityAndVersionGuards",
|
||||||
|
"symlinkAndLockProtection",
|
||||||
|
"fileTransactionRollback",
|
||||||
|
"publishedExampleFiniteRun"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
|
||||||
|
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
|
||||||
|
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
|
||||||
|
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
|
||||||
|
"cmd/go-omni/projectgen/generate.go": "19afc56ed90fbdb7e6c77b5a7509a63d5176c0b576946e42b4241942b7dd1da6",
|
||||||
|
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
|
||||||
|
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
|
||||||
|
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
|
||||||
|
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
|
||||||
|
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
|
||||||
|
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
|
||||||
|
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
|
||||||
|
}
|
||||||
|
}
|
||||||
341
reports/headerPropagationRemote.json
Normal file
341
reports/headerPropagationRemote.json
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.30.0-preview",
|
||||||
|
"frameworkSum": "h1:sUtS24GAmm1QQYYqdV2XWZ3STR2V/KPqtxRcXGokH48=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 145.32,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"freshModuleCache",
|
||||||
|
"fixedVersionNoReplace",
|
||||||
|
"contractChangePreview",
|
||||||
|
"persistentLogicSignatureFailure",
|
||||||
|
"businessSourcePreserved",
|
||||||
|
"fixedConsumerBuild",
|
||||||
|
"rootGoRun",
|
||||||
|
"nethttpAndNativeSignals",
|
||||||
|
"listenerReleased",
|
||||||
|
"default404And405",
|
||||||
|
"goOwnedErrorCatalog",
|
||||||
|
"unwrappedDownloads",
|
||||||
|
"rawCallback",
|
||||||
|
"multipart400And413",
|
||||||
|
"nethttpAndNativeTransfers",
|
||||||
|
"sharedGoCatalogExport",
|
||||||
|
"documentOnlyErrorSelection",
|
||||||
|
"workspaceCheckNoWrites",
|
||||||
|
"staleCatalogRejected",
|
||||||
|
"twoIndependentServiceBuilds",
|
||||||
|
"initializationAndListenFailureCleanup",
|
||||||
|
"drainBeforeDependencyClose",
|
||||||
|
"generatedConsumerRaceTests",
|
||||||
|
"errorParamsAndInheritance",
|
||||||
|
"manualHooksAndScalars",
|
||||||
|
"dependencyFatalShutdown",
|
||||||
|
"fatalProcessExitCode",
|
||||||
|
"initializationShutdownNotification",
|
||||||
|
"configuredBuiltinEndpoints",
|
||||||
|
"projectHealthEnvelope",
|
||||||
|
"endpointConflictCleanup",
|
||||||
|
"responseArrayNullableNull",
|
||||||
|
"primitiveAndDynamicArrays",
|
||||||
|
"strictRequestPolicies",
|
||||||
|
"declarativeMultipartBinding",
|
||||||
|
"backgroundStopDrainResourceOrder",
|
||||||
|
"backgroundInitAndListenFailureCleanup",
|
||||||
|
"backgroundTimeoutRetainsResourcesAndExits",
|
||||||
|
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
|
||||||
|
"recursiveResponseTypedHTTP",
|
||||||
|
"recursiveResponseReferenceGraph",
|
||||||
|
"recursiveResponseRegenerationAndDrift",
|
||||||
|
"remoteHTTPClientRaceTests",
|
||||||
|
"remoteHTTPProxyAndTimeouts",
|
||||||
|
"remoteHTTPHeaderIsolation",
|
||||||
|
"remoteHTTPPoolAndDrainBudgets",
|
||||||
|
"remoteHTTPExample",
|
||||||
|
"remoteExplicitConfigurationRaceTests",
|
||||||
|
"literalAndRequiredDefaultReferences",
|
||||||
|
"remoteCheckConfigBeforeDependencies",
|
||||||
|
"removedImplicitEnvironmentRules",
|
||||||
|
"remoteSignedReadOptionsAndLegacyLinks",
|
||||||
|
"remoteReadSignatureTamperingAndExpiry",
|
||||||
|
"remoteLocalPayloadRangesAndHEAD",
|
||||||
|
"remoteOSSNativeV1V4Signatures",
|
||||||
|
"remoteS3R2ReadOptions",
|
||||||
|
"remoteSignedReadHTTPExample",
|
||||||
|
"remoteConfiguredHeaderPropagation",
|
||||||
|
"remoteHeaderW3CPrecedenceAndValidation",
|
||||||
|
"remoteHeaderTelemetryExample",
|
||||||
|
"remotePreciseModuleDiagnostics",
|
||||||
|
"remoteJSONSuccessStatuses",
|
||||||
|
"remoteRejectedStatusBodyCleanup",
|
||||||
|
"remoteCopyTypePreflight",
|
||||||
|
"remoteEmptyCollectionTypeRejection",
|
||||||
|
"remoteLoggerTimeZoneIsolation",
|
||||||
|
"remoteCORSPolicyRace",
|
||||||
|
"remoteCORSErrorResponses",
|
||||||
|
"remoteCORSTransports",
|
||||||
|
"generatedCORSConfigOnly",
|
||||||
|
"remoteUnifiedBodyPolicyTransports",
|
||||||
|
"remoteBodyLimitsBeforeMiddleware",
|
||||||
|
"remoteHardLimitConflictBeforeServing",
|
||||||
|
"remoteCanceledHandlerCleanupBarrier",
|
||||||
|
"remoteExternalServingResourceRetention",
|
||||||
|
"remoteAdmissionCloseRace",
|
||||||
|
"remoteRuntimeFinishRetainsHTTPDependencies",
|
||||||
|
"remoteHTTP2DisconnectedHandlerDrain",
|
||||||
|
"remoteNativeEarly413AndBusinessHTTP200",
|
||||||
|
"remoteHTTPSafetyExampleBuild",
|
||||||
|
"remoteTransactionStages",
|
||||||
|
"remoteTransactionCauseChains",
|
||||||
|
"remoteCommitOutcomeUnknown",
|
||||||
|
"remoteCancellationOutcomeUnknown",
|
||||||
|
"remoteTransactionPanicAndConcurrency",
|
||||||
|
"remoteTransactionPublicErrors",
|
||||||
|
"remoteTransactionExample",
|
||||||
|
"remoteI18nRuntimeRace",
|
||||||
|
"remoteI18nGeneratedConsumer",
|
||||||
|
"remoteI18nHTTPBusinessInvariants",
|
||||||
|
"remoteI18nSourceOnlyFallback",
|
||||||
|
"remoteCLDRSamplesAndBrowserConformance",
|
||||||
|
"remoteI18nCLIExtractionCheckExportTypes",
|
||||||
|
"remoteI18nNamespaceIsolation",
|
||||||
|
"remoteBrowserRaceAndFailureRetention",
|
||||||
|
"remoteJSONReaderCausePreservation",
|
||||||
|
"remoteTypeScriptConsumerSkippedWithoutTSC",
|
||||||
|
"flatFieldComposition",
|
||||||
|
"copyxThreeEndpointConsumer",
|
||||||
|
"explicitNullAndMoneyPolicies",
|
||||||
|
"fieldAdditionWithoutCopyRuleChanges",
|
||||||
|
"middlewareFactoryInjectionAndCleanup",
|
||||||
|
"optionalRepositoryPreservesBusiness",
|
||||||
|
"generatedUnifiedConfig",
|
||||||
|
"jsonYamlXmlAndCustomDecoder",
|
||||||
|
"configValidationBeforeDependencies",
|
||||||
|
"configEnvProvenance",
|
||||||
|
"configExtensionPreserved",
|
||||||
|
"configLoaderDriftCheck"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
|
||||||
|
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
|
||||||
|
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
|
||||||
|
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
|
||||||
|
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
|
||||||
|
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
|
||||||
|
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
|
||||||
|
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
|
||||||
|
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
|
||||||
|
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
|
||||||
|
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
|
||||||
|
"cmd/go-omni/apigen/generate.go": "4eb7f9bed7f1a9823870275eadedeb87e7400a6cb07c7ac6c2e8c310fdafc9bf",
|
||||||
|
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
|
||||||
|
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
|
||||||
|
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
|
||||||
|
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
|
||||||
|
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
|
||||||
|
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
|
||||||
|
"scripts/verifyManagedRelease.py": "7c7be0eff8eecdc0de54f6c619e00e4150e02f6511bd87fb78c12d6addd4a531",
|
||||||
|
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
|
||||||
|
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
|
||||||
|
"cmd/go-omni/apigen/responseShape.go": "b0d573b5dbdfe397b0ca2bfc2e79c85c589bd11b2269f7206b3828cf0ff5afd7",
|
||||||
|
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
|
||||||
|
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
|
||||||
|
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
|
||||||
|
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
|
||||||
|
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
|
||||||
|
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
|
||||||
|
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
|
||||||
|
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
|
||||||
|
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
|
||||||
|
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
|
||||||
|
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
|
||||||
|
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
|
||||||
|
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
|
||||||
|
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
|
||||||
|
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
|
||||||
|
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
|
||||||
|
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
|
||||||
|
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
|
||||||
|
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
|
||||||
|
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
|
||||||
|
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
|
||||||
|
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
|
||||||
|
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
|
||||||
|
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
|
||||||
|
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
|
||||||
|
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
|
||||||
|
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
|
||||||
|
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
|
||||||
|
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
|
||||||
|
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
|
||||||
|
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
|
||||||
|
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
|
||||||
|
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
|
||||||
|
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
|
||||||
|
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
|
||||||
|
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
|
||||||
|
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
|
||||||
|
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
|
||||||
|
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
|
||||||
|
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
|
||||||
|
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
|
||||||
|
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
|
||||||
|
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
|
||||||
|
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
|
||||||
|
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
|
||||||
|
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
|
||||||
|
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
|
||||||
|
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
|
||||||
|
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
|
||||||
|
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
|
||||||
|
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
|
||||||
|
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
|
||||||
|
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
|
||||||
|
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
|
||||||
|
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
|
||||||
|
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
|
||||||
|
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
|
||||||
|
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
|
||||||
|
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
|
||||||
|
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
|
||||||
|
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
|
||||||
|
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
|
||||||
|
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
|
||||||
|
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
|
||||||
|
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
|
||||||
|
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
|
||||||
|
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
|
||||||
|
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
|
||||||
|
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
|
||||||
|
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
|
||||||
|
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
|
||||||
|
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
|
||||||
|
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
|
||||||
|
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
|
||||||
|
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
|
||||||
|
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
|
||||||
|
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
|
||||||
|
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
|
||||||
|
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
|
||||||
|
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
|
||||||
|
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
|
||||||
|
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
|
||||||
|
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
|
||||||
|
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
|
||||||
|
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
|
||||||
|
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
|
||||||
|
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
|
||||||
|
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
|
||||||
|
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
|
||||||
|
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
|
||||||
|
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
|
||||||
|
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
|
||||||
|
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
|
||||||
|
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
|
||||||
|
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
|
||||||
|
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
|
||||||
|
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
|
||||||
|
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
|
||||||
|
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
|
||||||
|
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
|
||||||
|
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
|
||||||
|
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
|
||||||
|
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
|
||||||
|
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
|
||||||
|
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
|
||||||
|
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
|
||||||
|
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
|
||||||
|
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
|
||||||
|
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
|
||||||
|
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
|
||||||
|
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
|
||||||
|
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
|
||||||
|
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
|
||||||
|
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
|
||||||
|
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
|
||||||
|
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
|
||||||
|
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
|
||||||
|
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
|
||||||
|
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
|
||||||
|
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
|
||||||
|
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
|
||||||
|
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
|
||||||
|
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
|
||||||
|
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
|
||||||
|
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
|
||||||
|
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
|
||||||
|
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
|
||||||
|
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
|
||||||
|
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
|
||||||
|
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
|
||||||
|
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
|
||||||
|
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15",
|
||||||
|
"storage/upload.go": "9ff2930ba389163acc8063b51107c09a60290609ba384de8004c498ce09f20a1",
|
||||||
|
"storage/read_test.go": "9b11ab9640e98c1c9fa7ce1e2587df217e874e3a6a6e27a0dd0ade6fbf3d5e1a",
|
||||||
|
"storage/signedUpload.go": "a8d31bcbbad64796f04611e69271e94d917c9bcd5aeabd7a97ea155b0093c814",
|
||||||
|
"storage/multipart_test.go": "4053f680f7795db2a6b4aa23590b3669eec7a609dd92c4a1e20891a5299274ee",
|
||||||
|
"storage/multipart.go": "3b718ebe6da681c89a3f797892c88e7acb15c295df2376d64ac35da0976ed69d",
|
||||||
|
"storage/signed_test.go": "eaffd40894018814e976477f96cf0fbaecfa1987caad78830d9fc8e96995882f",
|
||||||
|
"storage/upload_test.go": "12b60419807ed64b89e30861674e19abee7b0b7bb0e8ec9ac5b3aa2e0710a1da",
|
||||||
|
"storage/storage.go": "cb670872f01cf1d7b4ebf21cec9fc6f52ad93bfdf523b2f28bbb8d9c7fe39a6c",
|
||||||
|
"storage/signed.go": "ae4b144137627be35718c40743ebc2f063ec667c24b6d8984ccfcca034a73787",
|
||||||
|
"storage/read.go": "92787c74ae024822f82b5b84bffc9f8a355f1f2565b99ca7fbefe49157ef4851",
|
||||||
|
"storage/s3/upload.go": "3ce0b3626249ffe7a041606d6a9481743af8a0d4868fadea20ed3ed44ee0c765",
|
||||||
|
"storage/s3/read_test.go": "9b64fda5c83ba41508ec23cf41aeeb9826b677caaa851dcbeb04abe1f43eed65",
|
||||||
|
"storage/s3/config.go": "626ec6a2c0b06c95ee7eb7aa0832b190cfcdeb8a127b6b89e99204040bc3b041",
|
||||||
|
"storage/s3/upload_test.go": "a41fdbd100d9c79a3606c5942f98017afe26d841b251d96e979df87d0af93867",
|
||||||
|
"storage/s3/read.go": "319b57f2ecf435b71085c7cdcafd50a1e30caa5f357f83d4db3cdcef085792b6",
|
||||||
|
"storage/s3/s3_test.go": "9ef284926ec9936ddfec8c52a9bf04fc7b4be55c43a535f6c5a428d1535f6fec",
|
||||||
|
"storage/s3/s3.go": "016bef5948e97f3d043d2e858716f1c02d9f27c4e02802bdc1be3860116f4b6c",
|
||||||
|
"storage/local/read_test.go": "ef1f7feb0887ea4b1f30304dfaf542fe9788b6e17856c942f1cd98448656194f",
|
||||||
|
"storage/local/local_test.go": "3a2241840968df8e37fd67169c3ef708228449e81f65aff908c8b60d49434dc0",
|
||||||
|
"storage/local/local.go": "5d1ca75d0d2ca87981eef84a60bf98d0feefa2a9594940d6bd231877f4f8ec02",
|
||||||
|
"storage/oss/upload.go": "556129611f9e5549cac8a36b4026bfb19c112a295b55192579f9ac116e20626f",
|
||||||
|
"storage/oss/read_test.go": "a1c273cfedd2f3ee31ce407a7ddd6fd5ad02c65ad632a91bc7f3ead3712b5bbd",
|
||||||
|
"storage/oss/oss_test.go": "555a954010b1218804dc7951fc75e42eae24eba9a99c0871f5ba787f3fc87d45",
|
||||||
|
"storage/oss/upload_test.go": "08eb0ed157f3743288d781f61d4d2b7bfe77852a8259c5e598c719d35889409e",
|
||||||
|
"storage/oss/oss.go": "4216ce9f3fe3ac5418a5b809db3cef7e99dcca72a25a2a12301f5897cda85dd5",
|
||||||
|
"storage/oss/read.go": "429a9a9f6c586a885d7cccb76c9d572d53a79df637ff128caacb2eac35b8d862",
|
||||||
|
"storage/storagetest/contract.go": "e7ad7242102437882269d4066462059d7974586b36a82d18639eca10fd932be9",
|
||||||
|
"storage/storagetest/uploads.go": "9b66f86dd94ccdf30c7d603d432bc2a55956608898e46c1274fa27d16fa1a969",
|
||||||
|
"examples/signedRead/main.go": "f921a3c5fbcbfa84291c0c95a9c2c211319a8f0537acc5ec640238f251e652e1",
|
||||||
|
"examples/signedRead/main_test.go": "6ab559c442b2aa355231efe0342b9ec3617bf97bb1f6dfd7ad304b9a452e4fc4",
|
||||||
|
"telemetry/errorCatalog_test.go": "eb4f487e226dfb45f179dbe13b5eba5a23a478b3417eea7a9710230ce71f9ad5",
|
||||||
|
"telemetry/telemetry_test.go": "a44531e5c9efc69d99bf3fd73bafc904bc5dae0f886676cdcf0bda30552e4b3b",
|
||||||
|
"telemetry/telemetry.go": "043f295a62bef4d4a92b8918f6d201ac54a15afd6823a01d64ac0656c2bede5d",
|
||||||
|
"telemetry/adapters.go": "a892ef0c2b268caf266a3ea0f833ecf7e9228ff59aeaeecae6fe4a2170ba51e2",
|
||||||
|
"telemetry/headers.go": "a65f275a78e69bfe0b9c91bb7d4947609891ea34b6b7377e436b26b53f6ef74a",
|
||||||
|
"examples/telemetryHeaders/main.go": "f331d9cf2beb45d95fe1c3ba3e5d720e99b16e76511fe7d2c54bbcae97202592",
|
||||||
|
"examples/telemetryHeaders/main_test.go": "abe3a6d1d7f3348d0802dacc25f8fb267e592819949e567991be6dbd19783d88"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
reports/headerPropagationWorkspaceRemote.json
Normal file
34
reports/headerPropagationWorkspaceRemote.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.30.0-preview",
|
||||||
|
"frameworkSum": "h1:sUtS24GAmm1QQYYqdV2XWZ3STR2V/KPqtxRcXGokH48=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 17.58,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"remoteAPIModule",
|
||||||
|
"remoteGenericModule",
|
||||||
|
"explicitKinds",
|
||||||
|
"manifestInferredKinds",
|
||||||
|
"mixedFormatWithProjectSkip",
|
||||||
|
"mixedGeneration",
|
||||||
|
"mixedCheck",
|
||||||
|
"nativeGoWorkBuild",
|
||||||
|
"nativeGoWorkRaceTests",
|
||||||
|
"moduleAndWorkspaceMetadataPreserved",
|
||||||
|
"bothProjectFailuresAggregated",
|
||||||
|
"nonzeroFailureExit",
|
||||||
|
"failedCheckIsReadOnly",
|
||||||
|
"crossProjectPreflight",
|
||||||
|
"missingGeneratedOutputRestored"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/apigen/workspace.go": "cc39732ed9a41bb2dfa14933a830a7f10e8e3b2edc2ada07c376a738c0ec77e0",
|
||||||
|
"cmd/go-omni/apigen/workspaceMixed_test.go": "119f2a2aadc2b3ee2471bed1309c6ad8e36488b258f2ab1ba5ba5bab4219643f",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog_test.go": "372874322aece46714351f088b5741642fe4e63d95062743a575958fa58b8b39",
|
||||||
|
"cmd/go-omni/workspace.go": "b7bbd3d9d58b3987090edf7e3714d7d8396e7c332174022d918a52e16396397d",
|
||||||
|
"cmd/go-omni/workspace_test.go": "0170d18158325262765d8b3067f55ca3ce9fe994f2d030cafee3e82b13bfcd4b",
|
||||||
|
"scripts/verifyWorkspaceRelease.py": "0495dc97c68ac17a7969088cf94b1cbbfa6e53f18fb36147c64bc34edb260bca"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,3 +31,13 @@
|
|||||||
旧 nethttp 手写普通路由新增默认 8 MiB 限额,大请求应显式配置;生成契约默认 1 MiB 保持。取消后的等待默认 5s,需计入服务停机预算。手写资源拥有者须将运行错误传给 Runtime.Finish,并在资源保留信号下跳过自己的额外关闭操作。自建 goroutine、Hijack 后的工作仍须登记后台生命周期。
|
旧 nethttp 手写普通路由新增默认 8 MiB 限额,大请求应显式配置;生成契约默认 1 MiB 保持。取消后的等待默认 5s,需计入服务停机预算。手写资源拥有者须将运行错误传给 Runtime.Finish,并在资源保留信号下跳过自己的额外关闭操作。自建 goroutine、Hijack 后的工作仍须登记后台生命周期。
|
||||||
|
|
||||||
自定义 Runner、独立 native/http2.Server、反向代理的额外限额由各自拥有者配置;使用 WithTransport 获取统一装配。请求体、multipart 单文件和幂等缓存限额保持各自用途。已发送的流式响应无法撤回,先完成有界读取再执行业务。
|
自定义 Runner、独立 native/http2.Server、反向代理的额外限额由各自拥有者配置;使用 WithTransport 获取统一装配。请求体、multipart 单文件和幂等缓存限额保持各自用途。已发送的流式响应无法撤回,先完成有界读取再执行业务。
|
||||||
|
|
||||||
|
## 远程发布验收
|
||||||
|
|
||||||
|
空模块缓存安装远程固定版本后,10 组生成消费者和 98 项发布检查通过,耗时 107.57 秒,无本地 replace;覆盖上传限额、取消收尾、HTTP/2 断连、生成项目依赖与日志保留。标签固定在 `3b00bb55d189cbe480c7d15c8d14bcd09d3f2d52`,见 [远程验收记录](httpSafetyRemote.json)。
|
||||||
|
|
||||||
|
模块校验和:`h1:IYc1zEBQX6GTPvORAC9Q7ezlzKlGn1nVnK8T68L/DGA=`。运行命令:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm exec --yes --package=typescript@5.6.3 -- python3 scripts/verifyManagedRelease.py --version v0.27.0-preview --report reports/httpSafetyRemote.json
|
||||||
|
```
|
||||||
|
|||||||
292
reports/httpSafetyRemote.json
Normal file
292
reports/httpSafetyRemote.json
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.27.0-preview",
|
||||||
|
"frameworkSum": "h1:IYc1zEBQX6GTPvORAC9Q7ezlzKlGn1nVnK8T68L/DGA=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 107.57,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"freshModuleCache",
|
||||||
|
"fixedVersionNoReplace",
|
||||||
|
"contractChangePreview",
|
||||||
|
"persistentLogicSignatureFailure",
|
||||||
|
"businessSourcePreserved",
|
||||||
|
"fixedConsumerBuild",
|
||||||
|
"rootGoRun",
|
||||||
|
"nethttpAndNativeSignals",
|
||||||
|
"listenerReleased",
|
||||||
|
"default404And405",
|
||||||
|
"goOwnedErrorCatalog",
|
||||||
|
"unwrappedDownloads",
|
||||||
|
"rawCallback",
|
||||||
|
"multipart400And413",
|
||||||
|
"nethttpAndNativeTransfers",
|
||||||
|
"sharedGoCatalogExport",
|
||||||
|
"documentOnlyErrorSelection",
|
||||||
|
"workspaceCheckNoWrites",
|
||||||
|
"staleCatalogRejected",
|
||||||
|
"twoIndependentServiceBuilds",
|
||||||
|
"initializationAndListenFailureCleanup",
|
||||||
|
"drainBeforeDependencyClose",
|
||||||
|
"generatedConsumerRaceTests",
|
||||||
|
"errorParamsAndInheritance",
|
||||||
|
"manualHooksAndScalars",
|
||||||
|
"dependencyFatalShutdown",
|
||||||
|
"fatalProcessExitCode",
|
||||||
|
"initializationShutdownNotification",
|
||||||
|
"configuredBuiltinEndpoints",
|
||||||
|
"projectHealthEnvelope",
|
||||||
|
"endpointConflictCleanup",
|
||||||
|
"responseArrayNullableNull",
|
||||||
|
"primitiveAndDynamicArrays",
|
||||||
|
"strictRequestPolicies",
|
||||||
|
"declarativeMultipartBinding",
|
||||||
|
"backgroundStopDrainResourceOrder",
|
||||||
|
"backgroundInitAndListenFailureCleanup",
|
||||||
|
"backgroundTimeoutRetainsResourcesAndExits",
|
||||||
|
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
|
||||||
|
"remoteHTTPClientRaceTests",
|
||||||
|
"remoteHTTPProxyAndTimeouts",
|
||||||
|
"remoteHTTPHeaderIsolation",
|
||||||
|
"remoteHTTPPoolAndDrainBudgets",
|
||||||
|
"remoteHTTPExample",
|
||||||
|
"remoteExplicitConfigurationRaceTests",
|
||||||
|
"literalAndRequiredDefaultReferences",
|
||||||
|
"remoteCheckConfigBeforeDependencies",
|
||||||
|
"removedImplicitEnvironmentRules",
|
||||||
|
"remotePreciseModuleDiagnostics",
|
||||||
|
"remoteJSONSuccessStatuses",
|
||||||
|
"remoteRejectedStatusBodyCleanup",
|
||||||
|
"remoteCopyTypePreflight",
|
||||||
|
"remoteEmptyCollectionTypeRejection",
|
||||||
|
"remoteLoggerTimeZoneIsolation",
|
||||||
|
"remoteCORSPolicyRace",
|
||||||
|
"remoteCORSErrorResponses",
|
||||||
|
"remoteCORSTransports",
|
||||||
|
"generatedCORSConfigOnly",
|
||||||
|
"remoteUnifiedBodyPolicyTransports",
|
||||||
|
"remoteBodyLimitsBeforeMiddleware",
|
||||||
|
"remoteHardLimitConflictBeforeServing",
|
||||||
|
"remoteCanceledHandlerCleanupBarrier",
|
||||||
|
"remoteExternalServingResourceRetention",
|
||||||
|
"remoteAdmissionCloseRace",
|
||||||
|
"remoteRuntimeFinishRetainsHTTPDependencies",
|
||||||
|
"remoteHTTP2DisconnectedHandlerDrain",
|
||||||
|
"remoteNativeEarly413AndBusinessHTTP200",
|
||||||
|
"remoteHTTPSafetyExampleBuild",
|
||||||
|
"remoteTransactionStages",
|
||||||
|
"remoteTransactionCauseChains",
|
||||||
|
"remoteCommitOutcomeUnknown",
|
||||||
|
"remoteCancellationOutcomeUnknown",
|
||||||
|
"remoteTransactionPanicAndConcurrency",
|
||||||
|
"remoteTransactionPublicErrors",
|
||||||
|
"remoteTransactionExample",
|
||||||
|
"remoteI18nRuntimeRace",
|
||||||
|
"remoteI18nGeneratedConsumer",
|
||||||
|
"remoteI18nHTTPBusinessInvariants",
|
||||||
|
"remoteI18nSourceOnlyFallback",
|
||||||
|
"remoteCLDRSamplesAndBrowserConformance",
|
||||||
|
"remoteTypeScriptConsumer",
|
||||||
|
"remoteI18nCLIExtractionCheckExportTypes",
|
||||||
|
"remoteI18nNamespaceIsolation",
|
||||||
|
"remoteBrowserRaceAndFailureRetention",
|
||||||
|
"remoteJSONReaderCausePreservation",
|
||||||
|
"flatFieldComposition",
|
||||||
|
"copyxThreeEndpointConsumer",
|
||||||
|
"explicitNullAndMoneyPolicies",
|
||||||
|
"fieldAdditionWithoutCopyRuleChanges",
|
||||||
|
"middlewareFactoryInjectionAndCleanup",
|
||||||
|
"optionalRepositoryPreservesBusiness",
|
||||||
|
"generatedUnifiedConfig",
|
||||||
|
"jsonYamlXmlAndCustomDecoder",
|
||||||
|
"configValidationBeforeDependencies",
|
||||||
|
"configEnvProvenance",
|
||||||
|
"configExtensionPreserved",
|
||||||
|
"configLoaderDriftCheck"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
|
||||||
|
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
|
||||||
|
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
|
||||||
|
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
|
||||||
|
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
|
||||||
|
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
|
||||||
|
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
|
||||||
|
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
|
||||||
|
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
|
||||||
|
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
|
||||||
|
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
|
||||||
|
"cmd/go-omni/apigen/generate.go": "4bf8f3cee4d2fc832c344f6fae7446a56fbfd673cb3816feab926e1d39c7eca3",
|
||||||
|
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
|
||||||
|
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
|
||||||
|
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
|
||||||
|
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
|
||||||
|
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
|
||||||
|
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
|
||||||
|
"scripts/verifyManagedRelease.py": "6860e3f440c4d4f51a55085e50066b1a43cd31d5068c2f7a01a708c16823aef7",
|
||||||
|
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
|
||||||
|
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
|
||||||
|
"cmd/go-omni/apigen/responseShape.go": "5b705130b66a3d4aaed842025b75abe54e6427acac8366366ca88ae9862429e1",
|
||||||
|
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
|
||||||
|
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
|
||||||
|
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
|
||||||
|
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
|
||||||
|
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
|
||||||
|
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
|
||||||
|
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
|
||||||
|
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
|
||||||
|
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
|
||||||
|
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
|
||||||
|
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
|
||||||
|
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
|
||||||
|
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
|
||||||
|
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
|
||||||
|
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
|
||||||
|
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
|
||||||
|
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
|
||||||
|
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
|
||||||
|
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
|
||||||
|
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
|
||||||
|
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
|
||||||
|
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
|
||||||
|
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
|
||||||
|
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
|
||||||
|
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
|
||||||
|
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
|
||||||
|
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
|
||||||
|
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
|
||||||
|
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
|
||||||
|
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
|
||||||
|
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
|
||||||
|
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
|
||||||
|
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
|
||||||
|
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
|
||||||
|
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
|
||||||
|
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
|
||||||
|
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
|
||||||
|
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
|
||||||
|
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
|
||||||
|
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
|
||||||
|
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
|
||||||
|
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
|
||||||
|
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
|
||||||
|
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
|
||||||
|
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
|
||||||
|
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
|
||||||
|
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
|
||||||
|
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
|
||||||
|
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
|
||||||
|
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
|
||||||
|
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
|
||||||
|
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
|
||||||
|
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
|
||||||
|
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
|
||||||
|
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
|
||||||
|
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
|
||||||
|
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
|
||||||
|
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
|
||||||
|
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
|
||||||
|
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
|
||||||
|
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
|
||||||
|
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
|
||||||
|
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
|
||||||
|
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
|
||||||
|
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
|
||||||
|
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
|
||||||
|
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
|
||||||
|
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
|
||||||
|
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
|
||||||
|
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
|
||||||
|
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
|
||||||
|
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
|
||||||
|
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
|
||||||
|
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
|
||||||
|
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
|
||||||
|
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
|
||||||
|
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
|
||||||
|
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
|
||||||
|
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
|
||||||
|
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
|
||||||
|
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
|
||||||
|
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
|
||||||
|
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
|
||||||
|
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
|
||||||
|
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
|
||||||
|
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
|
||||||
|
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
|
||||||
|
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
|
||||||
|
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
|
||||||
|
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
|
||||||
|
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
|
||||||
|
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
|
||||||
|
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
|
||||||
|
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
|
||||||
|
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
|
||||||
|
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
|
||||||
|
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
|
||||||
|
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
|
||||||
|
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
|
||||||
|
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
|
||||||
|
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
|
||||||
|
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
|
||||||
|
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
|
||||||
|
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
|
||||||
|
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
|
||||||
|
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
|
||||||
|
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
|
||||||
|
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
|
||||||
|
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
|
||||||
|
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
|
||||||
|
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
|
||||||
|
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
|
||||||
|
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
|
||||||
|
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
|
||||||
|
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
|
||||||
|
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
|
||||||
|
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
|
||||||
|
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
|
||||||
|
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
|
||||||
|
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
|
||||||
|
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
|
||||||
|
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
|
||||||
|
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
|
||||||
|
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
|
||||||
|
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
|
||||||
|
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
|
||||||
|
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
|
||||||
|
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
|
||||||
|
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
|
||||||
|
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
|
||||||
|
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
|
||||||
|
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
reports/programAcceptance.md
Normal file
40
reports/programAcceptance.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# v0.28 通用工程验收
|
||||||
|
|
||||||
|
本轮新增 `project new/generate/check`,默认生成普通 Go 程序,不包含 API、网络监听、外部服务初始化或业务任务。项目维护配置、svc 和 Run;模板维护入口、加载与生命周期装配。
|
||||||
|
|
||||||
|
## 发布前回归
|
||||||
|
|
||||||
|
2026-09-29,macOS arm64,Go 1.27.1 / Go 1.22.12:
|
||||||
|
|
||||||
|
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./...`:56 个测试包通过,含现有 API 生成消费者与新通用工程消费者。
|
||||||
|
- `GOTOOLCHAIN=go1.22.12 GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./...`:56 个测试包通过。
|
||||||
|
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
|
||||||
|
- 最终补充真实进程异常退出断言后,新生成器再次通过 race 集成回归与 Go 1.22 集成回归。
|
||||||
|
- `examples/program` 在临时副本中使用显式本地替换,完成 tidy、race、配置检查、实际一次性运行、生成漂移检查;仓库示例不含本地 replace。
|
||||||
|
|
||||||
|
## 验证边界
|
||||||
|
|
||||||
|
生成保护覆盖:手写文件逐字节保留、修改生成文件报冲突、缺失生成文件补回、缺失手写文件显式报错、只读 check/预览、模板升级漂移、工程与模块身份、最低依赖版本、路径/符号链接和锁保护、写入冲突后的事务回滚。
|
||||||
|
|
||||||
|
实际生成的消费者保留全部入口/加载/装配代码,只修改项目所有文件,覆盖:
|
||||||
|
|
||||||
|
- 一次性正常完成,主流程 defer 结束后反序关闭资源。
|
||||||
|
- Init 返回错误或 panic,Run 返回错误、panic、Goexit,以及组件报告致命故障。
|
||||||
|
- 初始化到一半失败后停止并等待已经启动的工作。
|
||||||
|
- 停止接收后等待处理与确认完成,期间 WorkContext 仍可用。
|
||||||
|
- 排空超时后请求取消,宽限期内完成则可清理;仍未结束则保留共享资源和日志。
|
||||||
|
- 主运行入口、后台工作、失败初始化后的工作不响应取消,均不会提前关闭资源。
|
||||||
|
- 显式环境引用、固定值不被隐式覆盖、非法配置阻止初始化,check-config 不打开日志文件。
|
||||||
|
- 真实构建的进程:SIGINT/SIGTERM 正常退出码 0;Run 错误/panic、主流程不响应停止导致超时,退出码 1。
|
||||||
|
- 默认进程通过本机 lsof 检查,无 TCP/UDP 端口;生成目录和源码无 API/Handler/Swagger/数据库/队列启动。
|
||||||
|
|
||||||
|
文件替换只对单文件原子,普通失败会回滚,整个工程不具备崩溃事务性。未登记的 goroutine、不遵守完成契约的项目组件、SIGKILL 和进程崩溃无法由模板保证收尾。新模板不改变既有调度器或队列的重试/投递语义。
|
||||||
|
|
||||||
|
## 远程发布验收
|
||||||
|
|
||||||
|
2026-09-30,两个独立空模块缓存安装远程 v0.28.0-preview,均未使用本地 replace:
|
||||||
|
|
||||||
|
- [通用工程远程报告](programRemote.json):30 项检查通过,耗时 26.45 秒。测试直接从下载的框架模块运行,并通过远程 CLI 生成实际消费者;包含示例的一次性运行。
|
||||||
|
- [既有生成项目远程报告](programManagedRemote.json):98 项检查、10 组消费者通过,耗时 111.07 秒。覆盖原有配置、HTTP 关闭、上传策略、国际化及 TypeScript 消费者;TypeScript 5.6.3 通过 npm exec 提供,未跳过该检查。
|
||||||
|
- 两组报告的远程模块 checksum 一致:`h1:nD9Xtn7RhHy81n+qp96rlcJlxpDc276Per+dt1low1g=`。
|
||||||
|
- 发布标签固定在代码提交 `0b6c0043b6c3b58648645aeee03bf80801e6aa3f`;标签对象 `50caea1fc81a52080c0f0832d8c4a63cac786859`。代码与标签已推送,验收文档随后独立提交,不移动发布标签。
|
||||||
292
reports/programManagedRemote.json
Normal file
292
reports/programManagedRemote.json
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.0-preview",
|
||||||
|
"frameworkSum": "h1:nD9Xtn7RhHy81n+qp96rlcJlxpDc276Per+dt1low1g=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 111.07,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"freshModuleCache",
|
||||||
|
"fixedVersionNoReplace",
|
||||||
|
"contractChangePreview",
|
||||||
|
"persistentLogicSignatureFailure",
|
||||||
|
"businessSourcePreserved",
|
||||||
|
"fixedConsumerBuild",
|
||||||
|
"rootGoRun",
|
||||||
|
"nethttpAndNativeSignals",
|
||||||
|
"listenerReleased",
|
||||||
|
"default404And405",
|
||||||
|
"goOwnedErrorCatalog",
|
||||||
|
"unwrappedDownloads",
|
||||||
|
"rawCallback",
|
||||||
|
"multipart400And413",
|
||||||
|
"nethttpAndNativeTransfers",
|
||||||
|
"sharedGoCatalogExport",
|
||||||
|
"documentOnlyErrorSelection",
|
||||||
|
"workspaceCheckNoWrites",
|
||||||
|
"staleCatalogRejected",
|
||||||
|
"twoIndependentServiceBuilds",
|
||||||
|
"initializationAndListenFailureCleanup",
|
||||||
|
"drainBeforeDependencyClose",
|
||||||
|
"generatedConsumerRaceTests",
|
||||||
|
"errorParamsAndInheritance",
|
||||||
|
"manualHooksAndScalars",
|
||||||
|
"dependencyFatalShutdown",
|
||||||
|
"fatalProcessExitCode",
|
||||||
|
"initializationShutdownNotification",
|
||||||
|
"configuredBuiltinEndpoints",
|
||||||
|
"projectHealthEnvelope",
|
||||||
|
"endpointConflictCleanup",
|
||||||
|
"responseArrayNullableNull",
|
||||||
|
"primitiveAndDynamicArrays",
|
||||||
|
"strictRequestPolicies",
|
||||||
|
"declarativeMultipartBinding",
|
||||||
|
"backgroundStopDrainResourceOrder",
|
||||||
|
"backgroundInitAndListenFailureCleanup",
|
||||||
|
"backgroundTimeoutRetainsResourcesAndExits",
|
||||||
|
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
|
||||||
|
"remoteHTTPClientRaceTests",
|
||||||
|
"remoteHTTPProxyAndTimeouts",
|
||||||
|
"remoteHTTPHeaderIsolation",
|
||||||
|
"remoteHTTPPoolAndDrainBudgets",
|
||||||
|
"remoteHTTPExample",
|
||||||
|
"remoteExplicitConfigurationRaceTests",
|
||||||
|
"literalAndRequiredDefaultReferences",
|
||||||
|
"remoteCheckConfigBeforeDependencies",
|
||||||
|
"removedImplicitEnvironmentRules",
|
||||||
|
"remotePreciseModuleDiagnostics",
|
||||||
|
"remoteJSONSuccessStatuses",
|
||||||
|
"remoteRejectedStatusBodyCleanup",
|
||||||
|
"remoteCopyTypePreflight",
|
||||||
|
"remoteEmptyCollectionTypeRejection",
|
||||||
|
"remoteLoggerTimeZoneIsolation",
|
||||||
|
"remoteCORSPolicyRace",
|
||||||
|
"remoteCORSErrorResponses",
|
||||||
|
"remoteCORSTransports",
|
||||||
|
"generatedCORSConfigOnly",
|
||||||
|
"remoteUnifiedBodyPolicyTransports",
|
||||||
|
"remoteBodyLimitsBeforeMiddleware",
|
||||||
|
"remoteHardLimitConflictBeforeServing",
|
||||||
|
"remoteCanceledHandlerCleanupBarrier",
|
||||||
|
"remoteExternalServingResourceRetention",
|
||||||
|
"remoteAdmissionCloseRace",
|
||||||
|
"remoteRuntimeFinishRetainsHTTPDependencies",
|
||||||
|
"remoteHTTP2DisconnectedHandlerDrain",
|
||||||
|
"remoteNativeEarly413AndBusinessHTTP200",
|
||||||
|
"remoteHTTPSafetyExampleBuild",
|
||||||
|
"remoteTransactionStages",
|
||||||
|
"remoteTransactionCauseChains",
|
||||||
|
"remoteCommitOutcomeUnknown",
|
||||||
|
"remoteCancellationOutcomeUnknown",
|
||||||
|
"remoteTransactionPanicAndConcurrency",
|
||||||
|
"remoteTransactionPublicErrors",
|
||||||
|
"remoteTransactionExample",
|
||||||
|
"remoteI18nRuntimeRace",
|
||||||
|
"remoteI18nGeneratedConsumer",
|
||||||
|
"remoteI18nHTTPBusinessInvariants",
|
||||||
|
"remoteI18nSourceOnlyFallback",
|
||||||
|
"remoteCLDRSamplesAndBrowserConformance",
|
||||||
|
"remoteTypeScriptConsumer",
|
||||||
|
"remoteI18nCLIExtractionCheckExportTypes",
|
||||||
|
"remoteI18nNamespaceIsolation",
|
||||||
|
"remoteBrowserRaceAndFailureRetention",
|
||||||
|
"remoteJSONReaderCausePreservation",
|
||||||
|
"flatFieldComposition",
|
||||||
|
"copyxThreeEndpointConsumer",
|
||||||
|
"explicitNullAndMoneyPolicies",
|
||||||
|
"fieldAdditionWithoutCopyRuleChanges",
|
||||||
|
"middlewareFactoryInjectionAndCleanup",
|
||||||
|
"optionalRepositoryPreservesBusiness",
|
||||||
|
"generatedUnifiedConfig",
|
||||||
|
"jsonYamlXmlAndCustomDecoder",
|
||||||
|
"configValidationBeforeDependencies",
|
||||||
|
"configEnvProvenance",
|
||||||
|
"configExtensionPreserved",
|
||||||
|
"configLoaderDriftCheck"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
|
||||||
|
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
|
||||||
|
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
|
||||||
|
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
|
||||||
|
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
|
||||||
|
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
|
||||||
|
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
|
||||||
|
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
|
||||||
|
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
|
||||||
|
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
|
||||||
|
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
|
||||||
|
"cmd/go-omni/apigen/generate.go": "e89e8b9cabc18903d231ef22396ff94b9f2f6d233df889669ed3644cf283317f",
|
||||||
|
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
|
||||||
|
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
|
||||||
|
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
|
||||||
|
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
|
||||||
|
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
|
||||||
|
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
|
||||||
|
"scripts/verifyManagedRelease.py": "6860e3f440c4d4f51a55085e50066b1a43cd31d5068c2f7a01a708c16823aef7",
|
||||||
|
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
|
||||||
|
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
|
||||||
|
"cmd/go-omni/apigen/responseShape.go": "5b705130b66a3d4aaed842025b75abe54e6427acac8366366ca88ae9862429e1",
|
||||||
|
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
|
||||||
|
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
|
||||||
|
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
|
||||||
|
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
|
||||||
|
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
|
||||||
|
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
|
||||||
|
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
|
||||||
|
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
|
||||||
|
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
|
||||||
|
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
|
||||||
|
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
|
||||||
|
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
|
||||||
|
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
|
||||||
|
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
|
||||||
|
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
|
||||||
|
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
|
||||||
|
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
|
||||||
|
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
|
||||||
|
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
|
||||||
|
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
|
||||||
|
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
|
||||||
|
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
|
||||||
|
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
|
||||||
|
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
|
||||||
|
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
|
||||||
|
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
|
||||||
|
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
|
||||||
|
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
|
||||||
|
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
|
||||||
|
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
|
||||||
|
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
|
||||||
|
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
|
||||||
|
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
|
||||||
|
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
|
||||||
|
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
|
||||||
|
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
|
||||||
|
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
|
||||||
|
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
|
||||||
|
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
|
||||||
|
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
|
||||||
|
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
|
||||||
|
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
|
||||||
|
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
|
||||||
|
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
|
||||||
|
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
|
||||||
|
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
|
||||||
|
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
|
||||||
|
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
|
||||||
|
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
|
||||||
|
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
|
||||||
|
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
|
||||||
|
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
|
||||||
|
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
|
||||||
|
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
|
||||||
|
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
|
||||||
|
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
|
||||||
|
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
|
||||||
|
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
|
||||||
|
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
|
||||||
|
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
|
||||||
|
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
|
||||||
|
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
|
||||||
|
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
|
||||||
|
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
|
||||||
|
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
|
||||||
|
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
|
||||||
|
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
|
||||||
|
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
|
||||||
|
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
|
||||||
|
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
|
||||||
|
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
|
||||||
|
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
|
||||||
|
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
|
||||||
|
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
|
||||||
|
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
|
||||||
|
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
|
||||||
|
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
|
||||||
|
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
|
||||||
|
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
|
||||||
|
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
|
||||||
|
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
|
||||||
|
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
|
||||||
|
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
|
||||||
|
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
|
||||||
|
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
|
||||||
|
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
|
||||||
|
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
|
||||||
|
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
|
||||||
|
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
|
||||||
|
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
|
||||||
|
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
|
||||||
|
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
|
||||||
|
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
|
||||||
|
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
|
||||||
|
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
|
||||||
|
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
|
||||||
|
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
|
||||||
|
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
|
||||||
|
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
|
||||||
|
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
|
||||||
|
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
|
||||||
|
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
|
||||||
|
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
|
||||||
|
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
|
||||||
|
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
|
||||||
|
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
|
||||||
|
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
|
||||||
|
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
|
||||||
|
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
|
||||||
|
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
|
||||||
|
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
|
||||||
|
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
|
||||||
|
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
|
||||||
|
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
|
||||||
|
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
|
||||||
|
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
|
||||||
|
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
|
||||||
|
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
|
||||||
|
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
|
||||||
|
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
|
||||||
|
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
|
||||||
|
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
|
||||||
|
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
|
||||||
|
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
|
||||||
|
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
|
||||||
|
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
|
||||||
|
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
|
||||||
|
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
|
||||||
|
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
|
||||||
|
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
|
||||||
|
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
|
||||||
|
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15"
|
||||||
|
}
|
||||||
|
}
|
||||||
74
reports/programRemote.json
Normal file
74
reports/programRemote.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.0-preview",
|
||||||
|
"frameworkSum": "h1:nD9Xtn7RhHy81n+qp96rlcJlxpDc276Per+dt1low1g=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 26.45,
|
||||||
|
"tests": [
|
||||||
|
"TestConflictRollsBackEarlierWrites",
|
||||||
|
"TestUnsafePaths",
|
||||||
|
"TestGeneratedProgramConsumer",
|
||||||
|
"TestOwnershipAndDrift",
|
||||||
|
"TestRecordedTemplateUpgrade",
|
||||||
|
"TestProjectGuards/module",
|
||||||
|
"TestProjectGuards/oldFramework",
|
||||||
|
"TestProjectGuards/manifest",
|
||||||
|
"TestProjectGuards/lock",
|
||||||
|
"TestProjectGuards/otherGenerator",
|
||||||
|
"TestProjectGuards/symlink",
|
||||||
|
"TestProjectGuards"
|
||||||
|
],
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"installedVersionDefault",
|
||||||
|
"generatedConsumerBuildRaceVet",
|
||||||
|
"defaultNoAPIFiles",
|
||||||
|
"defaultNoNetworkListener",
|
||||||
|
"finiteProgram",
|
||||||
|
"SIGINT",
|
||||||
|
"SIGTERM",
|
||||||
|
"initializationFailureAndPanic",
|
||||||
|
"runErrorPanicGoexit",
|
||||||
|
"fatalComponentFailure",
|
||||||
|
"nonzeroProcessFailureAndTimeoutExits",
|
||||||
|
"partialStartupDraining",
|
||||||
|
"acknowledgementBeforeResourceClose",
|
||||||
|
"workContextSurvivesGracefulStop",
|
||||||
|
"cancellationGrace",
|
||||||
|
"unresponsiveRunAndBackgroundWork",
|
||||||
|
"retainedResourcesAndLogger",
|
||||||
|
"explicitConfigurationOnly",
|
||||||
|
"checkConfigWithoutDependencies",
|
||||||
|
"handwrittenFilesPreserved",
|
||||||
|
"generatedFileConflict",
|
||||||
|
"missingGeneratedFileRestored",
|
||||||
|
"missingHandwrittenFileNotRecreated",
|
||||||
|
"readOnlyCheckAndDryRun",
|
||||||
|
"templateDriftUpgrade",
|
||||||
|
"manifestIdentityAndVersionGuards",
|
||||||
|
"symlinkAndLockProtection",
|
||||||
|
"fileTransactionRollback",
|
||||||
|
"publishedExampleFiniteRun"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
|
||||||
|
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
|
||||||
|
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
|
||||||
|
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
|
||||||
|
"cmd/go-omni/projectgen/generate.go": "c4b1ba2d1f09eca02a031330ed9a1e486bab711b37c77b72f0f9dd0906e1f8e4",
|
||||||
|
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
|
||||||
|
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
|
||||||
|
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
|
||||||
|
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
|
||||||
|
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
|
||||||
|
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
|
||||||
|
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
reports/recursiveResponseAcceptance.md
Normal file
32
reports/recursiveResponseAcceptance.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# v0.28.2 递归响应验收
|
||||||
|
|
||||||
|
范围:`.api` 响应 DTO 的自引用与相互引用,使用通用 Node/Branch/Leaf;不加入接入项目的业务字段或业务行为。非递归响应保持原生成路径,递归请求与非法值循环仍拒绝。
|
||||||
|
|
||||||
|
## 本地回归
|
||||||
|
|
||||||
|
2026-09-30,macOS arm64:
|
||||||
|
|
||||||
|
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./...`:56 个测试包通过,apigen 集成包 112.675 秒。
|
||||||
|
- `GOTOOLCHAIN=go1.22.12 GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./...`:56 个测试包通过,apigen 集成包 91.536 秒。
|
||||||
|
- 新增生成文档断言后,重新运行专项 `go test -race -count=1 ./cmd/go-omni/apigen -run Recursive`(开启集成):通过,4.318 秒。
|
||||||
|
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
|
||||||
|
- 示例通过实际 CLI 导出后,`@apidevtools/swagger-cli@4.0.4 validate` 确认完整 OpenAPI 3.0.3 文档有效。
|
||||||
|
|
||||||
|
## 覆盖内容
|
||||||
|
|
||||||
|
- 强类型三层树、递归 slice/map/pointer、可空对象、raw、数组根响应以及相互引用的有限值,生成消费者真实 HTTP 返回和 Go JSON 解码。
|
||||||
|
- `$ref` 目标完整、没有被忽略的引用同级属性、可空引用的 null 分支、循环引用不展开、文档压缩幂等。
|
||||||
|
- 跨源文件引用、声明顺序无关、直接值与引用混合的合法类型、scalar 输出 schema、字段约束、描述、精确 int64 示例。
|
||||||
|
- 有限递归示例、空集合、缺失/未知字段、深层非法值、异构数组错误、非法 null 与额外 JSON 值。
|
||||||
|
- 非法直接值循环、相互值循环、字段组合循环、递归请求及间接递归请求、未知类型、非法标签、重复字段、上传字段、双指针、固定数组和错误 map 键。
|
||||||
|
- 反复生成、手写代码保留、只读检查、递归字段变更诊断;请求绑定与原有生成消费者继续全量回归。
|
||||||
|
|
||||||
|
## 发布验收
|
||||||
|
|
||||||
|
三个独立空模块缓存安装固定远程 `v0.28.2-preview`,均无本地 replace,全部通过:
|
||||||
|
|
||||||
|
- [响应与兼容报告](recursiveResponseRemote.json):101 项检查、11 组生成消费者,160.57 秒。包含强类型递归 HTTP、完整引用图、再生成和漂移检查,以及原有生命周期、上传、配置、错误和 i18n 回归。
|
||||||
|
- [通用工程报告](recursiveResponseProgramRemote.json):30 项检查,73.66 秒。
|
||||||
|
- [混合工作区报告](recursiveResponseWorkspaceRemote.json):16 项检查,66.19 秒。
|
||||||
|
|
||||||
|
三次下载的框架 checksum 一致:`h1:QjHXXV184SO45GCXLyejeVDdCzLa5EtRu2ngyYpLzYI=`。代码与固定标签指向 `0c5b571b31b68d25cdb3e8640970e2a53ddcbea6`;远程验收记录另行提交,不移动标签。
|
||||||
74
reports/recursiveResponseProgramRemote.json
Normal file
74
reports/recursiveResponseProgramRemote.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.2-preview",
|
||||||
|
"frameworkSum": "h1:QjHXXV184SO45GCXLyejeVDdCzLa5EtRu2ngyYpLzYI=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 73.66,
|
||||||
|
"tests": [
|
||||||
|
"TestConflictRollsBackEarlierWrites",
|
||||||
|
"TestUnsafePaths",
|
||||||
|
"TestGeneratedProgramConsumer",
|
||||||
|
"TestOwnershipAndDrift",
|
||||||
|
"TestRecordedTemplateUpgrade",
|
||||||
|
"TestProjectGuards/module",
|
||||||
|
"TestProjectGuards/oldFramework",
|
||||||
|
"TestProjectGuards/manifest",
|
||||||
|
"TestProjectGuards/lock",
|
||||||
|
"TestProjectGuards/otherGenerator",
|
||||||
|
"TestProjectGuards/symlink",
|
||||||
|
"TestProjectGuards"
|
||||||
|
],
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"installedVersionDefault",
|
||||||
|
"generatedConsumerBuildRaceVet",
|
||||||
|
"defaultNoAPIFiles",
|
||||||
|
"defaultNoNetworkListener",
|
||||||
|
"finiteProgram",
|
||||||
|
"SIGINT",
|
||||||
|
"SIGTERM",
|
||||||
|
"initializationFailureAndPanic",
|
||||||
|
"runErrorPanicGoexit",
|
||||||
|
"fatalComponentFailure",
|
||||||
|
"nonzeroProcessFailureAndTimeoutExits",
|
||||||
|
"partialStartupDraining",
|
||||||
|
"acknowledgementBeforeResourceClose",
|
||||||
|
"workContextSurvivesGracefulStop",
|
||||||
|
"cancellationGrace",
|
||||||
|
"unresponsiveRunAndBackgroundWork",
|
||||||
|
"retainedResourcesAndLogger",
|
||||||
|
"explicitConfigurationOnly",
|
||||||
|
"checkConfigWithoutDependencies",
|
||||||
|
"handwrittenFilesPreserved",
|
||||||
|
"generatedFileConflict",
|
||||||
|
"missingGeneratedFileRestored",
|
||||||
|
"missingHandwrittenFileNotRecreated",
|
||||||
|
"readOnlyCheckAndDryRun",
|
||||||
|
"templateDriftUpgrade",
|
||||||
|
"manifestIdentityAndVersionGuards",
|
||||||
|
"symlinkAndLockProtection",
|
||||||
|
"fileTransactionRollback",
|
||||||
|
"publishedExampleFiniteRun"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
|
||||||
|
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
|
||||||
|
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
|
||||||
|
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
|
||||||
|
"cmd/go-omni/projectgen/generate.go": "f4388d73901a0c5d5f3aac58d8ee1d1aa69ea395019875ec3c4cffc49fb1179d",
|
||||||
|
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
|
||||||
|
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
|
||||||
|
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
|
||||||
|
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
|
||||||
|
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
|
||||||
|
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
|
||||||
|
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
|
||||||
|
}
|
||||||
|
}
|
||||||
295
reports/recursiveResponseRemote.json
Normal file
295
reports/recursiveResponseRemote.json
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.2-preview",
|
||||||
|
"frameworkSum": "h1:QjHXXV184SO45GCXLyejeVDdCzLa5EtRu2ngyYpLzYI=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 160.57,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"freshModuleCache",
|
||||||
|
"fixedVersionNoReplace",
|
||||||
|
"contractChangePreview",
|
||||||
|
"persistentLogicSignatureFailure",
|
||||||
|
"businessSourcePreserved",
|
||||||
|
"fixedConsumerBuild",
|
||||||
|
"rootGoRun",
|
||||||
|
"nethttpAndNativeSignals",
|
||||||
|
"listenerReleased",
|
||||||
|
"default404And405",
|
||||||
|
"goOwnedErrorCatalog",
|
||||||
|
"unwrappedDownloads",
|
||||||
|
"rawCallback",
|
||||||
|
"multipart400And413",
|
||||||
|
"nethttpAndNativeTransfers",
|
||||||
|
"sharedGoCatalogExport",
|
||||||
|
"documentOnlyErrorSelection",
|
||||||
|
"workspaceCheckNoWrites",
|
||||||
|
"staleCatalogRejected",
|
||||||
|
"twoIndependentServiceBuilds",
|
||||||
|
"initializationAndListenFailureCleanup",
|
||||||
|
"drainBeforeDependencyClose",
|
||||||
|
"generatedConsumerRaceTests",
|
||||||
|
"errorParamsAndInheritance",
|
||||||
|
"manualHooksAndScalars",
|
||||||
|
"dependencyFatalShutdown",
|
||||||
|
"fatalProcessExitCode",
|
||||||
|
"initializationShutdownNotification",
|
||||||
|
"configuredBuiltinEndpoints",
|
||||||
|
"projectHealthEnvelope",
|
||||||
|
"endpointConflictCleanup",
|
||||||
|
"responseArrayNullableNull",
|
||||||
|
"primitiveAndDynamicArrays",
|
||||||
|
"strictRequestPolicies",
|
||||||
|
"declarativeMultipartBinding",
|
||||||
|
"backgroundStopDrainResourceOrder",
|
||||||
|
"backgroundInitAndListenFailureCleanup",
|
||||||
|
"backgroundTimeoutRetainsResourcesAndExits",
|
||||||
|
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
|
||||||
|
"recursiveResponseTypedHTTP",
|
||||||
|
"recursiveResponseReferenceGraph",
|
||||||
|
"recursiveResponseRegenerationAndDrift",
|
||||||
|
"remoteHTTPClientRaceTests",
|
||||||
|
"remoteHTTPProxyAndTimeouts",
|
||||||
|
"remoteHTTPHeaderIsolation",
|
||||||
|
"remoteHTTPPoolAndDrainBudgets",
|
||||||
|
"remoteHTTPExample",
|
||||||
|
"remoteExplicitConfigurationRaceTests",
|
||||||
|
"literalAndRequiredDefaultReferences",
|
||||||
|
"remoteCheckConfigBeforeDependencies",
|
||||||
|
"removedImplicitEnvironmentRules",
|
||||||
|
"remotePreciseModuleDiagnostics",
|
||||||
|
"remoteJSONSuccessStatuses",
|
||||||
|
"remoteRejectedStatusBodyCleanup",
|
||||||
|
"remoteCopyTypePreflight",
|
||||||
|
"remoteEmptyCollectionTypeRejection",
|
||||||
|
"remoteLoggerTimeZoneIsolation",
|
||||||
|
"remoteCORSPolicyRace",
|
||||||
|
"remoteCORSErrorResponses",
|
||||||
|
"remoteCORSTransports",
|
||||||
|
"generatedCORSConfigOnly",
|
||||||
|
"remoteUnifiedBodyPolicyTransports",
|
||||||
|
"remoteBodyLimitsBeforeMiddleware",
|
||||||
|
"remoteHardLimitConflictBeforeServing",
|
||||||
|
"remoteCanceledHandlerCleanupBarrier",
|
||||||
|
"remoteExternalServingResourceRetention",
|
||||||
|
"remoteAdmissionCloseRace",
|
||||||
|
"remoteRuntimeFinishRetainsHTTPDependencies",
|
||||||
|
"remoteHTTP2DisconnectedHandlerDrain",
|
||||||
|
"remoteNativeEarly413AndBusinessHTTP200",
|
||||||
|
"remoteHTTPSafetyExampleBuild",
|
||||||
|
"remoteTransactionStages",
|
||||||
|
"remoteTransactionCauseChains",
|
||||||
|
"remoteCommitOutcomeUnknown",
|
||||||
|
"remoteCancellationOutcomeUnknown",
|
||||||
|
"remoteTransactionPanicAndConcurrency",
|
||||||
|
"remoteTransactionPublicErrors",
|
||||||
|
"remoteTransactionExample",
|
||||||
|
"remoteI18nRuntimeRace",
|
||||||
|
"remoteI18nGeneratedConsumer",
|
||||||
|
"remoteI18nHTTPBusinessInvariants",
|
||||||
|
"remoteI18nSourceOnlyFallback",
|
||||||
|
"remoteCLDRSamplesAndBrowserConformance",
|
||||||
|
"remoteTypeScriptConsumer",
|
||||||
|
"remoteI18nCLIExtractionCheckExportTypes",
|
||||||
|
"remoteI18nNamespaceIsolation",
|
||||||
|
"remoteBrowserRaceAndFailureRetention",
|
||||||
|
"remoteJSONReaderCausePreservation",
|
||||||
|
"flatFieldComposition",
|
||||||
|
"copyxThreeEndpointConsumer",
|
||||||
|
"explicitNullAndMoneyPolicies",
|
||||||
|
"fieldAdditionWithoutCopyRuleChanges",
|
||||||
|
"middlewareFactoryInjectionAndCleanup",
|
||||||
|
"optionalRepositoryPreservesBusiness",
|
||||||
|
"generatedUnifiedConfig",
|
||||||
|
"jsonYamlXmlAndCustomDecoder",
|
||||||
|
"configValidationBeforeDependencies",
|
||||||
|
"configEnvProvenance",
|
||||||
|
"configExtensionPreserved",
|
||||||
|
"configLoaderDriftCheck"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
|
||||||
|
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
|
||||||
|
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
|
||||||
|
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
|
||||||
|
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
|
||||||
|
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
|
||||||
|
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
|
||||||
|
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
|
||||||
|
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
|
||||||
|
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
|
||||||
|
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
|
||||||
|
"cmd/go-omni/apigen/generate.go": "20579cb9509382ec75cae5d21e84da4eaed470b854978ce55398bdfdc6d68ebf",
|
||||||
|
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
|
||||||
|
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
|
||||||
|
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
|
||||||
|
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
|
||||||
|
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
|
||||||
|
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
|
||||||
|
"scripts/verifyManagedRelease.py": "a726d2a7480926edd0538a737bf381866e83ac32defd4e34b1901c9d445012c5",
|
||||||
|
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
|
||||||
|
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
|
||||||
|
"cmd/go-omni/apigen/responseShape.go": "b0d573b5dbdfe397b0ca2bfc2e79c85c589bd11b2269f7206b3828cf0ff5afd7",
|
||||||
|
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
|
||||||
|
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
|
||||||
|
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
|
||||||
|
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
|
||||||
|
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
|
||||||
|
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
|
||||||
|
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
|
||||||
|
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
|
||||||
|
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
|
||||||
|
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
|
||||||
|
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
|
||||||
|
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
|
||||||
|
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
|
||||||
|
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
|
||||||
|
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
|
||||||
|
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
|
||||||
|
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
|
||||||
|
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
|
||||||
|
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
|
||||||
|
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
|
||||||
|
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
|
||||||
|
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
|
||||||
|
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
|
||||||
|
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
|
||||||
|
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
|
||||||
|
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
|
||||||
|
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
|
||||||
|
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
|
||||||
|
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
|
||||||
|
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
|
||||||
|
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
|
||||||
|
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
|
||||||
|
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
|
||||||
|
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
|
||||||
|
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
|
||||||
|
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
|
||||||
|
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
|
||||||
|
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
|
||||||
|
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
|
||||||
|
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
|
||||||
|
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
|
||||||
|
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
|
||||||
|
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
|
||||||
|
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
|
||||||
|
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
|
||||||
|
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
|
||||||
|
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
|
||||||
|
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
|
||||||
|
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
|
||||||
|
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
|
||||||
|
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
|
||||||
|
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
|
||||||
|
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
|
||||||
|
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
|
||||||
|
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
|
||||||
|
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
|
||||||
|
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
|
||||||
|
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
|
||||||
|
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
|
||||||
|
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
|
||||||
|
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
|
||||||
|
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
|
||||||
|
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
|
||||||
|
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
|
||||||
|
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
|
||||||
|
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
|
||||||
|
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
|
||||||
|
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
|
||||||
|
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
|
||||||
|
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
|
||||||
|
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
|
||||||
|
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
|
||||||
|
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
|
||||||
|
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
|
||||||
|
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
|
||||||
|
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
|
||||||
|
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
|
||||||
|
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
|
||||||
|
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
|
||||||
|
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
|
||||||
|
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
|
||||||
|
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
|
||||||
|
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
|
||||||
|
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
|
||||||
|
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
|
||||||
|
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
|
||||||
|
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
|
||||||
|
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
|
||||||
|
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
|
||||||
|
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
|
||||||
|
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
|
||||||
|
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
|
||||||
|
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
|
||||||
|
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
|
||||||
|
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
|
||||||
|
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
|
||||||
|
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
|
||||||
|
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
|
||||||
|
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
|
||||||
|
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
|
||||||
|
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
|
||||||
|
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
|
||||||
|
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
|
||||||
|
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
|
||||||
|
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
|
||||||
|
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
|
||||||
|
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
|
||||||
|
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
|
||||||
|
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
|
||||||
|
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
|
||||||
|
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
|
||||||
|
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
|
||||||
|
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
|
||||||
|
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
|
||||||
|
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
|
||||||
|
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
|
||||||
|
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
|
||||||
|
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
|
||||||
|
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
|
||||||
|
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
|
||||||
|
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
|
||||||
|
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
|
||||||
|
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
|
||||||
|
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
|
||||||
|
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
|
||||||
|
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
|
||||||
|
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
|
||||||
|
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
|
||||||
|
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
|
||||||
|
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
|
||||||
|
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
|
||||||
|
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
reports/recursiveResponseWorkspaceRemote.json
Normal file
34
reports/recursiveResponseWorkspaceRemote.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.2-preview",
|
||||||
|
"frameworkSum": "h1:QjHXXV184SO45GCXLyejeVDdCzLa5EtRu2ngyYpLzYI=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 66.19,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"remoteAPIModule",
|
||||||
|
"remoteGenericModule",
|
||||||
|
"explicitKinds",
|
||||||
|
"manifestInferredKinds",
|
||||||
|
"mixedFormatWithProjectSkip",
|
||||||
|
"mixedGeneration",
|
||||||
|
"mixedCheck",
|
||||||
|
"nativeGoWorkBuild",
|
||||||
|
"nativeGoWorkRaceTests",
|
||||||
|
"moduleAndWorkspaceMetadataPreserved",
|
||||||
|
"bothProjectFailuresAggregated",
|
||||||
|
"nonzeroFailureExit",
|
||||||
|
"failedCheckIsReadOnly",
|
||||||
|
"crossProjectPreflight",
|
||||||
|
"missingGeneratedOutputRestored"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/apigen/workspace.go": "cc39732ed9a41bb2dfa14933a830a7f10e8e3b2edc2ada07c376a738c0ec77e0",
|
||||||
|
"cmd/go-omni/apigen/workspaceMixed_test.go": "119f2a2aadc2b3ee2471bed1309c6ad8e36488b258f2ab1ba5ba5bab4219643f",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog_test.go": "372874322aece46714351f088b5741642fe4e63d95062743a575958fa58b8b39",
|
||||||
|
"cmd/go-omni/workspace.go": "b7bbd3d9d58b3987090edf7e3714d7d8396e7c332174022d918a52e16396397d",
|
||||||
|
"cmd/go-omni/workspace_test.go": "0170d18158325262765d8b3067f55ca3ce9fe994f2d030cafee3e82b13bfcd4b",
|
||||||
|
"scripts/verifyWorkspaceRelease.py": "0495dc97c68ac17a7969088cf94b1cbbfa6e53f18fb36147c64bc34edb260bca"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
reports/signedReadAcceptance.md
Normal file
43
reports/signedReadAcceptance.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# v0.29.0 通用签名读取验收
|
||||||
|
|
||||||
|
范围:Local、S3/R2、OSS 的签名读取选项和本地范围读取。接口不包含业务身份、租户、分享记录、数据库、撤销或固定有效期。旧 Presigner、旧本地链接和默认下载行为兼容。
|
||||||
|
|
||||||
|
## 本地回归
|
||||||
|
|
||||||
|
2026-09-30,macOS arm64,Go 1.27.1:
|
||||||
|
|
||||||
|
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./...`:57 个测试包通过,apigen 集成包 123.143 秒。
|
||||||
|
- `GOTOOLCHAIN=go1.22.12 GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./...`:57 个测试包通过,apigen 集成包 113.212 秒。
|
||||||
|
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
|
||||||
|
- `go run ./examples/signedRead -check`:真实回环 HTTP 预览、中文文件名、下载、HEAD、Range 通过。
|
||||||
|
- `python3 scripts/verifyUploads.py --output reports/signedReadMinio.json`:真实隔离 MinIO 的新读取选项、原上传/分片与存储契约通过,见 [报告](signedReadMinio.json)和[日志](signedReadMinio.txt)。独立容器无外部网络、不映射端口,已清理,原有容器保持运行。
|
||||||
|
|
||||||
|
首次 MinIO 回归中,原有契约故意发送错误长度触发 SDK 重试退避,耗尽契约的 10 秒预算。测试专用 client 改为 MaxRetries=1,与现有协议测试一致;生产 client 配置未变,修正后通过。
|
||||||
|
|
||||||
|
## 覆盖内容
|
||||||
|
|
||||||
|
- 默认 attachment、inline、可选 MIME、ASCII/中文/特殊字符文件名,拒绝 CR/LF、控制字符、路径、过长参数、非法 MIME 和 disposition。
|
||||||
|
- 本地签名绑定版本、key、有效期、全部响应选项,重复参数、未知参数、签名篡改、版本剥离和旧下载 URL 注入 inline 均拒绝,拒绝前不读存储。
|
||||||
|
- 30 分钟实际签名到期时间、TTL 上下限、精确到期边界、取消 context;过期 Range/HEAD/条件请求不得读取文件或返回缓存命中。
|
||||||
|
- 本地真实文件 GET/HEAD 内容长度、前缀/后缀/开放区间/截断/多段范围、206/416,Seek 不暴露元数据,按范围读取而非全文件缓冲;顺序读取 Store 回退完整 200。
|
||||||
|
- 错误及条件响应保持 private/no-store,HEAD 成功与失败均无响应体。
|
||||||
|
- OSS SDK V1/V4 独立重新计算签名,验证响应头在签名前加入,中文文件名、配置域名、到期时间及选项篡改;拒绝未签名配置。S3/R2 验证原生 SDK 查询参数与实际到期时间。
|
||||||
|
- MinIO 实际 GET/Range 的响应头、字节和状态正确,篡改 disposition/type/cache/expiry 被服务端拒绝。
|
||||||
|
|
||||||
|
## 验证边界
|
||||||
|
|
||||||
|
OSS/R2 未使用云账号实测;SDK/MinIO 验证不能代替实际桶和域名验收。OSS 默认域名可能强制下载,其响应头覆盖受平台规则限制,需部署方检查实际 200/206。
|
||||||
|
|
||||||
|
浏览器控制工具不可用,隔离 Chrome/Chromium 截图尝试未获得成功渲染证据,因此不计为浏览器预览通过。真实 PDF 的 HTTP 响应已检查,浏览器视觉预览与实际云域名接入仍需项目侧回归。
|
||||||
|
|
||||||
|
## 发布验收
|
||||||
|
|
||||||
|
三个独立空模块缓存安装远程固定 `v0.29.0-preview`,均无本地 replace,全部通过:
|
||||||
|
|
||||||
|
- [综合远程报告](signedReadRemote.json):107 项检查、11 组生成消费者,138.76 秒。包括下载模块中的签名读取、旧链接兼容、篡改/过期、范围读取、OSS V1/V4、S3/R2 和真实 HTTP 示例,以及原有生成器、生命周期、配置、i18n 等回归。
|
||||||
|
- [通用工程报告](signedReadProgramRemote.json):30 项,31.82 秒。
|
||||||
|
- [混合工作区报告](signedReadWorkspaceRemote.json):16 项,25.55 秒。
|
||||||
|
|
||||||
|
首次综合检查因生成的基础消费者未引入可选云存储 SDK,缺少相应 go.sum 条目而中止。验收脚本增加固定版本的可选适配器依赖准备后,从空缓存重跑全部通过;框架发布代码未修改。
|
||||||
|
|
||||||
|
三次下载 checksum 一致:`h1:FV3AzbYWBFQ7qnF3T2FWypGv2PSecCJfcOuNa2yKzRk=`。固定标签对应代码 `f0c62152d72b1b50672c29f5191ad88f8be3eee4`。验收脚本、说明和远程报告另行提交,不移动标签。
|
||||||
111
reports/signedReadMinio.json
Normal file
111
reports/signedReadMinio.json
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"startedAt": "2026-09-30T14:35:32.113432+00:00",
|
||||||
|
"architecture": "arm64",
|
||||||
|
"goVersion": "go version go1.27.1 darwin/arm64",
|
||||||
|
"image": "quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z",
|
||||||
|
"imageId": "sha256:8f08aee614800a237906bd48114d733e5ac5bfac4ccdf731f141b0e880d7a253",
|
||||||
|
"sourceSha256": {
|
||||||
|
"go.mod": "e3877788b18b605a007530f80b684f06a7ae308844c2959a8750ed55ae8037a3",
|
||||||
|
"go.sum": "53c136b153ec7c91731f7c927dfda540fbd683037f3fbd1f3d067b1ec7aabc15",
|
||||||
|
"internal/storagefixture/multipart.go": "da9ec938b3bdb872e10e4f874b6a0224121c14a631dcf4e86d7ebbc1cd34a770",
|
||||||
|
"internal/storagefixture/server.go": "defacc905246a643faf5371e437f9ce458f05461e0a922766034d1f9ffdaf003",
|
||||||
|
"storage/local/local.go": "5d1ca75d0d2ca87981eef84a60bf98d0feefa2a9594940d6bd231877f4f8ec02",
|
||||||
|
"storage/local/local_test.go": "3a2241840968df8e37fd67169c3ef708228449e81f65aff908c8b60d49434dc0",
|
||||||
|
"storage/local/read_test.go": "ef1f7feb0887ea4b1f30304dfaf542fe9788b6e17856c942f1cd98448656194f",
|
||||||
|
"storage/multipart.go": "3b718ebe6da681c89a3f797892c88e7acb15c295df2376d64ac35da0976ed69d",
|
||||||
|
"storage/multipart_test.go": "4053f680f7795db2a6b4aa23590b3669eec7a609dd92c4a1e20891a5299274ee",
|
||||||
|
"storage/oss/oss.go": "4216ce9f3fe3ac5418a5b809db3cef7e99dcca72a25a2a12301f5897cda85dd5",
|
||||||
|
"storage/oss/oss_test.go": "555a954010b1218804dc7951fc75e42eae24eba9a99c0871f5ba787f3fc87d45",
|
||||||
|
"storage/oss/read.go": "429a9a9f6c586a885d7cccb76c9d572d53a79df637ff128caacb2eac35b8d862",
|
||||||
|
"storage/oss/read_test.go": "a1c273cfedd2f3ee31ce407a7ddd6fd5ad02c65ad632a91bc7f3ead3712b5bbd",
|
||||||
|
"storage/oss/upload.go": "556129611f9e5549cac8a36b4026bfb19c112a295b55192579f9ac116e20626f",
|
||||||
|
"storage/oss/upload_test.go": "08eb0ed157f3743288d781f61d4d2b7bfe77852a8259c5e598c719d35889409e",
|
||||||
|
"storage/read.go": "92787c74ae024822f82b5b84bffc9f8a355f1f2565b99ca7fbefe49157ef4851",
|
||||||
|
"storage/read_test.go": "9b11ab9640e98c1c9fa7ce1e2587df217e874e3a6a6e27a0dd0ade6fbf3d5e1a",
|
||||||
|
"storage/s3/config.go": "626ec6a2c0b06c95ee7eb7aa0832b190cfcdeb8a127b6b89e99204040bc3b041",
|
||||||
|
"storage/s3/read.go": "319b57f2ecf435b71085c7cdcafd50a1e30caa5f357f83d4db3cdcef085792b6",
|
||||||
|
"storage/s3/read_test.go": "9b64fda5c83ba41508ec23cf41aeeb9826b677caaa851dcbeb04abe1f43eed65",
|
||||||
|
"storage/s3/s3.go": "016bef5948e97f3d043d2e858716f1c02d9f27c4e02802bdc1be3860116f4b6c",
|
||||||
|
"storage/s3/s3_test.go": "9ef284926ec9936ddfec8c52a9bf04fc7b4be55c43a535f6c5a428d1535f6fec",
|
||||||
|
"storage/s3/upload.go": "3ce0b3626249ffe7a041606d6a9481743af8a0d4868fadea20ed3ed44ee0c765",
|
||||||
|
"storage/s3/upload_test.go": "a41fdbd100d9c79a3606c5942f98017afe26d841b251d96e979df87d0af93867",
|
||||||
|
"storage/signed.go": "ae4b144137627be35718c40743ebc2f063ec667c24b6d8984ccfcca034a73787",
|
||||||
|
"storage/signedUpload.go": "a8d31bcbbad64796f04611e69271e94d917c9bcd5aeabd7a97ea155b0093c814",
|
||||||
|
"storage/signed_test.go": "eaffd40894018814e976477f96cf0fbaecfa1987caad78830d9fc8e96995882f",
|
||||||
|
"storage/storage.go": "cb670872f01cf1d7b4ebf21cec9fc6f52ad93bfdf523b2f28bbb8d9c7fe39a6c",
|
||||||
|
"storage/storagetest/contract.go": "e7ad7242102437882269d4066462059d7974586b36a82d18639eca10fd932be9",
|
||||||
|
"storage/storagetest/uploads.go": "9b66f86dd94ccdf30c7d603d432bc2a55956608898e46c1274fa27d16fa1a969",
|
||||||
|
"storage/upload.go": "9ff2930ba389163acc8063b51107c09a60290609ba384de8004c498ce09f20a1",
|
||||||
|
"storage/upload_test.go": "12b60419807ed64b89e30861674e19abee7b0b7bb0e8ec9ac5b3aa2e0710a1da"
|
||||||
|
},
|
||||||
|
"commands": [
|
||||||
|
[
|
||||||
|
"go",
|
||||||
|
"test",
|
||||||
|
"-p=2",
|
||||||
|
"-c",
|
||||||
|
"-o",
|
||||||
|
"/var/folders/wm/1sg73x8n7879sp3t6m4jpnrr0000gn/T/goOmniUpload3cba6ibp/s3.test",
|
||||||
|
"./storage/s3"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--name",
|
||||||
|
"go-omni-upload-c388755df4ec",
|
||||||
|
"--platform",
|
||||||
|
"linux/arm64",
|
||||||
|
"--cpus",
|
||||||
|
"2",
|
||||||
|
"--memory",
|
||||||
|
"2048m",
|
||||||
|
"--memory-swap",
|
||||||
|
"2048m",
|
||||||
|
"--pids-limit",
|
||||||
|
"256",
|
||||||
|
"--network",
|
||||||
|
"none",
|
||||||
|
"--read-only",
|
||||||
|
"--cap-drop",
|
||||||
|
"ALL",
|
||||||
|
"--security-opt",
|
||||||
|
"no-new-privileges",
|
||||||
|
"--user",
|
||||||
|
"1000:1000",
|
||||||
|
"--tmpfs",
|
||||||
|
"/tmp:rw,nosuid,noexec,size=64m,mode=1777",
|
||||||
|
"--tmpfs",
|
||||||
|
"/data:rw,nosuid,noexec,size=128m,mode=1777",
|
||||||
|
"-v",
|
||||||
|
"/var/folders/wm/1sg73x8n7879sp3t6m4jpnrr0000gn/T/goOmniUpload3cba6ibp:/test:ro",
|
||||||
|
"-e",
|
||||||
|
"GOMAXPROCS=2",
|
||||||
|
"-e",
|
||||||
|
"MINIO_ROOT_USER=goOmniTest",
|
||||||
|
"-e",
|
||||||
|
"MINIO_ROOT_PASSWORD=goOmniTestSecret",
|
||||||
|
"-e",
|
||||||
|
"GO_OMNI_S3_TEST_ADDR=127.0.0.1:9000",
|
||||||
|
"--entrypoint",
|
||||||
|
"sh",
|
||||||
|
"quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z",
|
||||||
|
"-c",
|
||||||
|
"set -eu\nminio --version\nminio server /data --address 127.0.0.1:9000 --console-address 127.0.0.1:9001 >/tmp/minio.log 2>&1 &\nserver=$!\ntrap 'kill \"$server\" 2>/dev/null || true; wait \"$server\" 2>/dev/null || true' EXIT\n/test/s3.test -test.v -test.run '^TestS3Integration$' -test.count=1 -test.timeout=60s || { cat /tmp/minio.log; exit 1; }\n"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"containerLimits": {
|
||||||
|
"cpus": 2,
|
||||||
|
"memoryBytes": 2147483648,
|
||||||
|
"pids": 256,
|
||||||
|
"network": "none"
|
||||||
|
},
|
||||||
|
"notes": [
|
||||||
|
"MinIO verifies S3 signatures; OSS/R2 real cloud accounts are not exercised."
|
||||||
|
],
|
||||||
|
"preExistingContainersStillRunning": true,
|
||||||
|
"cleanedContainer": "go-omni-upload-c388755df4ec",
|
||||||
|
"finishedAt": "2026-09-30T14:35:34.723540+00:00",
|
||||||
|
"logSha256": "c25db6c44baee149c1e3d6d3b7f81faac9483eb79709bed25f61b03d7b9b3133"
|
||||||
|
}
|
||||||
7
reports/signedReadMinio.txt
Normal file
7
reports/signedReadMinio.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
minio version RELEASE.2025-09-07T16-13-09Z (commit-id=07c3a429bfed433e49018cb0f78a52145d4bedeb)
|
||||||
|
Runtime: go1.24.6 linux/arm64
|
||||||
|
License: GNU AGPLv3 - https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
Copyright: 2015-2025 MinIO, Inc.
|
||||||
|
=== RUN TestS3Integration
|
||||||
|
--- PASS: TestS3Integration (0.63s)
|
||||||
|
PASS
|
||||||
74
reports/signedReadProgramRemote.json
Normal file
74
reports/signedReadProgramRemote.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.29.0-preview",
|
||||||
|
"frameworkSum": "h1:FV3AzbYWBFQ7qnF3T2FWypGv2PSecCJfcOuNa2yKzRk=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 31.82,
|
||||||
|
"tests": [
|
||||||
|
"TestConflictRollsBackEarlierWrites",
|
||||||
|
"TestUnsafePaths",
|
||||||
|
"TestGeneratedProgramConsumer",
|
||||||
|
"TestOwnershipAndDrift",
|
||||||
|
"TestRecordedTemplateUpgrade",
|
||||||
|
"TestProjectGuards/module",
|
||||||
|
"TestProjectGuards/oldFramework",
|
||||||
|
"TestProjectGuards/manifest",
|
||||||
|
"TestProjectGuards/lock",
|
||||||
|
"TestProjectGuards/otherGenerator",
|
||||||
|
"TestProjectGuards/symlink",
|
||||||
|
"TestProjectGuards"
|
||||||
|
],
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"installedVersionDefault",
|
||||||
|
"generatedConsumerBuildRaceVet",
|
||||||
|
"defaultNoAPIFiles",
|
||||||
|
"defaultNoNetworkListener",
|
||||||
|
"finiteProgram",
|
||||||
|
"SIGINT",
|
||||||
|
"SIGTERM",
|
||||||
|
"initializationFailureAndPanic",
|
||||||
|
"runErrorPanicGoexit",
|
||||||
|
"fatalComponentFailure",
|
||||||
|
"nonzeroProcessFailureAndTimeoutExits",
|
||||||
|
"partialStartupDraining",
|
||||||
|
"acknowledgementBeforeResourceClose",
|
||||||
|
"workContextSurvivesGracefulStop",
|
||||||
|
"cancellationGrace",
|
||||||
|
"unresponsiveRunAndBackgroundWork",
|
||||||
|
"retainedResourcesAndLogger",
|
||||||
|
"explicitConfigurationOnly",
|
||||||
|
"checkConfigWithoutDependencies",
|
||||||
|
"handwrittenFilesPreserved",
|
||||||
|
"generatedFileConflict",
|
||||||
|
"missingGeneratedFileRestored",
|
||||||
|
"missingHandwrittenFileNotRecreated",
|
||||||
|
"readOnlyCheckAndDryRun",
|
||||||
|
"templateDriftUpgrade",
|
||||||
|
"manifestIdentityAndVersionGuards",
|
||||||
|
"symlinkAndLockProtection",
|
||||||
|
"fileTransactionRollback",
|
||||||
|
"publishedExampleFiniteRun"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
|
||||||
|
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
|
||||||
|
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
|
||||||
|
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
|
||||||
|
"cmd/go-omni/projectgen/generate.go": "da0cfb726c81ebe1b3dcf306130650f65d67c70df7bdc6b0899251d75d7f9b94",
|
||||||
|
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
|
||||||
|
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
|
||||||
|
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
|
||||||
|
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
|
||||||
|
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
|
||||||
|
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
|
||||||
|
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
|
||||||
|
}
|
||||||
|
}
|
||||||
331
reports/signedReadRemote.json
Normal file
331
reports/signedReadRemote.json
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.29.0-preview",
|
||||||
|
"frameworkSum": "h1:FV3AzbYWBFQ7qnF3T2FWypGv2PSecCJfcOuNa2yKzRk=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 138.76,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"freshModuleCache",
|
||||||
|
"fixedVersionNoReplace",
|
||||||
|
"contractChangePreview",
|
||||||
|
"persistentLogicSignatureFailure",
|
||||||
|
"businessSourcePreserved",
|
||||||
|
"fixedConsumerBuild",
|
||||||
|
"rootGoRun",
|
||||||
|
"nethttpAndNativeSignals",
|
||||||
|
"listenerReleased",
|
||||||
|
"default404And405",
|
||||||
|
"goOwnedErrorCatalog",
|
||||||
|
"unwrappedDownloads",
|
||||||
|
"rawCallback",
|
||||||
|
"multipart400And413",
|
||||||
|
"nethttpAndNativeTransfers",
|
||||||
|
"sharedGoCatalogExport",
|
||||||
|
"documentOnlyErrorSelection",
|
||||||
|
"workspaceCheckNoWrites",
|
||||||
|
"staleCatalogRejected",
|
||||||
|
"twoIndependentServiceBuilds",
|
||||||
|
"initializationAndListenFailureCleanup",
|
||||||
|
"drainBeforeDependencyClose",
|
||||||
|
"generatedConsumerRaceTests",
|
||||||
|
"errorParamsAndInheritance",
|
||||||
|
"manualHooksAndScalars",
|
||||||
|
"dependencyFatalShutdown",
|
||||||
|
"fatalProcessExitCode",
|
||||||
|
"initializationShutdownNotification",
|
||||||
|
"configuredBuiltinEndpoints",
|
||||||
|
"projectHealthEnvelope",
|
||||||
|
"endpointConflictCleanup",
|
||||||
|
"responseArrayNullableNull",
|
||||||
|
"primitiveAndDynamicArrays",
|
||||||
|
"strictRequestPolicies",
|
||||||
|
"declarativeMultipartBinding",
|
||||||
|
"backgroundStopDrainResourceOrder",
|
||||||
|
"backgroundInitAndListenFailureCleanup",
|
||||||
|
"backgroundTimeoutRetainsResourcesAndExits",
|
||||||
|
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
|
||||||
|
"recursiveResponseTypedHTTP",
|
||||||
|
"recursiveResponseReferenceGraph",
|
||||||
|
"recursiveResponseRegenerationAndDrift",
|
||||||
|
"remoteHTTPClientRaceTests",
|
||||||
|
"remoteHTTPProxyAndTimeouts",
|
||||||
|
"remoteHTTPHeaderIsolation",
|
||||||
|
"remoteHTTPPoolAndDrainBudgets",
|
||||||
|
"remoteHTTPExample",
|
||||||
|
"remoteExplicitConfigurationRaceTests",
|
||||||
|
"literalAndRequiredDefaultReferences",
|
||||||
|
"remoteCheckConfigBeforeDependencies",
|
||||||
|
"removedImplicitEnvironmentRules",
|
||||||
|
"remoteSignedReadOptionsAndLegacyLinks",
|
||||||
|
"remoteReadSignatureTamperingAndExpiry",
|
||||||
|
"remoteLocalPayloadRangesAndHEAD",
|
||||||
|
"remoteOSSNativeV1V4Signatures",
|
||||||
|
"remoteS3R2ReadOptions",
|
||||||
|
"remoteSignedReadHTTPExample",
|
||||||
|
"remotePreciseModuleDiagnostics",
|
||||||
|
"remoteJSONSuccessStatuses",
|
||||||
|
"remoteRejectedStatusBodyCleanup",
|
||||||
|
"remoteCopyTypePreflight",
|
||||||
|
"remoteEmptyCollectionTypeRejection",
|
||||||
|
"remoteLoggerTimeZoneIsolation",
|
||||||
|
"remoteCORSPolicyRace",
|
||||||
|
"remoteCORSErrorResponses",
|
||||||
|
"remoteCORSTransports",
|
||||||
|
"generatedCORSConfigOnly",
|
||||||
|
"remoteUnifiedBodyPolicyTransports",
|
||||||
|
"remoteBodyLimitsBeforeMiddleware",
|
||||||
|
"remoteHardLimitConflictBeforeServing",
|
||||||
|
"remoteCanceledHandlerCleanupBarrier",
|
||||||
|
"remoteExternalServingResourceRetention",
|
||||||
|
"remoteAdmissionCloseRace",
|
||||||
|
"remoteRuntimeFinishRetainsHTTPDependencies",
|
||||||
|
"remoteHTTP2DisconnectedHandlerDrain",
|
||||||
|
"remoteNativeEarly413AndBusinessHTTP200",
|
||||||
|
"remoteHTTPSafetyExampleBuild",
|
||||||
|
"remoteTransactionStages",
|
||||||
|
"remoteTransactionCauseChains",
|
||||||
|
"remoteCommitOutcomeUnknown",
|
||||||
|
"remoteCancellationOutcomeUnknown",
|
||||||
|
"remoteTransactionPanicAndConcurrency",
|
||||||
|
"remoteTransactionPublicErrors",
|
||||||
|
"remoteTransactionExample",
|
||||||
|
"remoteI18nRuntimeRace",
|
||||||
|
"remoteI18nGeneratedConsumer",
|
||||||
|
"remoteI18nHTTPBusinessInvariants",
|
||||||
|
"remoteI18nSourceOnlyFallback",
|
||||||
|
"remoteCLDRSamplesAndBrowserConformance",
|
||||||
|
"remoteTypeScriptConsumer",
|
||||||
|
"remoteI18nCLIExtractionCheckExportTypes",
|
||||||
|
"remoteI18nNamespaceIsolation",
|
||||||
|
"remoteBrowserRaceAndFailureRetention",
|
||||||
|
"remoteJSONReaderCausePreservation",
|
||||||
|
"flatFieldComposition",
|
||||||
|
"copyxThreeEndpointConsumer",
|
||||||
|
"explicitNullAndMoneyPolicies",
|
||||||
|
"fieldAdditionWithoutCopyRuleChanges",
|
||||||
|
"middlewareFactoryInjectionAndCleanup",
|
||||||
|
"optionalRepositoryPreservesBusiness",
|
||||||
|
"generatedUnifiedConfig",
|
||||||
|
"jsonYamlXmlAndCustomDecoder",
|
||||||
|
"configValidationBeforeDependencies",
|
||||||
|
"configEnvProvenance",
|
||||||
|
"configExtensionPreserved",
|
||||||
|
"configLoaderDriftCheck"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
|
||||||
|
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
|
||||||
|
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
|
||||||
|
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
|
||||||
|
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
|
||||||
|
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
|
||||||
|
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
|
||||||
|
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
|
||||||
|
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
|
||||||
|
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
|
||||||
|
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
|
||||||
|
"cmd/go-omni/apigen/generate.go": "c97c8c797105516e8e6870efffd662c7894e99d882a36207dee0162016b728fd",
|
||||||
|
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
|
||||||
|
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
|
||||||
|
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
|
||||||
|
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
|
||||||
|
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
|
||||||
|
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
|
||||||
|
"scripts/verifyManagedRelease.py": "4ef18cd08909ce0792af150539219c542facecc98204f1dd938c0646acc32e4e",
|
||||||
|
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
|
||||||
|
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
|
||||||
|
"cmd/go-omni/apigen/responseShape.go": "b0d573b5dbdfe397b0ca2bfc2e79c85c589bd11b2269f7206b3828cf0ff5afd7",
|
||||||
|
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
|
||||||
|
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
|
||||||
|
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
|
||||||
|
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
|
||||||
|
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
|
||||||
|
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
|
||||||
|
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
|
||||||
|
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
|
||||||
|
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
|
||||||
|
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
|
||||||
|
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
|
||||||
|
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
|
||||||
|
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
|
||||||
|
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
|
||||||
|
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
|
||||||
|
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
|
||||||
|
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
|
||||||
|
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
|
||||||
|
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
|
||||||
|
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
|
||||||
|
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
|
||||||
|
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
|
||||||
|
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
|
||||||
|
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
|
||||||
|
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
|
||||||
|
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
|
||||||
|
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
|
||||||
|
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
|
||||||
|
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
|
||||||
|
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
|
||||||
|
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
|
||||||
|
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
|
||||||
|
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
|
||||||
|
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
|
||||||
|
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
|
||||||
|
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
|
||||||
|
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
|
||||||
|
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
|
||||||
|
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
|
||||||
|
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
|
||||||
|
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
|
||||||
|
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
|
||||||
|
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
|
||||||
|
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
|
||||||
|
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
|
||||||
|
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
|
||||||
|
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
|
||||||
|
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
|
||||||
|
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
|
||||||
|
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
|
||||||
|
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
|
||||||
|
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
|
||||||
|
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
|
||||||
|
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
|
||||||
|
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
|
||||||
|
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
|
||||||
|
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
|
||||||
|
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
|
||||||
|
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
|
||||||
|
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
|
||||||
|
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
|
||||||
|
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
|
||||||
|
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
|
||||||
|
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
|
||||||
|
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
|
||||||
|
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
|
||||||
|
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
|
||||||
|
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
|
||||||
|
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
|
||||||
|
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
|
||||||
|
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
|
||||||
|
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
|
||||||
|
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
|
||||||
|
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
|
||||||
|
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
|
||||||
|
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
|
||||||
|
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
|
||||||
|
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
|
||||||
|
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
|
||||||
|
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
|
||||||
|
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
|
||||||
|
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
|
||||||
|
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
|
||||||
|
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
|
||||||
|
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
|
||||||
|
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
|
||||||
|
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
|
||||||
|
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
|
||||||
|
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
|
||||||
|
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
|
||||||
|
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
|
||||||
|
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
|
||||||
|
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
|
||||||
|
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
|
||||||
|
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
|
||||||
|
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
|
||||||
|
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
|
||||||
|
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
|
||||||
|
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
|
||||||
|
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
|
||||||
|
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
|
||||||
|
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
|
||||||
|
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
|
||||||
|
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
|
||||||
|
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
|
||||||
|
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
|
||||||
|
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
|
||||||
|
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
|
||||||
|
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
|
||||||
|
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
|
||||||
|
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
|
||||||
|
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
|
||||||
|
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
|
||||||
|
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
|
||||||
|
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
|
||||||
|
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
|
||||||
|
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
|
||||||
|
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
|
||||||
|
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
|
||||||
|
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
|
||||||
|
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
|
||||||
|
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
|
||||||
|
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
|
||||||
|
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
|
||||||
|
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
|
||||||
|
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
|
||||||
|
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
|
||||||
|
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
|
||||||
|
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
|
||||||
|
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
|
||||||
|
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
|
||||||
|
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15",
|
||||||
|
"storage/upload.go": "9ff2930ba389163acc8063b51107c09a60290609ba384de8004c498ce09f20a1",
|
||||||
|
"storage/read_test.go": "9b11ab9640e98c1c9fa7ce1e2587df217e874e3a6a6e27a0dd0ade6fbf3d5e1a",
|
||||||
|
"storage/signedUpload.go": "a8d31bcbbad64796f04611e69271e94d917c9bcd5aeabd7a97ea155b0093c814",
|
||||||
|
"storage/multipart_test.go": "4053f680f7795db2a6b4aa23590b3669eec7a609dd92c4a1e20891a5299274ee",
|
||||||
|
"storage/multipart.go": "3b718ebe6da681c89a3f797892c88e7acb15c295df2376d64ac35da0976ed69d",
|
||||||
|
"storage/signed_test.go": "eaffd40894018814e976477f96cf0fbaecfa1987caad78830d9fc8e96995882f",
|
||||||
|
"storage/upload_test.go": "12b60419807ed64b89e30861674e19abee7b0b7bb0e8ec9ac5b3aa2e0710a1da",
|
||||||
|
"storage/storage.go": "cb670872f01cf1d7b4ebf21cec9fc6f52ad93bfdf523b2f28bbb8d9c7fe39a6c",
|
||||||
|
"storage/signed.go": "ae4b144137627be35718c40743ebc2f063ec667c24b6d8984ccfcca034a73787",
|
||||||
|
"storage/read.go": "92787c74ae024822f82b5b84bffc9f8a355f1f2565b99ca7fbefe49157ef4851",
|
||||||
|
"storage/s3/upload.go": "3ce0b3626249ffe7a041606d6a9481743af8a0d4868fadea20ed3ed44ee0c765",
|
||||||
|
"storage/s3/read_test.go": "9b64fda5c83ba41508ec23cf41aeeb9826b677caaa851dcbeb04abe1f43eed65",
|
||||||
|
"storage/s3/config.go": "626ec6a2c0b06c95ee7eb7aa0832b190cfcdeb8a127b6b89e99204040bc3b041",
|
||||||
|
"storage/s3/upload_test.go": "a41fdbd100d9c79a3606c5942f98017afe26d841b251d96e979df87d0af93867",
|
||||||
|
"storage/s3/read.go": "319b57f2ecf435b71085c7cdcafd50a1e30caa5f357f83d4db3cdcef085792b6",
|
||||||
|
"storage/s3/s3_test.go": "9ef284926ec9936ddfec8c52a9bf04fc7b4be55c43a535f6c5a428d1535f6fec",
|
||||||
|
"storage/s3/s3.go": "016bef5948e97f3d043d2e858716f1c02d9f27c4e02802bdc1be3860116f4b6c",
|
||||||
|
"storage/local/read_test.go": "ef1f7feb0887ea4b1f30304dfaf542fe9788b6e17856c942f1cd98448656194f",
|
||||||
|
"storage/local/local_test.go": "3a2241840968df8e37fd67169c3ef708228449e81f65aff908c8b60d49434dc0",
|
||||||
|
"storage/local/local.go": "5d1ca75d0d2ca87981eef84a60bf98d0feefa2a9594940d6bd231877f4f8ec02",
|
||||||
|
"storage/oss/upload.go": "556129611f9e5549cac8a36b4026bfb19c112a295b55192579f9ac116e20626f",
|
||||||
|
"storage/oss/read_test.go": "a1c273cfedd2f3ee31ce407a7ddd6fd5ad02c65ad632a91bc7f3ead3712b5bbd",
|
||||||
|
"storage/oss/oss_test.go": "555a954010b1218804dc7951fc75e42eae24eba9a99c0871f5ba787f3fc87d45",
|
||||||
|
"storage/oss/upload_test.go": "08eb0ed157f3743288d781f61d4d2b7bfe77852a8259c5e598c719d35889409e",
|
||||||
|
"storage/oss/oss.go": "4216ce9f3fe3ac5418a5b809db3cef7e99dcca72a25a2a12301f5897cda85dd5",
|
||||||
|
"storage/oss/read.go": "429a9a9f6c586a885d7cccb76c9d572d53a79df637ff128caacb2eac35b8d862",
|
||||||
|
"storage/storagetest/contract.go": "e7ad7242102437882269d4066462059d7974586b36a82d18639eca10fd932be9",
|
||||||
|
"storage/storagetest/uploads.go": "9b66f86dd94ccdf30c7d603d432bc2a55956608898e46c1274fa27d16fa1a969",
|
||||||
|
"examples/signedRead/main.go": "f921a3c5fbcbfa84291c0c95a9c2c211319a8f0537acc5ec640238f251e652e1",
|
||||||
|
"examples/signedRead/main_test.go": "6ab559c442b2aa355231efe0342b9ec3617bf97bb1f6dfd7ad304b9a452e4fc4"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
reports/signedReadWorkspaceRemote.json
Normal file
34
reports/signedReadWorkspaceRemote.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.29.0-preview",
|
||||||
|
"frameworkSum": "h1:FV3AzbYWBFQ7qnF3T2FWypGv2PSecCJfcOuNa2yKzRk=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 25.55,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"remoteAPIModule",
|
||||||
|
"remoteGenericModule",
|
||||||
|
"explicitKinds",
|
||||||
|
"manifestInferredKinds",
|
||||||
|
"mixedFormatWithProjectSkip",
|
||||||
|
"mixedGeneration",
|
||||||
|
"mixedCheck",
|
||||||
|
"nativeGoWorkBuild",
|
||||||
|
"nativeGoWorkRaceTests",
|
||||||
|
"moduleAndWorkspaceMetadataPreserved",
|
||||||
|
"bothProjectFailuresAggregated",
|
||||||
|
"nonzeroFailureExit",
|
||||||
|
"failedCheckIsReadOnly",
|
||||||
|
"crossProjectPreflight",
|
||||||
|
"missingGeneratedOutputRestored"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/apigen/workspace.go": "cc39732ed9a41bb2dfa14933a830a7f10e8e3b2edc2ada07c376a738c0ec77e0",
|
||||||
|
"cmd/go-omni/apigen/workspaceMixed_test.go": "119f2a2aadc2b3ee2471bed1309c6ad8e36488b258f2ab1ba5ba5bab4219643f",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog_test.go": "372874322aece46714351f088b5741642fe4e63d95062743a575958fa58b8b39",
|
||||||
|
"cmd/go-omni/workspace.go": "b7bbd3d9d58b3987090edf7e3714d7d8396e7c332174022d918a52e16396397d",
|
||||||
|
"cmd/go-omni/workspace_test.go": "0170d18158325262765d8b3067f55ca3ce9fe994f2d030cafee3e82b13bfcd4b",
|
||||||
|
"scripts/verifyWorkspaceRelease.py": "0495dc97c68ac17a7969088cf94b1cbbfa6e53f18fb36147c64bc34edb260bca"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
reports/workspaceAcceptance.md
Normal file
34
reports/workspaceAcceptance.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# v0.28.1 混合工作区验收
|
||||||
|
|
||||||
|
2026-09-30,macOS arm64。范围为 CLI 工程识别与命令分发,不改变运行时接口、生成模板归属或业务规则。
|
||||||
|
|
||||||
|
## 发布前回归
|
||||||
|
|
||||||
|
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./...`:56 个测试包通过,包含实际生成消费者。
|
||||||
|
- `GOTOOLCHAIN=go1.22.12 GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./cmd/go-omni/...`:5 个 CLI 包通过,含 API 与通用工程消费者。
|
||||||
|
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
|
||||||
|
|
||||||
|
## 行为覆盖
|
||||||
|
|
||||||
|
- 显式 kind 与按清单自动识别,两类工程混合及纯通用工程工作区。
|
||||||
|
- 原有无 kind 的 API 清单、首次生成时显式 api 入口保持兼容。
|
||||||
|
- 未知类型、类型与已有清单冲突、两种清单并存、project 携带 api、错误 module、损坏清单、重复 JSON 键、越界/重叠路径与符号链接均拒绝。
|
||||||
|
- check 汇总两个工程的生成漂移,也在一个工程身份错误时继续检查其他工程;失败与成功均不写文件。
|
||||||
|
- format 对通用工程显示 SKIP,即使它的 Go 文件存在漂移也不修改;API 格式化规则保持。
|
||||||
|
- 任意一类工程预检失败时,另一类工程的待补生成文件不被提前写入;所有预检通过后可补回缺失产物。
|
||||||
|
- 手写文件、go.work、go.work.sum、各 module 的 go.mod/go.sum 保持。
|
||||||
|
- 原有显式共享 Go 错误目录提供方在加入通用工程后仍可生成、检查,失败不改项目文件。
|
||||||
|
- 真实 CLI 创建两个独立 module,执行混合 format/generate/check,然后用 Go 原生 go.work 完成 race 测试与多模块构建。
|
||||||
|
- 真实 CLI 遇到两个工程漂移时汇总输出,退出码 1;失败生成保持原工作区内容,修复后可恢复生成与检查。
|
||||||
|
|
||||||
|
检查只涉及生成产物,不替代业务测试。显式 errorCatalogs 仍执行受信任的 Go 导出函数;生成事务按单个工程提交,不保证跨模块的崩溃原子性。
|
||||||
|
|
||||||
|
## 远程发布验收
|
||||||
|
|
||||||
|
三个独立空模块缓存安装固定远程 v0.28.1-preview,均未使用本地 replace:
|
||||||
|
|
||||||
|
- [混合工作区报告](workspaceRemote.json):16 项检查通过,耗时 21.74 秒。远程 CLI 创建两类 module,完成显式/隐式类型识别、混合生成检查、真实 go.work race/构建、失败汇总与写入前预检。
|
||||||
|
- [通用工程报告](workspaceProgramRemote.json):30 项检查通过,耗时 25.69 秒,含真实进程退出、资源保留与手写代码保护。
|
||||||
|
- [既有生成器报告](workspaceManagedRemote.json):98 项检查、10 组消费者通过,耗时 108.92 秒。包含配置、HTTP 安全、国际化与 TypeScript;TypeScript 5.6.3 通过 npm exec 提供,未跳过。
|
||||||
|
- 三组远程模块 checksum 一致:`h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=`。
|
||||||
|
- 发布标签固定在代码提交 `0d2c6040e2694795f203304148dbaec00318c608`,标签对象 `e0e483b2741f92c8e44a0e45e56d78a920cb3ebb`。验收文档随后独立提交,发布标签不移动。
|
||||||
292
reports/workspaceManagedRemote.json
Normal file
292
reports/workspaceManagedRemote.json
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.1-preview",
|
||||||
|
"frameworkSum": "h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 108.92,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"freshModuleCache",
|
||||||
|
"fixedVersionNoReplace",
|
||||||
|
"contractChangePreview",
|
||||||
|
"persistentLogicSignatureFailure",
|
||||||
|
"businessSourcePreserved",
|
||||||
|
"fixedConsumerBuild",
|
||||||
|
"rootGoRun",
|
||||||
|
"nethttpAndNativeSignals",
|
||||||
|
"listenerReleased",
|
||||||
|
"default404And405",
|
||||||
|
"goOwnedErrorCatalog",
|
||||||
|
"unwrappedDownloads",
|
||||||
|
"rawCallback",
|
||||||
|
"multipart400And413",
|
||||||
|
"nethttpAndNativeTransfers",
|
||||||
|
"sharedGoCatalogExport",
|
||||||
|
"documentOnlyErrorSelection",
|
||||||
|
"workspaceCheckNoWrites",
|
||||||
|
"staleCatalogRejected",
|
||||||
|
"twoIndependentServiceBuilds",
|
||||||
|
"initializationAndListenFailureCleanup",
|
||||||
|
"drainBeforeDependencyClose",
|
||||||
|
"generatedConsumerRaceTests",
|
||||||
|
"errorParamsAndInheritance",
|
||||||
|
"manualHooksAndScalars",
|
||||||
|
"dependencyFatalShutdown",
|
||||||
|
"fatalProcessExitCode",
|
||||||
|
"initializationShutdownNotification",
|
||||||
|
"configuredBuiltinEndpoints",
|
||||||
|
"projectHealthEnvelope",
|
||||||
|
"endpointConflictCleanup",
|
||||||
|
"responseArrayNullableNull",
|
||||||
|
"primitiveAndDynamicArrays",
|
||||||
|
"strictRequestPolicies",
|
||||||
|
"declarativeMultipartBinding",
|
||||||
|
"backgroundStopDrainResourceOrder",
|
||||||
|
"backgroundInitAndListenFailureCleanup",
|
||||||
|
"backgroundTimeoutRetainsResourcesAndExits",
|
||||||
|
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
|
||||||
|
"remoteHTTPClientRaceTests",
|
||||||
|
"remoteHTTPProxyAndTimeouts",
|
||||||
|
"remoteHTTPHeaderIsolation",
|
||||||
|
"remoteHTTPPoolAndDrainBudgets",
|
||||||
|
"remoteHTTPExample",
|
||||||
|
"remoteExplicitConfigurationRaceTests",
|
||||||
|
"literalAndRequiredDefaultReferences",
|
||||||
|
"remoteCheckConfigBeforeDependencies",
|
||||||
|
"removedImplicitEnvironmentRules",
|
||||||
|
"remotePreciseModuleDiagnostics",
|
||||||
|
"remoteJSONSuccessStatuses",
|
||||||
|
"remoteRejectedStatusBodyCleanup",
|
||||||
|
"remoteCopyTypePreflight",
|
||||||
|
"remoteEmptyCollectionTypeRejection",
|
||||||
|
"remoteLoggerTimeZoneIsolation",
|
||||||
|
"remoteCORSPolicyRace",
|
||||||
|
"remoteCORSErrorResponses",
|
||||||
|
"remoteCORSTransports",
|
||||||
|
"generatedCORSConfigOnly",
|
||||||
|
"remoteUnifiedBodyPolicyTransports",
|
||||||
|
"remoteBodyLimitsBeforeMiddleware",
|
||||||
|
"remoteHardLimitConflictBeforeServing",
|
||||||
|
"remoteCanceledHandlerCleanupBarrier",
|
||||||
|
"remoteExternalServingResourceRetention",
|
||||||
|
"remoteAdmissionCloseRace",
|
||||||
|
"remoteRuntimeFinishRetainsHTTPDependencies",
|
||||||
|
"remoteHTTP2DisconnectedHandlerDrain",
|
||||||
|
"remoteNativeEarly413AndBusinessHTTP200",
|
||||||
|
"remoteHTTPSafetyExampleBuild",
|
||||||
|
"remoteTransactionStages",
|
||||||
|
"remoteTransactionCauseChains",
|
||||||
|
"remoteCommitOutcomeUnknown",
|
||||||
|
"remoteCancellationOutcomeUnknown",
|
||||||
|
"remoteTransactionPanicAndConcurrency",
|
||||||
|
"remoteTransactionPublicErrors",
|
||||||
|
"remoteTransactionExample",
|
||||||
|
"remoteI18nRuntimeRace",
|
||||||
|
"remoteI18nGeneratedConsumer",
|
||||||
|
"remoteI18nHTTPBusinessInvariants",
|
||||||
|
"remoteI18nSourceOnlyFallback",
|
||||||
|
"remoteCLDRSamplesAndBrowserConformance",
|
||||||
|
"remoteTypeScriptConsumer",
|
||||||
|
"remoteI18nCLIExtractionCheckExportTypes",
|
||||||
|
"remoteI18nNamespaceIsolation",
|
||||||
|
"remoteBrowserRaceAndFailureRetention",
|
||||||
|
"remoteJSONReaderCausePreservation",
|
||||||
|
"flatFieldComposition",
|
||||||
|
"copyxThreeEndpointConsumer",
|
||||||
|
"explicitNullAndMoneyPolicies",
|
||||||
|
"fieldAdditionWithoutCopyRuleChanges",
|
||||||
|
"middlewareFactoryInjectionAndCleanup",
|
||||||
|
"optionalRepositoryPreservesBusiness",
|
||||||
|
"generatedUnifiedConfig",
|
||||||
|
"jsonYamlXmlAndCustomDecoder",
|
||||||
|
"configValidationBeforeDependencies",
|
||||||
|
"configEnvProvenance",
|
||||||
|
"configExtensionPreserved",
|
||||||
|
"configLoaderDriftCheck"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
|
||||||
|
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
|
||||||
|
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
|
||||||
|
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
|
||||||
|
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
|
||||||
|
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
|
||||||
|
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
|
||||||
|
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
|
||||||
|
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
|
||||||
|
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
|
||||||
|
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
|
||||||
|
"cmd/go-omni/apigen/generate.go": "92abd77a4f4172d5388a6b4efc43910eea281fa30a7ceab7aee75cab6fbb03c1",
|
||||||
|
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
|
||||||
|
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
|
||||||
|
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
|
||||||
|
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
|
||||||
|
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
|
||||||
|
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
|
||||||
|
"scripts/verifyManagedRelease.py": "6860e3f440c4d4f51a55085e50066b1a43cd31d5068c2f7a01a708c16823aef7",
|
||||||
|
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
|
||||||
|
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
|
||||||
|
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
|
||||||
|
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
|
||||||
|
"cmd/go-omni/apigen/responseShape.go": "5b705130b66a3d4aaed842025b75abe54e6427acac8366366ca88ae9862429e1",
|
||||||
|
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
|
||||||
|
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
|
||||||
|
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
|
||||||
|
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
|
||||||
|
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
|
||||||
|
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
|
||||||
|
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
|
||||||
|
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
|
||||||
|
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
|
||||||
|
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
|
||||||
|
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
|
||||||
|
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
|
||||||
|
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
|
||||||
|
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
|
||||||
|
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
|
||||||
|
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
|
||||||
|
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
|
||||||
|
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
|
||||||
|
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
|
||||||
|
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
|
||||||
|
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
|
||||||
|
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
|
||||||
|
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
|
||||||
|
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
|
||||||
|
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
|
||||||
|
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
|
||||||
|
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
|
||||||
|
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
|
||||||
|
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
|
||||||
|
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
|
||||||
|
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
|
||||||
|
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
|
||||||
|
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
|
||||||
|
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
|
||||||
|
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
|
||||||
|
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
|
||||||
|
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
|
||||||
|
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
|
||||||
|
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
|
||||||
|
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
|
||||||
|
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
|
||||||
|
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
|
||||||
|
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
|
||||||
|
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
|
||||||
|
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
|
||||||
|
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
|
||||||
|
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
|
||||||
|
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
|
||||||
|
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
|
||||||
|
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
|
||||||
|
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
|
||||||
|
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
|
||||||
|
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
|
||||||
|
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
|
||||||
|
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
|
||||||
|
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
|
||||||
|
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
|
||||||
|
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
|
||||||
|
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
|
||||||
|
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
|
||||||
|
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
|
||||||
|
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
|
||||||
|
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
|
||||||
|
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
|
||||||
|
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
|
||||||
|
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
|
||||||
|
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
|
||||||
|
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
|
||||||
|
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
|
||||||
|
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
|
||||||
|
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
|
||||||
|
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
|
||||||
|
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
|
||||||
|
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
|
||||||
|
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
|
||||||
|
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
|
||||||
|
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
|
||||||
|
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
|
||||||
|
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
|
||||||
|
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
|
||||||
|
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
|
||||||
|
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
|
||||||
|
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
|
||||||
|
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
|
||||||
|
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
|
||||||
|
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
|
||||||
|
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
|
||||||
|
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
|
||||||
|
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
|
||||||
|
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
|
||||||
|
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
|
||||||
|
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
|
||||||
|
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
|
||||||
|
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
|
||||||
|
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
|
||||||
|
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
|
||||||
|
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
|
||||||
|
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
|
||||||
|
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
|
||||||
|
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
|
||||||
|
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
|
||||||
|
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
|
||||||
|
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
|
||||||
|
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
|
||||||
|
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
|
||||||
|
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
|
||||||
|
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
|
||||||
|
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
|
||||||
|
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
|
||||||
|
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
|
||||||
|
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
|
||||||
|
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
|
||||||
|
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
|
||||||
|
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
|
||||||
|
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
|
||||||
|
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
|
||||||
|
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
|
||||||
|
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
|
||||||
|
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
|
||||||
|
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
|
||||||
|
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
|
||||||
|
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
|
||||||
|
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
|
||||||
|
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
|
||||||
|
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
|
||||||
|
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
|
||||||
|
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
|
||||||
|
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
|
||||||
|
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
|
||||||
|
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
|
||||||
|
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
|
||||||
|
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
|
||||||
|
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
|
||||||
|
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
|
||||||
|
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
|
||||||
|
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15"
|
||||||
|
}
|
||||||
|
}
|
||||||
74
reports/workspaceProgramRemote.json
Normal file
74
reports/workspaceProgramRemote.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.1-preview",
|
||||||
|
"frameworkSum": "h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 25.69,
|
||||||
|
"tests": [
|
||||||
|
"TestConflictRollsBackEarlierWrites",
|
||||||
|
"TestUnsafePaths",
|
||||||
|
"TestGeneratedProgramConsumer",
|
||||||
|
"TestOwnershipAndDrift",
|
||||||
|
"TestRecordedTemplateUpgrade",
|
||||||
|
"TestProjectGuards/module",
|
||||||
|
"TestProjectGuards/oldFramework",
|
||||||
|
"TestProjectGuards/manifest",
|
||||||
|
"TestProjectGuards/lock",
|
||||||
|
"TestProjectGuards/otherGenerator",
|
||||||
|
"TestProjectGuards/symlink",
|
||||||
|
"TestProjectGuards"
|
||||||
|
],
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"installedVersionDefault",
|
||||||
|
"generatedConsumerBuildRaceVet",
|
||||||
|
"defaultNoAPIFiles",
|
||||||
|
"defaultNoNetworkListener",
|
||||||
|
"finiteProgram",
|
||||||
|
"SIGINT",
|
||||||
|
"SIGTERM",
|
||||||
|
"initializationFailureAndPanic",
|
||||||
|
"runErrorPanicGoexit",
|
||||||
|
"fatalComponentFailure",
|
||||||
|
"nonzeroProcessFailureAndTimeoutExits",
|
||||||
|
"partialStartupDraining",
|
||||||
|
"acknowledgementBeforeResourceClose",
|
||||||
|
"workContextSurvivesGracefulStop",
|
||||||
|
"cancellationGrace",
|
||||||
|
"unresponsiveRunAndBackgroundWork",
|
||||||
|
"retainedResourcesAndLogger",
|
||||||
|
"explicitConfigurationOnly",
|
||||||
|
"checkConfigWithoutDependencies",
|
||||||
|
"handwrittenFilesPreserved",
|
||||||
|
"generatedFileConflict",
|
||||||
|
"missingGeneratedFileRestored",
|
||||||
|
"missingHandwrittenFileNotRecreated",
|
||||||
|
"readOnlyCheckAndDryRun",
|
||||||
|
"templateDriftUpgrade",
|
||||||
|
"manifestIdentityAndVersionGuards",
|
||||||
|
"symlinkAndLockProtection",
|
||||||
|
"fileTransactionRollback",
|
||||||
|
"publishedExampleFiniteRun"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
|
||||||
|
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
|
||||||
|
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
|
||||||
|
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
|
||||||
|
"cmd/go-omni/projectgen/generate.go": "c200bb490623622aef4586baef2d3becf158ca6a6833abb9abe8d1cb9b138d6a",
|
||||||
|
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
|
||||||
|
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
|
||||||
|
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
|
||||||
|
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
|
||||||
|
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
|
||||||
|
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
|
||||||
|
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
|
||||||
|
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
reports/workspaceRemote.json
Normal file
34
reports/workspaceRemote.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"passed": true,
|
||||||
|
"frameworkVersion": "v0.28.1-preview",
|
||||||
|
"frameworkSum": "h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=",
|
||||||
|
"freshModuleCache": true,
|
||||||
|
"noLocalReplace": true,
|
||||||
|
"durationSeconds": 21.74,
|
||||||
|
"checks": [
|
||||||
|
"remoteCLIInstall",
|
||||||
|
"remoteAPIModule",
|
||||||
|
"remoteGenericModule",
|
||||||
|
"explicitKinds",
|
||||||
|
"manifestInferredKinds",
|
||||||
|
"mixedFormatWithProjectSkip",
|
||||||
|
"mixedGeneration",
|
||||||
|
"mixedCheck",
|
||||||
|
"nativeGoWorkBuild",
|
||||||
|
"nativeGoWorkRaceTests",
|
||||||
|
"moduleAndWorkspaceMetadataPreserved",
|
||||||
|
"bothProjectFailuresAggregated",
|
||||||
|
"nonzeroFailureExit",
|
||||||
|
"failedCheckIsReadOnly",
|
||||||
|
"crossProjectPreflight",
|
||||||
|
"missingGeneratedOutputRestored"
|
||||||
|
],
|
||||||
|
"sourceHashes": {
|
||||||
|
"cmd/go-omni/apigen/workspace.go": "cc39732ed9a41bb2dfa14933a830a7f10e8e3b2edc2ada07c376a738c0ec77e0",
|
||||||
|
"cmd/go-omni/apigen/workspaceMixed_test.go": "119f2a2aadc2b3ee2471bed1309c6ad8e36488b258f2ab1ba5ba5bab4219643f",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog_test.go": "372874322aece46714351f088b5741642fe4e63d95062743a575958fa58b8b39",
|
||||||
|
"cmd/go-omni/workspace.go": "b7bbd3d9d58b3987090edf7e3714d7d8396e7c332174022d918a52e16396397d",
|
||||||
|
"cmd/go-omni/workspace_test.go": "0170d18158325262765d8b3067f55ca3ce9fe994f2d030cafee3e82b13bfcd4b",
|
||||||
|
"scripts/verifyWorkspaceRelease.py": "0495dc97c68ac17a7969088cf94b1cbbfa6e53f18fb36147c64bc34edb260bca"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ def main():
|
|||||||
checks += ["sharedGoCatalogExport", "documentOnlyErrorSelection", "workspaceCheckNoWrites", "staleCatalogRejected", "twoIndependentServiceBuilds"]
|
checks += ["sharedGoCatalogExport", "documentOnlyErrorSelection", "workspaceCheckNoWrites", "staleCatalogRejected", "twoIndependentServiceBuilds"]
|
||||||
|
|
||||||
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version)
|
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version)
|
||||||
tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject", "TestPrimitiveArrayConsumer", "TestCopyxThreeEndpointConsumer", "TestGeneratedConfigurationConsumer", "TestGeneratedCORSConsumer", "TestI18nGeneratedConsumer"]
|
tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject", "TestPrimitiveArrayConsumer", "TestCopyxThreeEndpointConsumer", "TestGeneratedConfigurationConsumer", "TestGeneratedCORSConsumer", "TestI18nGeneratedConsumer", "TestRecursiveResponseConsumer"]
|
||||||
print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True)
|
print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True)
|
||||||
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env)
|
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env)
|
||||||
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||||
@@ -135,6 +135,7 @@ def main():
|
|||||||
assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch"
|
assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch"
|
||||||
checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "primitiveAndDynamicArrays", "strictRequestPolicies", "declarativeMultipartBinding", "backgroundStopDrainResourceOrder", "backgroundInitAndListenFailureCleanup", "backgroundTimeoutRetainsResourcesAndExits"]
|
checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "primitiveAndDynamicArrays", "strictRequestPolicies", "declarativeMultipartBinding", "backgroundStopDrainResourceOrder", "backgroundInitAndListenFailureCleanup", "backgroundTimeoutRetainsResourcesAndExits"]
|
||||||
checks += ["generatedIncompleteHTTPDrainRetainsDependenciesAndLogger"]
|
checks += ["generatedIncompleteHTTPDrainRetainsDependenciesAndLogger"]
|
||||||
|
checks += ["recursiveResponseTypedHTTP", "recursiveResponseReferenceGraph", "recursiveResponseRegenerationAndDrift"]
|
||||||
|
|
||||||
# Run tests from the downloaded module, not the framework checkout.
|
# Run tests from the downloaded module, not the framework checkout.
|
||||||
http_output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/httpc"], starter, env)
|
http_output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/httpc"], starter, env)
|
||||||
@@ -160,7 +161,24 @@ def main():
|
|||||||
checks += ["remoteExplicitConfigurationRaceTests", "literalAndRequiredDefaultReferences",
|
checks += ["remoteExplicitConfigurationRaceTests", "literalAndRequiredDefaultReferences",
|
||||||
"remoteCheckConfigBeforeDependencies", "removedImplicitEnvironmentRules"]
|
"remoteCheckConfigBeforeDependencies", "removedImplicitEnvironmentRules"]
|
||||||
|
|
||||||
|
# The generated starter imports no cloud storage SDK. Resolve these
|
||||||
|
# optional adapter dependencies before testing their downloaded packages;
|
||||||
|
# lazy module loading otherwise leaves their sums out of starter/go.sum.
|
||||||
|
run(["go", "get", MODULE + "/storage/oss@" + args.version,
|
||||||
|
MODULE + "/storage/s3@" + args.version,
|
||||||
|
MODULE + "/telemetry@" + args.version], starter, env)
|
||||||
|
typescript_available = shutil.which("tsc", path=env.get("PATH")) is not None
|
||||||
|
i18n_tests = {"TestIndependentImmutableBundlesAndFallback", "TestResourceAndParameterDiagnostics", "TestStrictLoadingAndExplicitOverlay", "TestPinnedCLDRSamples", "TestBrowserConformance", "TestSourceLocaleCanBeFallbackOnly"}
|
||||||
|
if typescript_available:
|
||||||
|
i18n_tests.add("TestTypeScriptConsumer")
|
||||||
for package, expected in {
|
for package, expected in {
|
||||||
|
"storage": {"TestReadOptions", "TestReadSignatureAndExpiry", "TestReadRangeStreamingAndCache"},
|
||||||
|
"storage/local": {"TestSignedPayloadRanges"},
|
||||||
|
"storage/oss": {"TestReadOptionsNativeSignatures"},
|
||||||
|
"storage/s3": {"TestReadOptionsS3AndR2"},
|
||||||
|
"examples/signedRead": {"TestSignedReadHTTP"},
|
||||||
|
"telemetry": {"TestConfiguredHeaderPropagation", "TestConfiguredHeaderPropagationPrefersW3CAndRejectsInvalidSetup"},
|
||||||
|
"examples/telemetryHeaders": {"TestTelemetryHeadersExample"},
|
||||||
"copyx": {"TestCheckTypesAndEmptyCollections", "TestCheckRulesAndConverterBoundaries", "TestCopierCheckConcurrentAndOptionsIsolation"},
|
"copyx": {"TestCheckTypesAndEmptyCollections", "TestCheckRulesAndConverterBoundaries", "TestCopierCheckConcurrentAndOptionsIsolation"},
|
||||||
"logx": {"TestLoggerTimeZoneIsolation", "TestLoggerTimeZoneValidationAndDST", "TestLoggerTimeZoneConcurrentChildren"},
|
"logx": {"TestLoggerTimeZoneIsolation", "TestLoggerTimeZoneValidationAndDST", "TestLoggerTimeZoneConcurrentChildren"},
|
||||||
"cors": {"TestPolicyPreflightAndActual", "TestPolicyValidation", "TestPolicyCredentialsWildcardAndDuplicates", "TestPolicyPreservesHTTPAndSnapshot"},
|
"cors": {"TestPolicyPreflightAndActual", "TestPolicyValidation", "TestPolicyCredentialsWildcardAndDuplicates", "TestPolicyPreservesHTTPAndSnapshot"},
|
||||||
@@ -171,7 +189,7 @@ def main():
|
|||||||
"transport/http2": {"TestShutdownWaitsForHandlerAfterClientDisconnect"},
|
"transport/http2": {"TestShutdownWaitsForHandlerAfterClientDisconnect"},
|
||||||
"transport/native": {"TestApplicationRejectsUnreadBodyPromptly", "TestBodyRejectionWithBusinessHTTP200DoesNotDrain"},
|
"transport/native": {"TestApplicationRejectsUnreadBodyPromptly", "TestBodyRejectionWithBusinessHTTP200DoesNotDrain"},
|
||||||
"contract": {"TestBodyPolicyPropagatesOuterLimit", "TestJSONClosingDelimiterPreservesReaderCause"},
|
"contract": {"TestBodyPolicyPropagatesOuterLimit", "TestJSONClosingDelimiterPreservesReaderCause"},
|
||||||
"i18n": {"TestIndependentImmutableBundlesAndFallback", "TestResourceAndParameterDiagnostics", "TestStrictLoadingAndExplicitOverlay", "TestPinnedCLDRSamples", "TestBrowserConformance", "TestSourceLocaleCanBeFallbackOnly", "TestTypeScriptConsumer"},
|
"i18n": i18n_tests,
|
||||||
"i18n/httpi18n": {"TestNegotiationAndExplicitResolvers"},
|
"i18n/httpi18n": {"TestNegotiationAndExplicitResolvers"},
|
||||||
"i18n/errori18n": {"TestCatalogAdapterIsExplicitAndDetached"},
|
"i18n/errori18n": {"TestCatalogAdapterIsExplicitAndDetached"},
|
||||||
"database": {"TestInTxStagesAndCauses", "TestInTxCommitOutcomeUnknown", "TestInTxCancellationAndOwnership", "TestInTxCancellationRace", "TestInTxSuccessPanicAndConcurrency", "TestInTxPublicErrorCompatibility"},
|
"database": {"TestInTxStagesAndCauses", "TestInTxCommitOutcomeUnknown", "TestInTxCancellationAndOwnership", "TestInTxCancellationRace", "TestInTxSuccessPanicAndConcurrency", "TestInTxPublicErrorCompatibility"},
|
||||||
@@ -179,7 +197,16 @@ def main():
|
|||||||
output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/" + package], starter, env)
|
output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/" + package], starter, env)
|
||||||
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||||
passed = {event.get("Test") for event in events if event.get("Action") == "pass"}
|
passed = {event.get("Test") for event in events if event.get("Action") == "pass"}
|
||||||
assert expected <= passed
|
assert expected <= passed, f"{package} missing tests: {sorted(expected - passed)}"
|
||||||
|
signed_read_output = run(["go", "run", MODULE + "/examples/signedRead", "-check"], starter, env)
|
||||||
|
assert "PDF preview, Chinese filename, download, HEAD and Range passed" in signed_read_output
|
||||||
|
checks += ["remoteSignedReadOptionsAndLegacyLinks", "remoteReadSignatureTamperingAndExpiry",
|
||||||
|
"remoteLocalPayloadRangesAndHEAD", "remoteOSSNativeV1V4Signatures",
|
||||||
|
"remoteS3R2ReadOptions", "remoteSignedReadHTTPExample"]
|
||||||
|
header_output = run(["go", "run", MODULE + "/examples/telemetryHeaders"], starter, env)
|
||||||
|
assert "traceId=11111111111111111111111111111111" in header_output
|
||||||
|
checks += ["remoteConfiguredHeaderPropagation", "remoteHeaderW3CPrecedenceAndValidation",
|
||||||
|
"remoteHeaderTelemetryExample"]
|
||||||
checks += ["remotePreciseModuleDiagnostics", "remoteJSONSuccessStatuses", "remoteRejectedStatusBodyCleanup",
|
checks += ["remotePreciseModuleDiagnostics", "remoteJSONSuccessStatuses", "remoteRejectedStatusBodyCleanup",
|
||||||
"remoteCopyTypePreflight", "remoteEmptyCollectionTypeRejection", "remoteLoggerTimeZoneIsolation",
|
"remoteCopyTypePreflight", "remoteEmptyCollectionTypeRejection", "remoteLoggerTimeZoneIsolation",
|
||||||
"remoteCORSPolicyRace", "remoteCORSErrorResponses", "remoteCORSTransports", "generatedCORSConfigOnly"]
|
"remoteCORSPolicyRace", "remoteCORSErrorResponses", "remoteCORSTransports", "generatedCORSConfigOnly"]
|
||||||
@@ -215,9 +242,10 @@ def main():
|
|||||||
browser_output = run(["node", "--test", downloaded / "i18n/browser/runtime.test.mjs"], starter, env)
|
browser_output = run(["node", "--test", downloaded / "i18n/browser/runtime.test.mjs"], starter, env)
|
||||||
assert "# fail 0" in browser_output
|
assert "# fail 0" in browser_output
|
||||||
checks += ["remoteI18nRuntimeRace", "remoteI18nGeneratedConsumer", "remoteI18nHTTPBusinessInvariants",
|
checks += ["remoteI18nRuntimeRace", "remoteI18nGeneratedConsumer", "remoteI18nHTTPBusinessInvariants",
|
||||||
"remoteI18nSourceOnlyFallback", "remoteCLDRSamplesAndBrowserConformance", "remoteTypeScriptConsumer",
|
"remoteI18nSourceOnlyFallback", "remoteCLDRSamplesAndBrowserConformance",
|
||||||
"remoteI18nCLIExtractionCheckExportTypes", "remoteI18nNamespaceIsolation",
|
"remoteI18nCLIExtractionCheckExportTypes", "remoteI18nNamespaceIsolation",
|
||||||
"remoteBrowserRaceAndFailureRetention", "remoteJSONReaderCausePreservation"]
|
"remoteBrowserRaceAndFailureRetention", "remoteJSONReaderCausePreservation"]
|
||||||
|
checks.append("remoteTypeScriptConsumer" if typescript_available else "remoteTypeScriptConsumerSkippedWithoutTSC")
|
||||||
|
|
||||||
checks += ["flatFieldComposition", "copyxThreeEndpointConsumer", "explicitNullAndMoneyPolicies", "fieldAdditionWithoutCopyRuleChanges", "middlewareFactoryInjectionAndCleanup", "optionalRepositoryPreservesBusiness"]
|
checks += ["flatFieldComposition", "copyxThreeEndpointConsumer", "explicitNullAndMoneyPolicies", "fieldAdditionWithoutCopyRuleChanges", "middlewareFactoryInjectionAndCleanup", "optionalRepositoryPreservesBusiness"]
|
||||||
|
|
||||||
@@ -246,6 +274,10 @@ def main():
|
|||||||
sources += ["app/body.go", "app/router.go", "app/requests.go", "app/bodyPolicy_test.go", "app/requests_test.go", "app/transport_test.go",
|
sources += ["app/body.go", "app/router.go", "app/requests.go", "app/bodyPolicy_test.go", "app/requests_test.go", "app/transport_test.go",
|
||||||
"httpx/context.go", "internal/httplimits/limits.go", "lifecycle/runtime_test.go", "transport/http2/server.go", "transport/http2/lifecycle_test.go",
|
"httpx/context.go", "internal/httplimits/limits.go", "lifecycle/runtime_test.go", "transport/http2/server.go", "transport/http2/lifecycle_test.go",
|
||||||
"transport/native/server.go", "transport/native/earlyReject_test.go", "examples/httpSafety/main.go"]
|
"transport/native/server.go", "transport/native/earlyReject_test.go", "examples/httpSafety/main.go"]
|
||||||
|
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "storage").rglob("*.go")]
|
||||||
|
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "examples/signedRead").glob("*.go")]
|
||||||
|
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "telemetry").glob("*.go")]
|
||||||
|
sources += [str(p.relative_to(ROOT)) for p in (ROOT / "examples/telemetryHeaders").glob("*.go")]
|
||||||
report = {"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
|
report = {"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
|
||||||
"freshModuleCache": True, "noLocalReplace": True,
|
"freshModuleCache": True, "noLocalReplace": True,
|
||||||
"durationSeconds": round(time.monotonic() - started, 2), "checks": checks,
|
"durationSeconds": round(time.monotonic() - started, 2), "checks": checks,
|
||||||
|
|||||||
85
scripts/verifyProgramRelease.py
Normal file
85
scripts/verifyProgramRelease.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify the generic project CLI and its consumer from a fresh remote module cache."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
from verifyErrorInheritanceRelease import run
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
MODULE = "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--version", required=True)
|
||||||
|
parser.add_argument("--report", type=Path, default=ROOT / "reports/programRemote.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not re.fullmatch(r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", args.version):
|
||||||
|
parser.error("a fixed release version is required")
|
||||||
|
env = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")}
|
||||||
|
env.update(GOWORK="off", GOFLAGS="", GOPRIVATE="git.yuntuo.hk", GONOSUMDB="git.yuntuo.hk")
|
||||||
|
started = time.monotonic()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="goOmniProgramRelease-") as directory:
|
||||||
|
temp = Path(directory)
|
||||||
|
(temp / "bin").mkdir()
|
||||||
|
env.update(GOBIN=str(temp / "bin"), GOMODCACHE=str(temp / "modcache"))
|
||||||
|
print(f"Installing remote CLI {args.version} with an empty module cache", flush=True)
|
||||||
|
run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env)
|
||||||
|
cli = temp / "bin/go-omni"
|
||||||
|
# No version argument and no local replacement: installed CLI owns the default.
|
||||||
|
starter = temp / "program"
|
||||||
|
run([cli, "project", "new", starter, "--module", "example.com/released"], temp, env)
|
||||||
|
run(["go", "mod", "tidy"], starter, env)
|
||||||
|
dependency = json.loads(run(["go", "list", "-m", "-json", MODULE], starter, env))
|
||||||
|
assert dependency["Version"] == args.version and dependency.get("Sum") and not dependency.get("Replace")
|
||||||
|
downloaded = Path(dependency["Dir"])
|
||||||
|
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version)
|
||||||
|
print("Testing downloaded generator, shared file transaction and generated program lifecycle", flush=True)
|
||||||
|
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s",
|
||||||
|
"./cmd/go-omni/projectgen", "./cmd/go-omni/internal/genfs"], downloaded, env)
|
||||||
|
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||||
|
passed = [e["Test"] for e in events if e.get("Action") == "pass" and "Test" in e]
|
||||||
|
assert "TestGeneratedProgramConsumer" in passed and "TestConflictRollsBackEarlierWrites" in passed, output
|
||||||
|
assert not any(e.get("Action") == "fail" for e in events), output
|
||||||
|
evidence = "".join(e.get("Output", "") for e in events)
|
||||||
|
assert "no local replace" in evidence, evidence
|
||||||
|
# Check the committed standalone example with only project-owned additions.
|
||||||
|
for name in ("internal/config/config.go", "internal/run/run.go", "etc/config.yaml"):
|
||||||
|
text = (ROOT / "examples/program" / name).read_text().replace("example.com/program", "example.com/released")
|
||||||
|
(starter / name).write_text(text)
|
||||||
|
run([cli, "project", "check"], starter, env)
|
||||||
|
run(["go", "run", ".", "-check-config"], starter, env)
|
||||||
|
example_output = run(["go", "run", "."], starter, env)
|
||||||
|
assert "program.completed" in example_output, example_output
|
||||||
|
run(["go", "test", "./..."], starter, env)
|
||||||
|
sources = list((ROOT / "cmd/go-omni/projectgen").rglob("*.go"))
|
||||||
|
sources += list((ROOT / "cmd/go-omni/projectgen/templates").glob("*.tmpl"))
|
||||||
|
sources += list((ROOT / "cmd/go-omni/internal/genfs").glob("*.go"))
|
||||||
|
sources += [ROOT / "cmd/go-omni/project.go", ROOT / "scripts/verifyProgramRelease.py"]
|
||||||
|
report = {
|
||||||
|
"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
|
||||||
|
"freshModuleCache": True, "noLocalReplace": True,
|
||||||
|
"durationSeconds": round(time.monotonic() - started, 2), "tests": passed,
|
||||||
|
"checks": ["remoteCLIInstall", "installedVersionDefault", "generatedConsumerBuildRaceVet",
|
||||||
|
"defaultNoAPIFiles", "defaultNoNetworkListener", "finiteProgram", "SIGINT", "SIGTERM",
|
||||||
|
"initializationFailureAndPanic", "runErrorPanicGoexit", "fatalComponentFailure",
|
||||||
|
"nonzeroProcessFailureAndTimeoutExits", "partialStartupDraining", "acknowledgementBeforeResourceClose", "workContextSurvivesGracefulStop",
|
||||||
|
"cancellationGrace", "unresponsiveRunAndBackgroundWork", "retainedResourcesAndLogger",
|
||||||
|
"explicitConfigurationOnly", "checkConfigWithoutDependencies", "handwrittenFilesPreserved",
|
||||||
|
"generatedFileConflict", "missingGeneratedFileRestored", "missingHandwrittenFileNotRecreated",
|
||||||
|
"readOnlyCheckAndDryRun", "templateDriftUpgrade", "manifestIdentityAndVersionGuards",
|
||||||
|
"symlinkAndLockProtection", "fileTransactionRollback", "publishedExampleFiniteRun"],
|
||||||
|
"sourceHashes": {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(sources)},
|
||||||
|
}
|
||||||
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
print(f"PASS remote generic program release {args.version}: {len(report['checks'])} checks", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
65
scripts/verifyWorkspaceRelease.py
Normal file
65
scripts/verifyWorkspaceRelease.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Exercise mixed workspace generation/check and native Go multi-module builds remotely."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from verifyErrorInheritanceRelease import run
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
MODULE = "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--version", required=True)
|
||||||
|
parser.add_argument("--report", type=Path, default=ROOT / "reports/workspaceRemote.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not re.fullmatch(r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", args.version):
|
||||||
|
parser.error("a fixed release version is required")
|
||||||
|
env = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")}
|
||||||
|
env.update(GOWORK="off", GOFLAGS="", GOPRIVATE="git.yuntuo.hk", GONOSUMDB="git.yuntuo.hk")
|
||||||
|
started = time.monotonic()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="goOmniWorkspaceRelease-") as directory:
|
||||||
|
temp = Path(directory)
|
||||||
|
(temp / "bin").mkdir()
|
||||||
|
env.update(GOBIN=str(temp / "bin"), GOMODCACHE=str(temp / "modcache"))
|
||||||
|
print(f"Installing remote CLI {args.version} in an empty cache", flush=True)
|
||||||
|
run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env)
|
||||||
|
dependency = json.loads(run(["go", "mod", "download", "-json", MODULE + "@" + args.version], temp, env))
|
||||||
|
assert dependency["Version"] == args.version and dependency.get("Sum")
|
||||||
|
downloaded = Path(dependency["Dir"])
|
||||||
|
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(temp / "bin/go-omni"), GO_OMNI_RELEASE_VERSION=args.version)
|
||||||
|
print("Testing mixed API/project consumers and go.work builds from the downloaded release", flush=True)
|
||||||
|
output = run(["go", "test", "-race", "-json", "-count=1", "-timeout=240s", "./cmd/go-omni",
|
||||||
|
"-run", "^TestMixedWorkspaceConsumer$"], downloaded, env)
|
||||||
|
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||||
|
passed = [e["Test"] for e in events if e.get("Action") == "pass" and "Test" in e]
|
||||||
|
assert passed == ["TestMixedWorkspaceConsumer"], output
|
||||||
|
evidence = "".join(e.get("Output", "") for e in events)
|
||||||
|
assert evidence.count("; no local replace") == 2, evidence
|
||||||
|
assert dependency["Sum"] in evidence, evidence
|
||||||
|
sources = ["cmd/go-omni/apigen/workspace.go", "cmd/go-omni/apigen/workspaceMixed_test.go",
|
||||||
|
"cmd/go-omni/apigen/workspaceCatalog_test.go", "cmd/go-omni/workspace.go",
|
||||||
|
"cmd/go-omni/workspace_test.go", "scripts/verifyWorkspaceRelease.py"]
|
||||||
|
report = {
|
||||||
|
"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
|
||||||
|
"freshModuleCache": True, "noLocalReplace": True,
|
||||||
|
"durationSeconds": round(time.monotonic() - started, 2),
|
||||||
|
"checks": ["remoteCLIInstall", "remoteAPIModule", "remoteGenericModule", "explicitKinds",
|
||||||
|
"manifestInferredKinds", "mixedFormatWithProjectSkip", "mixedGeneration", "mixedCheck",
|
||||||
|
"nativeGoWorkBuild", "nativeGoWorkRaceTests", "moduleAndWorkspaceMetadataPreserved",
|
||||||
|
"bothProjectFailuresAggregated", "nonzeroFailureExit", "failedCheckIsReadOnly",
|
||||||
|
"crossProjectPreflight", "missingGeneratedOutputRestored"],
|
||||||
|
"sourceHashes": {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in sources},
|
||||||
|
}
|
||||||
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
print(f"PASS remote mixed workspace {args.version}: {len(report['checks'])} checks", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -6,12 +6,42 @@
|
|||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| 读写删除 | Store | 支持 | 支持 | 支持 |
|
| 读写删除 | Store | 支持 | 支持 | 支持 |
|
||||||
| 临时下载 | Presigner | NewSigner | 支持 | 支持 |
|
| 临时下载 | Presigner | NewSigner | 支持 | 支持 |
|
||||||
|
| 带选项的预览/下载 | ReadPresigner | NewSigner | 支持 | 支持 |
|
||||||
| 限定大小/类型的直传 | UploadPresigner | NewUploadSigner | 支持 | 支持,需要配置签名 Header |
|
| 限定大小/类型的直传 | UploadPresigner | NewUploadSigner | 支持 | 支持,需要配置签名 Header |
|
||||||
| 创建、上传、查询、完成、取消分片 | MultipartStore | 当前不提供 | 支持 | 支持 |
|
| 创建、上传、查询、完成、取消分片 | MultipartStore | 当前不提供 | 支持 | 支持 |
|
||||||
| 分片直传签名 | PartPresigner | 当前不提供 | 支持 | 支持,需要配置签名 Header |
|
| 分片直传签名 | PartPresigner | 当前不提供 | 支持 | 支持,需要配置签名 Header |
|
||||||
|
|
||||||
没有固定业务字段、目录规则或身份模型。鉴权、对象归属、业务配额由应用定义。SDK 客户端和连接池由应用持有;适配器不会在一次调用后关闭客户端。
|
没有固定业务字段、目录规则或身份模型。鉴权、对象归属、业务配额由应用定义。SDK 客户端和连接池由应用持有;适配器不会在一次调用后关闭客户端。
|
||||||
|
|
||||||
|
## 签名预览与下载
|
||||||
|
|
||||||
|
`v0.29.0-preview` 新增独立的可选接口 `ReadPresigner`,不扩充原 Store/Presigner,不要求第三方适配器修改代码。原 `PresignGet(ctx, key, ttl)` 和既有本地下载链接继续有效、默认 attachment。
|
||||||
|
|
||||||
|
```go
|
||||||
|
// signer 可以是 storage.NewSigner 返回的本地 Handler,或 S3/R2、OSS Store。
|
||||||
|
link, err := signer.PresignGetWithOptions(ctx, key, 30*time.Minute, storage.ReadOptions{
|
||||||
|
Disposition: storage.DispositionInline,
|
||||||
|
FileName: "示例报告.pdf",
|
||||||
|
ContentType: "application/pdf",
|
||||||
|
})
|
||||||
|
if err != nil { return err }
|
||||||
|
// 把 link.URL 和 link.ExpiresAt 返回给已授权的调用方。
|
||||||
|
```
|
||||||
|
|
||||||
|
TTL 仍为 1 秒到 1 小时,30 分钟无需改配置。`ReadLink` 包含 `url` 和 `expiresAt`,到期时间取自实际签名,精确到秒(不足一秒的部分舍弃);临时云凭据可能更早失效。无需额外请求 Header,客户端直接打开 URL。可通过 `storage.ReadPresigner` 做能力判断;不支持时由调用方明确处理,不应静默降级成公开地址。
|
||||||
|
|
||||||
|
`Disposition` 只接受 inline/attachment,空值为 attachment。`FileName` 是显示名,与存储 key 无关;中文使用标准 `filename*` 编码,拒绝控制字符(包括 CR/LF)、路径分隔符及超长值。`ContentType` 为空时沿用对象元数据,可显式指定合法 MIME 类型;它只控制响应,不转换或验证文件内容。PDF 应在上传时就保存 `application/pdf` 元数据。inline 请求内联显示,能否展示还取决于浏览器的格式支持与用户设置。
|
||||||
|
|
||||||
|
本地签名绑定 key、到期时间、版本及全部选项,拒绝重复/未知参数和篡改;先验签、验有效期,再读取对象或处理条件请求。Local 使用受文件内容边界约束的流,GET/HEAD 的长度不包含内部元数据;支持单段/多段 Range、206/416,以及 Last-Modified 条件请求。HEAD 返回完整对象头、无响应体,忽略 Range。自定义 Store 的 Body 实现 `io.ReadSeeker` 时复用同一行为;仅支持顺序读取时返回完整 200,不宣称支持范围读取,也不把整个对象加载到内存。
|
||||||
|
|
||||||
|
云端适配器在 OSS/S3 原生 SDK 签名前传入响应参数,禁止先签名后拼参数或替换域名。Range 由云存储处理;云端 GET 签名不能直接改成 HEAD 使用。新接口请求 `Cache-Control: private, no-store`,本地成功、错误及条件响应均保持该策略;云端响应头覆盖受平台规则约束,需检查实际 200/206 响应与代理缓存配置。
|
||||||
|
|
||||||
|
OSS 默认域名或加速域名可能强制下载。需要自定义域名时,在应用装配 SDK client 时设置已绑定桶的域名和 `WithUseCName(true)`,再传给适配器;SDK 的区域、签名版本、凭据由应用配置。例如在现有 SDK config 上调用 `WithEndpoint("https://files.example").WithUseCName(true)`。参见 [OSS 预览规则](https://help.aliyun.com/zh/oss/how-to-ensure-an-object-is-previewed-when-you-access-the-object)与 [GetObject 响应参数规则](https://www.alibabacloud.com/help/en/oss/developer-reference/getobject)。
|
||||||
|
|
||||||
|
签发权限、对象是否不可变、审计和前端链接复用由项目维护。框架不添加分享表、撤销、租户或数据库依赖;重新签发不会撤销旧链接,到期不会收回已下载内容,也不会中断已开始的传输。本地入口使用显式配置的公开 baseURL,不猜测 Host/转发头,不公开磁盘目录。应用与代理日志应隐藏查询参数。
|
||||||
|
|
||||||
|
完整离线 [预览/下载示例](../examples/signedRead/README.md),包含真实 HTTP 检查与一页 PDF。OSS V1/V4、S3/R2 SDK 签名和隔离 MinIO 有回归;OSS/R2 云账号尚未实测,部署方仍需对实际桶、域名及浏览器进行验收。
|
||||||
|
|
||||||
## 服务端上传限制
|
## 服务端上传限制
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -85,7 +115,7 @@ if err != nil { return err }
|
|||||||
request, err := signer.PresignPut(ctx, key, size, "image/png", time.Minute)
|
request, err := signer.PresignPut(ctx, key, size, "image/png", time.Minute)
|
||||||
```
|
```
|
||||||
|
|
||||||
Handler 检查签名、有效期、单值参数、Content-Length/Content-Type,以及实际内容大小和嗅探类型,成功返回 201。本地存储只开放一个受控入口,不把磁盘目录映射成公共目录。原 `NewSigner` 继续只服务下载。请求超时、CORS、并发限制和速率限制由承载该 Handler 的服务配置。
|
Handler 检查签名、有效期、单值参数、Content-Length/Content-Type,以及实际内容大小和嗅探类型,成功返回 201。本地存储只开放一个受控入口,不把磁盘目录映射成公共目录。`NewSigner` 提供签名读取,不接受上传;需要上传时使用 `NewUploadSigner`。请求超时、CORS、并发限制和速率限制由承载该 Handler 的服务配置。
|
||||||
|
|
||||||
## 分片与续传
|
## 分片与续传
|
||||||
|
|
||||||
|
|||||||
@@ -151,14 +151,31 @@ func (s *Store) Get(ctx context.Context, key string) (storage.Object, error) {
|
|||||||
}
|
}
|
||||||
info.ModifiedAt = stat.ModTime()
|
info.ModifiedAt = stat.ModTime()
|
||||||
ok = true
|
ok = true
|
||||||
return storage.Object{Info: info, Body: &body{Reader: storage.ContextReader{Context: ctx, Reader: io.LimitReader(f, info.Size)}, Closer: f}}, nil
|
// Expose only the payload as a seekable section. Seeks are relative to the
|
||||||
|
// object, never to our private metadata header or other disk bytes.
|
||||||
|
return storage.Object{Info: info, Body: &body{section: io.NewSectionReader(f, int64(length)+4, info.Size), file: f, ctx: ctx}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type body struct {
|
type body struct {
|
||||||
io.Reader
|
section *io.SectionReader
|
||||||
io.Closer
|
file *os.File
|
||||||
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *body) Read(p []byte) (int, error) {
|
||||||
|
if err := b.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return b.section.Read(p)
|
||||||
|
}
|
||||||
|
func (b *body) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if err := b.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return b.section.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
func (b *body) Close() error { return b.file.Close() }
|
||||||
|
|
||||||
func (s *Store) Delete(ctx context.Context, key string) error {
|
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||||
if err := storage.Check(ctx, key); err != nil {
|
if err := storage.Check(ctx, key); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
125
storage/local/read_test.go
Normal file
125
storage/local/read_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSignedPayloadRanges(t *testing.T) {
|
||||||
|
store, err := New(t.TempDir(), 1<<20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
data := "%PDF-1.4\n" + strings.Repeat("0123456789", 100) + "\n%%EOF\n"
|
||||||
|
ctx := context.Background()
|
||||||
|
if _, err = store.Put(ctx, "private/document", strings.NewReader(data), int64(len(data)), "application/pdf"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
signer, _ := storage.NewSigner(store, "https://files.example/read", []byte(strings.Repeat("s", 32)))
|
||||||
|
link, err := signer.PresignGetWithOptions(ctx, "private/document", 30*time.Minute, storage.ReadOptions{Disposition: storage.DispositionInline, FileName: "示例.pdf"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
method, span string
|
||||||
|
status int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"GET", "", 200, data}, {"HEAD", "", 200, ""}, {"HEAD", "bytes=0-3", 200, ""},
|
||||||
|
{"GET", "bytes=0-7", 206, data[:8]}, {"GET", "bytes=20-", 206, data[20:]}, {"GET", "bytes=-6", 206, data[len(data)-6:]},
|
||||||
|
{"GET", "bytes=1000-999999", 206, data[1000:]}, {"GET", "bytes=999999-", 416, ""}, {"GET", "bytes=bad", 416, ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.method+tc.span, func(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(tc.method, link.URL, nil)
|
||||||
|
if tc.span != "" {
|
||||||
|
r.Header.Set("Range", tc.span)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
signer.ServeHTTP(w, r)
|
||||||
|
if w.Code != tc.status {
|
||||||
|
t.Fatal(w.Code)
|
||||||
|
}
|
||||||
|
if tc.status != 416 && w.Body.String() != tc.want {
|
||||||
|
t.Fatal("wrong payload slice")
|
||||||
|
}
|
||||||
|
if tc.status == 206 && (w.Header().Get("Content-Length") != strconv.Itoa(len(tc.want)) || !strings.HasSuffix(w.Header().Get("Content-Range"), "/"+strconv.Itoa(len(data)))) {
|
||||||
|
t.Fatal(w.Header())
|
||||||
|
}
|
||||||
|
if tc.status == 200 && w.Header().Get("Content-Length") != strconv.Itoa(len(data)) {
|
||||||
|
t.Fatal("metadata included in length")
|
||||||
|
}
|
||||||
|
if tc.span == "bytes=999999-" && w.Header().Get("Content-Range") != "bytes */"+strconv.Itoa(len(data)) {
|
||||||
|
t.Fatal(w.Header())
|
||||||
|
}
|
||||||
|
if w.Header().Get("Cache-Control") != "private, no-store" {
|
||||||
|
t.Fatal("cache policy")
|
||||||
|
}
|
||||||
|
if tc.status != 416 {
|
||||||
|
kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Disposition"))
|
||||||
|
if err != nil || kind != "inline" || params["filename"] != "示例.pdf" || w.Header().Get("Content-Type") != "application/pdf" {
|
||||||
|
t.Fatal(w.Header(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r := httptest.NewRequest("GET", link.URL, nil)
|
||||||
|
r.Header.Set("Range", "bytes=0-3,10-13")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
signer.ServeHTTP(w, r)
|
||||||
|
kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Type"))
|
||||||
|
if err != nil || w.Code != 206 || kind != "multipart/byteranges" {
|
||||||
|
t.Fatal(w.Header(), err)
|
||||||
|
}
|
||||||
|
reader := multipart.NewReader(w.Body, params["boundary"])
|
||||||
|
for _, want := range []string{data[:4], data[10:14]} {
|
||||||
|
p, err := reader.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := io.ReadAll(p)
|
||||||
|
if err != nil || string(got) != want {
|
||||||
|
t.Fatal("multipart bytes", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := reader.NextPart(); err != io.EOF {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Random access cannot expose the managed header and still observes context.
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
object, err := store.Get(ctx, "private/document")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer object.Body.Close()
|
||||||
|
seeker := object.Body.(io.ReadSeeker)
|
||||||
|
if size, err := seeker.Seek(0, io.SeekEnd); err != nil || size != int64(len(data)) {
|
||||||
|
t.Fatal(size, err)
|
||||||
|
}
|
||||||
|
if _, err := seeker.Seek(-1, io.SeekStart); err == nil {
|
||||||
|
t.Fatal("seek into header")
|
||||||
|
}
|
||||||
|
if _, err := seeker.Seek(0, io.SeekStart); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
prefix := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(seeker, prefix); err != nil || string(prefix) != data[:8] {
|
||||||
|
t.Fatal("metadata exposed", err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
if _, err := seeker.Seek(0, io.SeekStart); err != context.Canceled {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := seeker.Read(prefix); err != context.Canceled {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
storage/oss/read.go
Normal file
59
storage/oss/read.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
||||||
|
sdk "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) PresignGetWithOptions(ctx context.Context, key string, ttl time.Duration, options storage.ReadOptions) (storage.ReadLink, error) {
|
||||||
|
if err := storage.Check(ctx, key); err != nil {
|
||||||
|
return storage.ReadLink{}, err
|
||||||
|
}
|
||||||
|
if ttl < time.Second || ttl > time.Hour {
|
||||||
|
return storage.ReadLink{}, errors.New("storage/oss: TTL must be 1s..1h")
|
||||||
|
}
|
||||||
|
headers, err := options.ResponseHeaders()
|
||||||
|
if err != nil {
|
||||||
|
return storage.ReadLink{}, err
|
||||||
|
}
|
||||||
|
request := &sdk.GetObjectRequest{Bucket: sdk.Ptr(s.bucket), Key: sdk.Ptr(key), ResponseContentDisposition: sdk.Ptr(headers.Get("Content-Disposition")), ResponseCacheControl: sdk.Ptr(headers.Get("Cache-Control"))}
|
||||||
|
if contentType := headers.Get("Content-Type"); contentType != "" {
|
||||||
|
request.ResponseContentType = sdk.Ptr(contentType)
|
||||||
|
}
|
||||||
|
r, err := s.client.Presign(ctx, request, sdk.PresignExpires(ttl))
|
||||||
|
if err != nil {
|
||||||
|
return storage.ReadLink{}, err
|
||||||
|
}
|
||||||
|
if len(r.SignedHeaders) != 0 {
|
||||||
|
return storage.ReadLink{}, errors.New("storage/oss: direct GET requires a signature without additional request headers")
|
||||||
|
}
|
||||||
|
u, err := url.Parse(r.URL)
|
||||||
|
if err != nil {
|
||||||
|
return storage.ReadLink{}, errors.New("storage/oss: invalid signed URL")
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
var expires time.Time
|
||||||
|
if q.Get("x-oss-signature") != "" {
|
||||||
|
start, e := time.Parse("20060102T150405Z", q.Get("x-oss-date"))
|
||||||
|
seconds, e2 := strconv.ParseInt(q.Get("x-oss-expires"), 10, 64)
|
||||||
|
if e != nil || e2 != nil || seconds < 1 || seconds > 3600 {
|
||||||
|
return storage.ReadLink{}, errors.New("storage/oss: invalid signed expiry")
|
||||||
|
}
|
||||||
|
expires = start.Add(time.Duration(seconds) * time.Second)
|
||||||
|
} else {
|
||||||
|
seconds, e := strconv.ParseInt(q.Get("Expires"), 10, 64)
|
||||||
|
if e != nil || q.Get("Signature") == "" {
|
||||||
|
return storage.ReadLink{}, errors.New("storage/oss: missing signed expiry")
|
||||||
|
}
|
||||||
|
expires = time.Unix(seconds, 0).UTC()
|
||||||
|
}
|
||||||
|
return storage.ReadLink{URL: r.URL, ExpiresAt: expires}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.ReadPresigner = (*Store)(nil)
|
||||||
140
storage/oss/read_test.go
Normal file
140
storage/oss/read_test.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"mime"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
||||||
|
sdk "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||||
|
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
|
||||||
|
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/signer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recompute signatures independently from the SDK using fixed test credentials.
|
||||||
|
// This catches response parameters appended after signing, not just their presence.
|
||||||
|
func validReadSignature(u *url.URL, version sdk.SignatureVersionType) bool {
|
||||||
|
q := u.Query()
|
||||||
|
if version == sdk.SignatureVersionV1 {
|
||||||
|
var keys []string
|
||||||
|
for key := range q {
|
||||||
|
if strings.HasPrefix(key, "response-") {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
var params []string
|
||||||
|
for _, key := range keys {
|
||||||
|
params = append(params, key+"="+q.Get(key))
|
||||||
|
}
|
||||||
|
message := "GET\n\n\n" + q.Get("Expires") + "\n/testbucket/folder/file.pdf?" + strings.Join(params, "&")
|
||||||
|
mac := hmac.New(sha1.New, []byte("fixtureSecret"))
|
||||||
|
mac.Write([]byte(message))
|
||||||
|
return hmac.Equal([]byte(q.Get("Signature")), []byte(base64.StdEncoding.EncodeToString(mac.Sum(nil))))
|
||||||
|
}
|
||||||
|
signature := q.Get("x-oss-signature")
|
||||||
|
q.Del("x-oss-signature")
|
||||||
|
canonical := "GET\n/testbucket/folder/file.pdf\n" + strings.ReplaceAll(q.Encode(), "+", "%20") + "\n\n\nUNSIGNED-PAYLOAD"
|
||||||
|
hash := sha256.Sum256([]byte(canonical))
|
||||||
|
_, scope, _ := strings.Cut(q.Get("x-oss-credential"), "/")
|
||||||
|
message := "OSS4-HMAC-SHA256\n" + q.Get("x-oss-date") + "\n" + scope + "\n" + hex.EncodeToString(hash[:])
|
||||||
|
key := []byte("aliyun_v4fixtureSecret")
|
||||||
|
for _, part := range strings.Split(scope, "/") {
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(part))
|
||||||
|
key = mac.Sum(nil)
|
||||||
|
}
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(message))
|
||||||
|
return hmac.Equal([]byte(signature), []byte(hex.EncodeToString(mac.Sum(nil))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOptionsNativeSignatures(t *testing.T) {
|
||||||
|
for name, version := range map[string]sdk.SignatureVersionType{"v1": sdk.SignatureVersionV1, "v4": sdk.SignatureVersionV4} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
cfg := sdk.LoadDefaultConfig().WithRegion("cn-hangzhou").WithEndpoint("https://files.example").WithUseCName(true).WithSignatureVersion(version).WithCredentialsProvider(credentials.NewStaticCredentialsProvider("fixtureAccess", "fixtureSecret"))
|
||||||
|
store, err := New(sdk.NewClient(cfg), "testbucket", 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, options := range []storage.ReadOptions{{}, {Disposition: storage.DispositionInline, FileName: "报告 2026.pdf", ContentType: "application/pdf"}, {Disposition: storage.DispositionAttachment, FileName: "download.pdf"}} {
|
||||||
|
before := time.Now().UTC().Truncate(time.Second)
|
||||||
|
link, err := store.PresignGetWithOptions(context.Background(), "folder/file.pdf", 30*time.Minute, options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(link.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
if u.Host != "files.example" || u.Path != "/folder/file.pdf" || !validReadSignature(u, version) {
|
||||||
|
t.Fatal("SDK response overrides were not signed")
|
||||||
|
}
|
||||||
|
disposition, params, err := mime.ParseMediaType(q.Get("response-content-disposition"))
|
||||||
|
want := options.Disposition
|
||||||
|
if want == "" {
|
||||||
|
want = "attachment"
|
||||||
|
}
|
||||||
|
if err != nil || disposition != want || params["filename"] != options.FileName || q.Get("response-content-type") != options.ContentType || q.Get("response-cache-control") != "private, no-store" {
|
||||||
|
t.Fatal("wrong signed headers", err)
|
||||||
|
}
|
||||||
|
if link.ExpiresAt.Before(before.Add(30*time.Minute)) || link.ExpiresAt.After(time.Now().Add(30*time.Minute)) {
|
||||||
|
t.Fatal("expiry drift")
|
||||||
|
}
|
||||||
|
var expires time.Time
|
||||||
|
if version == sdk.SignatureVersionV4 {
|
||||||
|
start, _ := time.Parse("20060102T150405Z", q.Get("x-oss-date"))
|
||||||
|
seconds, _ := strconv.Atoi(q.Get("x-oss-expires"))
|
||||||
|
expires = start.Add(time.Duration(seconds) * time.Second)
|
||||||
|
} else {
|
||||||
|
seconds, _ := strconv.ParseInt(q.Get("Expires"), 10, 64)
|
||||||
|
expires = time.Unix(seconds, 0)
|
||||||
|
}
|
||||||
|
if !expires.Equal(link.ExpiresAt) {
|
||||||
|
t.Fatal("result does not match signature expiry")
|
||||||
|
}
|
||||||
|
for _, name := range []string{"response-content-disposition", "response-content-type", "response-cache-control"} {
|
||||||
|
changed := *u
|
||||||
|
query := changed.Query()
|
||||||
|
query.Set(name, "changed")
|
||||||
|
changed.RawQuery = query.Encode()
|
||||||
|
if validReadSignature(&changed, version) {
|
||||||
|
t.Fatal("unsigned override", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ttl := range []time.Duration{0, time.Second - time.Nanosecond, time.Hour + time.Nanosecond} {
|
||||||
|
if _, err := store.PresignGetWithOptions(context.Background(), "file", ttl, storage.ReadOptions{}); err == nil {
|
||||||
|
t.Fatal("TTL accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, options := range []storage.ReadOptions{{FileName: "x\r\nY: z"}, {ContentType: "text/plain\r\nY: z"}, {Disposition: "bad"}} {
|
||||||
|
if _, err := store.PresignGetWithOptions(context.Background(), "file", time.Minute, options); err == nil {
|
||||||
|
t.Fatal("invalid header accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
if _, err := store.PresignGetWithOptions(ctx, "file", time.Minute, storage.ReadOptions{}); err != context.Canceled {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// A caller-supplied unsigned SDK configuration cannot masquerade as a link.
|
||||||
|
client := sdk.NewClient(sdk.LoadDefaultConfig().WithRegion("cn-hangzhou").WithEndpoint("https://files.example").WithCredentialsProvider(credentials.NewStaticCredentialsProvider("fixtureAccess", "fixtureSecret")), func(o *sdk.Options) { o.Signer = &signer.NopSigner{} })
|
||||||
|
store, _ := New(client, "testbucket", 64)
|
||||||
|
if _, err := store.PresignGetWithOptions(context.Background(), "file", time.Minute, storage.ReadOptions{}); err == nil {
|
||||||
|
t.Fatal("unsigned URL accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
72
storage/read.go
Normal file
72
storage/read.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DispositionAttachment = "attachment"
|
||||||
|
DispositionInline = "inline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadOptions controls the response to one signed GET. Empty Disposition means
|
||||||
|
// attachment; empty ContentType preserves object metadata. FileName is a display
|
||||||
|
// name, not an object key or filesystem path. All options are signed.
|
||||||
|
type ReadOptions struct {
|
||||||
|
Disposition string
|
||||||
|
FileName string
|
||||||
|
ContentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLink grants GET access without additional request headers. ExpiresAt is
|
||||||
|
// the expiry encoded in the signature, at second precision. Provider credentials
|
||||||
|
// may expire earlier. Do not log the URL. Issuing another link does not revoke it.
|
||||||
|
type ReadLink struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPresigner is optional; existing Store and Presigner implementations do not
|
||||||
|
// need to change. Cloud GET signatures do not also authorize HEAD requests.
|
||||||
|
type ReadPresigner interface {
|
||||||
|
PresignGetWithOptions(context.Context, string, time.Duration, ReadOptions) (ReadLink, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseHeaders validates options and returns a fresh set of response headers
|
||||||
|
// for provider adapters to include BEFORE signing, requesting private, no-store.
|
||||||
|
func (o ReadOptions) ResponseHeaders() (http.Header, error) {
|
||||||
|
disposition := o.Disposition
|
||||||
|
if disposition == "" {
|
||||||
|
disposition = DispositionAttachment
|
||||||
|
}
|
||||||
|
if disposition != DispositionAttachment && disposition != DispositionInline {
|
||||||
|
return nil, errors.New("storage: disposition must be inline or attachment")
|
||||||
|
}
|
||||||
|
if len(o.FileName) > 1024 || !utf8.ValidString(o.FileName) || strings.IndexFunc(o.FileName, unicode.IsControl) >= 0 || strings.ContainsAny(o.FileName, `/\`) || o.FileName == "." || o.FileName == ".." {
|
||||||
|
return nil, errors.New("storage: invalid display filename")
|
||||||
|
}
|
||||||
|
params := map[string]string{}
|
||||||
|
if o.FileName != "" {
|
||||||
|
params["filename"] = o.FileName
|
||||||
|
}
|
||||||
|
h := http.Header{"Content-Disposition": {mime.FormatMediaType(disposition, params)}, "Cache-Control": {"private, no-store"}}
|
||||||
|
if o.ContentType != "" {
|
||||||
|
if !utf8.ValidString(o.ContentType) || strings.IndexFunc(o.ContentType, unicode.IsControl) >= 0 {
|
||||||
|
return nil, ErrContentType
|
||||||
|
}
|
||||||
|
kind, err := MediaType(o.ContentType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, params, _ := mime.ParseMediaType(o.ContentType)
|
||||||
|
h.Set("Content-Type", mime.FormatMediaType(kind, params))
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
219
storage/read_test.go
Normal file
219
storage/read_test.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadOptions(t *testing.T) {
|
||||||
|
for _, name := range []string{"", "report.pdf", "报告 2026.pdf", `a";b.pdf`, "100%.pdf"} {
|
||||||
|
h, err := (ReadOptions{Disposition: DispositionInline, FileName: name, ContentType: "Application/PDF"}).ResponseHeaders()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
disposition, params, err := mime.ParseMediaType(h.Get("Content-Disposition"))
|
||||||
|
if err != nil || disposition != "inline" || params["filename"] != name || h.Get("Content-Type") != "application/pdf" || h.Get("Cache-Control") != "private, no-store" {
|
||||||
|
t.Fatal("invalid read headers", h, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h, err := (ReadOptions{}).ResponseHeaders()
|
||||||
|
if err != nil || h.Get("Content-Disposition") != "attachment" || h.Get("Content-Type") != "" {
|
||||||
|
t.Fatal(h, err)
|
||||||
|
}
|
||||||
|
for _, options := range []ReadOptions{
|
||||||
|
{Disposition: "form-data"}, {Disposition: "Inline"}, {Disposition: "inline\r\nX: y"},
|
||||||
|
{FileName: "x\r\nX: y"}, {FileName: "a\x00b"}, {FileName: "a\tb"}, {FileName: "bad\xff"}, {FileName: "a/b"}, {FileName: `a\b`}, {FileName: ".."}, {FileName: strings.Repeat("a", 1025)},
|
||||||
|
{ContentType: "bad"}, {ContentType: "image/*"}, {ContentType: "application/pdf\r\nX: y"}, {ContentType: "application/pdf\x00"}, {ContentType: strings.Repeat("x", 257)},
|
||||||
|
} {
|
||||||
|
if _, err := options.ResponseHeaders(); err == nil {
|
||||||
|
t.Fatal("invalid options accepted", options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type readTestStore struct {
|
||||||
|
calls, bytes int
|
||||||
|
streaming bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*readTestStore) Put(context.Context, string, io.Reader, int64, string) (Info, error) {
|
||||||
|
panic("unused")
|
||||||
|
}
|
||||||
|
func (*readTestStore) Delete(context.Context, string) error { panic("unused") }
|
||||||
|
func (s *readTestStore) Get(context.Context, string) (Object, error) {
|
||||||
|
s.calls++
|
||||||
|
body := &readTestBody{Reader: strings.NewReader("0123456789"), store: s}
|
||||||
|
var stream io.ReadCloser = body
|
||||||
|
if s.streaming {
|
||||||
|
stream = struct {
|
||||||
|
io.Reader
|
||||||
|
io.Closer
|
||||||
|
}{body, body}
|
||||||
|
}
|
||||||
|
return Object{Info: Info{Size: 10, ContentType: "application/pdf", ModifiedAt: time.Unix(1000, 0)}, Body: stream}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type readTestBody struct {
|
||||||
|
*strings.Reader
|
||||||
|
store *readTestStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *readTestBody) Read(p []byte) (int, error) {
|
||||||
|
n, err := b.Reader.Read(p)
|
||||||
|
b.store.bytes += n
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
func (*readTestBody) Close() error { return nil }
|
||||||
|
|
||||||
|
func TestReadSignatureAndExpiry(t *testing.T) {
|
||||||
|
store := &readTestStore{}
|
||||||
|
s, _ := NewSigner(store, "https://files.example/read", []byte(strings.Repeat("s", 32)))
|
||||||
|
now := time.Unix(1800000000, 0).UTC()
|
||||||
|
s.now = func() time.Time { return now }
|
||||||
|
link, err := s.PresignGetWithOptions(context.Background(), "folder/file", 30*time.Minute, ReadOptions{Disposition: DispositionInline, FileName: "报告.pdf", ContentType: "application/pdf"})
|
||||||
|
if err != nil || !link.ExpiresAt.Equal(now.Add(30*time.Minute)) {
|
||||||
|
t.Fatal("incorrect expiry", err)
|
||||||
|
}
|
||||||
|
serve := func(method, address string, headers http.Header) *httptest.ResponseRecorder {
|
||||||
|
r := httptest.NewRequest(method, address, nil)
|
||||||
|
if headers != nil {
|
||||||
|
r.Header = headers
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(w, r)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
for _, method := range []string{"GET", "HEAD"} {
|
||||||
|
w := serve(method, link.URL, nil)
|
||||||
|
if w.Code != 200 || w.Header().Get("Content-Type") != "application/pdf" || w.Header().Get("Cache-Control") != "private, no-store" {
|
||||||
|
t.Fatal(w)
|
||||||
|
}
|
||||||
|
if method == "HEAD" && w.Body.Len() != 0 {
|
||||||
|
t.Fatal("HEAD body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
old, err := s.PresignGet(context.Background(), "folder/file", 30*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if w := serve("GET", old.String(), nil); w.Code != 200 || w.Header().Get("Content-Disposition") != "attachment" {
|
||||||
|
t.Fatal("legacy download changed")
|
||||||
|
}
|
||||||
|
mutations := []func(url.Values){
|
||||||
|
func(q url.Values) { q.Set("key", "other") }, func(q url.Values) { q.Set("expires", "1800001900") }, func(q url.Values) { q.Set("signature", "bad") },
|
||||||
|
func(q url.Values) { q.Set("filename", "other.pdf") }, func(q url.Values) { q.Set("disposition", "attachment") }, func(q url.Values) { q.Set("contentType", "text/html") },
|
||||||
|
func(q url.Values) { q.Set("v", "3") }, func(q url.Values) { q.Del("v") }, func(q url.Values) {
|
||||||
|
for _, k := range []string{"v", "filename", "disposition", "contentType"} {
|
||||||
|
q.Del(k)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func(q url.Values) { q.Set("extra", "x") }, func(q url.Values) { q.Set("filename", "evil\r\nX: yes") },
|
||||||
|
}
|
||||||
|
for _, key := range []string{"key", "expires", "signature", "v", "disposition", "filename", "contentType"} {
|
||||||
|
key := key
|
||||||
|
mutations = append(mutations, func(q url.Values) { q.Add(key, q.Get(key)) })
|
||||||
|
}
|
||||||
|
for _, mutate := range mutations {
|
||||||
|
u, _ := url.Parse(link.URL)
|
||||||
|
q := u.Query()
|
||||||
|
mutate(q)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
before := store.calls
|
||||||
|
if w := serve("GET", u.String(), http.Header{"Range": {"bytes=0-1"}}); w.Code != 403 || store.calls != before {
|
||||||
|
t.Fatal("tampered link reached storage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u := *old
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("disposition", "inline")
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
if serve("GET", u.String(), nil).Code != 403 {
|
||||||
|
t.Fatal("legacy download upgraded by query")
|
||||||
|
}
|
||||||
|
if serve("GET", link.URL+"&bad=%ZZ", nil).Code != 403 {
|
||||||
|
t.Fatal("malformed query accepted")
|
||||||
|
}
|
||||||
|
before := store.calls
|
||||||
|
now = link.ExpiresAt.Add(-time.Nanosecond)
|
||||||
|
if serve("GET", link.URL, nil).Code != 200 {
|
||||||
|
t.Fatal("early expiration")
|
||||||
|
}
|
||||||
|
before = store.calls
|
||||||
|
now = link.ExpiresAt
|
||||||
|
for _, method := range []string{"GET", "HEAD"} {
|
||||||
|
w := serve(method, link.URL, http.Header{"Range": {"bytes=0-1"}, "If-Modified-Since": {"Fri, 01 Jan 2100 00:00:00 GMT"}})
|
||||||
|
if w.Code != 403 || store.calls != before || w.Header().Get("Cache-Control") != "private, no-store" {
|
||||||
|
t.Fatal("expired link reached storage or cache")
|
||||||
|
}
|
||||||
|
if method == "HEAD" && w.Body.Len() != 0 {
|
||||||
|
t.Fatal("HEAD error has body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ttl := range []time.Duration{0, time.Second - time.Nanosecond, time.Hour + time.Nanosecond} {
|
||||||
|
if _, err := s.PresignGetWithOptions(context.Background(), "key", ttl, ReadOptions{}); err == nil {
|
||||||
|
t.Fatal("invalid TTL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ttl := range []time.Duration{time.Second, time.Hour} {
|
||||||
|
if _, err := s.PresignGetWithOptions(context.Background(), "key", ttl, ReadOptions{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
if _, err := s.PresignGetWithOptions(ctx, "key", time.Minute, ReadOptions{}); err != context.Canceled {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadRangeStreamingAndCache(t *testing.T) {
|
||||||
|
for _, streaming := range []bool{false, true} {
|
||||||
|
store := &readTestStore{streaming: streaming}
|
||||||
|
s, _ := NewSigner(store, "https://files.example/read", []byte(strings.Repeat("s", 32)))
|
||||||
|
link, _ := s.PresignGetWithOptions(context.Background(), "key", 30*time.Minute, ReadOptions{})
|
||||||
|
for _, method := range []string{"GET", "HEAD"} {
|
||||||
|
before := store.bytes
|
||||||
|
r := httptest.NewRequest(method, link.URL, nil)
|
||||||
|
r.Header.Set("Range", "bytes=2-4")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(w, r)
|
||||||
|
if method == "HEAD" {
|
||||||
|
if w.Code != 200 || w.Body.Len() != 0 || store.bytes != before {
|
||||||
|
t.Fatal("HEAD read body", w)
|
||||||
|
}
|
||||||
|
} else if streaming {
|
||||||
|
if w.Code != 200 || w.Body.String() != "0123456789" || w.Header().Get("Accept-Ranges") != "" {
|
||||||
|
t.Fatal("stream fallback", w)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if w.Code != 206 || w.Body.String() != "234" || store.bytes-before != 3 {
|
||||||
|
t.Fatal("range buffered full body", w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.Header().Get("Cache-Control") != "private, no-store" {
|
||||||
|
t.Fatal("missing cache policy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !streaming {
|
||||||
|
for _, h := range []http.Header{{"Range": {"bytes=99-100"}}, {"If-Modified-Since": {"Fri, 01 Jan 2100 00:00:00 GMT"}}} {
|
||||||
|
r := httptest.NewRequest("GET", link.URL, nil)
|
||||||
|
r.Header = h
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
s.ServeHTTP(w, r)
|
||||||
|
if w.Code != 416 && w.Code != 304 {
|
||||||
|
t.Fatal(w.Code)
|
||||||
|
}
|
||||||
|
if w.Header().Get("Cache-Control") != "private, no-store" {
|
||||||
|
t.Fatal("cache policy lost on error/conditional")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user