feat: release v0.28.1 mixed API and generic project workspaces
This commit is contained in:
@@ -199,6 +199,31 @@ go-omni workspace check --file omni.workspace.json
|
|||||||
|
|
||||||
各服务保持独立 module;可显式填写各服务的 `api`(相对服务目录)和 `module`。所有服务预检通过后再写;每个服务独立提交文件事务,不承诺跨项目的崩溃原子性。check 汇总格式与生成漂移;业务编译/测试仍在各 module 执行。
|
各服务保持独立 module;可显式填写各服务的 `api`(相对服务目录)和 `module`。所有服务预检通过后再写;每个服务独立提交文件事务,不承诺跨项目的崩溃原子性。check 汇总格式与生成漂移;业务编译/测试仍在各 module 执行。
|
||||||
|
|
||||||
|
### 混合 API 与通用工程
|
||||||
|
|
||||||
|
v0.28.1-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 包和函数:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
## 创建与运行
|
## 创建与运行
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-preview
|
||||||
go-omni project new demo --module example.com/demo
|
go-omni project new demo --module example.com/demo
|
||||||
cd demo
|
cd demo
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@@ -78,4 +78,4 @@ go test ./...
|
|||||||
|
|
||||||
check 检查模板漂移和必要文件,不检查业务正确性,也不能替代 Go 编译与测试。新的模板版本会重新生成受管文件;不自动合并项目所有文件或更新 go.mod。未知的新清单版本会要求使用兼容 CLI。
|
check 检查模板漂移和必要文件,不检查业务正确性,也不能替代 Go 编译与测试。新的模板版本会重新生成受管文件;不自动合并项目所有文件或更新 go.mod。未知的新清单版本会要求使用兼容 CLI。
|
||||||
|
|
||||||
原有 `new` / `api new` 及 API 工程行为保留。通用工程使用自己的清单和命令,不能交给 `api generate` 或 `add` 转换;新增业务包直接写普通 Go。现有 `workspace` 命令仍面向 API 契约工程;多个通用工程可分别执行 `project check --dir <目录>`,本版不引入混合工作区配置。
|
原有 `new` / `api new` 及 API 工程行为保留。通用工程使用自己的清单和命令,不能交给 `api generate` 或 `add` 转换;新增业务包直接写普通 Go。从 v0.28.1-preview 起,`workspace generate/check` 可以同时处理 API 与通用工程。清单可填写 `kind: api | project`,省略时按生成清单识别;format 对通用工程显示 SKIP。见 [完整混合示例](examples/mixedWorkspace/README.md)。各 module 仍独立,workspace 命令不维护 go.work 或模块依赖。
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
GoOmni 是面向多个项目复用的 Go 基础框架。通用工程使用 `project new` 生成配置、日志、依赖装配、生命周期和普通 Go 运行入口;需要 HTTP 接口时可选择 `.api` 工程生成,也可以在已有项目中按需引入独立模块。运行方式、业务类型和外部依赖由项目决定。
|
GoOmni 是面向多个项目复用的 Go 基础框架。通用工程使用 `project new` 生成配置、日志、依赖装配、生命周期和普通 Go 运行入口;需要 HTTP 接口时可选择 `.api` 工程生成,也可以在已有项目中按需引入独立模块。运行方式、业务类型和外部依赖由项目决定。
|
||||||
|
|
||||||
|
**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.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.28.0-preview。同时修复 Go 1.22 下请求体限额错误的识别。
|
**v0.26 改动**:通用 [i18n](i18n/README.md),提供独立词典、语言匹配、回退、命名参数/条件/复数、可选 HTTP 与错误适配、检查/导出 CLI 及前端运行时。文案由项目维护;[完整示例](examples/i18n/README.md)可直接运行。CLI 与框架使用同版 v0.28.1-preview。同时修复 Go 1.22 下请求体限额错误的识别。
|
||||||
|
|
||||||
**从这份 README 开始接入。** 当前版本 **`v0.28.0-preview`**,通用工程采用根目录入口和 `config / svc / run`;API 工程使用 `config / svc / logic / handler`。可以固定版本开展业务接入;文件维护边界和已有服务迁移步骤见 [项目指南](PROJECT.md),版本变化与验收见 [发布说明](RELEASE.md)。
|
**从这份 README 开始接入。** 当前版本 **`v0.28.1-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)。
|
||||||
|
|
||||||
@@ -40,6 +42,7 @@ 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) |
|
| 通用 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)、[已有项目接入](#已有项目按模块接入) |
|
||||||
@@ -72,7 +75,7 @@ Go **1.22+**。框架不依赖 Gin,默认使用标准库网络层,提供可
|
|||||||
## 通用工程快速开始
|
## 通用工程快速开始
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-preview
|
||||||
go-omni project new demo --module example.com/demo
|
go-omni project new demo --module example.com/demo
|
||||||
cd demo
|
cd demo
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@@ -177,7 +180,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.28.0-preview
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-preview
|
||||||
export PATH="$(go env GOPATH)/bin:$PATH"
|
export PATH="$(go env GOPATH)/bin:$PATH"
|
||||||
go-omni --help
|
go-omni --help
|
||||||
```
|
```
|
||||||
@@ -334,7 +337,7 @@ go-omni new --api catalog.api \
|
|||||||
在业务项目的 **go.mod 所在目录**执行:
|
在业务项目的 **go.mod 所在目录**执行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get git.yuntuo.hk/k2safe/go-omni@v0.28.0-preview
|
go get git.yuntuo.hk/k2safe/go-omni@v0.28.1-preview
|
||||||
```
|
```
|
||||||
|
|
||||||
已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。
|
已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。
|
||||||
@@ -665,7 +668,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.28.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.28.1-preview`。v0.21 及更早项目先按 [配置迁移](CONFIG.md#v022-迁移旧模式已移除) 整理旧标签、文件与 Load,再升级依赖与 CLI 并重新生成,框架维护的 main/runtimeGen 自动更新;v0.13 及更早项目按 [升级预览](DEVELOPMENT.md#v013-项目升级) 处理,不直接覆盖手写入口或业务文件。使用过旧 mapping 的项目必须先完成 [移除迁移](COPY.md#从旧-mapping-迁移),旧配置或旧生成文件会阻止生成。升级后执行生成检查与业务回归。版本变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。
|
||||||
|
|
||||||
## 按需深入
|
## 按需深入
|
||||||
|
|
||||||
|
|||||||
16
RELEASE.md
16
RELEASE.md
@@ -1,3 +1,19 @@
|
|||||||
|
# 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 均通过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# v0.28.0-preview
|
# v0.28.0-preview
|
||||||
|
|
||||||
新增通用 Go 工程生成命令 `project new/generate/check`。默认只有配置、日志、svc 依赖装配、生命周期与项目维护的 Run 入口,不生成 API、HTTP Handler、Swagger、业务任务或网络监听,也不自动连接数据库/队列或重试任务。常驻和一次性程序由普通 Go 代码决定。
|
新增通用 Go 工程生成命令 `project new/generate/check`。默认只有配置、日志、svc 依赖装配、生命周期与项目维护的 Run 入口,不生成 API、HTTP Handler、Swagger、业务任务或网络监听,也不自动连接数据库/队列或重试任务。常驻和一次性程序由普通 Go 代码决定。
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# 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.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 与框架。
|
||||||
@@ -18,7 +20,7 @@ API 新项目采用根目录 `main.go` 与 `internal/config、svc、logic、hand
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 仓库读取权限与 PATH 配置见根目录 README
|
# 仓库读取权限与 PATH 配置见根目录 README
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-preview
|
||||||
go-omni new --module example.com/hello ./hello
|
go-omni new --module example.com/hello ./hello
|
||||||
|
|
||||||
# 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在
|
# 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在
|
||||||
@@ -53,14 +55,14 @@ main、Gen.go、OpenAPI 和 manifest 由生成器维护;业务文件按字节
|
|||||||
|
|
||||||
## 版本与迁移
|
## 版本与迁移
|
||||||
|
|
||||||
CLI 与框架依赖固定使用 v0.28.0-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换:
|
CLI 与框架依赖固定使用 v0.28.1-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.28.0-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
|
当前模板与依赖以 v0.28.1-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
|
||||||
|
|
||||||
## 数据库迁移命令
|
## 数据库迁移命令
|
||||||
|
|
||||||
|
|||||||
@@ -23,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.28.0-preview"
|
const currentVersion = "v0.28.1-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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
||||||
const currentVersion = "v0.28.0-preview"
|
const currentVersion = "v0.28.1-preview"
|
||||||
const minimumVersion = "v0.28.0-preview"
|
const minimumVersion = "v0.28.0-preview"
|
||||||
const manifestPath = ".goOmni/projectManifest.json"
|
const manifestPath = ".goOmni/projectManifest.json"
|
||||||
const lockPath = ".goOmni/project.lock"
|
const lockPath = ".goOmni/project.lock"
|
||||||
|
|||||||
@@ -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.28.0-preview"
|
const defaultFrameworkVersion = "v0.28.1-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
|
||||||
|
}
|
||||||
39
examples/mixedWorkspace/README.md
Normal file
39
examples/mixedWorkspace/README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# API 与通用工程混合工作区
|
||||||
|
|
||||||
|
本例只定义生成工程的类型与目录,不包含业务模型、外部服务或特定调度方式。使用 v0.28.1-preview 或更新版本 CLI。
|
||||||
|
|
||||||
|
下面从一个空目录创建两个独立 Go module;API 工程来自 API 模板,通用工程默认不监听端口。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
28
reports/workspaceAcceptance.md
Normal file
28
reports/workspaceAcceptance.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 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 导出函数;生成事务按单个工程提交,不保证跨模块的崩溃原子性。
|
||||||
|
|
||||||
|
## 远程发布验收
|
||||||
|
|
||||||
|
发布后从独立空模块缓存安装固定 CLI,执行混合工作区与既有生成器验收;结果完成后补入 JSON 报告。
|
||||||
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()
|
||||||
Reference in New Issue
Block a user