#!/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 无破坏性变更。');