GraphCMSとHugoを連携してGithub Pagesで公開する(4)〜Webhook通知をなんとかGithub Actionsに到達させる

# 概要

前回まででGraphCMSにクエリー投げてコンテンツを取得し、Hugoでビルドするところまでの流れは説明した。
本記事ではGithub Actionsを利用してコンテンツ更新とともにビルド・公開の流れを解説したい。

# Github Actionsの設定

Github Actionsを利用してHugoのコンテンツをビルド・公開する方法は以前HugoとContentfulとの連携で解説した。
今回はこんな感じで実装してみた。

> この時点ではrepository_dispatch:は不要だが、後でWebhook通知をうけるため必要なのでつけておく

```yaml
name: github pages

on:
  push:
    branches:
      - main
  repository_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
          fetch-depth: 0

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true

      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'
      - name: Cache pip
        uses: actions/cache@v2
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-
            ${{ runner.os }}-
      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Build
        env:
          GRAPHCMS_ENDPOINT: ${{ secrets.GRAPHCMS_ENDPOINT }}
          GRAPHCMS_TOKEN: ${{ secrets.GRAPHCMS_TOKEN }}
          BASE_URL: ${{ secrets.BASE_URL }}
        run: python -m app && hugo --minify --baseUrl="${BASE_URL}"

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public

```

GithubのSecretsに環境変数を設定してプッシュする。
デプロイが成功するとgh-pagesブランチが生成され、ビルド成果物が配置されるのでGithub pagesの設定を行う。
Github pagesの公開サイトURLをBASE_URLとして渡しているが、直接config.tomlのbaseURLに書き込んでもよい。

