はじめに
GraphQL では、クライアントから見ると「スキーマ=契約」となりますが、サーバ側の開発が進むと、うっかりその契約を壊してしまうことがあります。
- フィールドを削除した
- nullable → non-nullable にした
- 引数の型を変えた
など、「ちょっと変えただけ」のつもりでも、既存クエリがいきなり落ちる 破壊的変更(Breaking Change) になります。
本記事では、Gitの開発中ブランチに対して定期的に、GraphQLスキーマの差分を静的に解析し、「これまで提供していた機能が、リリース後も使える状態かどうか」を毎日自動チェックする仕組みを紹介します。
ポイントは次の 3 つです。
- スキーマを自動生成して「統合スキーマファイル」を作る
-
@graphql-inspector/cliでブランチ間のスキーマ差分を取得する - Jenkins + 内製 Web UI で、担当チームが毎日レビューする
そこで、プロジェクト内の *.schema.graphql をすべて拾って 1 つのスキーマにマージし、「統合スキーマファイル」を作成する小さなスクリプトを用意しました。
技術的には複数のスキーマファイルを個別に比較することも可能ですが、「統合スキーマファイル」を作ることで以下の利点があります:
- ファイル構成の変更(ファイルの移動・リネーム・分割・統合など)に影響されずにスキーマ全体を比較できる
- 型の定義が複数のファイルにまたがっている場合でも、全体としての整合性を1回のdiffで確認できる
- 比較の簡便性(1回のdiffで全体を比較できる)
背景:なぜ静的解析でチェックしたかったか
当たり前ですが、E2E テストや結合テストだけで後方互換性を守るのは大変です。
- テストで全クエリ・全組み合わせを網羅するのは現実的ではない
- テストが足りないところから、いつの間にか壊れる
- リリース直前に「この変更、既存クライアント大丈夫?」と慌てる
そこで、
挙動までは見ないが、スキーマレベルで
「これまでのクエリ・フィールドが“使えなくなっていないか”」だけでも
自動でチェックしたい
というモチベーションで、静的解析ベースの仕組みを作りました。
運用面では、
- エンジニアが快適に開発できる環境づくりをミッションにした横断チームがあり、各アプリケーションチームが「意識しなくても後方互換性が守られている」状態を作りたかった
という事情もあります。
全体構成
作った仕組みは、大きく次の 4 つのパートで構成されています。
-
統合 GraphQL 統合スキーマファイルの自動生成
- プロジェクト内の
*.schema.graphqlを集めてマージし、
gen.merged.schema.graphqlを生成するスクリプトを用意 - これを
npm run mcgraphdocのような npm script から呼び出す
- プロジェクト内の
-
ブランチ間スキーマ差分の取得
-
graphql_schema_diff_check.batで
リリース済みブランチrelease/stableと
開発中ブランチdevelop/nextのスキーマを比較し、diff.logを生成
-
-
Web UI での差分レビュー
-
app.py(Flask)でdiff.logをパースし、
破壊的変更・警告・安全な変更を色分け表示 - 各差分に対して「OK チェック」+「コメント(備考)」を付けて保存
-
-
Jenkins + 担当チームによる日次運用
- Jenkins が毎朝バッチを実行
- 担当チームが差分を確認し、破壊的変更があればアプリ開発側へ連携
以下、それぞれを簡潔に紹介します。
統合 GraphQL スキーマの自動生成
実装上、GraphQL の SDL(type Query { ... } など)はモジュールごとに分散していることが多いと思います。
そこで、プロジェクト内の *.schema.graphql をすべて拾って 1 つのスキーマにマージする小さなスクリプトを用意しました。
やっていること
-
fast-globでpackages/**/sdl/**/*.schema.graphqlのようなパターンで SDL ファイルを収集 -
graphqlのparse()でDocumentNodeに変換 -
@graphql-tools/mergeで typeDefs をマージ -
@graphql-tools/schemaでGraphQLSchemaを生成 -
printSchemaで SDL 文字列にしてファイル出力
具体的にはこんな感じです:
// build-schema.mjs のようなファイルを用意しておくイメージ
import fs from "fs";
import glob from "fast-glob";
import { parse, printSchema } from "graphql";
import { mergeTypeDefs } from "@graphql-tools/merge";
import { makeExecutableSchema } from "@graphql-tools/schema";
async function buildMergedSchema() {
// プロジェクト構成に合わせてパスは調整してください
const files = await glob("packages/**/sdl/**/*.schema.graphql");
const documents = files.map((file) =>
parse(fs.readFileSync(file, "utf8"))
);
const mergedTypeDefs = mergeTypeDefs(documents);
const schema = makeExecutableSchema({ typeDefs: mergedTypeDefs });
fs.writeFileSync("gen.merged.schema.graphql", printSchema(schema));
}
buildMergedSchema().catch((e) => {
console.error(e);
process.exit(1);
});
package.json の scripts に次のように登録しておきます。
{
"scripts": {
"mcgraphdoc": "node build-schema.mjs"
}
}
これで、npm run mcgraphdoc を実行すれば、統合スキーマ gen.merged.schema.graphql を生成できるようになります。
ブランチ間スキーマ比較バッチ(graphql_schema_diff_check.bat)
次は、リリース済みブランチと開発中ブランチのスキーマを比較するバッチです。
ここでは名前の例として、
- リリース済みブランチ:
release/stable - 開発中ブランチ:
develop/next
としています。
処理の流れ
- プロジェクトルート(例:
mcx-service)に移動 -
release/stableをチェックアウトしてnpm run mcgraphdoc→
gen.merged.schema_release.graphqlとして保存 -
develop/nextをチェックアウトしてnpm run mcgraphdoc→
gen.merged.schema_develop.graphqlとして保存 -
@graphql-inspector/cli diffで 2 つのスキーマを比較 →diff.logに出力 -
diff.logを Git にコミットして共有
具体的にはこんな感じです:
@echo off
setlocal
set RELEASE_BRANCH=release/stable
set DEVELOP_BRANCH=develop/next
cd /d C:\path\to\mcx-service
REM リリース済みスキーマ生成
git checkout %RELEASE_BRANCH%
git pull
npm run mcgraphdoc
copy /Y gen.merged.schema.graphql ..\gen.merged.schema_release.graphql
REM 開発中スキーマ生成
git checkout %DEVELOP_BRANCH%
git pull
npm run mcgraphdoc
copy /Y gen.merged.schema.graphql ..\gen.merged.schema_develop.graphql
REM GraphQL Inspector で差分取得
cd /d C:\path\to\diff-workdir
if not exist node_modules (
npm install @graphql-inspector/cli
)
npx @graphql-inspector/cli diff ^
gen.merged.schema_release.graphql ^
gen.merged.schema_develop.graphql > diff.log
git add diff.log
git commit -m "update: GraphQL静的解析結果を更新"
git push
endlocal
やっていることの本質は、
2 つのブランチで統合スキーマファイルを生成 → GraphQL Inspector で diff → diff.log を Git 共有
です。
diff.log の読み方
@graphql-inspector/cli diff の出力は、記号付きのテキストです。
例:
[log]
✖ Field 'oldField' was removed from object type 'MyType'.
⚠ Field 'someField' changed type from 'String' to 'ID!'.
✔ Field 'newField' was added to object type 'MyType'.
[success] 1 breaking change, 1 warning, 1 safe change
ここで使っている主な記号は次の通りです。
-
✖/×: 破壊的変更 (Breaking Change) -
⚠/‼: 要注意の変更 (Warning / Dangerous Change) -
✔/√: 非破壊的な変更(追加・安全な変更) -
[success]: 差分なし or 集計メッセージ
このログを Flask アプリ側でパースし、UI 上で「破壊的変更」「警告」「変更・追加」に色分けして表示しています。
Web UI での差分レビュー(app.py)
app.py は Flask ベースの簡易ビューアで、やっていることはざっくり以下です。
-
diff.logを行単位で読み込む - ブロックの先頭行の記号(✖ / ⚠ / ✔ / [success])から、種別と CSS クラスを決める
- テーブルにしてブラウザ表示
- 各行ごとに「OK チェック」と「備考」入力欄を用意
- OK/備考は
status.jsonに保存・復元
記号→種別のマッピングだけ、雰囲気が分かる程度に載せておきます。
# 記号から表示用ラベルとCSSクラスを決めるイメージ
if symbol in ("×", "✖"):
css_class = "breaking"
label = "破壊的変更"
elif symbol in ("⚠", "‼"):
css_class = "warning"
label = "警告"
elif symbol in ("✔", "√"):
css_class = "nonbreaking"
label = "変更・追加"
else:
css_class = "other"
label = "その他"
status.json は、
- キー:差分の詳細テキスト(diff.log の内容)
- 値:
ok(true/false)とcomment(備考)
のようなシンプルな構造にしてあり、
同じ内容の差分には過去の OK 状態・コメントが自動で紐付くようにしています。
UI 側は普通の HTML+JavaScript なのでここでは省略しますが、
- 破壊的変更(✖)は赤
- 警告(⚠)は黄
- 変更・追加(✔)は通常/薄い緑
といった感じで色分けし、「どこから見るか」がひと目で分かるようにしています。
Jenkins と担当チームによる日次運用
仕組みを作っただけでは回らないので、運用もセットで回しています。
Jenkins の役割
- 毎朝決まった時間に
graphql_schema_diff_check.batを実行 - 最新の
diff.logが生成され、Git にコミットされる - 必要に応じて
diff.logや CSV を成果物として保存
担当チームのフロー
ここで登場するのは、いわゆる「共通基盤・プラットフォーム側」を担当しているチームです(名称は何でも構いません)。
-
朝、Web UI にアクセスして最新の差分一覧を確認
-
まずは ✖(破壊的変更)と ⚠(警告)に集中して内容をチェック
-
破壊的変更が疑われる差分について、
- どの API/画面/サービスに関係していそうか
- 本当に breaking なのか(未使用フィールドの削除などは許容される場合もある)
を確認
-
必要に応じてアプリケーション側のチームに連携(チャット・チケットなど)
-
意図された変更であり、影響範囲が把握できていれば、
- 該当行に OK チェックを入れる
- 備考欄に理由や関連チケット番号などを書く
備考欄には例えば、
- なぜ OK と判断したか
- どのクライアント/画面が影響するか
- 対応した PR / チケットの番号
- 将来削除する予定(deprecate → 削除)の方針
といった情報を書いてもらうようにしています。
これによって、
- 「誰が、どの差分を、どう判断したか」が後から追える
- 似たような変更が出てきたときに、過去の判断を参考にできる
というメリットが出てきます。
この仕組みで得られたもの
破壊的変更の早期検知
- リリース直前ではなく、開発中の段階(毎日)で破壊的変更の可能性を検知できます
- 「気づいたら release/stable と大きく乖離していた」という事態を防げます
「番人」の明確化
-
スキーマの互換性を見てくれる担当チームが決まっていることで、
- 誰も見ていなかった
- 責任の所在が曖昧
といった状態を避けられます。
アプリケーション側の負担軽減
- アプリケーションを開発している各チームは、「毎日 diff.log を全部読む」必要はありません
- 破壊的変更の疑いがあるものだけ通知を受けて対応すればよい、という形にしています
結果として、アプリ側は機能開発に集中しつつ、後方互換性の見張りは仕組みと担当チームが支える、という役割分担になりました。
ナレッジ・メトリクスの蓄積
-
status.jsonや CSV 出力から、- どの領域で Breaking が多いか
- どういう変更パターンが発生しがちか
といった傾向も後から分析できます。
これは、GraphQL 設計ガイドラインの改善や、欠陥・変更傾向の分析にも使えそうです。
限界と割り切り
もちろん、この仕組みだけで「完全な互換性保証」ができるわけではありません。
- スキーマは同じだが、Resolver のロジックが変わって挙動が変わる
- description の変更によって業務上の意味が変わる
といった 「意味・挙動」の変化 は、静的なスキーマ diff では検知できません。
そのため、
- この仕組みは スキーマ構造レベルの後方互換性チェック に特化
- 挙動やビジネスルールの互換性は、テストや仕様レビューで担保
という分担で運用しています。
まとめ
- プロジェクト内の
*.schema.graphqlを集めて統合スキーマファイルを自動生成 - リリース済みブランチ
release/stableと開発中ブランチdevelop/nextをgraphql_schema_diff_check.batで比較して差分を取得 -
@graphql-inspector/cliの結果をdiff.logに出力 -
app.pyで差分を Web UI に一覧表示 - Jenkins + 担当チームの運用で **毎日破壊的変更を監視
という仕組みを紹介しました。
挙動までは見ないものの、
- スキーマ構造レベルでの後方互換性を継続的に監視できる
- 破壊的変更を早期に検知し、判断理由を残せる
- アプリ側の負担を増やさず、開発体験(DX)を改善できる
という意味で、現場にフィットしたやり方になっていると感じています。
GraphQL の後方互換性に悩んでいるチームにとって、少しでも参考になれば幸いです。
