技術書典の原稿からPDF生成を自動化するためにやったこと

この記事は [Fringe81 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fringe81) の23日目の投稿です。

前回の技術書典7に社内有志でサークル参加しました。そのときに作った冊子のPDF生成自動化についてやったことと反省点とかいけてないなーと思ったことについて思い出しながら書いていきます。

# ことのはじまり
各自が適宜ブランチ切って作業していくという雑なスタイルを採用したはいいが、他のメンバーの成果物をレビューするときに

- fetch して
- checkout して
- PDF生成して

という手間を踏むのが大変だなと感じていました。

知人に自身の所属企業で技術書典の出展とりまとめみたいなことをしている人がいて、その人は[AWSでやっているということを書いていた](https://medium.com/mixi-developers/%E6%8A%80%E8%A1%93%E6%9B%B8%E5%85%B8%EF%BC%95%E3%81%AB%E5%87%BA%E5%85%B8%E3%81%97%E3%81%BE%E3%81%99-9cde1c8417e5?)ので、その人に話をききつつ自分はGCPのサービス群で自動PDF生成に挑戦しようと思ったのでした。

# やったこと
## 下準備
- ふつうに Re:VIEW プロジェクトを作って textlint / rake pdf できる状態を作る
- GitHubにリポジトリ作ってPushしとく

ここらは割愛。Re:VIEWとかrextlintのドキュメントみながらふつうにやったはず。

## ビルド用のコンテナイメージを作る
- Re:VIEWをやっている
- textlint したい

とかを考慮してなんかこんな感じのものを作って build して push しました。

```dockerfile
FROM vvakame/review:3.2

# setup
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      texlive-fonts-extra && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN npm install -g \
    textlint \
    textlint-plugin-review \
    textlint-rule-max-ten \
    textlint-rule-no-mix-dearu-desumasu \
    textlint-rule-prh \
    textlint-filter-rule-comments
```

## cloudbuild.yaml を書く
こんな感じのもの

```yaml
steps:
- name: 'gionxy/review:3.2'
  entrypoint: bash
  args:
    - -c
    - |
      cd articles && textlint *.re && rake pdf
  id: pdf-build
artifacts:
  objects:
    location: 'gs://[bucket_name]/$SHORT_SHA'
    paths: ['articles/[booktitle].pdf']
```
次のようなことが書いてあります:

- `gionxy/review:3.2` 使って `bash -c "cd articles && textlint *.re && rake pdf"` してね！
- そのあと `articles/[booktitle].pdf` を `gs://[bucket_name]/$SHORT_SHA` 以下に置いてね！

ここで `$SHORT_SHA` は[GitのコミットID（短いもの）に置換されます](https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values?hl=ja)。

### 反省点
例の記事にひきづられてなんとなく1コンテナで完結するように新しいイメージを作ってしまったんですが、よく考えればCloudBuildで使うんだからRe:VIEWコンテナとtextlintするコンテナは別でよかった。stepsを2つにして textlint → Re:VIEWビルド みたいに。

## Cloud Build とリポジトリを接続
https://cloud.google.com/cloud-build/docs/run-builds-on-github?hl=ja を参考に Cloud Build アプリをリポジトリに追加した。

こうすることで、全コミットに対してトリガーが発動するようになりました。今はわかりませんが、8月時点ではたとえばPRオープン時のみ起動、というような設定はなかったと思います。
PRに対してはChecks APIで結果が通知されるようになりますが、成功か失敗かくらいしか伝達されない非常にシンプルなもので、具体的な失敗の原因はリンクを踏んでコンソールを確認しないといけませんでした。

![Screen Shot 2019-12-23 at 22.00.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63551/b8d9383e-88ac-316e-1c7c-da103ce6a070.png)


## Cloud Build の結果を Cloud Functions で受け取る
最低限ビルドが成功したときの成果物URLくらいは通知されたかったので完了を受け取るそう方法がないか調べたところ、[`cloud-builds` というPubSubトピックに情報を投げてる](https://cloud.google.com/cloud-build/docs/send-build-notifications?hl=ja)ようだったので、そのイベントを拾って動作するスクリプトを Cloud Functions に置きました。

```python
import os
import sys
import json
import base64
import urllib.request


def check(event, context):
    if event['attributes']['status'] == 'SUCCESS':
        main(event, context)


def main(event, context):
    messages = json.loads(base64.b64decode(event['data']).decode('utf-8'))

    substitutions = messages['substitutions']

    repo_name = substitutions['REPO_NAME']
    commit_sha = substitutions['COMMIT_SHA']
    short_sha = substitutions['SHORT_SHA']

    url = f'https://api.github.com/search/issues?q=is:pr+repo:gion-xy/{repo_name}+sha:{commit_sha}'
    headers = dict(Authorization=f'token {os.environ["TOKEN"]}')
    search_request = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(search_request) as res:
        search_response = json.loads(res.read())

    if search_response['total_count'] == 0:
        print(f'{short_sha}: not PR commit', file=sys.stderr)
        exit(0)

    pull_request_info = search_response['items'][0]

    if pull_request_info['state'] != 'open':
        print(f'{short_sha}: PR is not open', file=sys.stderr)
        exit(0)

    pull_request_number = pull_request_info['number']

    body = f'PDFはこちら: https://storage.googleapis.com/f81-techbookfest7/{short_sha}/1chome-lab-1.pdf'

    post_request = urllib.request.Request(
        f'https://api.github.com/repos/gion-xy/{repo_name}/issues/{pull_request_number}/comments',
        data=json.dumps(dict(body=body)).encode(),
        headers=headers
    )
    with urllib.request.urlopen(post_request) as res:
        body = res.read()
```
`commit_sha` が含まれるPRを探すというなんというかすごく泥臭いスクリプトになりました。

この状態でPRを更新するとCI成功時にPRにコメントがつくようになった。やりましたね。Botからやるみたいなのよくわからなくて自分のトークン使って投稿してます。
![Screen Shot 2019-12-23 at 22.11.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63551/6d8930ab-3f75-2cd8-7b7c-6750aece7d16.png)

SlackにGitHub連携を入れてPRへのコメントが通知されるようにすればSlackからすぐに結果に飛べて便利。これは全てがうまくいって自画自賛している様子です。
![Screen Shot 2019-12-23 at 22.15.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63551/ece4760d-7055-6894-1429-64e1b60eb329.png)

# まとめ
Cloud BuildとGitHubを連携させてRe:VIEW文書の自動PDF生成を行う方法についてみてきました。
Cloud BuildのGitHub Appは当時出たてで機能も少なかったためにいろいろ工夫をする必要がありましたが、いまだと[ドキュメント](https://cloud.google.com/cloud-build/docs/run-builds-on-github?hl=ja)を見る限りPRに対してのみ反応できるようになっている雰囲気があり、より楽になっているのではないでしょうか。

技術書典8は残念ながら（社内有志としても個人としても）サークル参加を見送ることになりましたが、一般参加はする予定なので、この記事がサークル参加するみなさんの助けになったりすることがあれば嬉しいなと思います。というか現環境でこういうもの組むとやっぱり楽になってるのかどうかというところは知りたいですね。