ci: wire openapi diff workflow at root

This commit is contained in:
lili 2026-06-19 04:19:35 -07:00
parent 984c6db583
commit c66d2b9b82
7 changed files with 43 additions and 16 deletions

View File

@ -28,7 +28,7 @@
|---|---|---|
| [`rules/verification-and-anti-false-green.md`](rules/verification-and-anti-false-green.md) | ✅ | **脊柱规则**:完成=机械验证、机械门禁优先、反假绿(P0 已落地首批门禁) |
| [`rules/bc-boundaries.md`](rules/bc-boundaries.md) | ✅ | BC 边界:禁跨域 .dal 直连,ArchUnit 机械约束(已知违例 ContentMuseWorkOwnerFacade 单点登记整改) |
| [`rules/contract-first.md`](rules/contract-first.md) | ✅ | 契约先行:API=`docs/api-contracts/*`、DB=`sql/muse/V*` **原地 SSOT**;Flyway 卫生 + OpenAPI 结构 机械门禁(openapi-diff CI 待启用) |
| [`rules/contract-first.md`](rules/contract-first.md) | ✅ | 契约先行:API=`docs/api-contracts/*`、DB=`sql/muse/V*` **原地 SSOT**;Flyway 卫生 + OpenAPI 结构 机械门禁(openapi-diff 已接 root workflow,远端首跑待确认) |
| `rules/engineering-conventions.md` | ⏳ | 命名/分层/错误码/提交/PR(收敛 `docs/dev-baseline/global/01,02`) |
| `rules/security-and-reliability.md` | ⏳ | 安全/幂等/超时重试/可观测(收敛 `docs/dev-baseline/global/05,06`) |

View File

