Files
lingxi/scripts/openapi-diff.mjs
Stev_Wang b3a2a843bb build(ci): 建立单仓工具链与质量门禁
统一 pnpm 10 + Node 24.15.0 + Turborepo 单仓,落地 ESLint 9 flat、Prettier、commitlint、husky 与 Gitea CI,作为后续批次的一致门禁。
2026-11-02 17:20:17 +08:00

96 lines
3.0 KiB
JavaScript
Raw Permalink 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 无破坏性变更。');