0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust async-graphql のスキーマを CI で守る話

0
Last updated at Posted at 2026-04-24

はじめに

Rust で GraphQL サーバーを書くとき、事実上の定番である async-graphqlcode-first を採用しています。Rust の型と impl にアトリビュートを付けるだけで、GraphQL schema が自動的に組み上がる仕組みです。

これは書き味が最高な一方で、frontend 側にチェックインしてある schema.graphqls が手書きで更新されないと、気づかないうちに嘘をつき始めます。

自分の個人プロジェクト(nba-iso-flow.com)でも、backend では tradeNews にリネームしていたのに frontend の schema.graphqls には newsItems という古い名前が残っていた、という事案がありました。Ktor で手書きクエリしていたので、ビルド時にも実行時にも誰も気づかない、という地味に怖い状態です。

この記事では、

  1. DB 接続なしでスキーマを SDL にエクスポートする小さなバイナリを追加して、
  2. CI で diff を取って drift(乖離)を fail させる

までをやったので、実コードと設計判断を残しておきます。

環境

  • Rust: stable(執筆時 1.85)
  • async-graphql: 7.x
  • sqlx: 0.8(SQLX_OFFLINE を活用)
  • GitHub Actions(ubuntu-latest)

frontend 側(Kotlin/JS + Apollo Kotlin)の話は別記事にするので、ここでは SDL を生成する backend 側と CI に絞ります。

実装

1. print_schema バイナリを追加する

backend/src/bin/print_schema.rs に、SDL を stdout に吐くだけの薄いバイナリを置きます。

// backend/src/bin/print_schema.rs
use async_graphql::{EmptySubscription, Schema};
use nba_trade_scraper::graphql::{Mutation, Query};

fn main() {
    let schema = Schema::build(Query, Mutation, EmptySubscription).finish();
    print!("{}", schema.sdl());
}

async-graphql の Schema::build は Query / Mutation / Subscription の 型だけを要求します。PgPoolSqlitePool などの状態を渡さなくても Schema は組み立てられるので、このバイナリは DB も環境変数もなしで動くのがポイントです。

あとは生成するだけ。

cd backend
cargo run --bin print_schema > ../frontend/src/jsMain/graphql/schema.graphqls

