Marginalia

NotionヘッドレスCMS化記録 (3) GitHub Actionsと自動デプロイ

前回に引き続き、今回はNotionで書いた記事をGitレポジトリへ自動的に取り込み、Hugoでビルドしてデプロイするまでの流れで躓いた点をまとめる。今回が最終回だ。

Notionの変更からGitHub Actionsをトリガーしたい

まず思いつくのは Notion のデータベース更新からWebhookを受け取り、GitHub Actionsの repository_dispatch トリガーに連携する方法だが、残念ながら今のNotionにはまだWebhook機能がない。

IFTTTやzapierのようなサービスを使うと(何故か)Notionのデータベース変更イベントからアクションできるが、この程度のために外部サービスを使うのも癪なので、愚直ではあるが GitHub Actions の schedule トリガーで変更監視を定期実行するようにした。

また、最近のアップデートで workflow_dispatch トリガーが追加され、GitHubのUI上から手動でワークフローを開始できるのでデバッグ用に追加して次のようなワークフローができた。

name: sync-with-notion

on:
  schedule:
    - cron: '0/10 * * * *'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16.x'
          cache: yarn
      - run: yarn install
      - run: yarn notion:fetch
        env:
          NOTION_AUTH_TOKEN: ${{ secrets.NOTION_AUTH_TOKEN }}

DIffが生まれたらプルリクエストを作る

定期実行するワークフローで Notion API からMarkdownファイルを作成した結果、メインブランチと diff が発生したら、その変更を取り込むためのプルリクエストを自動生成する。これによりNotion の内容が(ほぼ)常にGitレポジトリへ一方的に同期される。

GitHub Actions で何らかのdiffを作ってプルリクエストを作るのは、 Create Pull Request アクションが非常に便利だ。一般的なユースケースをカバーした上でいろいろカスタマイズできる。こうしたワークフローで頻発するベースブランチとのコンフリクトや outdated 化などにも自動的に対応してくれる。

https://github.com/peter-evans/create-pull-request

botが作ったプルリクエストは別のワークフローをトリガーしない

ところが落とし穴がひとつあった。GitHub Actionsで提供される secrets.GITHUB_TOKEN を認証情報としてGitHub APIを呼び出すと、作成したプルリクエストは別のワークフローをトリガーしないことになっている。

When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run. This prevents you from accidentally creating recursive workflow runs.

プルリクエストが作成されたら Firebase Hosting のプレビューチャンネルへデプロイして表示確認できるようにしていたため、 Notion 記事反映のプルリクエストからプレビューが作られないのは問題である。

この問題を解決する方法も Create Pull Request の作者はフォローしていて、ドキュメントにいくつかの選択肢を載せてくれている。

結局のところ、デフォルトで提供されている secrets.GITHUB_TOKEN でさえなければ何でもいいので、今回はトークンのスコープを狭める観点からプライベートの GitHub Apps を使った方法を選んだ。詳細は書かないが、最終的に次のようなワークフローになった。

name: sync-with-notion

on:
  schedule:
    - cron: '0/10 * * * *'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16.x'
          cache: yarn
      # https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens
      - uses: tibdex/github-app-token@v1
        id: generate-token
        with:
          app_id: ${{ secrets.WORKER_APP_ID }}
          private_key: ${{ secrets.WORKER_APP_PRIVATE_KEY }}
      - run: yarn install
      - run: yarn notion:fetch
        env:
          NOTION_AUTH_TOKEN: ${{ secrets.NOTION_AUTH_TOKEN }}
      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v3
        with:
          token: ${{ steps.generate-token.outputs.token }}
          commit-message: 'fix: apply changes from Notion'
          branch: sync-with-notion
          delete-branch: true
          title: 'fix: apply changes from Notion'
          body: '@lacolaco Review and apply changes from Notion'

これで、Notionで記事を書けば約10分後にはGitHubに取り込みプルリクエストが作成され、自動デプロイされたプレビュー環境で表示のチェックができるようになった。完成である。

NotionにWebhookができればもう少しスマートになるが、今のAPIの使い方でもレートリミットなどは問題なさそうなのでしばらくはこのまま運用する。

まとめ

3回に分けて、NotionをこのブログのヘッドレスCMSとして使えるようにするにあたって苦労した点をまとめた。ほぼ自分用の備忘録と、実際にNotionで記事を書けると捗るのかどうかの実験を兼ねたものだが、もし似たようなものを作ろうとする誰かの参考になるなら幸いである。