DEV Community

Cover image for OpenAPI仕様の差分を比較し、CIで破壊的変更をブロックする方法
Akira
Akira

Posted on • Originally published at apidog.com

OpenAPI仕様の差分を比較し、CIで破壊的変更をブロックする方法

プルリクエストで openapi.yaml が編集され、CIチェックはグリーン。仕様は有効、リンターもクリーン、レビュー担当者2人が承認済み。ところが3日後、モバイルクライアントが、以前は存在していたレスポンスフィールドを読めずにヌルポインタでクラッシュしました。誰も意図的に削除したわけではありません。リファクタリング中に誰かがプロパティ名を変更し、レビューでは見落とされました。

今すぐApidogを試す

通常のOpenAPIバリデーターでは、この種の問題は検出できません。仕様が正しい構文で書かれていても、既存クライアントとの互換性を壊す可能性があります。必要なのは、新しい仕様をマージ前に既存仕様と比較し、「昨日まで動いていたクライアントを壊すか?」を機械的に判定することです。これがOpenAPI diffであり、CIのマージゲートとして実行すべきチェックです。

OpenAPI diffが比較するもの

OpenAPI diffは、2つの仕様ファイルを比較します。

  • base: ターゲットブランチ上の仕様。通常は現在稼働中の契約
  • head: プルリクエストで提案されている仕様

単なる git diff ではありません。OpenAPIの構造を理解し、パス、メソッド、パラメーター、スキーマ、レスポンスを比較して、変更を分類します。

安全な追加変更の例:

  • 新しいオプションのリクエストパラメーターを追加する
  • 新しいレスポンスフィールドを追加する
  • 新しいエンドポイントを追加する
  • リクエストボディに新しいEnum値を追加する

既存クライアントは、これらの変更があっても基本的には動作し続けます。

一方、後方互換性を壊す変更の例:

  • クライアントが読んでいるレスポンスフィールドを削除する
  • プロパティ名を変更する
  • オプションだったパラメーターを必須にする
  • 型を string から integer へ変更する
  • クライアントが送信する可能性のあるEnum値を削除する
  • エンドポイントまたはHTTPメソッドを削除する

OpenAPI diffツールの役割は、これらの変更を構造的に検出し、破壊的変更として報告することです。行単位の差分では、フォーマット変更の中に埋もれた required の変更を見落としがちです。構造的なdiffなら、どのパスのどのフィールドが互換性を壊したのかを直接示せます。

互換性ルールの背景を整理したい場合は、大規模なAPIのバージョン管理と非推奨化戦略も参考になります。diffは、レビュー担当者の記憶に頼らず、それらのルールをCIで強制する方法です。

oasdiffを使って破壊的変更を検出する

oasdiff は、OpenAPI diffでよく使われるオープンソースツールです。Go製の単一バイナリで、OpenAPI 3.0 / 3.1を比較できます。

主に使うサブコマンドは3つです。

oasdiff diff base-openapi.yaml head-openapi.yaml
Enter fullscreen mode Exit fullscreen mode

すべての差分を表示します。

oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
Enter fullscreen mode Exit fullscreen mode

後方互換性を壊す変更だけを検出します。CIゲートではこれを使います。

oasdiff changelog base-openapi.yaml head-openapi.yaml
Enter fullscreen mode Exit fullscreen mode

人間が読みやすい変更履歴を生成します。

マージゲートとして使う最小コマンドは次のとおりです。

oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
Enter fullscreen mode Exit fullscreen mode

base-openapi.yaml はターゲットブランチの仕様、head-openapi.yaml はPR側の仕様です。--fail-on ERR を付けると、重大な破壊的変更が見つかった場合にゼロ以外の終了コードで終了します。CIはこれを失敗として扱えます。

より厳格にしたい場合は WARN でも失敗させます。

oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on WARN
Enter fullscreen mode Exit fullscreen mode

実運用では、まず ERR で導入し、チームの運用に合わせて WARN まで広げるのが現実的です。

