ci / 质量门禁 (push) Canceled after 0s
- 建立 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 实施报告与本地开发说明
96 lines
3.0 KiB
JavaScript
96 lines
3.0 KiB
JavaScript
#!/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 无破坏性变更。');
|