api-diff-analyzerSkill api-diff-analyzer

api-diff-analyzer是一个专门用于比较API规范和检测破坏性变更的工具,它能够确保软件开发过程中SDK的兼容性和API的安全性演进。关键词包括API规范比较、破坏性变更检测、SDK兼容性、自动化CI/CD集成。

DevOps 0 次安装 0 次浏览 更新于 2/25/2026

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

最佳实践

  1. 版本规范 - 在版本控制中保留规范
  2. 自动化检查 - 在CI/CD中运行差异分析
  3. 阻止破坏性变更 - 在破坏性变更时失败构建
  4. 生成指南 - 创建迁移文档
  5. 仔细审查 - 人为审查边缘情况
  6. 先弃用 - 在移除之前先弃用
  7. 提前沟通 - 通知SDK团队变更
  8. 测试迁移 - 验证迁移指南是否有效

流程集成

此技能与以下流程集成:

  • 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需要单独的工具