Files
lingxi/scripts/openapi-diff.mjs
T
Stev_Wang 04f49690f4
ci / 质量门禁 (push) Canceled after 0s
feat(a0): 完成工程骨架与契约基线
- 建立 pnpm workspace + Turborepo 单仓、ESLint 9/Prettier/commitlint/husky 门禁
- NestJS + Fastify API:统一错误体、trace_id、Zod 管道、/health 真实依赖探活、OpenAPI 3.1 契约
- Prisma 基线:outbox_events/idempotency_keys/system_configs 与首版迁移、幂等种子
- shared 契约包:错误码、权限点、ULID、游标分页、命名/时间工具
- ui 包与 Web 壳层:Semi + Remix 图标统一出口、Cloud Blue 视觉、登录/工作台/错误页
- worker 骨架、Playwright 冒烟、Gitea CI、OpenAPI 快照与 oasdiff 回退脚本
- 补充 A0 实施报告与本地开发说明
2026-11-02 17:01:50 +08:00

96 lines
3.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* OpenAPI 兼容性检查(A0 回退实现)。
*
* 优先使用 oasdiff 二进制;未安装时使用本脚本的结构化回退检查:
* - 删除 path / operation
* - 删除 components.schemas
* - 请求体必填字段新增、字段类型变更
*
* 基线来自 git HEAD 中的快照;首次提交(快照尚未入库)时跳过。
*/
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
const SNAPSHOT = resolve(process.cwd(), 'openapi/lingxi.openapi.json');
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
if (!existsSync(SNAPSHOT)) {
console.error(`未找到 OpenAPI 快照:${SNAPSHOT},请先执行 pnpm openapi:export`);
process.exit(1);
}
const current = JSON.parse(readFileSync(SNAPSHOT, 'utf8'));
let baseline = null;
try {
const raw = execFileSync('git', ['show', `HEAD:${'openapi/lingxi.openapi.json'}`], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
});
baseline = JSON.parse(raw);
} catch {
console.log('基线快照尚未入库,跳过 OpenAPI diff(首次提交)。');
process.exit(0);
}
const breaking = [];
for (const [path, baselinePath] of Object.entries(baseline.paths ?? {})) {
const currentPath = current.paths?.[path];
if (!currentPath) {
breaking.push(`删除路径:${path}`);
continue;
}
for (const method of HTTP_METHODS) {
if (baselinePath[method] !== undefined && currentPath[method] === undefined) {
breaking.push(`删除操作:${method.toUpperCase()} ${path}`);
}
}
}
for (const schemaName of Object.keys(baseline.components?.schemas ?? {})) {
if (current.components?.schemas?.[schemaName] === undefined) {
breaking.push(`删除 schema:${schemaName}`);
}
}
const baselineSchemas = baseline.components?.schemas ?? {};
const currentSchemas = current.components?.schemas ?? {};
for (const [name, baselineSchema] of Object.entries(baselineSchemas)) {
const currentSchema = currentSchemas[name];
if (currentSchema === undefined) {
continue;
}
const baselineRequired = new Set(baselineSchema.required ?? []);
for (const required of currentSchema.required ?? []) {
if (!baselineRequired.has(required)) {
breaking.push(`schema ${name} 新增必填字段:${required}`);
}
}
for (const [property, baselineProperty] of Object.entries(baselineSchema.properties ?? {})) {
const currentProperty = currentSchema.properties?.[property];
if (
currentProperty !== undefined &&
baselineProperty.type !== undefined &&
currentProperty.type !== undefined &&
baselineProperty.type !== currentProperty.type
) {
breaking.push(
`schema ${name}.${property} 类型变更:${baselineProperty.type} -> ${currentProperty.type}`,
);
}
}
}
if (breaking.length > 0) {
console.error('检测到破坏性 OpenAPI 变更:');
for (const item of breaking) {
console.error(` - ${item}`);
}
process.exit(1);
}
console.log('OpenAPI 无破坏性变更。');