ibulog
いぶろぐ雑記
About Jobs Blog Tags
microCMSのAPIスキーマ差分をGitHub Actionsで自動管理する

こんにちは。サムネイルはいいかんじの細道です。

microCMSでは、現在のところはAPIスキーマのバージョン管理ができません。
バージョン管理をするには、APIスキーマのJSONを管理画面からダウンロードして、手動でGitなどで管理する必要がありました。

が、最近マネジメントAPIの拡充により、API経由でもAPIスキーマ情報の取得が可能になりました。

GET /api/v1/apis/{endpoint}

document.microcms.io
GET /api/v1/apis/{endpoint}

本記事では上記のAPIを使って、スキーマのJSONファイルをGitHubで管理し、差分が生じたら自動でPull Requestを作成してみます。

連携の流れ

流れとしては↓のイメージです。

  1. microCMSの管理画面でAPIスキーマを変更
  2. microCMSのWebhookが発火
  3. GitHub ActionsでWebhook受信
  4. Webhookの内容を元にAPIスキーマ取得
  5. 差分PR作成

それではつくっていきましょう。

GitHub Actions側の設定

いったんワークフローファイルのYAMLの全体像をお見せします。
メンテナビリティを考え、シェルではなく actions/github-script を使っているので、スキーマ取得処理などは別ファイルに切り出されています。
切り出した部分はのちほど説明します。

name: microCMS API Schema Update

on:
  repository_dispatch:
    types: [microcms-api-schema-update]

jobs:
  extract-api-name:
    runs-on: ubuntu-latest
    outputs:
      api_name: ${{ steps.extract-api-name.outputs.api_name }}
    steps:
      - name: Extract API name
        id: extract-api-name
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        with:
          script: |
            const payload = context.payload.client_payload;
            const apiName = payload?.api;

            if (!apiName || apiName === 'null') {
              core.setFailed('API名がWebhookのペイロードに含まれていません。');
              return;
            }

            core.setOutput('api_name', apiName);

  update-microcms-schema:
    runs-on: ubuntu-latest
    needs: extract-api-name
    permissions:
      contents: write
      pull-requests: write
    concurrency:
      group: ${{ github.workflow }}-${{ needs.extract-api-name.outputs.api_name }}
      cancel-in-progress: true
    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Update API schema file
        id: update-schema
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        env:
          MICROCMS_SERVICE_DOMAIN: ${{ secrets.MICROCMS_SERVICE_DOMAIN }}
          MICROCMS_MANAGEMENT_API_KEY: ${{ secrets.MICROCMS_MANAGEMENT_API_KEY }}
        with:
          script: |
            const { updateSchemaFile } = await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/update-schema-file.js`);
            await updateSchemaFile('${{ needs.extract-api-name.outputs.api_name }}', core);

      - name: Generate branch name
        id: generate-branch-name
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
        with:
          script: |
            const branchName = `update-schema-${{ needs.extract-api-name.outputs.api_name }}-${Date.now()}`;
            core.setOutput('branch_name', branchName);

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
        with:
          branch: ${{ steps.generate-branch-name.outputs.branch_name }}
          title: "chore: ${{ needs.extract-api-name.outputs.api_name }}のAPIスキーマ更新"
          body: |
            microCMSのAPIスキーマが更新されました。

            - API: `${{ needs.extract-api-name.outputs.api_name }}`

            このPRは自動生成されました。
          delete-branch: true

ワークフローの起動条件

今回はmicroCMSのWebhookをトリガーにするので、 repository_dispatch を使います。
types は任意の文字列で構いませんが、あとでmicroCMS側にも設定するので、わかりやすい名前がいいと思います。

on:
  repository_dispatch:
    types: [microcms-api-schema-update]

API名の取得

extract-api-name では、WebhookのペイロードからAPI名を取得しています。

extract-api-name:
  runs-on: ubuntu-latest
  outputs:
    api_name: ${{ steps.extract-api-name.outputs.api_name }}
  steps:
    - name: Extract API name
      id: extract-api-name
      uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
      with:
        script: |
          const payload = context.payload.client_payload;
          const apiName = payload?.api;

          if (!apiName || apiName === 'null') {
            core.setFailed('API名がWebhookのペイロードに含まれていません。');
            return;
          }

          core.setOutput('api_name', apiName);

なぜjobをわけているかというと、APIスキーマ単位でconcurrencyを設定したいため。

今回はすべてのAPIスキーマで共通のワークフローを使います。
そのため、受け取ったWebhookの内容をもとにconcurrencyのgroupを指定する必要があります。
が、concurrencyはjob単位までしか設定できないので、jobをわけてAPI名を事前に取得し、それを後続jobのconcurrencyに利用することになります。

また、ペイロードからAPI名を取得する処理は、後述するAPIスキーマを取得する処理とは異なり、JS部分を別ファイルに切り出していません。
これは処理が単純というのもありますが、別ファイルに切り出すとリポジトリのCheckoutが必要になり、規模が大きいリポジトリではボトルネックになるためです。

microCMSから受け取るWebhookのペイロード形式は以下のようなフォーマットです。ペイロードには context.payload.client_payload でアクセスできます。

{
  "service": "サービス名",
  "api": "API名",
  "type": "edit",
  "contents": null
}

細かい説明になりましたが、肝心のAPIスキーマ取得部分に移ります。

APIスキーマの取得・保存

続いて、API名を受け取って updateSchemaFile を実行する部分。

- name: Update API schema file
  uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
  env:
    MICROCMS_SERVICE_DOMAIN: ${{ secrets.MICROCMS_SERVICE_DOMAIN }}
    MICROCMS_MANAGEMENT_API_KEY: ${{ secrets.MICROCMS_MANAGEMENT_API_KEY }}
  with:
    script: |
      const { updateSchemaFile } = await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/update-schema-file.js`);
      await updateSchemaFile('${{ needs.extract-api-name.outputs.api_name }}', core);