@ -113,7 +113,7 @@ curl -s --noproxy '*' -X POST -H "Authorization: Bearer test1" -H "tenant-id: 1"
## 七、本地验证 openapi-diff 破坏性变更门禁(2026-06-17 实证)
`muse-cloud/.github/workflows/openapi-diff.yml` 用 `docker run tufin/oasdiff:v1.19.1` 在 PR 上逐域比对 base/head 契约;本地无 Docker/GHA 无法整跑,但 oasdiff 是 Go 工具,可 `go install` 同款版本验证「检测逻辑 + 逐域循环聚合」。
`.github/workflows/openapi-diff.yml` 用 `docker run tufin/oasdiff:v1.19.1` 在 PR 上逐域比对 base/head 契约;本地无 Docker/GHA 无法整跑,但 oasdiff 是 Go 工具,可 `go install` 同款版本验证「检测逻辑 + 逐域循环聚合」。2026-06-19 已从 `muse-cloud/.github/workflows/` 移到仓库根,否则自建 Gitea Actions 不会扫描触发。
```bash
# 一键复现(自举 oasdiff@v1.19.1 + 临时 venv 装 pyyaml,零全局污染;~首次 go install 稍慢)
@ -125,4 +125,4 @@ bash muse-cloud/scripts/verify-openapi-diff.sh # → 5 场景全绿即门禁
- **坑3:go install 自报 "version main"** 属正常——release ldflags 仅官方构建注入,源码默认 `version=main`;二进制行为仍是所钉 tag 的。
- **坑4:跨文件 `$ref` 靠目录结构**:events 契约 `$ref: '../openapi-base.yaml'`,故 base/head 必须各自保留 `docs/api-contracts/` 完整目录(workflow 用 `git worktree` 检出整树、脚本用 `cp -R` 整目录),单独拷 `openapi.yaml` 会解析失败。
- 实证(oasdiff v1.19.1,全部 7 域真实契约,5/5):无变更/新增可选参数/新增端点→**放行**(rc=0);参数变必填(`request-parameter-became-required`)/删端点(`api-path-removed-without-deprecation`)→**拦截**(rc=1)。
- **残留(脚本覆盖不到)**:GHA 触发管道(`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取)须一次真实 PR(改 `docs/api-contracts/**`)首跑确认。未首跑前不得宣称"CI 已实拦破坏性变更",只能称"检测逻辑+循环逻辑已本地实证"。
- **残留(脚本覆盖不到)**:远端 Gitea Actions 触发管道(`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取)须一次真实 PR(改 `docs/api-contracts/**`)首跑确认。未首跑前不得宣称"CI 已实拦破坏性变更",只能称"检测逻辑+循环逻辑已本地实证、root workflow 已接线"。

View File

@ -36,14 +36,14 @@
---
## 四、加固:openapi-diff 破坏性变更检测(CI 级,已 materialize / 检测逻辑已本地实证 / GHA 触发待首跑)
## 四、加固:openapi-diff 破坏性变更检测(CI 级,仓库根 workflow 已接线 / 检测逻辑已本地实证 / 远端首跑待确认)
存在性/结构门禁挡不住“悄悄改坏既有契约语义”(删字段、改必填、改类型)。**破坏性变更检测**用 `oasdiff` 在 PR 上比对 base 分支与 PR 的 OpenAPI,发现破坏性差异即红。
**已落地为独立 workflow** [`muse-cloud/.github/workflows/openapi-diff.yml`](../../muse-cloud/.github/workflows/openapi-diff.yml)(PR 改 `docs/api-contracts/**` 时触发,逐域 `oasdiff breaking ... --fail-on ERR`,镜像钉 `tufin/oasdiff:v1.19.1`)。workflow 即 SSOT,此处**不复制 YAML**(避免双源漂移)。
**已落地为独立 workflow** [`.github/workflows/openapi-diff.yml`](../../.github/workflows/openapi-diff.yml)(PR 改 `docs/api-contracts/**` 时触发,逐域 `oasdiff breaking ... --fail-on ERR`,镜像钉 `tufin/oasdiff:v1.19.1`)。workflow 即 SSOT,此处**不复制 YAML**(避免双源漂移)。2026-06-19 已从 `muse-cloud/.github/workflows/` 移到仓库根,因为自建 Gitea Actions 只扫描仓库根 `.github/workflows/`
**检测逻辑 + 逐域循环聚合:已本地实证**(2026-06-17,oasdiff v1.19.1)。复现脚本 [`muse-cloud/scripts/verify-openapi-diff.sh`](../../muse-cloud/scripts/verify-openapi-diff.sh) 忠实复刻 workflow 循环,对全部 7 域真实契约跑 5 场景全绿:无变更/新增可选参数/新增端点→放行;参数变必填(`request-parameter-became-required`)/删端点(`api-path-removed-without-deprecation`)→拦截。坑与配方见 [knowledge §七](../knowledge/external-deps-and-gotchas.md)。
> ⚠️ **残留**:GitHub Actions 触发管道(`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取)须一次真实 PR(改 `docs/api-contracts/**`)首跑确认。**首跑前**:可宣称"破坏性变更检测逻辑已本地实证(对真实契约拦真破坏、放行安全演化)",但**不得**宣称"CI 已在线上实拦"。
> ⚠️ **残留**:远端 Gitea Actions 触发管道(`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取)须一次真实 PR(改 `docs/api-contracts/**`)首跑确认。**首跑前**:可宣称"破坏性变更检测逻辑已本地实证(对真实契约拦真破坏、放行安全演化),root workflow 已接线",但**不得**宣称"CI 已在线上实拦"。
---

View File

@ -1,10 +1,10 @@
# OpenAPI 破坏性变更门禁(契约先行加固)
# 仅在 PR 改动 docs/api-contracts/** 时触发;逐域用 oasdiff 比对 base 与 PR 的 OpenAPI,发现破坏性差异即失败。
# 注:独立于主 maven 构建,避免误配影响主门禁
# 注:本文件必须位于仓库根 .github/workflows/ 下;自建 Gitea Actions 不扫描 muse-cloud/.github/workflows/
# 镜像钉版本 tufin/oasdiff:v1.19.1(机械门禁须可复现,避免 :latest 漂移改变判定口径)。
# 检测逻辑已本地验证(2026-06-17,oasdiff v1.19.1 对全部 7 域真实契约 5/5:无变更/新增可选参数/新增端点→放行,
# 参数变必填/删端点→拦截;复现脚本见 .agents/knowledge §七)。**残留:GitHub Actions 触发管道(PR paths 触发 +
# base.sha worktree + docker run)需一次真实 PR(改 docs/api-contracts/**)首跑确认。**
# 参数变必填/删端点→拦截;复现脚本见 .agents/knowledge §七)。残留:远端 Gitea Actions 首次 PR 触发、base.sha
# worktree 与 docker run 仍需一次真实 PR(改 docs/api-contracts/**)确认。
name: OpenAPI Breaking Change Check
on:

View File

@ -25,7 +25,7 @@
**本仓是 monorepo,物理承载实现代码,不是"纯设计文档仓"。**
- `git ls-files muse-cloud` = 4468 文件、无 `.gitmodules``muse-cloud/``muse-admin/``muse-studio/` 三个实现仓代码**就在本工作树里、被本仓版本控制**;CI 在 `muse-cloud/.github/workflows/`。
- `git ls-files muse-cloud` = 4468 文件、无 `.gitmodules``muse-cloud/``muse-admin/``muse-studio/` 三个实现仓代码**就在本工作树里、被本仓版本控制**;既有 Maven CI 在 `muse-cloud/.github/workflows/`,OpenAPI diff 因自建 Gitea Actions 扫描规则已接到仓库根 `.github/workflows/`。
- 因此:**agent 可以也应当修改这些实现代码**(遵循各自规范 + [`.agents/rules/`](.agents/rules/))。此前"本仓不承载代码/CI"的描述与磁盘矛盾,会让 agent 误判改动边界、绕路产生文档 churn,已订正。
- `design-docs/``docs/` 是设计/规格 SSOT;实现代码须反向对齐其正式文档。
@ -67,7 +67,7 @@ oh-my-muse/
1. **读后动手**:复杂任务先读 [`.agents/`](.agents/) 与相关 `docs/`,对齐事实再开工。
2. **机械门禁优先 / 完成=验证**:遵守 [`verification-and-anti-false-green`](.agents/rules/verification-and-anti-false-green.md)——无自动化绿证据不得声称完成;新规则必配机械门禁,不靠自觉。
3. **复杂/高危先评审**:跨模块 / 改用户可见行为 / 触外部服务·支付·数据 → 评审版 → 两轮评审 → 执行版,再写代码。
4. **契约先行**:改接口/数据结构先改契约(API=`docs/api-contracts/*`、DB=新增 `sql/muse/V*.sql`,均为原地 SSOT)再实现;遵守 [`contract-first`](.agents/rules/contract-first.md),已有 Flyway 卫生 + OpenAPI 结构机械门禁(openapi-diff 待启用)。
4. **契约先行**:改接口/数据结构先改契约(API=`docs/api-contracts/*`、DB=新增 `sql/muse/V*.sql`,均为原地 SSOT)再实现;遵守 [`contract-first`](.agents/rules/contract-first.md),已有 Flyway 卫生 + OpenAPI 结构机械门禁(openapi-diff 已接 root workflow,远端首跑待确认)。
5. **最小改动 + 中文注释**:只动相关代码,复用既有模式,不顺手重构;代码全简体中文注释,关键路径可追溯日志。
6. **无孤儿/拼接设计**:任何新结构/接口/模型须带入口、使用路径、失败路径、验收标准。
7. **证据规则**:区分已验证事实/推断/假设;无证据不声称"完成/修复/通过"。
@ -91,9 +91,9 @@ oh-my-muse/
- ✅ 机械门禁地基:CI 真跑测试(JDK21、触发分支已修为 `main`——此前误配 `master` 致 CI 从不运行)、门禁去硬编码(含分域计数派生化)、P0 冻结令。
- ✅ 入口与中枢骨架:本文件 + `.agents/README.md` + 脊柱规则。
- ✅ BC 边界 ArchUnit 门([`bc-boundaries`](.agents/rules/bc-boundaries.md)):**通用覆盖全业务 BC 间方向**;AI/knowledge 直连 content、market 直写 member 三处违例**均已整改消除**,豁免清单 `KNOWN_VIOLATION_EXEMPTIONS` **清空 → 全绿**(0 Architecture Violation;遗留:market-server pom 仍依赖 member-server,仅消除了 .dal 代码 import,见该规则 §三)。
- ✅ 契约先行门([`contract-first`](.agents/rules/contract-first.md)):Flyway 迁移卫生 + OpenAPI **存在性/结构**(注:挡不住语义破坏;openapi-diff CI 已materialize,需首次 CI 运行验证)。
- ✅ 契约先行门([`contract-first`](.agents/rules/contract-first.md)):Flyway 迁移卫生 + OpenAPI **存在性/结构**(注:挡不住语义破坏;openapi-diff root workflow 已接线,需远端首次 PR 运行验证)。
- ✅ loop 机械牙([`AgentsInfraIntegrityTest`](muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/arch/AgentsInfraIntegrityTest.java)):每业务 BC 有 `.agent`、README 索引每篇 `.agents` 文档、总账在——把写回/索引同步从自觉变机械。
- ✅ knowledge 蒸馏 / skills / workflow(AI 开发协议)/ 进度总账 + 7 BC `.agent`
- ⏳ 后续:openapi-diff CI 首跑验证、`dev-baseline` 收敛进 rules、Meta/schema 投影真实化、前端深页继续关 MSW 活体验收。`completed=测试证据`兜底(testFiles)、跨域 `.application` 边界、market-server→member-server pom 坐标收口、ai/平台预存红测试整改均已完成。
- ⏳ 后续:openapi-diff 远端 CI 首跑验证、`dev-baseline` 收敛进 rules、Meta/schema 投影真实化、前端深页继续关 MSW 活体验收。`completed=测试证据`兜底(testFiles)、跨域 `.application` 边界、market-server→member-server pom 坐标收口、ai/平台预存红测试整改均已完成。
> 进度只进 [`docs/mvp/进度总账.md`](docs/mvp/进度总账.md) + 各模块 `.agent`,不新增状态过程文档。

View File

@ -9,9 +9,9 @@
| 砖 | 内容 | 状态 | 机械证据 / 落点 |
|---|---|---|---|
| P0 | CI 真跑测试(JDK21、去 `-Dmaven.test.skip`)+ 覆盖台账去硬编码 + P0 冻结令 | ✅ | `.github/workflows/maven.yml`、`P1rApiCoverageReportTest`、[脊柱规则](../../.agents/rules/verification-and-anti-false-green.md) |
| P0 | CI 真跑测试(JDK21、去 `-Dmaven.test.skip`)+ 覆盖台账去硬编码 + P0 冻结令 | ✅ | `muse-cloud/.github/workflows/maven.yml`、`P1rApiCoverageReportTest`、[脊柱规则](../../.agents/rules/verification-and-anti-false-green.md) |
| ① | BC 边界 ArchUnit 门——**通用覆盖全业务 BC 间方向** | ✅ **全绿**(AI/knowledge/market 三处直连他域 DAL 违例均已整改,豁免清单清空) | `BcBoundaryArchTest`(1/0F,0 Architecture Violation,`KNOWN_VIOLATION_EXEMPTIONS`=空);[bc-boundaries](../../.agents/rules/bc-boundaries.md) §三 |
| ② | 契约先行门(Flyway 迁移卫生 + OpenAPI **存在性/结构**) | ✅ 绿;语义破坏由 openapi-diff 补强——**检测逻辑已本地实证**(2026-06-17,oasdiff v1.19.1,7 域真实契约 5/5),GHA 触发管道未接线(诊断见 §一 TODO;`mvn -B package` 已本地实证绿) | `ContractFirstGateTest`(2/0F);[openapi-diff.yml](../../muse-cloud/.github/workflows/openapi-diff.yml)+[verify 脚本](../../muse-cloud/scripts/verify-openapi-diff.sh);[contract-first](../../.agents/rules/contract-first.md) §四 |
| ② | 契约先行门(Flyway 迁移卫生 + OpenAPI **存在性/结构**) | ✅ 绿;语义破坏由 openapi-diff 补强——**检测逻辑已本地实证**(2026-06-17,oasdiff v1.19.1,7 域真实契约 5/5),root workflow 已接线,远端 Gitea Actions 首跑待确认 | `ContractFirstGateTest`(2/0F);[openapi-diff.yml](../../.github/workflows/openapi-diff.yml)+[verify 脚本](../../muse-cloud/scripts/verify-openapi-diff.sh);[contract-first](../../.agents/rules/contract-first.md) §四 |
| ⑦ | loop 机械牙 + CI 接电(round-2) | ✅ 绿 | `AgentsInfraIntegrityTest`(3/0F);`maven.yml` 触发分支修为 `main` |
| ③ | knowledge 蒸馏(定位架构 / 现状基线指针 / 决策) | ✅ | [`.agents/knowledge/`](../../.agents/knowledge/) |
| ④ | skills(黄金旅程“完成”定义 / 新增 BC 模块) | ✅ | [`.agents/skills/`](../../.agents/skills/) |
@ -22,7 +22,7 @@
**market 写路径整改(2026-06-14,ultracode)**:member 暴露写端口 `MuseAccountRecordProjectionApi` + DTO(member-server 实现读写自有 DAL、tenantId 由实现侧从上下文注入防伪造、事务沿用调用方),market 5 类改消费端口、移除 member.dal 依赖 → `KNOWN_VIOLATION_EXEMPTIONS` 清空、BC 门全绿。**附带修复**:round-2 重构 `ContentKnowledgeWorkOwnerFacade` 时遗留的旧测试 `KnowledgeWorkOwnerFacadeTest`(仍断言旧 WorkMapper 行为)已删除,其装配守卫/兜底两用例并入 `ContentKnowledgeWorkOwnerFacadeTest`(5/0F)——此为 round-2 一处假绿(当时构建在平台时区用例处中止、未真正跑到 knowledge),现已补正。
**后续基建 TODO**:openapi-diff CI 首跑验证(🔧 检测逻辑+循环聚合已本地实证 2026-06-17:oasdiff v1.19.1 对 7 域真实契约 5/5 拦真破坏/放行安全演化,镜像钉版本+复现脚本已落地;**仅余 GHA 触发管道未接线**——诊断 2026-06-17:maven.yml 在 `muse-cloud/.github/workflows/`、**仓库根无 `.github/workflows/`**、remote 是**自建 Gitea**(`101.200.34.71:2222` 非 github.com),GHA/Gitea-Actions 只读仓库根 `.github/workflows/`,故 monorepo 层级**不触发**(真 PR 也不会跑);需 workflow 移到仓库根[带路径过滤分 muse-cloud/admin/studio]或 muse-cloud 单独成库,并确认 Gitea Actions 已启。**`mvn -B package`[CI 确切命令]已本地实证全绿(2026-06-17,全 reactor 单测 + 打包步骤),接线后即应绿**——属基建接线决策,留人类)、`dev-baseline` 收敛进 rules。✅ 已完成项:`completed=测试证据`兜底(2026-06-19:coverage JSON 增 `testFiles`;生成器 `--check` + `P1rApiCoverageReportTest` 校验 completed 必须引用真实 `src/test/java` 证据文件且不得引用 coverage/gate 自身)、跨域 `.application` 边界门(2026-06-17:BcBoundaryArchTest 0 违例 + 反向证 238 例非空转)、market-server→member-server pom 坐标收口(2026-06-17:market-server 收窄至 member-api + tenant starter 直接声明)、ai/平台预存红测试整改(2026-06-15~17:三项全清)。
**后续基建 TODO**:openapi-diff 远端 CI 首跑验证(🔧 检测逻辑+循环聚合已本地实证 2026-06-17:oasdiff v1.19.1 对 7 域真实契约 5/5 拦真破坏/放行安全演化,镜像钉版本+复现脚本已落地;2026-06-19 已把 workflow 从 `muse-cloud/.github/workflows/` 移到仓库根 `.github/workflows/`,修正自建 Gitea Actions 只扫描根 workflow 的接线问题;**仅余远端真实 PR 首跑确认**:`pull_request` paths 触发、`base.sha` worktree、docker 镜像拉取、Gitea Actions 是否已启。**`mvn -B package`[CI 确切命令]已本地实证全绿(2026-06-17,全 reactor 单测 + 打包步骤),接线后即应绿**——属远端基建首跑,留人类)、`dev-baseline` 收敛进 rules。✅ 已完成项:`completed=测试证据`兜底(2026-06-19:coverage JSON 增 `testFiles`;生成器 `--check` + `P1rApiCoverageReportTest` 校验 completed 必须引用真实 `src/test/java` 证据文件且不得引用 coverage/gate 自身)、跨域 `.application` 边界门(2026-06-17:BcBoundaryArchTest 0 违例 + 反向证 238 例非空转)、market-server→member-server pom 坐标收口(2026-06-17:market-server 收窄至 member-api + tenant starter 直接声明)、ai/平台预存红测试整改(2026-06-15~17:三项全清)。
**🎯 全 reactor `mvn test` 史上首次全绿(2026-06-17)**:本特性分支 30+ commit 从未 push、maven.yml 只在 push/PR-to-main 触发故 **CI 从未在本分支真跑过**,积累若干潜伏红。以 `mvn -pl muse-server -am test -fae` 全 reactor 排查,逐模块解锁(上游失败 fail-fast 会 SKIP 下游),共清 **6 处**:① market `AppMuseMarketPublishControllerTest` 9 参 `PublishRecordItem` 构造(record 自 a00c758 加 marketAssetId/appealStatus 变 10 参)② coverage 台账 33 op serviceFiles 陈旧引用(本会话 facade 移 member-api 所致)③④ knowledge/ai 的 `Content*WorkOwnerFacadeTest` 仍断言 `@ConditionalOnBean`(2026-06-14 单体修复已去除、改 @Primary)⑤ ai `MuseAiEventPublishOutboxMapperTest` 真 PG 用例无 PG 时硬 fail→改 `assumeTrue` skip(与 P1r external-acceptance env 缺失即 skip 约定一致、honest 非假绿)⑥ muse-server `P1rKnowledgeMigrationSqlTest` 元测试钉死 `TARGET_VERSION="14"`(IT 已按 4d46d7a 动态自适应)→版本无关化。**全 reactor BUILD SUCCESS、0 失败**(真 PG 类无 PG 时 skip)。教训:跨模块重构后须跑 `-fae` 全量,勿只跑改动模块。**前端 muse-studio 同步复验绿(2026-06-17:`tsc -b` 0 错误 + `vitest run` 12 文件/47 用例全过;本分支前端改动 35 文件全在 studio、admin 未动)**——故**整分支(后端 `mvn -B package` + 前端 `tsc`/`vitest`)deterministic CI 检查全绿,均本会话实证**(e2e/playwright 需活体后端+浏览器,未在本轮跑;真 PG IT 多数 2026-06-17 已人工批准且本会话变更为 import-only+单测已覆盖,未重跑)。

View File

@ -124,6 +124,33 @@ class ContractFirstGateTest {
"缺少必备业务域 API 契约,期望包含 " + REQUIRED_API_DOMAINS + ",实际 " + foundDomains);
}
/**
* OpenAPI 破坏性变更门禁的 workflow 必须放在仓库根
*
* <p>背景:本仓是 monorepo,但自建 Gitea Actions 只扫描仓库根 {@code .github/workflows/}
* workflow 被放回 {@code muse-cloud/.github/workflows/},本地脚本仍可能绿,远端 PR 却不会触发,
* 这会把契约语义破坏门重新变成假绿因此这里用静态门禁锁住位置与关键配置</p>
*
* @throws IOException 读取 workflow 失败时抛出
*/
@Test
void openapi_diff_workflow_must_live_at_repo_root() throws IOException {
Path root = findRepoRoot();
Path rootWorkflow = root.resolve(".github/workflows/openapi-diff.yml");
Path misplacedWorkflow = root.resolve("muse-cloud/.github/workflows/openapi-diff.yml");
assertTrue(Files.exists(rootWorkflow), "openapi-diff workflow 必须位于仓库根: " + rootWorkflow);
assertFalse(Files.exists(misplacedWorkflow),
"openapi-diff workflow 不得放在 muse-cloud/.github/workflows/,否则 Gitea Actions 不会扫描触发: "
+ misplacedWorkflow);
String workflow = Files.readString(rootWorkflow);
assertTrue(workflow.contains("pull_request"), "openapi-diff workflow 必须由 PR 触发");
assertTrue(workflow.contains("docs/api-contracts/**"), "openapi-diff workflow 必须只监听 API 契约变更");
assertTrue(workflow.contains("github.event.pull_request.base.sha"), "openapi-diff workflow 必须比对 PR base sha");
assertTrue(workflow.contains("tufin/oasdiff:v1.19.1"), "openapi-diff workflow 必须钉 oasdiff 镜像版本");
assertTrue(workflow.contains("--fail-on ERR"), "openapi-diff workflow 必须在破坏性变更达到 ERR 时失败");
}
/** 解析一份 OpenAPI 规格并校验其为含必备顶层键的 OAS3 文档(解析失败会抛异常 → 测试红,即“写坏即拦截”)。 */
private static void assertValidOpenApi(Path spec) throws IOException {
assertTrue(Files.exists(spec), "OpenAPI 契约文件必须存在: " + spec);