LoginSignup
17
18

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-10-04

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

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

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

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

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

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

GitHubの公式ドキュメントにも書いてある のですが

シーケンス

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

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

WebHookを受ける

#どんなJSONが飛んできているかはrequestbin で観察しながら実装しました。

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ハッシュとったものらしいので、

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をたたく

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'
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だとこのへん に事細かに手順が書かれていて、かなり参考になります)

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

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

Kobito.95OW27.png

 
     ↓13秒後
 

Kobito.FpE7N3.png

キタ━━━━(゚∀゚)━━━━!!

ってなります。

 
 

まとめ

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

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

17
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
18