oasdiff は出力形式も柔軟です。テキストだけでなく、HTML、JSON、YAML、Markdownなどに出力できるため、CIアノテーションやPRコメント、変更履歴生成に組み込みやすいです。

JVM環境なら openapi-diff も選択肢

Java / JVMベースのプロジェクトでは、OpenAPITools/openapi-diff も使えます。Java 8以降で動作し、OpenAPI 3.x仕様を比較できます。

基本コマンドは次のとおりです。

openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
Enter fullscreen mode Exit fullscreen mode

--fail-on-incompatible を付けると、後方互換性を壊す変更がある場合だけゼロ以外の終了コードを返します。

用途別には次のオプションも使えます。

# 互換性の有無だけを機械的に取得
openapi-diff old-openapi.yaml new-openapi.yaml --state
Enter fullscreen mode Exit fullscreen mode
# 互換性に関係なく、何らかの変更があれば失敗
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-changed
Enter fullscreen mode Exit fullscreen mode

HTMLやMarkdownレポートも生成できるため、CIの成果物として差分レポートを残したい場合に便利です。すでにJVMランタイムを使っているチームなら導入しやすい選択肢です。軽量さを優先するなら oasdiff、JVM統合やレポート出力を重視するなら openapi-diff を選ぶとよいでしょう。

GitHub ActionsでOpenAPI diffをマージゲートにする

手元でdiffを実行しても、実行を忘れれば意味がありません。仕様ファイルを変更するすべてのPRで、自動的に実行する必要があります。

GitHub Actionsで oasdiff を使う例です。

name: openapi-diff

on:
  pull_request:
    paths:
      - "openapi.yaml"

jobs:
  breaking-changes:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get base spec
        run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml

      - name: Install oasdiff
        run: |
          curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh

      - name: Diff for breaking changes
        run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
Enter fullscreen mode Exit fullscreen mode

重要なポイントは次の4つです。

  1. fetch-depth: 0 で履歴を取得する
  2. git show origin/${{ github.base_ref }}:openapi.yaml でベースブランチ上の仕様を取り出す
  3. PR側の openapi.yaml と比較する
  4. oasdiff breaking ... --fail-on ERR の終了コードでCIを失敗させる

これで、互換性を壊す仕様変更がPR内に含まれている場合、マージ前にチェックが赤くなります。

すべての破壊的変更が間違いとは限りません。メジャーバージョンアップや明示的な非推奨化の一環として、意図的に壊す場合もあります。その場合でも、デフォルトではブロックし、例外だけを明示的に許可する運用にすると安全です。

例:

  • PRに breaking-change-approved ラベルがある場合のみ通す
  • info.version のメジャーバージョンが上がっている場合のみ許可する
  • 承認者を追加で要求する

破壊的変更をどう扱うかは、APIバージョン管理戦略ガイドも参考になります。

diffだけでは検出できない問題

OpenAPI diffは、2つの仕様ファイルの差分を検出します。しかし、実装が仕様どおりに動いているかは検証しません。

たとえば次のケースです。

  • 仕様では created_at が必須だが、実装は返さなくなった
  • 仕様では 200 を返すが、特定条件で実サービスは 500 を返す
  • 仕様では string だが、実レスポンスでは null が返る

この場合、baseとheadの仕様に差分がなければOpenAPI diffは成功します。しかし、実際のAPIは契約に違反しています。

このギャップを埋めるには、ライブAPIを契約に対してテストする必要があります。仕様からテストを生成し、実行中のサービスへリクエストし、実レスポンスがOpenAPI定義と一致するか検証します。これが契約テストです。

ApidogとApidog CLIで契約テストをCIに入れる

Apidog は、OpenAPI仕様をもとにAPI設計、モック、テストを同じワークスペースで扱えます。OpenAPI仕様をインポートまたは同期すると、仕様からテストシナリオを生成できます。