これで frontend の schema.graphqls が常に backend の実装と一致した状態で commit されます。Rust のドキュメントコメント(///)は GraphQL の description にそのまま流れるので、日本語コメントも SDL に反映されます。

# 生成される schema.graphqls(抜粋)
type Query {
  """
  全てのトレードニュースを取得します(データベースから)

  最新100件のニュースを返します
  """
  tradeNews: [TradeNews!]!
  tradeNewsByCategory(category: String!): [TradeNews!]!
  # ...
}

2. CI で drift を検知する

.github/workflows/schema-check.yml を追加します。

name: GraphQL Schema Drift Check

on:
  pull_request:
    types: [opened, synchronize, reopened]
    paths:
      - 'backend/src/graphql.rs'
      - 'backend/src/bin/print_schema.rs'
      - 'backend/Cargo.toml'
      - 'backend/Cargo.lock'
      - 'frontend/src/jsMain/graphql/schema.graphqls'
      - '.github/workflows/schema-check.yml'

concurrency:
  group: schema-check-${{ github.event.pull_request.number }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always
  SQLX_OFFLINE: true

jobs:
  schema-drift:
    name: Verify SDL matches backend code-first schema
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo registry & target
        uses: Swatinem/rust-cache@v2
        with:
          workspaces: backend

      - name: Regenerate SDL from backend
        working-directory: backend
        run: |
          cargo run --bin print_schema --quiet > /tmp/schema.graphqls

      - name: Diff against committed schema.graphqls
        run: |
          if ! diff -u frontend/src/jsMain/graphql/schema.graphqls /tmp/schema.graphqls; then
            echo ""
            echo "❌ frontend/src/jsMain/graphql/schema.graphqls が backend のスキーマとズレてる"
            echo "   ローカルで以下を実行してコミットしてくれ:"
            echo ""
            echo "   cd backend && cargo run --bin print_schema > ../frontend/src/jsMain/graphql/schema.graphqls"
            echo ""
            exit 1
          fi
          echo "✅ SDL is in sync with backend code-first schema"

やってることは素朴で:

  1. cargo run --bin print_schema で SDL を /tmp/schema.graphqls に出す
  2. commit 済みの frontend/src/jsMain/graphql/schema.graphqlsdiff -u
  3. 差分があれば exit 1 で PR を fail させる

ワークフローを分けた理由は、既存の backend CI(テスト・clippy 等)とライフサイクルが違うからです。スキーマに無関係な変更のたびに走らせたくないので paths でフィルタしています。

ハマったポイント

1. SQLX_OFFLINE=true を忘れると CI で build が死ぬ

sqlx の query! マクロを使っている場合、デフォルトでは コンパイル時に DB へ接続して schema を確認しに行きます。CI で cargo run --bin print_schema を叩いたとき、対象の bin が DB を触らないのに、workspace 内の他のコードの compile-time check のせいで DB 接続を要求される、というパターンで死にます。

対策は env: SQLX_OFFLINE: true を workflow に入れておくこと。これで、事前に cargo sqlx prepare で生成しておいた query cache を見に行くモードになり、DB なしでもビルドが通ります。

env:
  SQLX_OFFLINE: true

2. schema.sdl() の出力順は実装順依存

async-graphql の Schema::sdl() は、型やフィールドの 定義順をそのまま出力します。Rust 側で impl の順番を入れ替えると diff が出て、本質的でない差分で PR がレッドになることがあります。

選択肢としては:

  • そのまま受け入れる(iso-flow はこれ)。実装順を動かさない運用に倒す
  • ソートを噛ませる。SDL を line ベースでソートする、または AST を舐めてカノニカル化するスクリプトを追加する

個人開発では前者で十分でした。チームが増えたら後者に倒すかもしれません。

3. cargo run でうっかり他の bin をビルドさせない

src/bin/ に DB を触る別のバイナリがあると、cargo run --bin print_schema でもリンク時に引っかかって build 時間がドカっと伸びる、ということはありませんが、将来 workspace が育ったら --package-p で範囲を絞るのが安全です。

実際には --bin print_schema でビルドされるのは依存関係のグラフに必要な分だけなので、SQLX_OFFLINE さえ通せば print_schema はサクッと動きます。

4. エラーメッセージに修正コマンドを書いておく

最初は diff -u の結果だけを出していたんですが、drift を起こした PR の作者(未来の自分)は「で、どうやって直すの?」となります。

echo "❌ frontend/src/jsMain/graphql/schema.graphqls が backend のスキーマとズレてる"
echo "   ローカルで以下を実行してコミットしてくれ:"
echo ""
echo "   cd backend && cargo run --bin print_schema > ../frontend/src/jsMain/graphql/schema.graphqls"

Actions の失敗ログから コピペして直せるようにしておくと、体験がだいぶ変わります。

まとめ

  • async-graphql は code-first だが、frontend に SDL を配る需要はあるので schema.graphqls は commit する
  • commit する以上は drift を機械的に検出する仕組みがほしい
  • Schema::sdl() を stdout に吐くだけの小さなバイナリを src/bin/ に置き、CI で diff を取るのが最小コストの解
  • SQLX_OFFLINE=truepaths フィルタで CI コストは十分に抑えられる

次回は、この SDL を入力にして Kotlin/JS + Apollo Kotlin で型安全 GraphQL クライアントを入れた話を書きます。Apollo Kotlin 4 が Kotlin 2.0 必須だったり、apollo-mockserver が死んでたり、地味に罠があるので共有する予定です。

サンプルリポジトリ

👉 toguri/rust-async-graphql-schema-ci

この記事の構成を最小サンプルとして切り出したリポです。cargo run --bin print_schema > schema.graphqlsschema-check.yml がそのまま動く形になっています。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?