プルリクエストで openapi.yaml が編集され、CIチェックはグリーン。仕様は有効、リンターもクリーン、レビュー担当者2人が承認済み。ところが3日後、モバイルクライアントが、以前は存在していたレスポンスフィールドを読めずにヌルポインタでクラッシュしました。誰も意図的に削除したわけではありません。リファクタリング中に誰かがプロパティ名を変更し、レビューでは見落とされました。
通常の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
すべての差分を表示します。
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
後方互換性を壊す変更だけを検出します。CIゲートではこれを使います。
oasdiff changelog base-openapi.yaml head-openapi.yaml
人間が読みやすい変更履歴を生成します。
マージゲートとして使う最小コマンドは次のとおりです。
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml はターゲットブランチの仕様、head-openapi.yaml はPR側の仕様です。--fail-on ERR を付けると、重大な破壊的変更が見つかった場合にゼロ以外の終了コードで終了します。CIはこれを失敗として扱えます。
より厳格にしたい場合は WARN でも失敗させます。
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on WARN
実運用では、まず 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
--fail-on-incompatible を付けると、後方互換性を壊す変更がある場合だけゼロ以外の終了コードを返します。
用途別には次のオプションも使えます。
# 互換性の有無だけを機械的に取得
openapi-diff old-openapi.yaml new-openapi.yaml --state
# 互換性に関係なく、何らかの変更があれば失敗
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-changed
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
重要なポイントは次の4つです。
-
fetch-depth: 0で履歴を取得する -
git show origin/${{ github.base_ref }}:openapi.yamlでベースブランチ上の仕様を取り出す - PR側の
openapi.yamlと比較する -
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
シナリオIDと環境IDを指定して実行します。
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
各オプションの意味は次のとおりです。
-
--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
契約テストが失敗すると、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
breaking-changes ジョブは仕様ファイルだけを比較するため高速です。contract-conformance ジョブは実際のAPIにアクセスするため、通常はステージング環境に対して実行します。
if: always() を付けてレポートをアップロードしている点も重要です。テストが失敗したときこそ、JUnitレポートやCLI出力を確認する必要があるためです。
実際のCIでApidog CLIを組み込む方法は、Apidog CLI GitHub Actionsガイドと、より広範なCI/CDパイプラインウォークスルーで確認できます。
まとめ
OpenAPI仕様の構文チェックだけでは、既存クライアントとの互換性は保証できません。
マージ前に最低限入れるべきチェックは次の2つです。
-
oasdiff breakingで仕様変更の破壊的変更を検出する - Apidog CLIでライブAPIを契約に対してテストする
この2層にすると、次の両方をPR段階で止められます。
- 仕様そのものが後方互換性を壊した
- 実装がOpenAPI契約からずれた
APIの破壊的変更は、リリース後に見つけるほど高くつきます。CIのマージゲートに入れて、レビューではなくパイプラインで検出するのが実装しやすく、再現性のある対策です。
Top comments (0)