生成されたテストでは、実レスポンスに対して次のような検証を行えます。

  • ステータスコードが仕様どおりか
  • 必須フィールドが存在するか
  • フィールド型がスキーマと一致するか
  • レスポンス構造がOpenAPI定義と一致するか

仕様変更のたびに手書きテストを追従させるより、仕様を中心にテストを保守する方が運用しやすくなります。Apidogをダウンロードし、既存のOpenAPI仕様をインポートして試せます。OpenAPI仕様をGitで管理する運用については、GitによるOpenAPI仕様のバージョン管理も参考になります。

CIでは Apidog CLI を使います。

npm install -g apidog-cli
Enter fullscreen mode Exit fullscreen mode

シナリオIDと環境IDを指定して実行します。

apidog run \
  --access-token $APIDOG_ACCESS_TOKEN \
  -t <scenarioId> \
  -e <environmentId> \
  -r junit,cli \
  --out-dir ./apidog-reports
Enter fullscreen mode Exit fullscreen mode

各オプションの意味は次のとおりです。

  • --access-token: Apidogのアクセストークン。CIシークレットで管理する
  • -t: 実行するシナリオID
  • -e: 実行対象の環境ID
  • -r junit,cli: CLI出力とJUnit XMLレポートを生成する
  • --out-dir: レポート出力先

シナリオIDや環境IDは、Apidogのシナリオ画面にあるCI/CDタブからコピーできます。すべてのオプションは完全なCLIガイドまたは次のコマンドで確認できます。

apidog run --help
Enter fullscreen mode Exit fullscreen mode

契約テストが失敗すると、apidog run はゼロ以外の終了コードで終了します。CIはその終了コードを読み取り、ジョブを失敗として扱います。つまり、OpenAPI diffと同じように、契約違反もマージ前に止められます。

完全なマージ前パイプライン例

OpenAPI diffと契約テストを組み合わせると、2種類の問題を検出できます。

  • OpenAPI diff: 仕様変更が既存クライアントを壊すかを検出する
  • 契約テスト: 実装が仕様どおりに動いているかを検証する

GitHub Actionsでは、別ジョブとして並列実行できます。

name: api-contract-gate

on:
  pull_request:
    paths:
      - "openapi.yaml"
      - "src/**"
      - "tests/**"

jobs:
  breaking-changes:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get base spec
        run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml

      - name: Install oasdiff
        run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh

      - name: Check breaking changes
        run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR

  contract-conformance:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install Apidog CLI
        run: npm install -g apidog-cli

      - name: Run contract tests
        run: |
          apidog run \
            --access-token "$APIDOG_ACCESS_TOKEN" \
            -t 605067 \
            -e 1629989 \
            -r junit,cli \
            --out-dir ./apidog-reports
        env:
          APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: apidog-report
          path: ./apidog-reports
Enter fullscreen mode Exit fullscreen mode

breaking-changes ジョブは仕様ファイルだけを比較するため高速です。contract-conformance ジョブは実際のAPIにアクセスするため、通常はステージング環境に対して実行します。

if: always() を付けてレポートをアップロードしている点も重要です。テストが失敗したときこそ、JUnitレポートやCLI出力を確認する必要があるためです。

実際のCIでApidog CLIを組み込む方法は、Apidog CLI GitHub Actionsガイドと、より広範なCI/CDパイプラインウォークスルーで確認できます。

まとめ

OpenAPI仕様の構文チェックだけでは、既存クライアントとの互換性は保証できません。

マージ前に最低限入れるべきチェックは次の2つです。

  1. oasdiff breaking で仕様変更の破壊的変更を検出する
  2. Apidog CLIでライブAPIを契約に対してテストする

この2層にすると、次の両方をPR段階で止められます。

  • 仕様そのものが後方互換性を壊した
  • 実装がOpenAPI契約からずれた

APIの破壊的変更は、リリース後に見つけるほど高くつきます。CIのマージゲートに入れて、レビューではなくパイプラインで検出するのが実装しやすく、再現性のある対策です。

Top comments (0)