[実際に作成したサイト](https://higebobo.github.io/hugo-graphcms-blog/)

![FireShot Capture 126 - Hugo GraphCMS Blog - 私のブログ - higebobo.github.io.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/a3e2d5f3-d3cf-18b3-a01c-469eb4f95c8a.png)

ここまでのソース → [Release v1\.3 · higebobo/hugo\-graphcms\-blog](https://github.com/higebobo/hugo-graphcms-blog/releases/tag/v1.3)

上記でGithubレポジトリにプッシュしたらGithub Actionsでビルド→Github Pagesで公開できるようになった。
そして本題であるGraphCMSでコンテンツを修正した場合にGithub Actionsを起動させる話をしたい。

# GraphCMS Webhookの利用

Webhook通知を使えばGithub APIを叩いてGithub Actionsを発火させることが可能である。
しかし、Github APIは所定のデータ形式を要求するため(後述)CotentfulやmicroCMSのように対応しているサービスであればよいがGraphCMSは今の所未対応である。
近い将来対応するであろうとは予測されるが、現実問題としてはどうするかという話をする。

## Github APIの設定

まずはGithub APIを利用したGithub Actionsとの連携について説明する。
Github Actionsを動かすためには次のようなリクエストを投げればよい。
(事前にGithub Personal Access Tokenを発行しておくこと)。

* エンドポイント: `https://api.github.com/repos/<Githubのアカウント>/<Githubのレポジトリ>/dispatches`
* メソッド: `POST`
* リクエスト
    * ヘッダ 
        * `Accept`: `application/vnd.github.v3+json`
        * `Authorization`: `token <取得したGithubのトークン>`
    * ボディ
        * `event_type`: `<適当なコメント(どこからリクエストされたか識別されやすいものがよい)>`
* レスポンス (成功時)
    * `204`: `No content`

curlを使う場合(環境変数は別途設定)

```shell
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GITHUB_TOKEN}" \
-d '{"event_type": "webhook test by curl"}' \
"https://api.github.com/repos/${GITHUB_ACCOUNT}/${GITHUB_REPO}/dispatches"
```

vs codeのREST Clientを使う場合(環境変数は.envで記述)

```
POST https://api.github.com/repos/{{$dotenv GITHUB_ACCOUNT}}/{{$dotenv GITHUB_REPO}}/dispatches
Accept: application/vnd.github.v3+json
Authorization: token {{$dotenv GITHUB_TOKEN}}

{
  "event_type": "webhook test by vs code"
}
```

成功しても`No content`なので何も返さない(エラーの場合はメッセージを返す)。
きちんと確認したい場合はステータスコードを取得するようにする。

ダッシュボードによる実行結果の確認

![FireShot Capture 128 - Actions · higebobo_hugo-graphcms-blog - github.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/e74413b9-2e07-54a6-9e16-3c8235e9032b.png)

上記でわかる通り、`event_type`で指定した値がActionsのログとして残る。
(レポジトリにプッシュした場合はコミットログが表示される)

Webhook通知のポイントとしてはリクエスト・ボディに`event_type`というキーを設定してPOSTする必要があり、これがGraphCMSでは**できない**。
GraphQLのクエリー結果のpayloadを返すか、何も返さないかしか今の所選択肢がないのだ。

簡単に実現するなら上記のcurl等によるリクエストを手動で実行すればよい。
コンテンツ更新頻度が高ければcronを利用して定期的に更新をかけるような対応するとか。

とはいえここで話が終わると身も蓋もない。
なので別の方法を解説する。

## 中継サービスの実装

Webhook通知は理想としては

    ヘッドレスCMSのWebhook通知 → Github Actions

としたい(Contentfulなどではこれでよい)。
が先に述べた通りGraphCMSはGithubに対応していない。なので

    ヘッドレスCMSのWebhook通知 → 中継サービス → Github Actions

となるような中継サービスを立てる。つまり、

**「右から来たものをから左へ受け流す」 ムーディ勝山**

方式を採用し、

**「ヘッドレスCMSから来たものをからGithubへ受け流す」**

とする。
クラウドサービスを使う場合、いくつか選択肢があるが、Pythonを使いたいならHeroku, PythonAnywhere, AWSあたりか。
Node.jsなら上記に加え(PythonAnywhereを除く), Netlify, Firebase, Vercelあたりも使えるだろう。
ちなみに今更だがVercelでホスティングするなら[GraphCMSに対応している](https://graphcms.com/blog/graphcms-vercel-integration)らしい。

### Heroku

Herokuの導入・使い方は他サイトを参照していただくとしてここでは割愛する。

#### Flask版

以下の構成でファイルを準備する。

```
.
├── Procfile
├── app.py
├── requirements.txt
└── runtime.txt
```

ソースは以下を参照。

https://gist.github.com/higebobo/f40af40a755e80e3c5c029d09e2393a1

実行する前に環境変数としてHerokuに`GITHUB_ENDPOINT`を設定する。

```shell
heroku config:add GITHUB_ENDPOINT=https://api.github.com/repos/xxx/xxx/dispatches
```
> 環境変数を設定するのでpython-dotenvは不要かと思うがローカルデバッグのために入れてある。

Curlを使った中継サーバへのWEB-APIアクセス

```shell
curl -X POST ${ENDPOINT} \
-H "X-Token: ${GITHUB_TOKEN}" \
-H "X-Info: Test by curl"
```

環境変数の説明

* `ENDPOINT`: Herokuで発行されたエンドポイント(**Github APIのエンドポイントではない**)
* `GITHUB_TOKEN`: Github APIのトークン

実行結果

```shell
{"status": "ok"}
```

![FireShot Capture 131 - Actions · higebobo_hugo-graphcms-blog - github.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/c2670fc0-090f-ccc0-2957-35ab6319ab4c.png)


ポイント解説

* `X-Token`で取得した値をGithubのトークンとしてそのまま使う
* `X-Info`は`event_type`の値として渡す。設定しない場合はデフォルト値を渡すので必須項目ではない

このCurlで行ったAPIコールをGraphQLのWebhookで行えばよい。

## GraphCMS Webhookの設定

最後にGraphCMSのWebhook通知の設定を行う。
ダッシュボードの左にある`Webhhoks`をクリックし、右上の`Create`をクリックすると以下のような画面が出る。

![Screenshot at 2021-10-08 16-06-40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/18790aeb-0020-8789-11cc-e4b287a00ae3.png)

今回は以下のように設定した。

* `Name`: Githubのアカウント名/レポジトリ名
* `Description`: 同上
* `Include payload`: オフ
* `Url`: 上記のHerokuのエンドポイント
* `Secret Key`: なし
* `Triggers`
    * `Content model`: `post` (作成した通知対象のモデル名)
    * `Stage`: `Published`
    * `Action`: なし  
* `Headers`
    * `X-Info`: `Webhook from GraphCMS` (自分の好みで)
    * `X-Token`: 取得したGithub Token

最後に`Create`をクリックするとこのように登録される。

![FireShot Capture 133 - GraphCMS - app.graphcms.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/d6f6ee00-aa7c-582d-a846-ddb95247d96f.png)

作成したWebhookを起動させる方法がわからないので(爆)、とりあえずコンテンツの更新をかけて、Github Actionsを確認する(どきどき)。

![FireShot Capture 135 - Actions · higebobo_hugo-graphcms-blog - github.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691494/6f1660d0-3416-f015-f207-9fc8a522522f.png)

よっしゃ。
念の為、Github Pagesがアップデートされているかも確認する(そもそもこれが更新されないと意味がない)。
こちらもできた。

## その他の中継サービスの実装

勉強がてらいくつかやってみた

### Heroku

#### Fast API版

fastapiはflaskとほぼ同じ感じでできる。
流儀にならってapp.pyをmain.pyに名前を変更した以外はflaskと同じファイル構成だ。

ソース一式はこちら。

https://gist.github.com/higebobo/2b8d18c14f3cb0c870f209388f8729cd

#### Express版

Node.jsで実装できると使える環境がぐっと増えるので試してみた。
不慣れなのでもっとすっきり書けるとは思うが。

```javascript
const express = require('express');
const request = require('request');
require('dotenv').config();

const app = express();

app.set('port', (process.env.PORT || 5000));

app.post('/', function (req, res, next) {
  if (req.headers.hasOwnProperty('x-token')) {
    // get headers from request
    var token = req.headers['x-token']
    var info = req.headers['x-info'] || 'From Node.js express and request'

    // set request settings
    const formData = {event_type: info};
    const options = {
      url: process.env.GITHUB_ENDPOINT,
      method: 'POST',
      headers: {
        'User-Agent': 'Node.js Express-Request Agent',
        'Accept': 'application/vnd.github.v3+json',
        'Authorization': 'token ' + token
      },
      form: JSON.stringify(formData)
    };

    // post data
    request(options, (error, response, body) => {
      // check error
      if(error !== null){
        console.error('error:', error);
        return(false);
      }
      console.log('statusCode:', response && response.statusCode);
    });
    // response
    res.json({'status': 'ok'});
  } else {
    res.status(400).json({'error': 'missing X-Token'})
  }
});

app.listen(app.get('port'), function () {
  console.log("Node app is running at localhost:" + app.get('port'));
});
```

https://gist.github.com/higebobo/841f8b73e1f65eac0b9da7e273f513a0

Expressを使うならNetlifyでも可能だ。
一応試してみたけどもNetlify Functionsの作法がよくわからないためソースが著しく汚くエラーハンドリングも適当すぎたのでお見せできるレベルではないので割愛させていただく。

Herokuに話を戻すと、今回Flask、Fast API、Expressとブランチを分けて実装してみたけども改めて気づいたことがあるので備忘録代わりに書いておく。

Git管理をHerokuで行う時の話だけども、まずブランチだが`master`ブランチにプッシュされたものがビルドされる。
なので`master`ブランチ以外を対象にしたい場合は以下の書式を使う

`express`ブランチを`master`としてプッシュ

```shell
git push heroku express:master
```

次、Pythonで作ったアプリをブランチ切ってNode.jsにした場合、そのままではエラーになる。
次の操作で`buildpack`を変更してやらなければならない。

Node.jsに変更したい場合

```shell
heroku buildpacks:set heroku/nodejs
```

Pythonに変更したい(戻したい)場合

```shell
 heroku buildpacks:set heroku/python
```

### AWS

最後に紹介するのはChaliceだ。
AWSが使えるのならこちらが一番簡単(公開までの工数が少ない)だと思う。

#### Chalice版

セットアップ

```shell
# バージョン確認
python -V
# 上記の結果
Python 3.9.6
# 仮想環境の構築
python -m venv venv39
# アクティベート
. venv39/bin/activate
# chaliceのインストール
pip install chalice
# プロジェクト作成
chalice new-project graphcms-github-proxy
# 作業ディレクトリへの移動
cd graphcms-github-proxy
```
上記で作成されたファイル

```.
├── .chalice
│   └── config.json
├── app.py
└── requirements.txt
```

`app.py`

```python
# -*- mode: python -*- -*- coding: utf-8 -*-
import json
import os
import urllib.request

from chalice import (Chalice, Response, BadRequestError)

app = Chalice(app_name='graphcms-github-proxy')


@app.route('/', methods=['POST'])
def index():
    request = app.current_request
    endpoint = os.getenv('GITHUB_ENDPOINT', 'http://localhost')
    token = request.headers.get('X-Token')
    info = request.headers.get('X-Info', 'Chalice Webhook')

    if not token:
        raise BadRequestError('Require X-Token')

    headers = {
        'Accept': 'application/vnd.github.v3+json',
        'Authorization': f'token {token}'
    }
    payload = {'event_type': info}

    req = urllib.request.Request(endpoint,
                                 data=json.dumps(payload).encode(),
                                 headers=headers)

    try:
        urllib.request.urlopen(req)
        status_code = 200
        resp = {'status': 'ok'}
    except Exception as e:
        status_code = 500
        resp = {'error': str(e)}

    return Response(body=json.dumps(resp),
                    status_code=status_code,
                    headers={'Content-Type': 'application/json'})

```

環境変数`GITHUB_ENDPOINT`の設定

`.chalice/config.json`

```json
{
  "version": "2.0",
  "app_name": "graphcms-github-proxy",
  "environment_variables": {
    "GITHUB_ENDPOINT": "https://api.github.com/repos/<あなたのアカウント>/<あなたのレポジトリ>/dispatches"
  },
  "stages": {
    "dev": {
      "api_gateway_stage": "api"
    }
  }
}
```

ローカルデバッグ

```shell
chalice local --stage dev --port 8000
```

デプロイ

```shell
chalice deploy
# 出力
Creating deployment package.
Reusing existing deployment package.
Creating IAM role: graphcms-github-proxy-dev
Creating lambda function: graphcms-github-proxy-dev
Creating Rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:xxxx:function:graphcms-github-proxy-dev
  - Rest API URL: https://xxxx.execute-api.ap-northeast-1.amazonaws.com/api/
```

`Rest API URL`のエンドポイントをGraphCMSのWebhookに登録してやればよい。

(おまけ:不要の場合)後片付け

```shell
chalice delete
# 出力
Deleting Rest API: xxxx
Deleting function: arn:aws:lambda:ap-northeast-1:xxxx:function:graphcms-github-proxy-dev
Deleting IAM role: graphcms-github-proxy-dev
```

# まとめ

中継サーバを介することでGithub ActionsにWebhook通知を届けることが可能となった。
この手法はいろいろと応用が効くと思う。