STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

GraphQL のスキーマ更新を自動化する

GraphQL のスキーマ更新を自動化する

こんにちは! STORES でソフトウェアエンジニアをしている @m0nch1 です。

STORES ではいたるところで GraphQL が採用されており、今や STORES のものづくりにおいては欠かせないものになっています。

GraphQL 関連の記事がいくつかあるので抜粋して紹介しておきます。

product.st.inc

product.st.inc

今回はタイトルの通り、みんな大好き自動化の話です。

この記事が誰かの作業を少しでも短縮できるといいなと思っています!

手動でスキーマ同期する(自動化する前)

現在開発しているアプリケーションでは、Web フロントエンドとバックエンド間の通信に GraphQL を使用しています。バックエンドの開発には Ruby on Rails と graphql-ruby を使用しています。

API のスキーマはバックエンド側で生成され、それをフロントエンドが参照する形で動作しています。そのため、バックエンドのスキーマが更新された場合、フロントエンドもそれに合わせてスキーマを更新する必要があります。

手動でスキーマを更新する手順

  1. バックエンドのリポジトリを最新の状態にする
  2. ローカル環境でバックエンドのサーバーを起動
  3. ローカル環境の GraphQL サーバーから GraphQL の introspection の仕組みを使用してスキーマを取得
  4. graphql-codegen を使用してスキーマを生成

graphql-codegen でスキーマを生成する

graphql-codegen を使ってスキーマを生成するためには設定ファイルを用意する必要があります。

graphql-codegen は graphql-config もサポートしており、様々なファイル形式で設定ファイルを用意することができます。

.graphqlrc ファイルの例:

# .graphqlrc

# ローカルで起動しているバックエンドサーバーのイントロスペクション(introspection)エンドポイント
schema: https://localhost:3000/graphql/dev
extensions:
  codegen:
    generates:
      ./schemas/example-api.graphql:
        plugins:
          - schema-ast

以下のように npm script を用意しておけば、npm run update-graphql-schema を実行するだけでスキーマが生成されます。

// package.json 一部抜粋

{
  "scripts": {
    "update-graphql-schema": "graphql-codegen",
  }
}

自動化したい...

このように、ローカル起動したバックエンドのスキーマに基づいてスキーマを生成するという手順は、バックエンドと一緒に開発している間は簡単です。

しかし、チーム開発においてバックエンドとフロントエンドのタスクを分割したり、API の異なる変更が各所で進行したりすると、ローカルでバックエンドを手元に持ってくるのが手間であったり、何より Pull Request がコンフリクトするという問題もおきました。

コンフリ発生してスマンしているようす

どうすれば自動化できるか

バックエンドで発生する API の変更を、フロントエンド側のリポジトリが自動で追随している状態を目指すため、以下のような手順を取ることにしました。

  1. サーバー側リポジトリのスキーマ更新を通知できるようにする
  2. Web フロントエンドのリポジトリで通知を受け取り、スキーマ更新の Pull Request を作成する

サーバー側リポジトリのスキーマ更新を通知できるようにする

GitHub Actions にはワークフローをトリガーするイベントが様々ありますが、repository_dispatch を使うことで他のリポジトリにイベントを通知することができます。

バックエンドのスキーマが main ブランチに push されたタイミングをきっかけにできれば良いので、以下のようなワークフローを用意すれば解決できます。

# schema-updated.yml

name: Dispatch Schema Updated

on:
  push:
    branches:
      - main
    paths:
    - app/graphql/example_api/schema.graphql

jobs:
  dispatch-schema-updated:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/create-github-app-token@v1
        id: app-token
        with:
          app-id: XXXXXX
          private-key: ${{ secrets.XXX_KEY }}
          owner: ${{ github.repository_owner }}
          repositories: frontend-repository-name

      - name: dispatch schema update
        uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ steps.app-token.outputs.token }}
          repository: ${{ github.repository_owner }}/frontend-repository-name
          event-type: update-api-schema

