ci: validate openapi contracts with redocly

This commit is contained in:
youfak 2026-08-07 20:59:37 +08:00
parent a92dcab333
commit 5e9e8c0d49
12 changed files with 84 additions and 3 deletions

View File

@ -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:

View File

@ -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: []

View File

@ -16,7 +16,9 @@ servers:
description: Distribution API
tags:
- name: Distribution
description: 一次性独占提取上游代理的受保护接口。
- name: Health
description: Distribution 监听器的存活与就绪探针。
paths:
/api/v1/proxies/extract:
post:

5
api/openapi/redocly.yaml Normal file
View File

@ -0,0 +1,5 @@
extends:
- minimal
rules:
tag-description: error

View File

@ -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) {

View File

@ -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",

View File

@ -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

View File

@ -21,7 +21,8 @@
- Admin OpenAPI状态、按 ID 游标分页的权威审计查询、Upstream 启停、Routing
切换和配置重载。
- 两份 OpenAPI 已进入 Go/CI 结构门禁覆盖本地引用闭合、operationId、响应和
security scheme完整标准工具验证仍待补齐。
security scheme固定版本的 Redocly CLI 还会按 OpenAPI 3.1 最小规则集验证两份文档,
并把 Tag 描述提升为错误。
- ProtobufWorker 注册、全量/增量 Snapshot、`usable_until`、ACK、运行态/
结果上报、Checker 任务与 Observation。

View File

@ -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 使用者的稳定说明。

View File

@ -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 代理不可用;

View File

@ -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"
}

View File

@ -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/... }