GraphQLスキーマをサーバーサイドからクライアントサイドに自動同期し、変更履歴も確認したい

似たような話が先日ありましたね。

こちらはGraphQLではなくてOpenAPI、かつサーバーサイド側のリポジトリからクライアントサイド側のリポジトリにプッシュする方式でした。

今関わっているプロダクトでは、1つのGraphQLスキーマが複数のリポジトリから必要とされていたり、それぞれで同期タイミングを制御できたらいいかなということもあったりで、クライアントサイドからGitHub Actionsの定期実行でプルする方式で更新をしています。

イメージは次のとおり:

name: Update GraphQL Schema

on:
  schedule:
    - cron: "0 0 * * 1-5" # JSTで月曜〜金曜の9時、これは適当です
  workflow_dispatch:

jobs:
  UpdateGraphQLSchema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: make update-graphql-schema
      - uses: peter-evans/create-pull-request@v4
        with:
          commit-message: Update schema.graphqls
          title: Update schema.graphqls
          body: ""
          branch: update-graphql-schema
          token: ${{ secrets.GITHUB_PAT }}

トリガーとしては、scheduleでの定期実行 OR workflow_dispatchでの手動実行になっています。

実際の更新部分は例として make update-graphql-schema としていますが、中身としてはApollo Clientのコマンドを実行してスキーマをダウンロードする(実態としてはIntrospectionを実行してスキーマを得る)ようにしています。ここについては、サーバーサイドのリポジトリをチェックアウトしてきて、元のスキーマファイルをコピーするようにしてきてもよいでしょう。

更新したスキーマをゲットできたら、あとは peter-evans/create-pull-request actionを使ってプルリクエスト(以下、PR)を作成しています。

ここで扱っているのはApollo Clientを使ったネイティブアプリで、Apollo iOSApollo Kotlinもアプリのビルド時にオンデマンドでコード生成を行うので、スキーマ更新と同時にコード生成をしてPRに含める、ということはありません。

さて、これはこれで単純でいいのですが、スキーマの変更内容がPRで確認できたとしても、その詳細(変更の意図や経緯)がこれだけでは分かりません。ということで、サーバーサイドのリポジトリの関連するPRを拾い上げて、スキーマ更新のPRに変更履歴という体で貼り付けていました。

しかしさすがに人力で関連PRを探しに行くのは面倒なので自動化したい。ということで、チームメンバーであるところの id:mangano-ito さんがやってくれました。

name: Update GraphQL Schema

on:
  schedule:
    - cron: "0 0 * * 1-5" # JSTで月曜〜金曜の9時、これは適当です
  workflow_dispatch:

jobs:
  UpdateGraphQLSchema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - id: generate-associated-pr-list
        uses: ./.github/actions/generate-associated-pr-list
        with:
          our-path: path/to/client/schema.graphqls
          their-path: path/to/server/schema.graphql
          their-owner: ...
          their-repo: ...
          github-token: ${{ secrets.GITHUB_PAT }}
      - run: make update-graphql-schema
      - uses: peter-evans/create-pull-request@v4
        with:
          commit-message: Update schema.graphqls
          title: Update schema.graphqls
          body:  |
            ## Associated Pull Requests
            ${{ steps.generate-associated-pr-list.outputs.result }}
          branch: update-graphql-schema
          token: ${{ secrets.GITHUB_PAT }}

generate-associated-pr-list というのがお手製のComposite Actionです。自分方のリポジトリのファイルと、相手方のリポジトリのファイルの変更日時から、相手方のファイルのその間のコミット・それに紐付くPRを拾い上げて、Markdownのリストとして出力してくれるものです。パラメーター名も、gitの衝突解消時のours, theirsという用語を基にしていてオシャレですね。

その実装の大事な部分だけ抜粋してみます:

runs:
  using: composite
  steps:
    - id: get-last-updated-date
      shell: bash
      env:
        TZ: UTC0
        PATH_TO_FILE: ${{ inputs.our-path }}
      run: |
        value=$(\
          git log -1 \
            --date="format-local:%Y-%m-%dT%H:%M:%SZ" \
            --pretty="format:%cd" \
            "${PATH_TO_FILE}" \
        )
        echo "::set-output name=value::$value"
    - id: find-prs
      uses: octokit/graphql-action@v2.x
      env:
        GITHUB_TOKEN: ${{ inputs.github-token }}
      with:
        query: |
          query FindAssociatedPullRequests(
            $since: GitTimestamp!,
            $path: String!,
            $owner: String!,
            $repo: String!,
          ) {
            repository(owner: $owner, name: $repo) {
              defaultBranchRef {
                target {
                  ... on Commit {
                    history(since: $since, path: $path) {
                      nodes {
                        associatedPullRequests(first: 1) {
                          nodes {
                            number
                            url
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        since: ${{ steps.get-last-updated-date.outputs.value }}
        path: ${{ inputs.their-path }}
        owner: ${{ inputs.their-owner }}
        repo: ${{ inputs.their-repo }}

GitHubのGraphQL API (v4) を上手く使って、特定のファイルの特定日時以降のコミット・それに紐付くPRをリストアップしています。これで人力で関連PRを探しに行かなくてよくなりました。めでたしめでたし。

このようなワークフロー・改善により、自分達のGraphQL APIとこれからも仲良く付き合っていきたい。