GitHubのPull RequestをレビューするオレオレCIを実装する

「厳密なテストは書きたくないけど微妙なルール」ってあるじゃないですか？

たとえば私はAndroidアプリをよく作ってますが、

* Fragmentにコンストラクタが実装されてない場合は気づけるようにしたい
* Activityのクラス名は必ず「〜〜Activity」、Fragmentのクラス名は必ず「〜〜Fragment」であってほしい

のように、「守って無くてもビルドは通るけど、守ってもらわないと困る」てきなやつです。

Circle CIとかで、そういうチェックスクリプトを走るよう記述すればぶっちゃけ十分なんですが、
「CircleCIつかえます( ｰ`дｰ´)ｷﾘｯ」っていうよりも「CircleCIとは別に自前でCI建てれます」っていうほうが格好がつくので、今回は自前でCIを建ててみました。

## まえおき：GitHubのPull Requestレビューの仕組み

[GitHubの公式ドキュメントにも書いてある](https://developer.github.com/guides/building-a-ci-server/) のですが

![シーケンス](https://qiita-image-store.s3.amazonaws.com/0/74571/f0bbb5e6-5b76-51e8-92a9-d0252b05f0ec.png)

こんなかんじで、WebHookを受けて、StatusをPOSTするだけの単純な仕組みで動いているみたいです。


## Python+Bottle on Heroku でオレオレCIを実装する

### WebHookを受ける

＃どんなJSONが飛んできているかは[requestbin](https://requestb.in/) で観察しながら実装しました。

```py:test.py
from bottle import Bottle, run, request
import os

app = Bottle()

@app.post('/event_handler')
def handle_event():
  payload = request.json
  event_name = request.get_header('X-GitHub-Event')
  if event_name == 'pull_request':
    if payload['action'] == 'opened' or payload['action'] == 'reopened':
      return handle_pull_request_created(payload['pull_request'])
    elif payload['action'] == 'synchronize':
      return handle_pull_request_updated(payload['pull_request'])

  return 'dame'

def handle_pull_request_created(pull_request):
    return 'ok: created'

def handle_pull_request_updated(pull_request):
    return 'ok: updated'

run(app, host='0.0.0.0', port=os.environ['PORT'])
```

これを適当にHerokuにデプロイして、テストしたいソースコードのリポジトリにWebHook設定を追加すれば、Pull Request追加時に `handle_pull_request_created` が実行されるようになります。


### セキュリティのため、WebHookのSecretをちゃんと考慮する

`request.get_header('X-Hub-Signature')` で `sha1=xxxxxxxxxxxxx` みたいな文字列が取れるはずです。

リクエストボディをSHA1ハッシュとったものらしいので、

```py:test
import hashlib
import hmac

digest = hmac.new(b'hogehoge123', request.body.read(), hashlib.sha1).hexdigest()
if "sha1="+digest == request.get_header('X-Hub-Signature'):
  # 正しいリクエスト
else:
  # 不正なリクエスト
```

のようなコードをかけば、検証ができます。 （`hogehoge123` がWebHook設定のときに指定したシークレット）

　

### いろいろチェックをして、ステータスPOSTのAPIをたたく

```py:test
from rq import Queue
from worker import conn
from reviewer import review
import json

def handle_pull_request_created(pull_request):
    Queue(connection=conn).enqueue(review, json.dumps(pull_request))
    return 'ok: created'
```

```py:reviewer.py

from time import sleep
import json
import requests
import os

def review(pull_request_json):
    pull_request = json.loads(pull_request_json)

    repo_name = pull_request['base']['repo']['full_name']
    sha = pull_request['head']['sha']
    set_github_status(repo_name, sha, 'pending', 'now checking OREORE')
    
    # ここで Pull Requestのチェックをいろいろやる
    check_coding_style(pull_request)
    another_check(pull_request)
    
    set_github_status(repo_name, sha, 'success', 'OREORE: success')    

def set_github_status(repo_name, sha, status, description):
    data = {
        'state': status,
        'context': 'continuous-integration/oreore_ci',
        'description': description
    }
    url = 'https://api.github.com/repos/{repo_name}/statuses/{sha}'.format(repo_name=repo_name, sha=sha)
    requests.post(url, data=json.dumps(data), headers={'Authorization': 'token {token}'.format(token=os.environ['GITHUB_ACCESS_TOKEN'])})

def check_coding_style(pull_request):
    sleep(3)

def another_check(pull_request):
    sleep(10)
```

Redis Queueを使って、非同期実行するようにしてるので、 `Queue(・・・).enqueue(review, ・・・)` みたいなことを書いています。
（Herokuだと[このへん](https://devcenter.heroku.com/articles/python-rq) に事細かに手順が書かれていて、かなり参考になります）

GitHubのAPI用トークンは、本来はOAuthとかで取るべきなんですが、今回は面倒なので、Private Access Tokenを取ってきて環境変数に入れる形にしてあります。


これで、適当なWebHook設定済みのリポジトリにPull Requestを出すと・・・

![Kobito.95OW27.png](https://qiita-image-store.s3.amazonaws.com/0/74571/2dee9d79-a1fb-b49b-2a49-ddcb1bf45624.png "Kobito.95OW27.png")

　
　　　　　↓13秒後
　

![Kobito.FpE7N3.png](https://qiita-image-store.s3.amazonaws.com/0/74571/8e9d38d9-9a2c-7929-a5d4-1d209f1becbe.png "Kobito.FpE7N3.png")


ｷﾀ━━━━(ﾟ∀ﾟ)━━━━!!

ってなります。

　
　

## まとめ

とりあえず、オレオレCIの大枠は作れました。
https://github.com/YusukeIwaki/oreore_ci にトライアルのコードを置いておきます。

今回は構造理解が目的なので、Python+Bottleのような超ライトなCIサーバにしましたが、
ちゃんとやるならRailsのAPIモードとかを使ったほうがいいかと思います。