From 5e9e8c0d4981e07ff2a767c2d225711e02ec03fb Mon Sep 17 00:00:00 2001 From: youfak Date: Fri, 7 Aug 2026 20:59:37 +0800 Subject: [PATCH] ci: validate openapi contracts with redocly --- .github/workflows/ci.yml | 10 ++++++++++ api/openapi/admin.yaml | 5 +++++ api/openapi/proxy-pool.yaml | 2 ++ api/openapi/redocly.yaml | 5 +++++ api/openapi/validation_test.go | 25 +++++++++++++++++++++++++ deploy/ci_test.go | 3 ++- docs/development/implementation-plan.md | 3 ++- docs/requirements/completion-audit.md | 3 ++- findings.md | 7 +++++++ progress.md | 2 ++ scripts/verify-openapi.ps1 | 21 +++++++++++++++++++++ scripts/verify.ps1 | 1 + 12 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 api/openapi/redocly.yaml create mode 100644 scripts/verify-openapi.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 855f077..102eeb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,16 @@ permissions: contents: read jobs: + openapi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + - shell: pwsh + run: ./scripts/verify-openapi.ps1 + proto: runs-on: ubuntu-latest steps: diff --git a/api/openapi/admin.yaml b/api/openapi/admin.yaml index d321017..057ecc5 100644 --- a/api/openapi/admin.yaml +++ b/api/openapi/admin.yaml @@ -10,10 +10,15 @@ servers: - url: http://127.0.0.1:8082 tags: - name: Status + description: 不含代理明细与凭据的控制面聚合状态。 - name: Audit + description: 权威管理状态变更的游标审计查询。 - name: Upstreams + description: 上游的启停与受控状态变更。 - name: Routing + description: Routing 当前上游的受控切换。 - name: Configuration + description: 经校验后原子发布的配置重载。 security: - AdminApiKey: [] - BasicAuth: [] diff --git a/api/openapi/proxy-pool.yaml b/api/openapi/proxy-pool.yaml index a807e0f..34de5bf 100644 --- a/api/openapi/proxy-pool.yaml +++ b/api/openapi/proxy-pool.yaml @@ -16,7 +16,9 @@ servers: description: Distribution API tags: - name: Distribution + description: 一次性独占提取上游代理的受保护接口。 - name: Health + description: Distribution 监听器的存活与就绪探针。 paths: /api/v1/proxies/extract: post: diff --git a/api/openapi/redocly.yaml b/api/openapi/redocly.yaml new file mode 100644 index 0000000..6d94e42 --- /dev/null +++ b/api/openapi/redocly.yaml @@ -0,0 +1,5 @@ +extends: + - minimal + +rules: + tag-description: error diff --git a/api/openapi/validation_test.go b/api/openapi/validation_test.go index b0db7a1..6f9119b 100644 --- a/api/openapi/validation_test.go +++ b/api/openapi/validation_test.go @@ -26,6 +26,31 @@ func TestOpenAPIDocumentsHaveClosedContracts(t *testing.T) { } } +func TestOpenAPITagsHaveDescriptions(t *testing.T) { + for _, name := range []string{"proxy-pool.yaml", "admin.yaml"} { + name := name + t.Run(name, func(t *testing.T) { + root := readOpenAPIRoot(t, name) + tags, ok := root["tags"].([]any) + if !ok || len(tags) == 0 { + t.Fatal("tags must be a non-empty array") + } + for _, rawTag := range tags { + tag, ok := rawTag.(map[string]any) + if !ok { + t.Errorf("tag has type %T, want object", rawTag) + continue + } + name, _ := tag["name"].(string) + description, _ := tag["description"].(string) + if strings.TrimSpace(description) == "" { + t.Errorf("tag %q has no description", name) + } + } + }) + } +} + func validateLocalReferences(t *testing.T, root map[string]any) { t.Helper() walkOpenAPI(root, func(path string, value any) { diff --git a/deploy/ci_test.go b/deploy/ci_test.go index d3f6c80..9fc086f 100644 --- a/deploy/ci_test.go +++ b/deploy/ci_test.go @@ -22,12 +22,13 @@ func TestCIValidatesProtocolAndDeploymentAssets(t *testing.T) { if !ok { t.Fatal("ci workflow has no jobs") } - for _, name := range []string{"proto", "deployment", "test", "race", "integration"} { + for _, name := range []string{"openapi", "proto", "deployment", "test", "race", "integration"} { if _, exists := jobs[name]; !exists { t.Errorf("ci workflow has no %s job", name) } } for _, required := range []string{ + "./scripts/verify-openapi.ps1", "./scripts/verify-proto.ps1", "docker compose -f deploy/docker-compose.yml config --quiet", "kubectl kustomize deploy/kubernetes/base", diff --git a/docs/development/implementation-plan.md b/docs/development/implementation-plan.md index dc54574..ccea966 100644 --- a/docs/development/implementation-plan.md +++ b/docs/development/implementation-plan.md @@ -344,7 +344,8 @@ Supervisor 也改为同时服从静态配置与管理态,消除两条启停消 校验本地 `$ref` 闭合、operationId 唯一、响应存在及 security scheme 引用; `scripts/verify-proto.ps1` 已可复现编译包含 imports/source info 的 descriptor, 并使用 SHA-256 固定的 `protoc` 35.0 安装器在 CI 完整验证 descriptor 与生成代码漂移; -OpenAPI 结构契约由 Go 测试在双平台 CI 执行。 +OpenAPI 结构契约由 Go 测试在双平台 CI 执行;`scripts/verify-openapi.ps1` 固定 +`@redocly/cli@2.25.4`,按 OpenAPI 3.1 最小规则集验证两份文档并将 Tag 描述作为错误。 ## Task 13: Deployment and Observability diff --git a/docs/requirements/completion-audit.md b/docs/requirements/completion-audit.md index 7a03503..37c2bd1 100644 --- a/docs/requirements/completion-audit.md +++ b/docs/requirements/completion-audit.md @@ -21,7 +21,8 @@ - Admin OpenAPI:状态、按 ID 游标分页的权威审计查询、Upstream 启停、Routing 切换和配置重载。 - 两份 OpenAPI 已进入 Go/CI 结构门禁,覆盖本地引用闭合、operationId、响应和 - security scheme;完整标准工具验证仍待补齐。 + security scheme;固定版本的 Redocly CLI 还会按 OpenAPI 3.1 最小规则集验证两份文档, + 并把 Tag 描述提升为错误。 - Protobuf:Worker 注册、全量/增量 Snapshot、`usable_until`、ACK、运行态/ 结果上报、Checker 任务与 Observation。 diff --git a/findings.md b/findings.md index 358dd59..13adaf0 100644 --- a/findings.md +++ b/findings.md @@ -261,3 +261,10 @@ Routing 自上而下匹配,首条命中停止;支持 Gateway 与 Extract 两 - Windows 本地 Go 运行环境为 `CGO_ENABLED=0` 且没有 C 编译器;Docker Engine 可用,但 `golang:1.26-bookworm` 与 `debian:bookworm-slim` 未缓存,Docker Desktop HTTPS 代理也不可用。 因此已完成 Go 全量、Compose 静态和 Kustomize 静态验证,容器端到端验证仍待具备镜像网络的环境。 + +## OpenAPI 契约审计(2026-08-07) + +- Go 结构契约继续锁定本地引用、operationId、响应与认证引用;额外以固定 + `@redocly/cli@2.25.4` 对两份 OpenAPI 3.1 文档执行标准验证,避免只依赖自定义遍历器。 +- 标准最小规则集中的 Tag 描述已提升为错误;Distribution、Health、Status、Audit、Upstreams、 + Routing 和 Configuration 标签均有面向 API 使用者的稳定说明。 diff --git a/progress.md b/progress.md index 39e6413..cceff45 100644 --- a/progress.md +++ b/progress.md @@ -2,6 +2,8 @@ ## 2026-08-07 +- OpenAPI 标准验证已固定为 `@redocly/cli@2.25.4`:`verify-openapi.ps1` 对两份 OpenAPI + 3.1 文档运行最小规则集,并将 Tag 描述提升为错误;本地统一校验与 GitHub Actions 均执行该门禁。 - 运行验证环境探测:Windows Go 为 `CGO_ENABLED=0`,系统没有可用 C 编译器;WSL 仅安装 `docker-desktop` 发行版。Docker Engine 可用,但 Dockerfile 所需的 `golang:1.26-bookworm`、`debian:bookworm-slim` 未缓存,且 Docker Desktop HTTPS 代理不可用; diff --git a/scripts/verify-openapi.ps1 b/scripts/verify-openapi.ps1 new file mode 100644 index 0000000..307d239 --- /dev/null +++ b/scripts/verify-openapi.ps1 @@ -0,0 +1,21 @@ +param( + [string]$Npx = "" +) + +$ErrorActionPreference = "Stop" + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +if ([string]::IsNullOrWhiteSpace($Npx)) { + $Npx = (Get-Command npx -ErrorAction Stop).Source +} +$npxCommand = Get-Command $Npx -ErrorAction Stop +$configuration = Join-Path $repositoryRoot "api/openapi/redocly.yaml" +$documents = @( + (Join-Path $repositoryRoot "api/openapi/proxy-pool.yaml"), + (Join-Path $repositoryRoot "api/openapi/admin.yaml") +) + +& $npxCommand.Source --yes "@redocly/cli@2.25.4" lint --config $configuration @documents +if ($LASTEXITCODE -ne 0) { + throw "OpenAPI validation failed with exit code $LASTEXITCODE" +} diff --git a/scripts/verify.ps1 b/scripts/verify.ps1 index 9fb97d2..e7c4b17 100644 --- a/scripts/verify.ps1 +++ b/scripts/verify.ps1 @@ -23,6 +23,7 @@ Invoke-Step "go vet" { go vet ./... } Invoke-Step "unit tests" { go test -timeout 60s ./... } Invoke-Step "protobuf contracts" { & (Join-Path $PSScriptRoot "verify-proto.ps1") } +Invoke-Step "OpenAPI contracts" { & (Join-Path $PSScriptRoot "verify-openapi.ps1") } if ((go env CGO_ENABLED) -eq "1") { Invoke-Step "race tests" { go test -race -timeout 60s ./internal/... }