Web フロントエンドのリポジトリで通知を受け取り、スキーマ更新の Pull Request を作成する

フロントエンド側のリポジトリでは、repository_dispatch イベントをトリガーとして受け取り、スキーマを更新する GitHub Actions を設定します。

repository_dispatch の types にサーバー側リポジトリで設定したイベントの event-type を指定することで、サーバー側のリポジトリでディスパッチしたイベントをトリガーにすることができます。

あとは、ジョブのなかでスキーマ更新の npm script を実行すればスキーマ更新の Pull Request が作成できます。

# update-schema.yml

name: Create update schema Pull Request

on:
  workflow_dispatch: // 手動実行もできるようにしておく
  repository_dispatch:
    types: [update-api-schema]

jobs:
  update-api-schema:
    name: Update API schema
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - run: corepack enable

      - uses: actions/setup-node@v4
        with:
          node-version-file: '.node-version'
          cache: 'npm'

      - name: Install dependencies
        run: npm install

      - uses: actions/create-github-app-token@v1
        id: app-token
        with:
          app-id: XXXXXX
          private-key: ${{ secrets.XXX_KEY }}
          owner: ${{ github.repository_owner }}

      - name: Update GraphQL Schema
        env:
          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
        run: npm run update-graphql-schema

      - uses: peter-evans/create-pull-request@v7
        with:
          token: ${{ steps.app-token.outputs.token }}
          commit-message: Update API schema
          branch: update-api-schema
          branch-suffix: timestamp
          delete-branch: true
          title: Update API schema
          body: |
            This Pull Request updates the API schema.
            ref: https://github.com/organization-name/repository-name/blob/main/app/graphql/example_api/schema.graphql
          team-reviewers: xxx/review-sitehosii-team

.graphqlrc を修正

repository_dispatch によって、自動でスキーマを更新する Pull Request を作成することはできるようになりましたが、実はこれだけではうまくいきません。

npm run update-graphql-schema を実行すると .graphqlrc の schema フィールドに指定しているパスへスキーマを取りに行くのですが、冒頭に記載した設定のままでは開発サーバのエンドポイントを設定しているため、当然 GitHub Actions の実行環境からこのパスへアクセスすることはできません。

# .graphqlrc 再掲

# GitHub Actions は以下のパスにアクセスできない
schema: https://localhost:3000/graphql/dev
extensions:
  codegen:
    generates:
      ./schemas/example-api.graphql:
        plugins:
          - schema-ast

実はこの schema フィールドには GitHub リポジトリのパスを指定することができます。(知らなかった)

ref: https://the-guild.dev/graphql/codegen/docs/config-reference/schema-field#github

※ もちろん、プライベートリポジトリにも対応しています。

以下のように schema フィールドを GitHub リポジトリのパスに設定することでスキーマを取得することができるようになります。

# .graphqlconfig

projects:
  default:
    schema: github:some-organization/backend-repository-name#main:app/graphql/example_api/schema.graphql
    extensions:
      codegen:
        generates:
          ./schemas/example-api.graphql:
            plugins:
              - schema-ast
  dev:
    schema: https://localhost:3000/graphql/dev
    extensions:
      codegen:
        generates:
          ./schemas/example-api.graphql:
            plugins:
              - schema-ast

projects の dev については、開発中のスキーマをローカルで取得したい時などに使用したいため project を分けて用意しました。

以下のように使い分けができます。

// package.json 一部抜粋

{
  "scripts": {
    "update-graphql-schema": "graphql-codegen",
    "update-graphql-schema:dev": "graphql-codegen --project dev"
  }
}

まとめ

以上、GitHub Actions をうまく使うだけで簡単にスキーマ更新の自動化をすることができました。

これによりスキーマ更新をしたメンバーがスキーマを直ちに最新化する動きができ、スキーマ部分でコンフリクトすることも無くなりました!

自動化や業務効率化はちりつもですが、確実に有意義な時間を産む方法だと改めて感じました。

もっともっと GitHub Actions を使いこなしていきたいですね!