api-diff-analyzer
你是 api-diff-analyzer - 一个专门用于比较API规范和检测破坏性变更的技能,确保SDK兼容性和安全的API演进。
概述
这个技能支持AI驱动的API差异分析,包括:
- 比较OpenAPI规范版本
- 按严重程度分类变更
- 自动检测破坏性变更
- 生成迁移指南
- 在CI中阻止破坏性变更
- 支持多种规范格式(OpenAPI、GraphQL、gRPC)
- 创建详细的变更报告
先决条件
- OpenAPI、GraphQL或Protobuf规范
- 具有规范历史的版本控制
- oasdiff、openapi-diff或类似工具
- 自动化检查的CI/CD流水线
能力
1. OpenAPI差异分析
比较OpenAPI规范:
// src/analyzer/openapi-diff.ts
import { parseSpec, diffSpecs } from './parser';
interface ApiChange {
type: 'breaking' | 'non-breaking' | 'info';
category: string;
path: string;
method?: string;
description: string;
oldValue?: unknown;
newValue?: unknown;
migration?: string;
}
interface DiffResult {
hasBreakingChanges: boolean;
changes: ApiChange[];
summary: {
breaking: number;
nonBreaking: number;
info: number;
};
report: string;
}
export async function analyzeApiDiff(
oldSpec: string,
newSpec: string,
options: DiffOptions = {}
): Promise<DiffResult> {
const oldApi = await parseSpec(oldSpec);
const newApi = await parseSpec(newSpec);
const changes: ApiChange[] = [];
// 分析路径
for (const [path, oldPathItem] of Object.entries(oldApi.paths)) {
const newPathItem = newApi.paths[path];
if (!newPathItem) {
changes.push({
type: 'breaking',
category: 'endpoint-removed',
path,
description: `Endpoint ${path} was removed`,
migration: `Update SDK to remove calls to ${path}`
});
continue;
}
// 分析方法
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
const oldOp = oldPathItem[method];
const newOp = newPathItem[method];
if (oldOp && !newOp) {
changes.push({
type: 'breaking',
category: 'method-removed',
path,
method,
description: `${method.toUpperCase()} ${path} was removed`
});
continue;
}
if (oldOp && newOp) {
// 检查参数
analyzeParameters(path, method, oldOp, newOp, changes);
// 检查请求体
analyzeRequestBody(path, method, oldOp, newOp, changes);
// 检查响应
analyzeResponses(path, method, oldOp, newOp, changes);
}
}
}
// 检查新端点(非破坏性)
for (const [path, newPathItem] of Object.entries(newApi.paths)) {
if (!oldApi.paths[path]) {
changes.push({
type: 'non-breaking',
category: 'endpoint-added',
path,
description: `New endpoint ${path} was added`
});
}
}
// 分析组件/模式
analyzeSchemas(oldApi.components?.schemas, newApi.components?.schemas, changes);
const summary = {
breaking: changes.filter(c => c.type === 'breaking').length,
nonBreaking: changes.filter(c => c.type === 'non-breaking').length,
info: changes.filter(c => c.type === 'info').length
};
return {
hasBreakingChanges: summary.breaking > 0,
changes,
summary,
report: generateReport(changes, summary)
};
}
function analyzeParameters(
path: string,
method: string,
oldOp: Operation,
newOp: Operation,
changes: ApiChange[]
): void {
const oldParams = new Map(oldOp.parameters?.map(p => [p.name, p]) || []);
const newParams = new Map(newOp.parameters?.map(p => [p.name, p]) || []);
// 检查已删除的参数
for (const [name, oldParam] of oldParams) {
if (!newParams.has(name)) {
changes.push({
type: oldParam.required ? 'breaking' : 'info',
category: 'parameter-removed',
path,
method,
description: `Parameter '${name}' was removed from ${method.toUpperCase()} ${path}`,
oldValue: oldParam
});
}
}
// 检查新必需参数
for (const [name, newParam] of newParams) {
const oldParam = oldParams.get(name);
if (!oldParam && newParam.required) {
changes.push({
type: 'breaking',
category: 'required-parameter-added',
path,
method,
description: `New required parameter '${name}' added to ${method.toUpperCase()} ${path}`,
newValue: newParam,
migration: `Update SDK calls to include '${name}' parameter`
});
}
if (oldParam && !oldParam.required && newParam.required) {
changes.push({
type: 'breaking',
category: 'parameter-required',
path,
method,
description: `Parameter '${name}' is now required in ${method.toUpperCase()} ${path}`,
oldValue: oldParam,
newValue: newParam
});
}
// 检查类型更改
if (oldParam && oldParam.schema?.type !== newParam.schema?.type) {
changes.push({
type: 'breaking',
category: 'parameter-type-changed',
path,
method,
description: `Parameter '${name}' type changed from '${oldParam.schema?.type}' to '${newParam.schema?.type}'`,
oldValue: oldParam,
newValue: newParam
});
}
}
}
function analyzeSchemas(
oldSchemas: Record<string, Schema> | undefined,
newSchemas: Record<string, Schema> | undefined,
changes: ApiChange[]
): void {
if (!oldSchemas || !newSchemas) return;
for (const [name, oldSchema] of Object.entries(oldSchemas)) {
const newSchema = newSchemas[name];
if (!newSchema) {
changes.push({
type: 'breaking',
category: 'schema-removed',
path: `#/components/schemas/${name}`,
description: `Schema '${name}' was removed`
});
continue;
}
// 检查已删除的属性
if (oldSchema.properties && newSchema.properties) {
for (const prop of Object.keys(oldSchema.properties)) {
if (!(prop in newSchema.properties)) {
changes.push({
type: 'breaking',
category: 'property-removed',
path: `#/components/schemas/${name}/${prop}`,
description: `Property '${prop}' was removed from schema '${name}'`
});
}
}
// 检查新必需属性
const oldRequired = new Set(oldSchema.required || []);
const newRequired = new Set(newSchema.required || []);
for (const prop of newRequired) {
if (!oldRequired.has(prop) && oldSchema.properties[prop]) {
changes.push({
type: 'breaking',
category: 'property-required',
path: `#/components/schemas/${name}/${prop}`,
description: `Property '${prop}' is now required in schema '${name}'`
});
}
}
}
}
}
2. 破坏性变更类别
全面的破坏性变更检测:
// src/rules/breaking-changes.ts
export const BREAKING_CHANGE_RULES = {
// 端点变更
'endpoint-removed': {
severity: 'major',
description: '移除端点会破坏所有消费者',
autoFix: false
},
'method-removed': {
severity: 'major',
description: '移除HTTP方法会破坏使用它的消费者',
autoFix: false
},
// 参数变更
'required-parameter-added': {
severity: 'major',
description: '添加必需参数会破坏现有调用',
autoFix: false
},
'parameter-removed': {
severity: 'minor',
description: '移除参数可能会破坏期望它的消费者',
autoFix: '先将参数设为可选'
},
'parameter-type-changed': {
severity: 'major',
description: '更改参数类型会破坏序列化',
autoFix: false
},
'parameter-required': {
severity: 'major',
description: '使可选参数必需会破坏调用',
autoFix: false
},
// 响应变更
'response-removed': {
severity: 'major',
description: '移除响应状态码会破坏错误处理',
autoFix: false
},
'response-body-changed': {
severity: 'major',
description: '更改响应结构会破坏反序列化',
autoFix: false
},
// 模式变更
'schema-removed': {
severity: 'major',
description: '移除模式会破坏类型引用',
autoFix: false
},
'property-removed': {
severity: 'major',
description: '移除属性会破坏访问它的消费者',
autoFix: false
},
'property-required': {
severity: 'major',
description: '使属性必需会破坏对象创建',
autoFix: false
},
'property-type-changed': {
severity: 'major',
description: '更改属性类型会破坏序列化',
autoFix: false
},
// 枚举变更
'enum-value-removed': {
severity: 'major',
description: '移除枚举值会破坏使用它的消费者',
autoFix: false
}
};
3. 迁移指南生成
为破坏性变更生成迁移指南:
// src/generator/migration-guide.ts
interface MigrationStep {
change: ApiChange;
action: string;
code?: {
before: string;
after: string;
language: string;
};
}
export function generateMigrationGuide(
oldVersion: string,
newVersion: string,
changes: ApiChange[]
): string {
const breakingChanges = changes.filter(c => c.type === 'breaking');
if (breakingChanges.length === 0) {
return `# 迁移指南:${oldVersion} 到 ${newVersion}
没有破坏性变更!你可以安全升级。`;
}
const sections: string[] = [
`# 迁移指南:${oldVersion} 到 ${newVersion}`,
'',
'## 概览',
'',
`此版本包含 **${breakingChanges.length} 破坏性变更**,需要更新你的代码。`,
'',
'## 破坏性变更',
''
];
// 按类别分组变更
const byCategory = groupBy(breakingChanges, 'category');
for (const [category, categoryChanges] of Object.entries(byCategory)) {
sections.push(`### ${formatCategory(category)}`);
sections.push('');
for (const change of categoryChanges) {
sections.push(`#### ${change.path}${change.method ? ` (${change.method.toUpperCase()})` : ''}`);
sections.push('');
sections.push(change.description);
sections.push('');
if (change.migration) {
sections.push('**迁移:**');
sections.push('');
sections.push(change.migration);
sections.push('');
}
// 添加代码示例
const codeExample = generateCodeExample(change);
if (codeExample) {
sections.push('**Before:**');
sections.push('```' + codeExample.language);
sections.push(codeExample.before);
sections.push('```');
sections.push('');
sections.push('**After:**');
sections.push('```' + codeExample.language);
sections.push(codeExample.after);
sections.push('```');
sections.push('');
}
}
}
return sections.join('
');
}
function generateCodeExample(change: ApiChange): CodeExample | null {
switch (change.category) {
case 'required-parameter-added':
return {
language: 'typescript',
before: `await sdk.users.create({ name: 'John' });`,
after: `await sdk.users.create({ name: 'John', email: 'john@example.com' });`
};
case 'endpoint-removed':
return {
language: 'typescript',
before: `await sdk.deprecated.oldMethod();`,
after: `await sdk.newNamespace.newMethod();`
};
default:
return null;
}
}
4. CI/CD集成
在CI中阻止破坏性变更:
name: API兼容性检查
on:
pull_request:
paths:
- 'openapi/**'
- 'api/**'
jobs:
check-breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 获取基础规范
run: |
git show origin/${{ github.base_ref }}:openapi/openapi.yaml > old-spec.yaml
- name: 安装oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: 检查破坏性变更
id: diff
run: |
oasdiff breaking old-spec.yaml openapi/openapi.yaml \
--fail-on ERR \
--format json > diff-result.json
echo "has_breaking=$(jq 'length > 0' diff-result.json)" >> $GITHUB_OUTPUT
- name: 生成报告
if: always()
run: |
oasdiff diff old-spec.yaml openapi/openapi.yaml \
--format markdown > CHANGES.md
- name: 在PR上评论
if: steps.diff.outputs.has_breaking == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('CHANGES.md', 'utf8');
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## ⚠️ 检测到破坏性API变更
${report}
请审查这些变更,并相应更新SDK。`
});
- name: 因破坏性变更失败
if: steps.diff.outputs.has_breaking == 'true'
run: |
echo "检测到破坏性变更!需要审查。"
exit 1
5. oasdiff CLI集成
使用oasdiff进行全面分析:
#!/bin/bash
# scripts/check-api-diff.sh
set -e
OLD_SPEC="${1:-main:openapi/openapi.yaml}"
NEW_SPEC="${2:-openapi/openapi.yaml}"
OUTPUT_FORMAT="${3:-text}"
echo "比较API规范..."
echo "旧版:$OLD_SPEC"
echo "新版:$NEW_SPEC"
echo ""
# 检查破坏性变更
echo "=== 破坏性变更 ==="
oasdiff breaking "$OLD_SPEC" "$NEW_SPEC" --format "$OUTPUT_FORMAT"
echo ""
echo "=== 完整差异 ==="
oasdiff diff "$OLD_SPEC" "$NEW_SPEC" --format "$OUTPUT_FORMAT"
# 摘要
echo ""
echo "=== 摘要 ==="
oasdiff summary "$OLD_SPEC" "$NEW_SPEC"
6. GraphQL模式差异
比较GraphQL模式:
// src/analyzer/graphql-diff.ts
import { buildSchema, printSchema, diff as graphqlDiff } from 'graphql';
interface GraphQLChange {
type: 'breaking' | 'dangerous' | 'non-breaking';
criticality: string;
message: string;
path: string;
}
export async function analyzeGraphQLDiff(
oldSchemaSDL: string,
newSchemaSDL: string
): Promise<GraphQLChange[]> {
const oldSchema = buildSchema(oldSchemaSDL);
const newSchema = buildSchema(newSchemaSDL);
const changes = graphqlDiff(oldSchema, newSchema);
return changes.map(change => ({
type: change.criticality.level,
criticality: change.criticality.reason || '',
message: change.message,
path: change.path || ''
}));
}
// GraphQL中的破坏性变更:
// - 移除类型
// - 移除字段
// - 将字段类型更改为不兼容的类型
// - 为字段添加必需参数
// - 移除枚举值
// - 更改联合成员
7. Protobuf/gRPC差异
比较Protobuf定义:
// src/analyzer/protobuf-diff.ts
import { execSync } from 'child_process';
interface ProtobufChange {
type: 'FILE' | 'MESSAGE' | 'FIELD' | 'ENUM' | 'SERVICE' | 'RPC';
category: 'ADDITION' | 'DELETION' | 'MODIFICATION';
breaking: boolean;
path: string;
description: string;
}
export function analyzeProtobufDiff(
oldProtoPath: string,
newProtoPath: string
): ProtobufChange[] {
// 使用buf进行protobuf破坏性变更检测
const result = execSync(
`buf breaking ${newProtoPath} --against ${oldProtoPath} --format json`,
{ encoding: 'utf8' }
);
const bufOutput = JSON.parse(result);
const changes: ProtobufChange[] = [];
for (const issue of bufOutput) {
changes.push({
type: issue.type,
category: issue.category,
breaking: true,
path: issue.path,
description: issue.message
});
}
return changes;
}
// Protobuf中的破坏性变更:
// - 更改字段编号
// - 更改字段类型
// - 移除必需字段
// - 将字段从可选更改为必需
// - 移除枚举值
// - 重命名消息/字段(线格式保持不变,但破坏生成的代码)
MCP服务器集成
此技能可以利用以下MCP服务器:
| 服务器 | 描述 | 安装 |
|---|---|---|
| Specmatic MCP | 契约测试和差异 | GitHub |
| mcp-openapi-schema | OpenAPI探索 | GitHub |
最佳实践
- 版本规范 - 在版本控制中保留规范
- 自动化检查 - 在CI/CD中运行差异分析
- 阻止破坏性变更 - 在破坏性变更时失败构建
- 生成指南 - 创建迁移文档
- 仔细审查 - 人为审查边缘情况
- 先弃用 - 在移除之前先弃用
- 提前沟通 - 通知SDK团队变更
- 测试迁移 - 验证迁移指南是否有效
流程集成
此技能与以下流程集成:
api-versioning-strategy.js- API版本管理backward-compatibility-management.js- 破坏性变更政策sdk-versioning-release-management.js- SDK发布api-design-specification.js- 规范管理
输出格式
{
"operation": "diff",
"oldVersion": "1.0.0",
"newVersion": "2.0.0",
"hasBreakingChanges": true,
"summary": {
"breaking": 3,
"nonBreaking": 5,
"info": 2
},
"changes": [
{
"type": "breaking",
"category": "required-parameter-added",
"path": "/users",
"method": "POST",
"description": "新增必需参数 'email'",
"migration": "更新SDK调用以包含email"
}
],
"migrationGuide": "# 迁移指南...",
"affectedEndpoints": ["/users", "/orders"]
}
错误处理
- 处理无效的规范格式
- 清晰报告解析错误
- 支持部分比较
- 警告过时功能
- 日志详细变更上下文
约束
- 需要两个版本的规范访问
- 复杂的模式变更可能需要手动审查
- 某些变更可能是误报
- 行为变更不总是可检测的
- GraphQL/gRPC需要单独的工具