updateSchemaFile の中身は以下のとおりです。

import fs from 'fs';
import path from 'path';

export const updateSchemaFile = async (apiName, core) => {
  // スキーマファイルのディレクトリがなければ作成
  const schemaDir = 'schemas';
  if (!fs.existsSync(schemaDir)) {
    fs.mkdirSync(schemaDir, { recursive: true });
  }

  const schemaFile = path.join(schemaDir, `${apiName}.json`);
  
  if (!process.env.MICROCMS_SERVICE_DOMAIN) {
    core.setFailed('MICROCMS_SERVICE_DOMAIN環境変数が設定されていません。');
    return;
  }
  if (!process.env.MICROCMS_MANAGEMENT_API_KEY) {
    core.setFailed('MICROCMS_MANAGEMENT_API_KEY環境変数が設定されていません。');
    return;
  }

  const serviceDomain = process.env.MICROCMS_SERVICE_DOMAIN;
  const managementApiKey = process.env.MICROCMS_MANAGEMENT_API_KEY;
  
  const apiUrl = `https://${serviceDomain}.microcms-management.io/api/v1/apis/${apiName}`;

  // APIスキーマを取得し、ファイルに保存
  try {
    const response = await fetch(apiUrl, {
      headers: {
        'X-MICROCMS-API-KEY': managementApiKey,
        'Content-Type': 'application/json',
      },
    });
    
    if (!response.ok) {
      core.setFailed(`APIスキーマの取得に失敗しました: ${response.statusText}`);
      return;
    }
    
    const schema = await response.json();
    fs.writeFileSync(schemaFile, JSON.stringify(schema, null, 2));
    core.info(`APIスキーマを${schemaFile}に保存しました。`);
  } catch (err) {
    core.setFailed(`エラーが発生しました:${err}`);
    return;
  }
};

やっていることはシンプル。

  1. API名をもとに、マネジメントAPIでAPIスキーマ情報を取得
  2. 取得したAPIスキーマ情報をJSONとして保存

core をわざわざ呼び出し元から渡しているのは、JS側でimportすると package.json での宣言が必要になるためです。
宣言してもいいっちゃいいんですが、 github-script のサンプルコードに従っています。

なお、microCMSのAPIスキーマは差分が生じないと保存できないので、差分検知の処理は入れていません。
厳密にはPRマージのタイミングなどが絡むと差分が生じないケースもあるのですが、のちのPR作成処理が吸収してくれるので、ここで考慮する必要はなさそうでした。

PR作成

あとはブランチ名が重複しないようにタイムスタンプを含めた名称を生成し、差分のコミット・PR作成をしていきます。
コミットやPR作成は peter-evans/create-pull-request が全部やってくれます。便利。

- name: Generate branch name
  id: generate-branch-name
  uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
  with:
    script: |
      const branchName = `update-schema-${{ needs.extract-api-name.outputs.api_name }}-${Date.now()}`;
      core.setOutput('branch_name', branchName);

- name: Create Pull Request
  uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
  with:
    branch: ${{ steps.generate-branch-name.outputs.branch_name }}
    title: "chore: ${{ needs.extract-api-name.outputs.api_name }}のAPIスキーマ更新"
    body: |
      microCMSのAPIスキーマが更新されました。

      - API: `${{ needs.extract-api-name.outputs.api_name }}`

      このPRは自動生成されました。
    delete-branch: true

最終的には、2ファイルを以下のように配置して、mainブランチにマージします。

.github
├── scripts
│   └── update-schema-file.js
└── workflows
    └── microcms-schema-update.yml

ワークフローで利用する環境変数をリポジトリに設定したら、GitHub側の設定は完了。

microCMS側の設定

microCMS側では、Webhookを設定します。
API設定 > Webhook > 追加と進んで、サービスは「GitHub Actions」を選択します。

設定項目は以下のとおり。

以上で設定は完了。APIスキーマを変更すると、GitHub側でワークフローが起動しPRが作成されると思います。

まとめ

マネジメントAPIの拡充によって、スキーマ変更の差分管理がかなり自動化できました。
APIの数だけmicroCMS側のWebhook設定が必要なので、ちょっと大変ではありますが…!

今回はAPIの削除には対応していないのが宿題ですね。やろうと思えばできる部分です。

おわり。